Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d40cc5a
li: drop course links on 4 teach posts, add vibe-coding reaction, reo…
pftg Aug 13, 2026
98cf2dd
li: simplify claude-code post - drop changelog/feature specifics, kee…
pftg Aug 13, 2026
12c4ac8
li: claude-code post - keep Opus + team mode (Paul's real history), p…
pftg Aug 13, 2026
ad08d53
li: revert claude-code to Paul's approved text (undo my over-rewrite)
pftg Aug 13, 2026
290ba07
li: light de-slop pass on staging / friends / price (surgical)
pftg Aug 13, 2026
b45ce85
li: human-voice pass on the queue - heavy on 2 backlog, light on poll…
pftg Aug 13, 2026
80420c9
li: compress 40k post's over-narrated paragraph (influencer register)
pftg Aug 14, 2026
61de071
li: remove BigBinary story post, reopen the mistakes slot
pftg Aug 14, 2026
43ab50b
li: new build-in-public post - the review tool I had AI build in half…
pftg Aug 14, 2026
35ad037
li: reorder full queue - front-load value, space clusters/AI/links, p…
pftg Aug 14, 2026
8647b7a
li: vendors - kill the 'A founder showed me... It barely worked' stag…
pftg Aug 14, 2026
23e5439
li: drop 'non-technical founder' ICP-label, fix friends slop, poll sh…
pftg Aug 14, 2026
aa26108
li: new opinion post - you don't need permission to fix what's hurtin…
pftg Aug 14, 2026
ad20970
li: add Claude Code caricature (Art tool / nano-banana-pro), mount pe…
pftg Aug 14, 2026
18c0d52
li: caricatures for all remaining imageless posts (Art tool / nano-ba…
pftg Aug 14, 2026
88bd825
docs+okf: image-by-pillar + caricature (Art tool) how-to
pftg Aug 14, 2026
f36d709
li board: ArrowUp goes to the board (adds to Left=prev / Right=next)
pftg Aug 14, 2026
ae04a8a
li board: ArrowRight from the board opens the first awaiting-verify p…
pftg Aug 14, 2026
c330540
li: better price exhibit - 'what proves demand' (comments vs payment)…
pftg Aug 14, 2026
3be124f
li: session handoff - next actions (stop building, start sending; RED…
pftg Aug 14, 2026
95585a8
Merge remote-tracking branch 'origin/master' into linkedin-review-board
pftg Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .okf/log.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Bundle Update Log

## 2026-08-14 (LinkedIn caricatures) - image type by pillar; the Art-tool cartoon pipeline

* **Update**: `linkedin-posts/README.md` gains "Image type by pillar" + "Generating
caricatures (the Art tool)".
* **The split**: not every LinkedIn post wants a house-style *exhibit*. Teach posts
(frameworks) do - Cutler/Rossi register. But **personal / opinion / story /
reaction** posts want a **New Yorker-style caricature** (editorial cartoon): a
data-diagram on a personal take reads as marketing, the opposite of the goal. Polls
show the poll (no image). Text-only stays valid for a blunt Holub take.
* **The tool**: the `Art` skill (`~/.claude/skills/Art`, Nano Banana Pro / Gemini 3
Pro image) generates the cartoons - `--workflow=Comics`, single-panel New Yorker
style, **no lettering**, output to `~/Downloads` first, view-and-gate, then wire in.
Needs a **billing-enabled** Google key in `~/.claude/.env` (free tier image quota =
0); the key is untracked, never in the repo. New lanes need a dev-only assets mount
in `config/development/hugo.toml`.
* **Recurring model quirk**: it sometimes adds a clean sign word ("MANAGER", "ERROR")
despite the no-text prompt - keep only if correct and it sharpens the gag.

## 2026-08-14 (gates) - every late defect was invisible to source-level matching

* **Update**: findings backlogged in `docs/20-29-testing-qa/20.10` §3b (P0-4,
Expand Down
3 changes: 3 additions & 0 deletions config/development/hugo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ ignoreFiles = ["\\.min\\.css$", "\\.min\\.js$"]
[[module.mounts]]
source = "linkedin-posts/course-promo/assets"
target = "static/linkedin-assets/course-promo"
[[module.mounts]]
source = "linkedin-posts/personal/assets"
target = "static/linkedin-assets/personal"

# Build optimizations for development speed
[build]
Expand Down
11 changes: 11 additions & 0 deletions layouts/linkedin/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,16 @@ <h1>LinkedIn Review Board</h1>
{{ template "cards" (dict "posts" $backlog) }}
</section>
</div>
{{ $firstReview := "" }}
{{ if gt (len $review) 0 }}{{ $firstReview = (index $review 0).RelPermalink }}{{ else if gt (len $approved) 0 }}{{ $firstReview = (index $approved 0).RelPermalink }}{{ end }}
{{ with $firstReview }}
<script>
// ArrowRight from the board opens the first post awaiting pre-verify
document.addEventListener('keyup', function(e){
if (/^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName) || e.metaKey || e.ctrlKey || e.altKey) return;
if (e.key === 'ArrowRight') window.location.href = '{{ . }}';
});
</script>
{{ end }}
</body>
</html>
13 changes: 11 additions & 2 deletions layouts/linkedin/single.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
.hint { font-size: .72rem; color: var(--muted); margin: .7rem 0 0; text-align: left; }
.hint code { background: #eceef1; padding: .05rem .3rem; border-radius: 3px; word-break: break-all; }
.no-img { color: #9aa0a8; font-size: .82rem; }
.poll-preview { text-align: left; margin-top: .3rem; }
.poll-preview p { font-weight: 700; font-size: .72rem; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin: 0 0 .7rem; }
.poll-preview ol { list-style: none; padding: 0; margin: 0; counter-reset: opt; }
.poll-preview li { counter-increment: opt; position: relative; border: 1.5px solid var(--accent); border-radius: 999px; padding: .55rem 1rem .55rem 2.1rem; margin-bottom: .55rem; font-size: .92rem; font-weight: 600; color: #16181d; }
.poll-preview li::before { content: counter(opt); position: absolute; left: .8rem; top: 50%; transform: translateY(-50%); font-size: .72rem; color: var(--accent); font-weight: 700; }
.col-meta { grid-column: 1 / -1; margin-top: .1rem; }
.panel { background: #fff; border: 1px solid var(--line); border-radius: 10px; padding: .9rem 1rem; font-size: .8rem; }
.panel-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: .7rem; }
Expand All @@ -58,6 +63,7 @@
{{ with .Params.image }}{{ $visualRaw = . }}{{ end }}
{{ if not $visualRaw }}{{ with .Params.visual }}{{ $visualRaw = index (split . " ") 0 }}{{ end }}{{ end }}
{{ if not $visualRaw }}{{ $visualRaw = .Params.design_file }}{{ end }}
{{ if eq $visualRaw "none" }}{{ $visualRaw = "" }}{{ end }}
{{ $assetsDir := printf "%s/assets/" (path.Dir .File.Filename) }}
{{ $chunks := split .RawContent "\n---\n" }}
{{ $bodyRaw := trim (index $chunks 0) "\n \t" }}
Expand Down Expand Up @@ -96,12 +102,14 @@ <h1>{{ or .Title (humanize .File.BaseFileName) }}</h1>
<div class="col-img">
<div class="block">
{{ if $visualRaw }}<div class="btn-row"><button class="copy-btn icon-only" id="copy-img" title="Copy image">{{ $copyIcon | safeHTML }}<span></span></button></div>{{ end }}
<h2>Visual</h2>
<h2>{{ if eq .Params.design "poll" }}Poll{{ else }}Visual{{ end }}</h2>
{{ if $visualRaw }}
<a href="/linkedin-assets/{{ $laneDir }}/{{ path.Base $visualRaw }}" target="_blank" rel="noopener" title="Open full size in a new tab (drag the image to attach it)">
<img id="post-img" src="/linkedin-assets/{{ $laneDir }}/{{ path.Base $visualRaw }}" alt="post visual" onerror="this.style.display='none';document.getElementById('noimg').style.display='block'" draggable="true" />
</a>
<p id="noimg" class="no-img" style="display:none">Image not found on disk.</p>
{{ else if eq .Params.design "poll" }}
<div class="poll-preview">{{ $tail | markdownify }}</div>
{{ else }}
<p class="no-img">No visual set for this post.</p>
{{ end }}
Expand Down Expand Up @@ -135,7 +143,7 @@ <h3>Details</h3>
</div>
{{ with .Params.posted_url }}{{ if hasPrefix . "http" }}<div class="field"><div class="k">LinkedIn</div><div class="v"><a href="{{ . }}" target="_blank" rel="noopener">View on LinkedIn &#8599;</a></div></div>{{ end }}{{ end }}
{{ with .Params.notes }}<div class="field"><div class="k">notes</div><div class="v">{{ . | markdownify }}</div></div>{{ end }}
{{ with $tail }}<div class="field"><div class="k">authoring (comment / attachment)</div><div class="v">{{ . | markdownify }}</div></div>{{ end }}
{{ with $tail }}{{ if ne $.Params.design "poll" }}<div class="field"><div class="k">authoring (comment / attachment)</div><div class="v">{{ . | markdownify }}</div></div>{{ end }}{{ end }}
{{ with .Params.icp_test }}<div class="field"><div class="k">ICP test</div><div class="v">{{ . | markdownify }}</div></div>{{ end }}
{{ with .Params.dedup_note }}<div class="field"><div class="k">dedup</div><div class="v">{{ . | markdownify }}</div></div>{{ end }}
{{ with .Params.hypothesis }}<div class="field"><div class="k">hypothesis</div><div class="v">{{ . | markdownify }}</div></div>{{ end }}
Expand Down Expand Up @@ -176,6 +184,7 @@ <h3>Details</h3>
if (/^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName) || e.metaKey || e.ctrlKey || e.altKey) return;
if (e.key === 'ArrowRight') { var n = document.getElementById('nav-next'); if (n) window.location.href = n.href; }
if (e.key === 'ArrowLeft') { var p = document.getElementById('nav-prev'); if (p) window.location.href = p.href; }
if (e.key === 'ArrowUp') { var b = document.querySelector('a.back'); if (b) window.location.href = b.href; }
});
</script>
</body>
Expand Down
46 changes: 46 additions & 0 deletions linkedin-posts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,52 @@ body, but keep the source clean. `image` supersedes the older `visual` / `design
fields (still read as fallbacks). `posted_url` is the canonical property for the
live LinkedIn post URL — the board links to it once set.

## Image type by pillar (what image for what post)

Grounded in the creators Paul rates: **Cutler/Rossi** lead with *diagrams and
editorial illustrations*; **Holub** often runs *text-only* and lets the take carry;
**Paolino** shows *real artifacts / screenshots*. Mapped to our pillars:

| Pillar | Image | Why |
|---|---|---|
| **Teach** (framework/tactic) | house-style refactoring.fm **exhibit** (SVG, see above) | the framework made visual (Cutler/Rossi register) |
| **Reaction / opinion / story** (personal) | a **New Yorker-style caricature** (editorial cartoon) | humour + humanity; a data-exhibit here reads as marketing |
| **Build-in-public** | a caricature, or a **real screenshot** of the artifact | show the thing (Paolino register) |
| **Poll** | the **poll itself** (no image) | the poll IS the visual; single images underperform text in 2026 |

One image, landscape ~3:2. Never a stock photo, never a logo-slathered card. Text-only
is always a valid choice for a blunt take (Holub) - a mediocre image hurts more than none.

## Generating caricatures (the Art tool)

Personal/opinion/story/build-in-public posts get a hand-drawn editorial cartoon via
the `Art` skill (Nano Banana Pro / Gemini 3 Pro image). Needs a **billing-enabled**
Google key in `~/.claude/.env` (`GOOGLE_API_KEY` + `GEMINI_API_KEY`) - the free tier
has a hard image quota of 0.

1. **Prompt** - one scene that IS the post's point, in this template:
> Single-panel New Yorker-style editorial cartoon, hand-drawn black linework,
> light cream background, minimal colour accent, **absolutely NO text / lettering /
> captions**. Scene: [the gag that illustrates the post]. Gestural imperfect
> linework, editorial sophistication, warm and human, gently satirical, not cutesy.
2. **Generate** (lands in `~/Downloads` first):
```
bun ~/.claude/skills/Art/Tools/Generate.ts --workflow=Comics \
--model nano-banana-pro --size 2K --aspect-ratio 3:2 \
--prompt "..." --output "$HOME/Downloads/<slug>.jpg"
```
(Model returns JPEG - fine for a cartoon on cream.)
3. **View + gate** - open the file and score the 4 new-media criteria (great look /
functional at a glance / earns the scroll / helpful not decorative); re-roll if it
misses. The model sometimes adds a clean sign word ("MANAGER", "ERROR") despite the
no-text rule - keep only if it's correct and sharpens the gag, else re-roll.
4. **Wire in** - copy to `linkedin-posts/<lane>/assets/<slug>.jpg`, set
`image: "assets/<slug>.jpg"`. A NEW lane needs a dev-only mount in
`config/development/hugo.toml` (`linkedin-posts/<lane>/assets` ->
`static/linkedin-assets/<lane>`). Verify it serves 200 on the board.

**Never commit the key** - it lives in `~/.claude/.env` (untracked, verified out of the repo).

## Poll posts (canonical structure)

Native LinkedIn polls follow ONE structure (reference: `first-move-poll`,
Expand Down
78 changes: 57 additions & 21 deletions linkedin-posts/content-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,29 +22,65 @@ links the course. Decoupling the give from the ask is what makes the ask land.

Status legend: `draft` · `revised` (idea-first, ready) · `approved` · `scheduled` · `posted`.

## Next actions (2026-08-14 handoff) - STOP BUILDING, START SENDING

All LI work is on branch `linkedin-review-board` (**PR #455, not merged**). 19 posts
drafted, illustrated (caricatures + exhibits), reordered. But the active bet (Vibe
Code Rescue) is **RED: zero outreach touches ever sent** - the bottleneck is
distribution, not demand, and 3+ weeks of finished content is *activity, not
validation*. **Do NOT make more posts until the first batch is live.** In priority:

1. **Schedule the ready LinkedIn run** - pre-verify founding-hypothesis (approved) →
claude-code → vendors → vibe-coding, schedule >=24h out. Assistant drives the
composer; Paul approves + attaches images. Starts UTM data.
2. **Start the direct rescue outreach** (the real bottleneck, stuck ~18 days) - send
~10 personalized touches from the 2607 kit (`outbound-openers.md` / SEND-SHEET).
This is what books calls; it starts the kill-criteria clock.
3. **Resolve the 2 gating decisions** - pricing ($2.5-10K vs $25-50K) + category name
(page-one occupied, competitor on the exact name). They frame every outreach message.
4. **Instrument** - log ICP replies in `metrics-ledger.md` / the 2607 pipeline sheet.

**Open items:** a real "I was wrong" story to replace the deleted BigBinary
(story-bank Q2); merge PR #455 once Paul has reviewed the board; the caricature tool
now works (billing on; `GEMINI_API_KEY`/`GOOGLE_API_KEY` in `~/.claude/.env`,
untracked - Paul may rotate). Status snapshot: `docs/business/operating-system.md` §1
(RED) + the Aug-2026 investor update.

## Schedule (pillar-tagged)

| Week of | Slot | Pillar | Post | Link? | Status |
|---|---|---|---|---|---|
| 2026-08-11 | Thu | story/opinion | `course-promo/week1-thu-validate-before-build` | course | **posted** (text) |
| 2026-08-18 | Tue | opinion | `icp-validation/week1-mon-jira-not-progress` | none | **scheduled** ✓ demo-rule visual |
| 2026-08-18 | Thu | teach | `course-promo/week1-tue-founding-hypothesis` | course | **approved** ✓ five-blank exhibit |
| 2026-08-25 | Tue | reaction (NEW) | `personal/reaction-claude-code-trust` | none | **drafted** ✓ (Paul review) |
| 2026-08-25 | Thu | teach | `course-promo/week1-thu-price-before-product` | drop link? | revised ✓ |
| 2026-09-01 | Tue | opinion | `icp-validation/week1-tue-staging-question` | blog (SCIPAB) | revised ✓ |
| 2026-09-01 | Thu | build-in-public (NEW) | `personal/bip-vendors-still-burning-founders` | none | **drafted** ✓ (Paul review) |
| 2026-09-08 | Tue | poll | `icp-validation/week1-wed-trust-signals-poll` | none | revised ✓ |
| 2026-09-08 | Thu | story | `course-promo/backlog-40k-mvp-chasing-fixes` | course | draft |
| 2026-09-15 | Tue | story/mistakes (NEW) | *needs a better story* - BigBinary draft backlogged (Paul didn't like it); keep the "analysis vs conviction" angle | none | to source |

**Ratio check (this 9-post window):** ~6 value (teach/opinion/story/reaction/BIP),
1 poll, 2 course-linked -> ~70/20/10. The two NEW slots (reaction + build-in-public)
are what keep it from being all funnel.

Value drafts still to slot (mostly teach/opinion - **drop the reflexive course link**
on the 70%): friends-politely-lying, mom-test-past-questions, ten-interviews-recap,
why-i-wrote-it (story/BIP), first-move-poll & interview-count-poll (polls),
backlog-vibe-coding (opinion). All are idea-first and visual-ready.
Reordered 2026-08-14 (Paul): front-load pure-value/personal posts; space the course
links (~15%), the AI-tooling posts, and the repetitive clusters (progress-visibility:
jira/staging/trust-signals; interview: friends/mom-test/ten-interviews). The board
sorts by each post's `proposed_for`, so this table = the live board order.

| Date | Pillar | Post | Link? | Status |
|---|---|---|---|---|
| Aug 13 | story/opinion | `course-promo/week1-thu-validate-before-build` | course | **posted** |
| Aug 18 | opinion | `icp-validation/week1-mon-jira-not-progress` | none | **scheduled** ✓ |
| Aug 19 | teach | `course-promo/week1-tue-founding-hypothesis` | course *(promo)* | **approved** ✓ |
| Aug 25 | reaction | `personal/reaction-claude-code-trust` | none | drafted ✓ |
| Aug 27 | build-in-public | `personal/bip-vendors-still-burning-founders` | none | drafted ✓ |
| Sep 1 | reaction | `personal/reaction-vibe-coding-front-back` | none | drafted ✓ |
| Sep 3 | teach | `course-promo/week1-thu-price-before-product` | none | revised ✓ |
| Sep 8 | opinion | `icp-validation/week1-tue-staging-question` | blog | revised ✓ |
| Sep 10 | teach/opinion | `course-promo/week2-mon-friends-politely-lying` | none | revised ✓ |
| Sep 15 | poll | `course-promo/week1-wed-first-move-poll` | none | draft |
| Sep 17 | build-in-public | `personal/bip-build-your-own-tools-with-ai` | none | drafted ✓ |
| Sep 22 | teach | `course-promo/week2-tue-mom-test-past-questions` | none | revised ✓ |
| Sep 24 | story | `course-promo/backlog-40k-mvp-chasing-fixes` | course *(promo)* | draft |
| Sep 29 | poll | `icp-validation/week1-wed-trust-signals-poll` | none | revised ✓ |
| Oct 1 | teach | `course-promo/week2-fri-ten-interviews-recap` | none | revised ✓ |
| Oct 6 | opinion | `icp-validation/backlog-vibe-coding-disposable` | none | draft |
| Oct 8 | poll | `course-promo/week2-wed-interview-count-poll` | none | draft |
| Oct 13 | story/promo | `course-promo/week1-fri-why-i-wrote-it` | course *(promo)* | draft |
| Oct 15 | opinion | `personal/opinion-permission-to-fix` | none | drafted ✓ |

**Ratio:** across 18 posts, only **3 course links** (founding, 40k, why-i-wrote-it =
~15%) + 1 blog link; the rest is value/personal/poll. **Aug 25 → Sep 3 is four
straight no-link posts** - the run that makes the feed read as a person with takes.

**Still open:** a real "I was wrong about a founder/idea" story to replace the deleted
BigBinary draft (story-bank Q2). Slot it when Paul has a case.

## Per-post checklist

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading