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

52 KiB
Raw Blame History

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.shFailed: 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:134added Step-5 checklist line: - [ ] No corporate buzzwords (leverage, synergy, paradigm shift, thought leader, disruptive, value proposition, ecosystem, holistic approach).
  • assets/checklists/quality-scorecard.md:32Works in 140 charactersWorks in 110-140 characters (Notes Mobile thresholdMobile band (floor + ceiling)).
  • assets/checklists/quality-scorecard.md:114Hook works in 140 chars?Hook works in 110-140 chars?.
  • assets/checklists/quality-scorecard.md:132added 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.shPassed: 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 ✓ (:137agents/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:131added 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:60added 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:194added 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.shPassed: 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:28added 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:45added 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.shPassed: 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).

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.

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 linegrep -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:123extended 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.shPassed: 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:129extended 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.shPassed: 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.shPassed: 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.