linkedin-studio/docs/hardening/log.md
Kjell Tore Guttormsen 8a3e109b35 fix(linkedin-studio): S7 harden batch — format-aware length band on per-post quality check
S7 close-out (1 edit / 1 command-file · lint 81/0 · counts 29/19/26/6):
- batch.md:95 Step-3b Quick Quality Check: replace vague "Character count in range" with a format-aware band (standard 1,200-1,800 · quick 150-500 · carousel/video caption per that format) — the old line named no band, and the 3a structure (:88-92) sums to 960-1640, under the 1,200 standard floor, so a ~1,000-char standard post passed "in range" unflagged

Axis-c (length band) closed; axes a/b/d PASS. Class: hybrid guided/stateful + post-emitting per post (each of 3-5 drafts is a feed post). Buzzword (:98) + no-body-link (:97) already present — the gap S2-S6 found is already closed in batch. Wiring verified: trend-spotter + content-planner resolve; all 5 node helpers (queueUpcoming/queueAdd 8-arg/queueFormatSummary/generateIcalFromQueue/writeIcalFile) export with matching signatures. 3 deferrals (3a single-structure vs mixed-format promise · no per-post differentiation gate by design · weekly_goal-unset micro-fallback). Local-only per push policy.
2026-06-18 15:29:31 +02:00

711 lines
52 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# LinkedIn Studio — Command Hardening Log
> Per-command audit trail for the hardening phase (`docs/hardening/brief.md` +
> `plan.md`). One **anchored** entry per command surface. The operator's parallel
> live-testing cross-references every change here.
>
> **Entry contract (SC-A…SC-E).** Each entry begins with the UNIQUE anchored header
> `### /linkedin:<command-name> — <one-line intent>` (coverage greps `^### /linkedin:<name>`,
> never the bare word). Every entry carries: **INTENT** · **SIMULATE** (persona + the
> CONCRETE before-output + friction log) · **EVALUATE** (4 axes, with the per-type
> **mechanical predicate** — never "N/A → judgment") · **HARDEN** (surgical diff +
> concrete after-output, or "no edit — passes") · **VERIFY** (lint `Failed: 0` + counts
> + the failing axis now passes). The cold `/trekreview` reviewer adjudicates every
> hardened command's before/after — the author does not self-certify.
>
> **Mechanical-predicate classes:** *post-emitting* (hook 110140 · length band ·
> no body link · no banned buzzword · topic→5 pillars) · *routing* (every emitted
> target resolves to a real `commands/<x>.md`) · *analytics* (graceful-degradation
> present + saves/dwell honesty intact) · *guided/stateful* (primary promised artifact
> actually produced; promised `subagent_type` targets resolve).
>
> **Stopping rule (anti-gold-plating):** harden until every axis returns pass OR a
> recorded deferral; no NICE-only polish beyond axis-pass.
>
> **Method discipline (learned the hard way in the S1 calibration — three stumbles).**
> Write the HARDEN / after-output section **only from the applied + re-grepped diff**, and
> assert a gap **only after reading the actual file line**. Every number (char counts,
> edit counts) must come from a tool, not from memory. The S1 calibration produced three
> assert-before-verify errors — (1) a non-existent inline buzzword list in `quick`, (2) a
> non-existent "unused Task" in `quick`, (3) `onboarding`/`first-post` edits asserted as
> landed when the Edits had failed on wrong strings, plus a false "25 commands → stale"
> claim (the file already said 29). All were caught by failed Edits / empty greps / the
> git status, and corrected here. This is exactly the failure mode the independent
> `/trekreview` oracle exists to catch — verify-before-assert is part of the method, not optional.
---
## Session 1 — Method calibration (`quick`) + Start journey
> S1 status: calibration corrected + method locked (operator, 2026-05-31). onboarding /
> first-post / setup follow below under the same entry shape. Field-notes inbox:
> **absent** at S1 start → graceful no-op (SC-I).
### /linkedin:quick — 5-minute 3-line post from a topic, ≤1 question, clipboard-ready
**INTENT.** `quick` is the Create journey's speed path: a topic in → a publishable
150500-char post out in ~5 minutes, max one question, auto-copied to clipboard. It must
honor the content-quality bar mechanically (hook **110140**, length **150500**, no body
links, no banned buzzword, topic maps to one of the 5 pillars, exactly one CTA) and the
algorithm bar (hook decisive before the mobile fold; links to first comment not body;
native; a CTA that invites comments without manufactured engagement-bait). Its defining
constraint is *speed* — "skip interrogation, generate immediately."
**SIMULATE.**
- **Persona (ICP):** the author — 1048 followers, "Validation", 5 pillars; voice =
direct/technical/low-formality, NO/EN mix, short sentences, contrarian openings
(grounded in the state file + the gitignored `authentic-voice-samples.local.md` the
command reads at runtime). `recent_posts: []` → no rotation conflict. *Fresh-adopter
path (no voice samples) reasoned, not separately re-drafted: `quick` reads voice from
SKILL.md and falls back to defaults; the personalization score is hidden < 3 posts and
the voice guardian is suppressed < 5 samples — degrades cleanly, no block, no dead-end.*
- **Invocation:** `/linkedin:quick AI-kodeassistenter i offentlig sektor`
(pillars "AI-rådgivning i offentlig sektor" / "Claude Code / agentisk koding").
- **Walk:** topic from `$ARGUMENTS` (no question) → load voice + pillars → infer type
(HOT TAKE — contrarian) → 3-line formula → auto-CTA → quality check →
de-AI/differentiation gate (skip unless commodity) → clipboard → present.
- **CONCRETE before-output** (produced under `quick.md` **as written**, pre-fix):
> Alle vil ha AI-kodeassistenter i staten. Men hvem tar ansvaret når koden tar feil i
> produksjon?
>
> Verktøyet skriver koden på sekunder. Ansvaret for at den er riktig flyttes ikke — det
> blir ditt, raskere enn før.
>
> Hvordan kvalitetssikrer dere AI-generert kode hos dere?
*(Pillar: Claude Code / agentisk koding · type: HOT TAKE · **hook = 95 chars** (node-verified) ·
body has 0 links / 0 buzzwords.)*
- **Friction log:** the **95-char hook passes `quick.md`'s own checks** — Step 2 said
"Hook (under 140 characters)" and the Step 5 checklist asked "Hook works in 140 chars?",
**both upper-bound only** — yet it breaks the canonical **110** floor (the PreToolUse gate:
"under 110: wasting prime real estate"). Same one-sidedness on length ("Under 500?" vs
the 150500 band). The Step 5 checklist also had **no buzzword line**, and a quick post is
auto-copied to clipboard (no file Write), so the PreToolUse *file* gate may not fire — the
checklist is the load-bearing surface. No dead-ends.
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** One-question speed path, 3-line formula, 8 templates,
auto-CTA, clipboard, why-hook/reach tips, `/linkedin:post` upgrade path — delivers the promise.
- **(b) algorithm bar — PASS.** No body link (links → first comment); native; CTA invites
comments (comments > reactions) without bait; de-AI gate cites the confirmed low-substance
down-rank. Consistent with `references/algorithm-signals-reference.md`.
- **(c) MECHANICAL predicate (post-emitting) — GAP → fixed.** no body link ✓ · topic→pillar ✓
· **hook band ✗** (spec + checklist enforced ≤140, omitted the 110 floor; the 95-char
before-hook is the live proof) · **length band ✗** (checklist enforced <500, omitted the
150 floor) · **buzzword ✗** (no checklist line; clipboard path bypasses the file gate).
- **(d) agent-wiring + graceful degradation — PASS (verified).** `quick` **conditionally**
delegates to `differentiation-checker` via `Task` (`quick.md:151`,
`subagent_type: linkedin-studio:differentiation-checker`) — only when the take is commodity,
preserving the 5-minute promise by default. `Task` in `allowed-tools` is therefore *used*,
not vestigial. Fresh-adopter degradation works (defaults; guardian suppressed < 5 samples).
**HARDEN (surgical — 2 edits, axis-c only — both grep-confirmed landed).**
- `commands/quick.md:68``**Line 1: Hook (under 140 characters)**``**Line 1: Hook (110-140 characters)**`.
- `commands/quick.md` Step 5 checklist — hook check `"Hook works in 140 chars?"`
`"Hook in the 110-140 band (not just under 140)?"`; length check `"Under 500 characters?"`
`"In the 150-500 band (not just under 500)?"`; **added** `"No corporate buzzwords?"`; count
`**All 6 = Yes?**``**All 7 = Yes?**`.
- **Deferrals:** none (the earlier "incomplete buzzword list" and "unused Task" findings were
file-misreads, struck — see Method discipline).
- **CONCRETE after-output** (under the hardened spec — hook expanded into the 110140 band):
> Alle vil ha AI-kodeassistenter i staten. Få spør hvem som tar ansvaret når assistenten
> foreslår noe ingen kan forklare etterpå.
>
> Verktøyet skriver koden på sekunder. Ansvaret for at den er riktig flyttes ikke — det
> blir ditt, raskere enn før.
>
> Hvordan kvalitetssikrer dere AI-generert kode hos dere?
*(**hook = 127 chars** (node-verified, in band) · 0 body links · 0 buzzwords · same pillar.)*
**VERIFY.**
- `bash scripts/test-runner.sh``Failed: 0` + exit 0 + counts 29/19/25/6 unchanged
(re-run AFTER the real edits; recorded at the session gate).
- Before/after delta: axis (c) failing sub-checks (hook 110 floor, length 150 floor,
buzzword) now pass — the 95-char before-hook slips `quick.md`'s old checks; the hardened
checklist catches it and the after-hook is 127 chars, buzzword-clean. The two edits are the cause.
- Disposition: **HARDENED** (2 edits, axis-c) · 0 deferrals · axes a/b/d PASS.
---
### /linkedin:onboarding — zero→published first post as one guided wizard (Start front-door)
**INTENT.** Multi-phase wizard taking a brand-new user from zero to a published first post
as one cohesive flow (profile → personalization → first-post), so they don't navigate the
full command surface alone. Primary artifacts: a saved profile + a drafted first post (the
S15-B1 inline-draft). Guided/stateful; inlines rather than spawning subagents.
**SIMULATE.**
- **Persona (fresh adopter — the right persona here):** just installed, no profile, no voice
samples, `recent_posts: []`.
- **Invocation:** `/linkedin:onboarding`.
- **Walk:** Phase 0 already-onboarded check → Phase 1 profile/topic-relevance checklist →
Phase 2 personalization (voice + user profile, or defaults when < 3 posts) → Phase 3 first
post (3.1 topic → 3.2 3-line draft → 3.3 quality check → 3.4 present+clipboard → 3.5 record)
→ Phase 4 summary. **CONCRETE artifact produced:** a refined first-post inline draft (S15-B1
path — **spot-confirmed still delivers:** Phase 3.2 "Draft the post… Line 1/2/3", 3.4
"present and copy").
- **Friction log:** Phase 3.2 said "Line 1 — Hook (**under 140** chars)" and the Phase 3.3
check asked "Hook works in **140** chars?" — **both upper-bound only**, so a 95-char hook
would pass while breaking the 110 floor. (Phase 3.2 line 204 already states "150-500
characters" + "No external links in the post body", so length + links were **NOT** gaps —
only the hook floor.) The description + Phase-4 tip hardcode "29 commands" — **accurate
today, not stale** (an earlier "25 commands" claim was a misread, struck).
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** Cohesive zero→post wizard; S15-B1 inline draft holds (Phase 3).
- **(b) algorithm bar — GAP → fixed.** Phase 3 emitted a post but enforced only the hook upper
bound; now the full 110140 band (length 150500 + no-body-links were already present).
- **(c) MECHANICAL predicate (guided/stateful) — PASS; one post-emitting sub-check fixed.**
Primary artifact (first-post draft) produced ✓; the one-sided hook bound in Phase 3.2 + 3.3 closed.
- **(d) agent-wiring + graceful degradation — PASS.** No `Task` in `allowed-tools` — onboarding
inlines every step (Phase 2 "delegate to setup" = inline its logic; commands aren't subagents,
so no broken `subagent_type`). Built for the no-profile path; degrades cleanly.
**HARDEN (surgical — 2 edits, hook floor — both grep-confirmed landed).**
- `commands/onboarding.md:200``**Line 1 — Hook (under 140 chars):**``**Line 1 — Hook (110-140 chars):**`.
- `commands/onboarding.md:209` (Phase 3.3 check) — `Hook works in 140 chars?``Hook in the 110-140 band (not just under 140)?`.
- **Deferred (NICE-only, stopping rule):** the hardcoded "29 commands" (description + Phase-4
tip) is correct now; making it count-free is drift-proofing, not an axis fix → recorded, not edited.
**VERIFY.** lint `Failed: 0` + counts unchanged (recorded at gate); S15-B1 inline-draft
spot-confirmed; hook floor now enforced in Phase 3.2 + 3.3. Disposition: **HARDENED** (2 edits) ·
1 recorded deferral.
---
### /linkedin:first-post — zero→published in <10 min, maximum hand-holding
**INTENT.** First-post accelerator: from "never posted" to "just published" in <10 min,
breaking the blank-page barrier. Produces one published first post. Guided/stateful; inlines.
**SIMULATE.**
- **Persona (fresh adopter):** no profile (Step 2 offers a voice quick-setup or 5-question
calibration; proceeds either way — momentum over completeness).
- **Invocation:** `/linkedin:first-post`.
- **Walk:** Step 1 welcome → Step 2 voice setup (samples or 5 Qs; graceful if none) → Step 3
topic (5 angles) → Step 4 write (3-line formula) → Step 5 simplified quality check → Step 6
present + clipboard → Step 7 record → Step 8 first-hour engagement guidance.
- **CONCRETE before (the gap):** Step 4 "Line 1: Hook (**under 140** characters)" and the Step 5
check "Hook works in **140** chars?" were **both upper-bound only** — a 95-char hook would pass.
(Step 4 already states "Target: 150-500 characters" and "No external links in the post body",
so length + links were **NOT** gaps — only the hook floor.)
- **Friction log:** the hook bound was one-sided in Step 4 + Step 5; length + no-links already covered.
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** Delivers zero→published with hand-holding; first-hour guidance present.
- **(b) algorithm bar — PASS.** First-hour engagement window cited; no-body-links already in Step 4 tips.
- **(c) MECHANICAL predicate (post-emitting) — GAP → fixed.** Hook bound was upper-only in Step 4 +
Step 5; now the full 110140 band (length 150500 + no-body-links already present).
- **(d) agent-wiring + graceful degradation — PASS.** No `Task`; inlines; no-profile/no-samples
path graceful. Differentiation-checker deliberately skipped (a first post optimizes for momentum).
**HARDEN (surgical — 2 edits, hook floor — both grep-confirmed landed).**
- `commands/first-post.md:101``**Line 1: Hook (under 140 characters)**``**Line 1: Hook (110-140 characters)**`.
- `commands/first-post.md:125` (Step 5 check) — `Hook works in 140 chars?``Hook in the 110-140 band (not just under 140)?`.
- **Deferrals:** none.
**VERIFY.** lint `Failed: 0` + counts unchanged; Step 4 ↔ Step 5 hook bound now consistent (both 110-140).
Disposition: **HARDENED** (2 edits) · 0 deferrals.
---
### /linkedin:setup — guided personalization (5 pillars + voice profile + prefs)
**INTENT.** Build the user's voice profile, expertise pillars, and content preferences into
the state/asset files so every post sounds like them. Primary artifact: a populated voice
profile + populated asset templates (8-category personalization score). Guided/stateful;
delegates voice to an agent.
**SIMULATE.**
- **Persona (fresh adopter):** no existing data (all templates at placeholder).
- **Invocation:** `/linkedin:setup`.
- **Walk:** Step 0 calculate score → Step 1 dashboard → Step 2 choose what to set up → Step 3a3f
sub-workflows (voice / case study / framework / post analysis / demographics / user profile) →
Step 4 recalculate → Step 5 continue or exit. **CONCRETE artifact:** e.g. Step 3a writes a real
voice profile to `assets/voice-samples/authentic-voice-samples.md` (placeholder sentinel removed).
- **Friction log:** none — no dead-ends, no ambiguous steps.
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** Delivers the personalization the description promises (score + 6 sub-flows).
- **(b) algorithm bar — PASS (n/a-direct).** Emits no post; the expertise/pillar capture is the
topic-relevance foundation the bar depends on.
- **(c) MECHANICAL predicate (guided/stateful) — PASS.** Primary artifact (populated profile/assets)
produced ✓; the promised `subagent_type: linkedin-studio:voice-trainer` (`setup.md:86`) **resolves**
to a real `agents/voice-trainer.md` (verified).
- **(d) agent-wiring + graceful degradation — PASS.** voice-trainer invoked via `Task` (correct
namespaced type); the placeholder-sentinel logic is explicit; samples optional → degrades if none.
**HARDEN.** **No edit — passes all four axes.** (Demonstrates a legitimate zero-edit pass; per the
plan, "command file modified" is not a coverage predicate — the `log.md` entry + `/trekreview` are.)
**VERIFY.** lint `Failed: 0` + counts unchanged; voice-trainer target resolves.
Disposition: **PASS, no edit** · 0 deferrals.
---
## Session 2 — Create I (`post`)
> S2 status: the v1 autonomous attempt fabricated and was reverted (→ v2 method; see `plan.md`
> header). This is the v2 re-run: parked at step 5 across a `/clear`, re-grounded from STATE.md's
> two P4 findings against the actual file lines before talking (jerndisiplin: assert-after-read).
> Field-notes inbox **absent** at S2 → graceful no-op (SC-I).
### /linkedin:post — interactive full-workflow post (angle→write→quality→refine→publish), 12001800 chars
**INTENT.** `post` is the Create journey's full path: from raw material (data / article / experience /
observation / opinion / question) to a publishable 1,2001,800-char thought-leadership post via an
8-step workflow — angle → format → structure+write → quality check → present → refine → pre-publish
→ deterministic state update. Post-emitting, so it must honor the content bar mechanically (hook
**110140**, length **1,2001,800**, no body links, no banned buzzword, topic→5 pillars) and the
algorithm bar (hook decisive before the mobile fold, links→first comment, native, comment-inviting
CTA without bait, de-AI substance signals).
**SIMULATE.**
- **Persona (ICP):** the author — 1048 followers, "Validation", 5 pillars; direct/technical NO/EN voice.
- **Invocation:** `/linkedin:post about AI-kodeassistenter i offentlig sektor`.
- **Walk:** Step 0 load state/SKILL → Step 1 type=Opinion → Step 2 angle (contrarian) → Step 3 medium
text → Step 4 structure (hook 110140 ✓ internally) → **Step 5 quality check (the gap)** → Step 6
present+clipboard → Step 7 refine → Step 8 pre-publish + state.
- **CONCRETE friction (spec-level, the live proof — anchored, not a generated post):** at **Step 5**
the quality checklist (`post.md:127135`, 9 items) had **no buzzword line**, so a draft opening
*"How to leverage AI to drive synergy in the public sector…"* would pass Step 5 unflagged — the
banned-list (content rule #4, `CLAUDE.md:124`) was never enforced at the gate. Step 5 also routes to
`quality-scorecard.md` (`post.md:125`), which **self-contradicted on the hook bound**: l.32 "Works in
**140** characters" + l.114 "Hook works in **140** chars?" (upper-only) vs l.128 "**110-140**" — the
same one-sided bound S1 fixed in the command files, surviving in the shared asset.
- **Friction log:** `post.md` itself is internally consistent on the hook (110140 at l.100/113/127);
the only command-side gap is the missing Step-5 buzzword line. The shared scorecard carried both
gaps (buzzword absent + two one-sided hook bounds). Scorecard is shared by 8 surfaces
(`pipeline · video · post · react · batch` + 3 skills) — fixing it is high-leverage, operator-approved.
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** Full 8-step workflow delivers the description's promise.
- **(b) algorithm bar — PASS.** No body link (`post.md:133/176`); comment-inviting CTA; de-AI gate
cites the confirmed reach-suppression (`post.md:137141`). Consistent with `algorithm-signals-reference.md`.
- **(c) MECHANICAL predicate (post-emitting) — GAP → fixed.** hook band ✓ · length band ✓ · no-body-link
✓ · topic→pillars ✓ · subagent resolves ✓ · **buzzword ✗** (Step-5 checklist had no line; grep NONE).
Shared scorecard added two sub-gaps: hook one-sided at l.32 + l.114.
- **(d) agent-wiring + graceful degradation — PASS.** `differentiation-checker` (`post.md:141`,
conditional) + `content-optimizer` (`post.md:166`, conditional) both resolve, both correctly
`subagent_type: linkedin-studio:`-namespaced. Voice→defaults; fresh-adopter path graceful.
**HARDEN (surgical — 4 edits across 2 files, all grep-confirmed landed).**
- `commands/post.md:134`**added** Step-5 checklist line: `- [ ] No corporate buzzwords (leverage,
synergy, paradigm shift, thought leader, disruptive, value proposition, ecosystem, holistic approach)`.
- `assets/checklists/quality-scorecard.md:32` — `Works in 140 characters` → `Works in 110-140
characters` (Notes `Mobile threshold` → `Mobile band (floor + ceiling)`).
- `assets/checklists/quality-scorecard.md:114` — `Hook works in 140 chars?` → `Hook works in 110-140 chars?`.
- `assets/checklists/quality-scorecard.md:132` — **added** Pre-Publish Basic Checklist line:
`- [ ] No corporate buzzwords (…)` (same banned list).
- **Deferrals:** none. **Scope note:** the 3 scorecard edits improve all 8 consumers; only `post` is
hardened this session — `react/batch/video/pipeline` get their own EVALUATE in S3/S5/S6/S8.
**VERIFY.**
- `bash scripts/test-runner.sh` → `Passed: 81 · Failed: 0 · Warnings: 0`; counts **29/19/26/6** unchanged.
- Anti-check grep: no one-sided bare `140 char` hook bound remains in the scorecard (`110-140` at
l.32/114/128). Buzzword line present in both files (`post.md:134`, `quality-scorecard.md:132`).
- Before/after delta: axis (c) buzzword sub-check now enforced at the load-bearing Step-5 gate; the
scorecard hook self-contradiction is resolved (all three mentions banded). The 4 edits are the cause.
- Disposition: **HARDENED** (4 edits, axis-c + shared-asset consistency) · 0 deferrals · axes a/b/d PASS.
---
## Session 3 — Create I (`react`)
> S3 status: re-grounded STATE's S3 predicate against the actual `react.md` lines BEFORE talk
> (jerndisiplin: assert-after-read). Field-notes inbox **absent** at S3 → graceful no-op (SC-I).
> All `file:line` below are tool-grounded against the FINAL (post-edit) file.
### /linkedin:react — URL-to-post pipeline (single + comparison), your-take-as-hook, 1,2001,800 chars
**INTENT.** `react` is the Create journey's **URL→post path**: fetch external content
(article / news / research / YouTube), extract the key points, pick an angle, and draft in the
author's voice. Post-emitting (medium **1,2001,800**), two paths — single-URL (Steps 18) +
comparison/multi-URL (Steps 1b8b). Must honor the content bar mechanically (hook **110140**,
length **1,2001,800**, no body link, no banned buzzword, topic→5 pillars) and the algorithm bar
(URL→first comment not body, post stands alone, *your reaction is the hook* not a summary,
comment-inviting CTA, de-AI substance). **Distinct from `post`/`quick`:** the value-add is YOUR
perspective *extending* the source, not echoing it — the commodity-reaction guard (differentiation-checker).
**SIMULATE.**
- **Persona (ICP):** the author — 1048 followers, "Validation", 5 pillars; direct/technical NO/EN voice.
- **Invocation:** `/linkedin:react <public-sector-AI news URL>`.
- **Walk:** Step 0 load → Step 1 single URL → Step 2 WebFetch extract → Step 3 classify (Product
Launch) → Step 4 angle (First-Take Analysis) → Step 5 draft (hook 110140 ✓) → **Step 6 quality
check (the gap)** → Step 7 present+clipboard → Step 8 state.
- **CONCRETE friction (spec-level, anchored — not a generated post):** at **Step 6** the quality
checklist (pre-edit `react.md:122128`, 7 items) enforced hook/links/standalone/voice/charcount/
topic/CTA but had **no buzzword line**, so a draft opening *"This launch lets agencies leverage AI
to unlock synergy across the value proposition…"* passes unflagged — the banned-list (content rule
#4, `CLAUDE.md:124`) was never enforced at the gate. **Step 6b** (`:237` "Same as Step 6, plus")
inherits the hole. `react` auto-copies to clipboard (`:144148`, no file Write) → the PreToolUse
**file** gate may not fire → Step 6 is the load-bearing surface. **Same gap S2 fixed in `post`.**
- **Second friction (axis d, URL-pipeline-specific):** `grep` for fetch-failure/paywall/transcript/
fallback → **NONE**. Step 2 (`:54`) "Use WebFetch to extract" had **no fallback** when extraction
yields nothing (paywall / JS-heavy SPA / **YouTube-no-transcript** — an explicitly accepted input,
`:37/:71`); Step 2b (`:185`) same. Risk: proceed on a thin/empty extraction or fabricate from the URL string.
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** Both paths, angle selection, your-take-not-summary, commodity
guard, clipboard, URL→first comment — delivers the description's promise.
- **(b) algorithm bar — PASS.** No body link (`react.md:115/125/235`); URL→first comment
(`:116/:144/:253`); stands alone (`:118/:126`); take-as-hook (`:101/:118`); de-AI gate bans
mechanical engagement-bait (`:135137`). Consistent with `references/algorithm-signals-reference.md`.
- **(c) MECHANICAL predicate (post-emitting) — GAP → fixed.** hook band ✓ (`:101/:124/:221`) ·
length band ✓ (`:119/:128/:239`) · no-body-link ✓ (`:115/:125/:235`) · topic→pillars ✓ (`:129`) ·
subagent resolves ✓ (`:137` → `agents/differentiation-checker.md`) · **buzzword ✗** (grep NONE; no
inline line in Step 6/6b).
- **(d) agent-wiring + graceful degradation — wiring PASS; degradation GAP → fixed.** `differentiation-checker`
resolves, namespaced, conditional (`:137`). **Graceful degradation:** no WebFetch-failure/empty-extraction
fallback on EITHER path (grep NONE) — YouTube-no-transcript the sharpest, since it is a stated accepted input.
- **Shared asset:** `quality-scorecard.md` already clean post-S2 (`110-140` at `:32/:114/:128` +
buzzword `:132`) — `react`'s referenced asset (`react.md:270`) is good; the gap was `react`'s OWN inline Step 6.
**HARDEN (surgical — 3 edits, 1 file, all grep-confirmed landed).**
- `commands/react.md:131` — **added** Step-6 checklist line: `- [ ] No corporate buzzwords (leverage,
synergy, paradigm shift, thought leader, disruptive, value proposition, ecosystem, holistic approach)`.
Covers Step 6b via "Same as Step 6".
- `commands/react.md:60` — **added** Step-2 WebFetch fallback: *"If WebFetch can't extract usable
content (paywall / JS-heavy / YouTube-no-transcript): say so, ask the user to paste the key points
or choose another URL; never fabricate from the URL string or title."*
- `commands/react.md:194` — **added** Step-2b pointer: *"If a source can't be extracted: apply the
Step 2 fallback — drop it if 2+ usable sources remain, else ask the user; never fabricate."*
- **Deferrals:** none.
- **CONCRETE after:** the "leverage…synergy…value proposition" draft now trips the Step-6 buzzword
line; a YouTube-no-transcript invocation now degrades to "paste the key points / pick another URL"
instead of a fabricated reaction.
**VERIFY.**
- `bash scripts/test-runner.sh` → `Passed: 81 · Failed: 0 · Warnings: 0`; counts **29/19/26/6** unchanged
(`ls commands/*.md | wc -l` == 29; `git diff --stat` = `react.md` only, +5 insertions, 0 deletions).
- Before/after delta: axis (c) buzzword sub-check now enforced at the load-bearing Step-6 gate (both
paths via "Same as Step 6"); axis (d) WebFetch-failure now has a sensible fallback on both paths. The 3 edits are the cause.
- Disposition: **HARDENED** (3 edits, axis-c + axis-d) · 0 deferrals · axes a/b PASS.
---
## Session 4 — Create III (`multiplatform`)
> S4 status: re-grounded the proposed predicates against the actual `multiplatform.md` lines
> BEFORE talk (jerndisiplin: assert-after-read). Field-notes inbox **absent** at S4 → graceful
> no-op (SC-I). All `file:line` below are tool-grounded against the FINAL (post-edit) file.
> **Class call (operator-agreed):** NOT post-emitting — output is Twitter/slides/YouTube, so the
> LinkedIn content predicates (hook 110140, length band, no-body-link, topic→5 pillars) do **not**
> apply per platform; imposing them here would be the exact category error the Create-III brief warns
> against. Tested as **guided/cross-format with a routing sub-predicate**.
### /linkedin:multiplatform — adapt existing LinkedIn content to short-/cross-format (Twitter thread · slides · YouTube script)
**INTENT.** `multiplatform` is the Create journey's **cross-format adapter**: take existing LinkedIn
content and re-shape it for other platforms (Twitter/X thread, presentation slides, YouTube script) —
*write once, publish everywhere*. It explicitly does **not** own long-form (newsletters / blog / essays
→ `/linkedin:newsletter`, the single long-form entry point). It is an *adapter*, not a post-generator:
the value-add is adaptation fidelity (each platform's own rules) + correct routing of out-of-scope
(long-form) requests, not LinkedIn-feed quality gating.
**SIMULATE.**
- **Persona (ICP):** the author — a finished LinkedIn post in drafts, wants to reuse it as a Twitter thread.
- **Invocation:** `/linkedin:multiplatform` → pick "Twitter/X thread".
- **Walk:** Step 0 (`:21`) load source from `drafts/` + state → Step 1 (`:31`) AskUserQuestion pick
Twitter → Twitter template (`:53`, hook ≤**280** + 🧵 + one-idea tweets + CTA) → Step 2 (`:115`) save
`drafts/multiplatform/twitter-[slug].md` (`:118`) + clipboard, "Copied" (`:123`). **Promised artifact
produced.**
- **CONCRETE friction (axis d, adapter-specific — anchored, not a generated post):** `grep` for an
empty-source / fabrication guard in Step 0 → only matched l.23 "Ask the user to provide … or read
from drafts" — **no fallback** for the case where drafts is empty/absent, state has no recent posts,
AND the user pasted nothing. Risk: the model **fabricates a LinkedIn post to adapt** rather than
stopping. (Same failure class as S3's WebFetch-no-extraction gap.)
- **Second friction (axis c, voice hygiene):** the banned-buzzword list (content rule #4) is
*platform-agnostic voice hygiene*, not a char rule — and adaptation **rewrites** the source
(condense the hook to 280, expand to a script), which can introduce buzzwords absent from the
clean source. Pre-edit the file mentioned buzzwords **nowhere** (full read). Operator-judged in
scope (light line, not a full gate).
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** Three adapters (Twitter/slides/YouTube), source-loading,
save + clipboard, long-form correctly delegated — delivers the description's promise.
- **(b) algorithm/platform bar — PASS.** Per-platform rules correct, **no LinkedIn-rule leak**:
Twitter "280 chars max" (`:53`); **no** `110`/`140` hook band anywhere in the file (grep NONE —
the desired result for a non-LinkedIn surface). Long-form routing intact (see (c)).
- **(c) MECHANICAL predicate (routing sub-predicate) — PASS; voice-hygiene sub-gap fixed.** Every
emitted `/linkedin:newsletter` (`:6/:34/:36/:132`) resolves to a real `commands/newsletter.md`
(verified, ls) ✓; both reference files resolve — `references/linkedin-formats.md` (`:128`) +
`engagement-frameworks.md` (`:129`) (verified, ls) ✓. **Buzzword voice-hygiene line absent** →
added (`:45`).
- **(d) agent-wiring + graceful degradation — wiring N/A; degradation GAP → fixed.** No subagents
invoked (`grep subagent_type` → NONE — nothing to resolve). **Graceful degradation:** no
empty-source/fabrication guard in Step 0 (grep matched only the weak "ask … or read from drafts")
→ added a stop-and-ask-never-fabricate fallback (`:28`).
**HARDEN (surgical — 2 edits, 1 file, both grep-confirmed landed).**
- `commands/multiplatform.md:28` — **added** Step-0 empty-source fallback: *"If none of these yields
source content (empty/absent drafts, no recent posts, and nothing pasted), **stop and ask the user
to paste the LinkedIn post to adapt** — never fabricate source content to adapt."*
- `commands/multiplatform.md:45` — **added** an "Adaptation Templates" voice-hygiene blockquote:
*"adaptation rewrites the source — keep the adapted copy free of the banned corporate buzzwords
(content rule #4: leverage, synergy, paradigm shift, thought leader, disruptive, value proposition,
ecosystem, holistic approach) on every platform; don't let the rewrite introduce them."*
- **Deferrals:** none. **Scope note (flagged, not touched):** Step 2's "Copied to clipboard." (`:123`)
is unconditional, but `clipboard-helper.mjs` is shared, tested infra used identically by ~9 content
commands — re-gating it here would be scope-creep into a shared pattern (surgical discipline).
**VERIFY.**
- `bash scripts/test-runner.sh` → `Passed: 81 · Failed: 0 · Warnings: 0`; counts **29/19/26/6**
unchanged (`ls commands/*.md|wc -l` == 29, `ls agents/*.md|wc -l` == 19; `git diff --stat` =
`multiplatform.md` only, +9 insertions, 0 deletions).
- Before/after delta: axis (d) empty-source now degrades to "paste the post / never fabricate"
instead of an invented source; axis (c) the rewrite is now bound by the banned-buzzword list on
every platform. The 2 edits are the cause.
- Disposition: **HARDENED** (2 edits, axis-c + axis-d) · 0 deferrals · axes a/b PASS · class
confirmed non-post-emitting (no LinkedIn predicate imposed).
---
## Session 5 — Create II (`carousel`)
> S5 status: re-grounded the proposed predicates against the actual `carousel.md` lines BEFORE
> talk (jerndisiplin: assert-after-read). Field-notes inbox **absent** at S5 → graceful no-op (SC-I).
> All `file:line` below are tool-grounded against the FINAL (post-edit) file.
> **Class call (operator-agreed):** hybrid **guided/stateful + post-emitting on the caption** — the
> deck/images are the guided artifact, but the caption IS LinkedIn feed text, so the post predicates
> apply *to the caption* (not to the slides). Slides are bound by mobile-readability rules, not the
> feed bar.
### /linkedin:carousel — template→slide-by-slide deck + caption + optional mcp-image, full deck to clipboard
**INTENT.** `carousel` is the Create journey's **document-post path**: from a chosen template
(How-To / Listicle / Story / Comparison / Framework) + a topic, produce a publish-ready carousel —
structured slide-by-slide copy (header + body + visual note per slide) + a feed caption + optional
slide images (mcp-image), all assembled into ONE clipboard payload. Carousels/documents are the
top organic format (~7%; `references/algorithm-signals-reference.md`). Hybrid class: the deck is the
guided/stateful artifact; the **caption is post-emitting** and must honor the content bar mechanically
on the caption (hook **110140**, no banned buzzword, topic→5 pillars) + graceful mcp-image degradation.
**SIMULATE.**
- **Persona (ICP):** the author — 1048 followers, "Validation", 5 pillars; direct/technical NO/EN voice.
Builds a **How-To Guide** carousel on a pillar topic; tested both mcp-image-available and -unavailable.
- **Invocation:** `/linkedin:carousel` → pick template 1 (How-To) → topic + audience.
- **Walk:** Step 0 (`:24`) load state/voice/recent → Step 1 (`:30`) AskUserQuestion template → Step 2
(`:50`) topic+audience → Step 3 (`:58`) slide copy → Step 4 (`:94`) caption (hook 110140 `:97`, band
300500 `:103`) → **Step 5 quality + De-AI gate (the gap)** → Step 5.5 (`:127`) mcp-image (graceful)
→ Step 6 (`:156`) present full deck → clipboard one-payload (`:191213`) → Step 7 (`:218`) state.
- **CONCRETE friction (spec-level, anchored — not a generated deck):** at **Step 5** the De-AI /
Differentiation gate (pre-edit `:121123`) confirmed *personal substance · original thinking ·
concrete specifics · genuine voice* and banned mechanical engagement-bait, but had **no buzzword
line** — `grep -niE "buzzword|leverage|synergy|paradigm"` over the whole file = **NONE**. So a
caption opening *"How to leverage AI to drive synergy across the public-sector ecosystem…"* would
pass Step 5 unflagged; content rule #4 (`CLAUDE.md:124`) was never enforced at the caption gate.
Same failure class S2/S3 fixed in `post`/`react`. (Caption generation in Step 4 is exactly where a
rewrite can slip buzzwords in.)
- **No dead-ends:** all 5 referenced files exist (`agents/differentiation-checker.md`,
`assets/templates/carousel-templates.md`, `references/algorithm-signals-reference.md`,
`hooks/scripts/clipboard-helper.mjs`, `hooks/scripts/state-updater.mjs` — `[ -f ]` all FINNES).
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** Template→slides→caption→full-deck-clipboard-as-one-payload
(`:191213`, explicitly "the whole carousel, not just the caption") + state — delivers the promise.
- **(b) algorithm bar — PASS.** ~7% (`:37`) matches the ref exactly (`algorithm-signals-reference.md:55`,
"Documents / carousels | Top organic format (~7%…)"); "document post" framing correct (`:170`, LinkedIn
removed native carousels Dec 2023); caption hook 110140 (`:97`).
- **(c) MECHANICAL predicate (post-emitting, on the caption) — GAP → fixed.** hook band ✓ (`:97`) ·
caption band 300500 ✓ (`:103`) · topic→pillars ✓ (`:26/:56`) · subagent resolves ✓ (`:125`) ·
**buzzword ✗** (grep NONE; De-AI gate had no rule-#4 line). *no-body-link not imposed: a carousel
caption is not a typical link site and the command never asks for links — minor, deferred (matches
S2S4 which left it untouched).*
- **(d) agent-wiring + graceful degradation — PASS.** `differentiation-checker` resolves, namespaced,
conditional (`:125`, `subagent_type: linkedin-studio:differentiation-checker`). **mcp-image graceful
degradation solid:** "If mcp-image is unavailable or fails, skip… degrades to text-only" (`:129`),
per-call failure logged + continue, all-fail → text-only Step-6 design-guide branch (`:154`, `:176`).
`state-updater` exports `updatePostTracking` (`:55`) + `writeState` (`:346`) — Step-7 wiring correct.
**HARDEN (surgical — 1 edit, 1 file, grep-confirmed landed).**
- `commands/carousel.md:123` — **extended** the Step-5 De-AI gate with content rule #4:
`**Strip corporate buzzwords** from the caption (Content Quality Rule #4: leverage, synergy,
paradigm shift, thought leader, disruptive, value proposition, ecosystem, holistic approach) —
caption generation can slip them in, and they trip the same low-substance signal.`
- **Deferrals:** *no-body-link on the caption* (low risk, command never emits links; S2S4 precedent)
+ two cosmetic notes recorded, not edited (slide line-count 5 vs 7 between `:7377` and `:87`; both
are NICE-only, below the stopping-rule's axis-pass bar).
- **CONCRETE after:** the "leverage…synergy…ecosystem" caption now trips the Step-5 buzzword line
before the deck is presented.
**VERIFY.**
- `bash scripts/test-runner.sh` → `Passed: 81 · Failed: 0 · Warnings: 0`; counts **29/19/26/6**
unchanged (`ls commands/*.md|wc -l` == 29, `ls agents/*.md|wc -l` == 19; `git diff --stat` =
`carousel.md` only, 1 line changed). Only `.md` touched → hooks/analytics `node --test` not needed.
- Before/after delta: axis (c) buzzword sub-check now enforced at the load-bearing Step-5 caption gate.
The 1 edit is the cause.
- Disposition: **HARDENED** (1 edit, axis-c) · 2 recorded deferrals (no-body-link + cosmetic) · axes
a/b/d PASS.
---
## Session 6 — Create II (`video`)
> S6 status: re-grounded the proposed predicates against the actual `video.md` lines BEFORE
> talk (jerndisiplin: assert-after-read). Field-notes inbox **absent** at S6 → graceful no-op (SC-I).
> All `file:line` below are tool-grounded against the FINAL (post-edit) file.
> **Class call (operator-agreed):** hybrid **guided/stateful + post-emitting on the post caption** —
> the script / captions / thumbnail are the guided artifact (bound by video rules: muted-autoplay,
> 2.5 wps, completion-rate), but the **POST CAPTION (200400) IS LinkedIn feed text**, so the post
> predicates apply *to the caption* (not the script). **Hook 110140 deliberately NOT imposed on the
> caption (operator-agreed):** the caption accompanies the video; the primary hook discipline is the
> muted-autoplay opening (`:100`), not a standalone-post hook — imposing it would be S4's category error.
### /linkedin:video — type→length→angle→video-scripter script + captions + 200400 post caption + first comment
**INTENT.** `video` is the Create journey's **video-script path**: from type (talking-head / screen-rec /
slideshow / convert-a-text-post) + length (30/60/90/120 s) produce a production-ready script via the
`video-scripter` agent — timing markers, visual/energy cues, captions, thumbnail, a 200400-char post
caption, and a first comment — then save to drafts + deterministic state update. Hybrid class: the script
is the guided/stateful artifact; the **post caption is post-emitting** and must honor the content bar
mechanically on the caption (no banned buzzword, topic→5 pillars, no body link) + 2.5-wps pacing +
graceful degradation when voice samples are absent.
**SIMULATE.**
- **Persona (ICP):** the author — 1048 followers, "Validation", 5 pillars; direct/technical NO/EN voice.
Builds a 60-s talking-head on a pillar topic. **+ fresh-adopter path** (no voice samples) reasoned to
exercise graceful degradation (relevant here because of the pre-edit "REQUIRED before scripting").
- **Invocation:** `/linkedin:video` → talking-head → 60 s → pillar topic.
- **Walk:** Step 0 load state/voice/refs → Step 1 type → Step 2 length (2.5-wps budgets) → Step 3 angle
(topic→pillars) → Step 4 `video-scripter` generates → **Step 5 quality + De-AI gate (the gap)** →
Step 6 present + clipboard post caption → Step 7 refine → Step 8 save + state.
- **CONCRETE friction (spec-level, anchored — not a generated script):**
- **(axis c)** at **Step 5** the only buzzword line (`:106`, pre-edit) sat under the **"Natural Speech
Test"** = the *script's spoken language*; the **caption** De-AI gate (`:129`) carried **no rule-#4
line** (`grep buzzword` → single hit at `:106`). A caption opening *"How to leverage AI to drive
synergy across the public-sector ecosystem…"* would pass the caption gate unflagged. Same failure
class S2/S3/S5 fixed in `post`/`react`/`carousel` (the caption is re-shaped from a text idea in
Step 4 — exactly where a rewrite slips buzzwords in).
- **(axis d)** `:42` (pre-edit) "voice-samples … **(REQUIRED before scripting)**" + `:90` "Match voice
against voice-samples/" had **no fallback** for a fresh adopter with an empty/absent samples dir —
"REQUIRED" reads as a hard block / dead-end. Same failure class as S3 (WebFetch-no-extraction) / S4
(empty-source).
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** 8-step workflow delivers the description's promise (script + pacing +
cues + captions + thumbnail + 200400 caption + first comment + refine + save + state).
- **(b) algorithm bar — PASS.** Muted-autoplay correctly de-mythologized (`:100`, "three-second hook =
folklore; LinkedIn's only official 3 s = minimum length"); captions = the enforceable spec (`:120`);
60 s sweet spot; 2.5-wps budgets exact (`:64/:66`, 75/30 = 150/60 = 225/90 = 300/120 = 2.5). *Honesty
note (deferred, operator-agreed):* `:69` "LinkedIn requires 30 % completion or **zero distribution**"
is more absolute than the declared SSOT `algorithm-signals-reference.md:56` (no 30 %, confidence
*medium*); it is consistent with `linkedin-formats.md` (cited at `:39`, l.58). Inter-reference tension,
NOT a video-command gap — deferred (brief Non-Goal: no re-litigation of the bar).
- **(c) MECHANICAL predicate (post-emitting, on the caption) — GAP → fixed.** no-body-link ✓ (`:125`) ·
topic→pillars ✓ (`:124` + Step 3) · caption band 200400 ✓ (`:121`) · subagent resolves ✓ (`:83/:131`) ·
**buzzword ✗** (grep: single hit `:106`, under Natural Speech = script not caption; caption De-AI gate
had no rule-#4 line). **hook 110140 deliberately not imposed** on the accompanying caption (class
boundary, operator-agreed; primary hook = muted-autoplay `:100`).
- **(d) agent-wiring + graceful degradation — wiring PASS; degradation GAP → fixed.** `video-scripter`
(`:83`) + `differentiation-checker` (`:131`, conditional) both resolve to real `agents/*.md`, both
`subagent_type: linkedin-studio:`-namespaced. **Graceful degradation:** the "REQUIRED before scripting"
voice-samples dead-end closed → fresh-adopter fallback to SKILL.md/defaults. (mcp-image n/a — video
generates no images.)
**HARDEN (surgical — 2 edits, 1 file, both grep-confirmed landed).**
- `commands/video.md:129` — **extended** the Step-5 De-AI caption gate with Content Quality Rule #4:
*"**Strip corporate buzzwords** from the post caption (… leverage, synergy, paradigm shift, thought
leader, disruptive, value proposition, ecosystem, holistic approach) — re-shaping a text idea into a
video caption can slip them in, and they trip the same low-substance signal."*
- `commands/video.md:42 + :45` — softened "(REQUIRED before scripting)" → "(read before scripting)" and
**added** a Step-0 graceful-degradation line: *"if `voice-samples/` is empty or absent, don't block —
fall back to the voice settings in SKILL.md (or neutral defaults) … the voice-guardian is suppressed
under 5 samples, so there is no hard gate to satisfy."*
- **Deferrals (2, recorded):** (1) hook 110140 on the caption — deliberate class boundary, not a gap
(operator-agreed); (2) `:69` "zero distribution" honesty — inter-reference tension, brief Non-Goal (no
re-litigation of the algorithm bar); leave to a future reference-consistency pass if desired.
- **CONCRETE after:** the "leverage…synergy…ecosystem" caption now trips the Step-5 buzzword line before
the script is presented; a fresh adopter with no voice samples now degrades to SKILL.md/defaults instead
of hitting a "REQUIRED" dead-end.
**VERIFY.**
- `bash scripts/test-runner.sh` → `Passed: 81 · Failed: 0 · Warnings: 0`, exit 0; counts **29/19/26/6**
unchanged (`ls commands/*.md|wc -l` == 29, `ls agents/*.md|wc -l` == 19; `git diff --stat` =
`video.md` only, +4/2). Only `.md` touched → hooks/analytics `node --test` not needed.
- Before/after delta: axis (c) buzzword sub-check now enforced at the load-bearing Step-5 caption gate;
axis (d) the voice-samples dead-end now degrades gracefully. The 2 edits are the cause.
- Disposition: **HARDENED** (2 edits, axis-c + axis-d) · 2 recorded deferrals (caption-hook boundary +
:69 honesty) · axes a/b PASS.
---
## Session 7 — Create II (`batch`)
> S7 status: re-grounded the proposed predicates against the actual `batch.md` lines BEFORE
> talk (jerndisiplin: assert-after-read). Field-notes inbox **absent** at S7 → graceful no-op (SC-I).
> All `file:line` below are tool-grounded against the FINAL (post-edit) file.
> **Class call (operator-agreed):** hybrid **guided/stateful + post-emitting per post** — each of
> the 35 drafts IS a feed post, so the post predicates apply *per post*; the orchestration around
> them (state-load, agent-delegation, scheduling, queue, `.ics`) is the guided/stateful layer.
### /linkedin:batch — one theme/pillar → 35 mixed-format feed posts, scheduled + queued + .ics
**INTENT.** `batch` is the Create journey's **volume path**: from one theme or pillar produce a full
week (35 posts) with varied angles + formats, each scheduled, queued (so it shows in
`/linkedin:calendar` + session-start reminders), and exported to a `.ics`. It is an **orchestrator** —
delegates angle discovery to `trend-spotter`, the balanced batch plan to `content-planner`, and owns
scheduling/queue/ical itself. Post-emitting per post, so each draft must honor the content bar
mechanically (hook **110140**, format-appropriate length band, no body link, no banned buzzword,
topic→5 pillars); guided/stateful for the orchestration (subagent targets + node helpers resolve;
graceful degradation).
**SIMULATE.**
- **Persona (ICP):** the author — 1048 followers, "Validation", 5 pillars; direct/technical NO/EN voice.
"Sunday prep": one theme → a week of **mixed-format** posts (standard → carousel → quick → video rotation).
- **Invocation:** `/linkedin:batch` → "One main theme" → 35 angles.
- **Walk:** Step 0 (`:23`) load state/SKILL/plan/templates → Step 1 (`:33`) theme/pillar (+ `trend-spotter`
for timely angles) → Step 2 (`:45`) `content-planner` plan + schedule (slot template by `weekly_goal`,
queue-conflict check `:58`) → **Step 3 per post: 3a draft (`:86`) → 3b Quick Quality Check (`:94`, the
gap) → 3c save (`:101`) → 3d queueAdd (`:124`)** → Step 4 review (`:130`) → Step 5 finalize + `.ics` (`:172`).
- **CONCRETE friction (spec-level, anchored — not a generated batch):** at **Step 3b** the per-post check
`:95` read **"Character count in range"** — **no band named** (range of *what?*), so it is unenforceable;
and the Step 3a "standard structure" (`:8892`: hook 110140 + context 200300 + insight 400800 +
implication 200300 + CTA 50100) **sums to 9601640**, whose floor sits **below** the canonical standard
band **1,2001,800** (CLAUDE.md rule #2). A "standard" post drafted to the structure can land ~1,000 chars
and pass `:95` "in range" unflagged. Complication: batch is **mixed-format**, so the band is
format-dependent — imposing 1,2001,800 on everything would wrongly fail a quick post (the S4 category
error in miniature). **Buzzword + no-body-link were NOT gaps here** (`:98` + `:97` already present — the
hole S2S6 kept finding is already closed in `batch`).
**EVALUATE (4 axes).**
- **(a) intention fidelity — PASS.** Theme→plan→schedule→per-post draft/check/save/queue→review→finalize+
`.ics` delivers the week-of-content promise. *Tension (deferred):* Step 3a imposes one "standard structure"
though Step 2 (`:76`) promises format variety — `batch` is an orchestrator, so carousel/video inherit their
own formats' rules; recorded, not edited.
- **(b) algorithm bar — PASS.** No body link (`:97`); buzzword-clean (`:98`); pillar-aligned scheduling
(`scheduling-strategy.md`); 5x5x5 reminder (`:169`). `batch` uses a deliberately lighter "Quick Quality
Check" (no per-post `differentiation-checker`) — breadth-over-depth by design for a week-at-once tool; *note,
not a fix.*
- **(c) MECHANICAL predicate (post-emitting, per post) — GAP → fixed.** hook 110140 ✓ (`:88`) · no-body-link
✓ (`:97`) · buzzword ✓ (`:98`) · topic→pillars ✓ (`:39/:77/:112`) · subagent resolves ✓ (`:43/:47`) ·
**length band ✗** (`:95` "in range" named no band; 3a structure sums 9601640, under the 1,200 standard floor).
- **(d) agent-wiring + graceful degradation — wiring PASS; PASS.** `trend-spotter` (`:43`) + `content-planner`
(`:47`) both resolve to real `agents/*.md`, both `subagent_type: linkedin-studio:`-namespaced. All 5 node
helpers resolve with matching signatures: `queueUpcoming(days=7)` / `queueAdd(…8 params)` (batch passes 8 args)
/ `queueFormatSummary(entries)` (queue-manager.mjs `:52/:63/:112`), `generateIcalFromQueue` / `writeIcalFile`
(ical-generator.mjs `:183/:201`). Degradation: no-plan→`:31` "create one first"; empty queue→`:181` guard.
*Minor (deferred):* no fallback if `weekly_goal` is unset (`:52` slot-template lookup) — micro, below axis-pass.
**HARDEN (surgical — 1 edit, 1 file, grep-confirmed landed).**
- `commands/batch.md:95` — `- Character count in range` → `- Character count in the band for this post's
format (standard 1,2001,800 · quick 150500 · carousel/video caption per that format) — not just "in range"`.
- **Deferrals (3, recorded):** (1) Step 3a single-structure vs mixed-format promise (orchestrator inherits
per-format rules); (2) no per-post differentiation gate (deliberate breadth-over-depth design); (3)
`weekly_goal`-unset fallback (micro-degradation). All below the stopping-rule's axis-pass bar.
- **CONCRETE after:** a ~1,000-char "standard" draft now trips the format band (under the 1,200 floor) instead
of passing the vague "in range"; a quick post is judged against 150500, not wrongly against 1,2001,800.
**VERIFY.**
- `bash scripts/test-runner.sh` → `Passed: 81 · Failed: 0 · Warnings: 0`; counts **29/19** confirmed
(`ls commands/*.md|wc -l` == 29, `ls agents/*.md|wc -l` == 19; `git diff --stat` = `batch.md` only,
1 insertion / 1 deletion). Only `.md` touched → hooks/analytics `node --test` not needed.
- Anti-check grep: bare vague `^- Character count in range$` → NONE (gone); format-band line present (`:95`).
- Before/after delta: axis (c) length-band sub-check now enforced + format-aware at the load-bearing Step-3b
gate. The 1 edit is the cause.
- Disposition: **HARDENED** (1 edit, axis-c) · 3 recorded deferrals · axes a/b/d PASS.
---