Router hardening (routing/agent/grant axes all PASS). Operator terminology rule: the "thought leadership" phrase must not appear in the plugin. Re-sourced the 2 occurrences in commands/linkedin.md (:4 frontmatter description, :16 body) to "LinkedIn Studio". Routing 28/28 + agent-pointers 10/10 resolve; grant-hygiene clean (no Glob orphan — third front-door). Surfaced plugin-wide (~85 lines / ~45 files) and promoted to S29 terminology-scrub in docs/hardening/plan.md.
measure is the Measure-journey routing front-door (v4.1): guided "how am I
doing?" -> routes to the analytics command that owns the work; delegates only,
never crunches numbers. Routing axis is clean — all 5 emitted targets
(import/report/analyze/audit/ab-test) resolve to commands/Y.md. Delegates-only
holds: the 3 crunch/analysis mentions (:7/:20/:40) are all no-do promises, no
leak.
Grant-hygiene was the one finding, and it differs from S24 create. There Glob
was a pure orphan (no filesystem check at all). Here measure HAS an optional
Step 0 ("if .../analytics/ holds imported data, note last import") that gives
Glob a real home — but the grant<->use link was implicit (body named no tool),
and Read hung on the same vague Step 0. Operator chose: make Glob explicit,
drop Read as redundant (the import-date need is met by a Glob listing).
2 edits, commands/measure.md only:
- allowed-tools :11: drop Read; net = {Glob, AskUserQuestion}.
- Step 0 :23: "You may Glob .../analytics/; if it holds imported data, note last
import" — gives the retained Glob grant a named body home.
Verify: re-grep final — Read as tool name NONE, allowed-tools = Glob +
AskUserQuestion (:11-12), Glob now in body :23; routing unchanged (5/5);
git diff --stat = 1 file +1/-2; test-runner 81/0/0 exit 0; counts 29/19
unchanged (.md-only). FIXED, 0 deferrals. Scope note: Glob-orphan question now
resolved for measure (it had a home via Step 0; create did not); linkedin (S26)
still to check independently.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
create is the Create-journey routing front-door (v4.1): guided "what to make?"
-> routes to the command that owns the format; delegates only, never drafts.
Routing axis is clean — all 8 emitted targets (post/quick/react/carousel/video/
multiplatform/batch/newsletter) resolve to commands/Y.md. Delegates-only holds:
every "draft" mention is the no-draft promise (:7/:22, long-form lock :66) or a
target-description (:38), no leak. The one finding was grant-hygiene: Glob was
declared (:14) but never used — the only file access is a fixed-path Read of the
state file (:26), which needs no pattern-matching. Same orphan-grant pattern as
S17-S22.
1 edit, commands/create.md only:
- allowed-tools :14: drop orphan Glob; net = {Read, AskUserQuestion}.
Verify: re-grep final — Glob NONE, allowed-tools = Read + AskUserQuestion;
git diff --stat = 1 file -1; test-runner 81/0/0 exit 0; counts 29/19 unchanged
(.md-only). FIXED, 0 deferrals. Scope note: Glob-orphan may recur in sibling
front-doors (measure S25, linkedin S26) — not expanded; each catches its own.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
Profile is a self-contained, prose-heavy Grow-tier audit command (no agent, no
routing, no state). Grant-hygiene was already clean (Read + AskUserQuestion, both
used, no orphans/missing). The one real axis was b': the file repeatedly framed
the relevance model as reading and VALIDATING your profile and gating distribution
on it — contradicting the SSOT (algorithm-signals-reference.md), which confirms
only topic/interest relevance as a ranking input (content matched to a viewer's
interests, incl. beyond your network) with no off-topic magnitude figure and no
weighted profile criteria. The file was internally inconsistent — its own soft,
SSOT-true framing already sat at :17/:25/:27.
12 edits, commands/profile.md only:
- desc :5: drop "validates your profile BEFORE distributing content".
- :31 + table: drop "the model evaluates five criteria (see SSOT)" false
attribution + HIGH/MEDIUM "Impact if Missing" postulated weights -> "LinkedIn
does not publish a profile-scoring breakdown ... practitioner heuristics",
column -> "Priority (heuristic)".
- :99/:161/:173/:230: reframe "first signal telling ... qualified" / "relevance
validation" / "checks if you're connected" / "if LinkedIn's AI read my profile,
would it believe" away from the profile-read-by-model mechanic.
- :25/:27/:51/:67: conflation fix — "your demonstrated expertise" / "tell the
relevance model what you're expert in" -> "a viewer's interests" / "topic-
relevance distribution" (SSOT wording) + no-percentage honesty.
- :24 (B2): "Goes to 10% of audience" -> "a slice of your network".
- :52/:61 (P3): "highest-weight search field" -> "highest-leverage" (self-
justified rest kept).
Verify: re-grep final file — all overclaim/conflation patterns NONE, new SSOT
text in place; grant-hygiene unchanged; test-runner 81/0/0 exit 0; counts 29/19
unchanged (.md-only). FIXED, 0 deferrals.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
Outreach is the first command in the hardening queue that both delegates to an
agent (network-builder) and mutates real state (recordOutreachContact), so the
weight fell on axis-d. Five fixes across two files (operator-approved scope
extension to the SSOT-diverging guide source):
- allowed-tools: ADD Bash (Step 8c runs `node state-updater.mjs --record-outreach`
and needs it — firsthour precedent grants Bash for the same pattern; the command
promised a mutation it couldn't execute), DROP orphan WebFetch + Write grants
(zero body use; :410/:1040 "Write a post" is prose). Now
Read·Glob·WebSearch·AskUserQuestion·Task·Bash.
- Step 7 pod-warning (axis-b'): replaced fabricated magnitudes ("30-55% reach
reduction", "shadow suppression", "credibility score", "penalizes",
"Post-March 2025", "low dwell") with the SSOT statement
(algorithm-signals-reference.md:98 — demote out of "Most Relevant" / scope to
own network / restrict repeat offenders) + cite.
- :652 DM-amplification "2-3x first-hour" (axis-b'): re-attributed 2-3x to collab
reach (where SSOT places it), kept the first-hour effect qualitative.
- collaborations-guide.md:349-370 (axis-b', scope extension): same SSOT rewrite of
the "March 2025 Crackdown" section so the Step 7 fix can't be re-rendered from a
stale reference (dropped "shadow banning ALL future content / 90+ days / 6+
months / permanent penalty" + speculative detection methods).
Verify: re-grep both files clean (orphans/fabricated stats NONE, Bash :21, SSOT
text in place); recordOutreachContact throwaway-fixture 10/10 PASS (additive,
$-safe, newest-first, untouched undeclared fields); test-runner 81/0/0 exit 0;
counts 29/19 unchanged (.md-only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
S16 analyze through the v2 interactive quality gate (analytics/diagnostic class —
prose-diagnosis + conditional agent-delegation, NOT CLI-backed: allowed-tools
Read/AskUserQuestion/Task, no Bash). 1 finding, 1 surgical edit in
commands/analyze.md, grep-confirmed + grounded persona sim (scenario 1, reach -50%):
- FUNN (axis-b'): Step 4 penalty checklist asked 'Did you use 5+ hashtags in posts?'
— a concrete-number penalty absent from BOTH the SSOT
(algorithm-signals-reference.md: no hashtag claim, deliberately omitted) AND the
cited troubleshooting-guide.md (its own checklist is 6 items, no hashtag).
Command-only unsourced folklore. Dropped analyze.md:149; kept :150 'tag unrelated
people for reach' (defensible — aligns with SSOT bait/pod-suppression direction,
no numeric claim).
Verified clean: interpret-mode wiring (analytics-interpreter resolves, real default
mode agent:6,39,141), shared data-dir seam (analyze:41 <-> agent:49),
/linkedin:profile cross-ref, graceful-degradation IF, saves/dwell honesty (no Bash,
no over-promise). External-link hard-penalty framing in troubleshooting-guide.md:127
vs SSOT logged as reference-pass follow-up (out of S16 scope — not touched).
Gate: test-runner 81/0/0 exit 0; counts 29/19 unchanged (.md-only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
import.md repeatedly promised baseline comparison, rolling 4-week averages, and
baselines.json/metadata.json the importer "creates" — but no code anywhere writes
either file; detectAlerts flags intra-batch std-dev from the batch's own mean, not
a stored baseline. Step 5b's `cat baselines.json` was always empty, so the command
fell forever to "first import — baselines will be established" on every import.
Rewrote 5 sections (Step 5, 5b, 7, State Tracking, Reference Files) to the truth:
real CLI fields + YYYY-MM-DD-<shortid> filename, honest intra-batch surfacing,
cross-week analysis deferred to /linkedin:report. Verified with a grounded run
against a throwaway fixture.
Axes a/b/c/d all PASS post-fix; lint 81/0/0, counts 29/19 unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
Reschedule action was under-determined vs its sibling actions: prose-only with
no bash snippet, referencing snake_case queue_update_status/queue_add that don't
match the exports (queueUpdateStatus/queueAdd). Replaced with a concrete queueAdd
one-liner using the same id (re-add moves the post in place, no duplicate).
Axes a/b/c/d all PASS post-fix; lint 81/0/0, counts 29/19 unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
Axis-b: command attributed ~70% of reach to the first 60 min (desc + body);
SSOT algorithm-signals-reference.md:81 pins ~70% to the first 15-30 min
sub-window (golden window 60-90 min). The command even cited that SSOT, so it
contradicted its own source. Re-attributed to '15-30 minutes decide ~70%',
framing the hour as the golden window worked.
Axis-a: footer labelled engagement-frameworks.md as carrying CEA, but CEA lives
in the SSOT + engagement-coach. Dropped 'CEA' from the footer label.
Verified clean: both agents namespaced + frontmatter name/model/color match
CLAUDE.md; recordFirstHourPlan exported/additive/$-safe; delegation + hand-off
sound. test-runner.sh 81/0/0; counts 29/19/26 unchanged. FIXED, 0 deferrals.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
S11 hardening of /linkedin:pivot. Class: pure state-mutation/orchestration
command, NOT post-emitting, invokes no agents — re-opens a long-form edition so
cleared gates re-run before lock. Feed-post predicates N/A.
BUG (P4, cross-file off-by-one): newsletter's resumption table treats
currentPhase as the LAST COMPLETED phase and resumes at the NEXT step
(draft->Step 4, consistency-quality->Step 5, factcheck-sweep->Step 5.5). The
moderate pivot default set to-phase: factcheck-sweep "so the new claims get
verified" — which resumes at Step 5.5 and SKIPS the Step 5 fact-check the
moderate pivot exists to run, violating the command's own invariant that a pivot
always invalidates the fact-check.
FIX (4 surgical edits, commands/pivot.md only):
1. moderate default factcheck-sweep -> consistency-quality (resumes Step 5)
2. structural label "(Step 3b)" -> "Step 4 (consistency)"
3. off-by-one guard callout explaining to-phase = currentPhase (last-completed)
4. anatomy enum drops factcheck-sweep -> draft | consistency-quality
gatesToRerun list + worked example unchanged (both correct).
4-axis verdict: a/b/c/d all PASS post-fix, 0 deferrals -> FIXED.
Gate: test-runner 81/0/0 · counts 29/19/26 unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
Step-3 optimize gate (:79) carried the buzzword line but as a bare
'- [ ] No corporate buzzwords' — not self-contained, leaning on the
quality-scorecard.md reference rather than the inline gate the AI executes.
Enumerated the banned list inline to match every other post-emitting gate
(post :134 / react :131 / carousel :123 / video :129 / scorecard :132),
removing pipeline as the odd-one-out.
Recorded deferral: Step-2 component structure (:58-62) sums 960-1640, floor
below the Step-3 band floor 1,200 — identical to batch's untouched 3a; the
gate (:77) already names the band and catches a minimum-path undershoot, so
no edit (editing Step 2 would exceed S7 precedent).
Gate: test-runner 81/0/0 · counts 29/19 unchanged · only .md touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016qgzo6rxthw7KuxHjn5vyE
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.
B2 (router tiering) was already delivered in S14, so S15 = B1 + B3 only.
No surface/count/version change -> within-v4.1.0 refinement (S11-S13 precedent).
- B1 (commands/onboarding.md): replace the "Run /linkedin:first-post" dead-end
hand-off in Phase 3 with the first-post drafting steps embedded inline (3.1 topic
-> 3.2 3-line draft -> 3.3 QC -> 3.4 present+clipboard -> 3.5 state-update that sets
first_post_date). Wizard now yields a draft in-flow; 0 dead-end strings. Stays within
the existing allowed-tools (Read/Bash/AskUserQuestion); UI-brief §12b scope-guard
honored (no provider seams / progressive-disclosure added).
- B3 (commands/carousel.md): Step 6 now assembles the ENTIRE deck (every slide's copy
+ the caption) into the clipboard payload, not just the caption; full-deck assembly
precedes the clipboard-helper.mjs call.
Independent /trekreview (2 Opus reviewers): brief-conformance 0 findings; code-correctness
1 MAJOR that is PRE-EXISTING and out of S15 scope (onboarding Phase 2 saves need Write in
allowed-tools; lines 142/157, untouched by the S15 diff) -> DEFERRED to next session per
"ekte design-funn -> neste sesjon". Verdict ALLOW for the delivered scope (not a WARN-override).
Gate: test-runner.sh 74/0/0; node --test 98/98; commands=29; v4.1.0 unchanged.
See docs/remediation/review.md for the full record (ALLOW + 1 deferred MAJOR).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the S8 re-review (BLOCK 3/4/1). The S8 fix patched only the 2 strings S7 named; the re-review found 6 more same-class survivors. Per the systemic read, this is a comprehensive sweep, not a per-line patch.
Reconciled every retired engagement-coefficient + model-fact survivor against the canonical references/algorithm-signals-reference.md (order, not coefficients; comment ≈ 2x a like; no model name/params):
- glossary.md: coefficient table + Save-Signal '10x weight' → canonical ordering (citation now true)
- engagement-frameworks.md, analytics-interpreter.md, content-optimizer.md, pipeline.md, engagement-coach.md: the 10x/8x/7-9x/2.5x/0.2x system (incl. 4 survivors the re-review did not cite) → ordering
- playbook: '15x more algorithmic boost' + video '5x more conversations' → directional, sourced
- profile.md + linkedin-voice/SKILL.md: '150B parameter foundation model' → '2026 relevance-ranking model'
- quality-scorecard.md: '360Brew Validation' → topic-relevance framing
- setup.md: 'thought leadership plugin' → 'LinkedIn Studio plugin'
Lint (MAJOR 4): rebuilt scripts/test-runner.sh STALE_STATS to forbid EVERY retired-class phrasing (not the 2 S7 strings) + widened scope to assets/checklists/. Targets retired phrasings (7-9x, (10x), '10x weight', '5x more conversations'), NOT bare 10x/15x/5x (legit 5x5x5 / cadence / pixel-dims / '10x your reach' hyperbole). Proven non-vacuous: catches all 10 retired strings, ignores all 10 legit uses.
Tests (MAJOR 7): added no-anchor fall-through tests for recordFirstHourPlan + recordOutreachContact (date scalar not written/reported, section still appended). MINOR 8: reflowed newsletter.md content-repurposer wiring onto one line.
test-runner.sh 66/0/0; node --test 94/94 (was 92, +2). NO push until /trekreview re-clears the gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Close the 5 findings from the S7 /trekreview release gate (review.md, verdict BLOCK):
- BLOCKER: comment-multiplier "5x" reconciled to the canonical order-only framing
(no fixed multiplier) in agents/engagement-coach.md, linkedin-growth-playbook,
linkedin-formats.md — per algorithm-signals-reference.md ("do not quote a comment
multiplier").
- BLOCKER: carousel rate "6.60%/6.6% (highest)" reconciled to "~7% top organic
format" in linkedin-formats.md:42 (was self-contradicting :50) and
assets/templates/carousel-templates.md.
- Lint hardening: test-runner.sh STALE_STATS now matches 6.60% + the 5x comment
folklore and scans agents/ + assets/templates/ — the grep that defines the
Phase-0 criterion now catches both BLOCKERs.
- MAJOR: onboarding.md command count 26 -> 27.
- MAJOR: add section-append-branch (production-path) tests for recordFirstHourPlan
+ recordOutreachContact against a template-layout fixture.
- MINOR: move date-scalar changes.push inside the write branch in state-updater.mjs.
Verify: node --test hooks/scripts/__tests__/*.test.mjs -> 92/92; bash
scripts/test-runner.sh -> 66/0/0. NO push until /trekreview re-confirms ALLOW/WARN.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step 19 (Wave 4 S4). Adds an expectation banner at the very top of
/linkedin:newsletter: a multi-session, multi-gate process (16 phases),
~4-8+ hours across several sessions, state maintained between sessions, with
a pointer to the short-form commands for feed posts. Sets realistic
expectations before the user starts. Pure docs/content.
Verify: grep -niE 'multi-session|multi-gate|16 phases|~[0-9].*hour' near top
→ matches; test-runner.sh exit 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step 17 (Wave 4 S4) of the audit remediation. Applies research/03 §D5 + the
two S2 residual fixes folded in. No new commands/agents (counts stay 27/19).
Newsletter (commands/newsletter.md): new "Distribution channel" section after
Step 10 teaching the HONEST native-newsletter mechanics — bypasses organic feed
ranking via ONE deduplicated notification per subscriber per edition (NOT a
three-touchpoint blast); the mass invite fires once → ~1-2K follower floor
(wait until you can spend it); realistic cold-start 0-100 subs months 1-3;
discloses non-export / no-canonical / no-read-analytics / per-subscriber decay;
explicit below-vs-above-floor decision rule. Sourced to research/03 D5.
Profile (commands/profile.md): new "Profile SEO" section — headline as the
highest-weight search field + a per-section keyword-target table
(headline/about/experience/skills/featured), consistency-over-stuffing rule.
Outreach (commands/outreach.md): Step 8c persists the pipeline board to tracked
state via the new recordOutreachContact mutation (mirrors Step 16's
recordFirstHourPlan): additive last_outreach_date/outreach_active scalars + a
non-R-initial ## Outreach Pipeline section in state-updater.mjs +
config/state-file.template.md + --record-outreach CLI branch. +7 tests
(state-updater 26→33, full hook suite 83→90).
Residual 1 (growth-playbook:216): 9:16 "distribution boost" → 4:5/1:1 guidance
(9:16 mobile-only opt-in; "immersive distribution" = uncorroborated heuristic).
Residual 2 (video-strategy-guide:300): "3-second test determines 70% retention"
→ "front-load value for muted autoplay" (three-second hook is folklore, not a
LinkedIn signal).
Verify: grep checks 1-5 pass; test-runner.sh exit 0 (stat-consistency green);
state-updater 33/33. [skip-docs] — tre-doc + version bump deferred to Step 21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire orphan agent #11 (engagement-coach) by giving it a command surface, and
add the tracked first-hour state the plan calls for (remediation Step 16).
- commands/firsthour.md (new, 27th command): post-publish first-hour /
reply-loop sprint. Delegates plan construction to engagement-coach via
Task (subagent_type: linkedin-studio:engagement-coach) — returns a grouped
target list (whales/inner-circle/ICPs/new connections), 2-3 seed
self-comments + 3-5 CEA replies in the user's voice, and a minute-by-minute
timeline anchored to publish time. Presents timeline/targets/drafts +
velocity checkpoints, auto-copies the drafts to clipboard, persists the
plan, then hands off to post-feedback-monitor for the 48h window.
- hooks/scripts/state-updater.mjs: new pure mutation recordFirstHourPlan()
mirroring updatePostTracking — additive by contract (inserts
last_firsthour_date after last_post_date when absent, creates the
## First-Hour Plans section when absent, never touches existing fields).
Section name is deliberately non-R-initial so it stays outside
pruneContentHistory's "## Recent Posts ... (?=\n## [^R])" capture window.
+ a --record-firsthour CLI branch for parity with the other mutations.
- config/state-file.template.md: additive scalars (last_firsthour_date,
firsthour_active) + the ## First-Hour Plans section.
- hooks/scripts/__tests__/state-updater.test.mjs: extend (existing file) with
7 recordFirstHourPlan tests — section creation, field insertion vs in-place
update (no duplication), round-trip non-interference, graceful empty
defaults, changes array.
- CLAUDE.md: register the command (## Commands 26 -> 27, table row).
- scripts/test-runner.sh: EXPECT_COMMANDS 26 -> 27 (registration guard).
Verify: grep 'subagent_type: linkedin-studio:engagement-coach' commands/ ->
firsthour.md; node --test state-updater -> 26/26; full hook suite -> 83/83;
bash scripts/test-runner.sh -> exit 0 (62 passed, commands 27/27).
Plan Step 16 (Wave 4 S3).
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve the video-advice contradiction on the two surfaces this step owns
(research/03 D1-D3):
- commands/video.md: rename the "3-Second Test" to a "Muted-Autoplay Test"
(front-load value for ~85% muted viewing; the "three-second hook" is
cross-platform folklore, not a LinkedIn signal); make captions the
enforceable spec (SRT or native auto-captions, indexed for search);
aspect ratio as guidance (4:5/1:1 preferred for broad distribution,
9:16 opt-in for the vertical video tab, crops to 1:1 on desktop); add
MP4-default + warn-only MOV/AVI + official upload limits to the
pre-recording reminder.
- references/linkedin-formats.md: reframe the 9:16 "distribution boost"
as uncorroborated heuristic; 4:5 "deprioritized" -> 4:5/1:1 preferred;
drop the "3-second hook"; MP4 safe-default + warn-only MOV/AVI; replace
"good video rewarded more than ever" with the honest "per-video reach
declining; documents/carousels out-engage video."
algorithm-signals-reference.md (the canonical magnitude source) was
already reconciled in Phase 0 (line 56 reads "declining ... 4:5/1:1
preferred, captions are the enforceable spec") and carries no boost copy,
so it is intentionally untouched here — and it is not in this step's
manifest.
Verify: grep 'must be 9:16|9:16 (1080|3-second hook' video.md
linkedin-formats.md -> none; 'captions' video.md -> 5; 'deprioritized'
linkedin-formats.md -> none; structure lint 61/61.
KNOWN RESIDUAL (flagged for follow-up, NOT in this step's scope):
- references/linkedin-growth-playbook-2025-2026.md:216 still carries a
"9:16 ... distribution boost" line. That file is owned by Step 17, but
Step 17's changes are newsletter-distribution — the video line would
fall through. Fold this into Step 17 or the review gate.
- references/video-strategy-guide.md:300 still says "the 3-second test
determines 70% of retention." That file is owned by NO plan step
(orphaned) — needs a home. Surface at session end for an operator
decision.
Plan Step 15 (Wave 4 S2). Counts unchanged.
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wire the orphan differentiation-checker (#10) into the five short-form
creation commands (post/quick/react/carousel/video) as a De-AI /
Differentiation Gate at each command's quality-check step: confirm the
LinkedIn-named substance signals (personal substance, original thinking,
concrete specifics, genuine voice) + a soft engagement-bait check, and
delegate an originality pass to linkedin-studio:differentiation-checker
when the angle risks commodity content. Add Task to allowed-tools in
quick/react/carousel (post/video already had it from Step 13).
Extend (not duplicate) hooks/prompts/voice-guardian.md's AI-pattern
section with the same named signals from research/01 D8 + research/03 D4.
Runtime-loaded prompt — no compile-hooks.py, no hooks.json change
(verified: compile-hooks --check reports no drift).
Test: new hooks/scripts/__tests__/linkedin-content-filter.test.mjs pins
the content/non-content boundary the gate is scoped by (14 tests).
Full hook suite 76/76, structure lint 61/61.
Plan Step 14 (Wave 4 S2). Counts unchanged (26 commands / 19 agents).
[skip-docs]: tre-doc + version bump deferred to Step 21 per remediation plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolves the orphan-agent audit finding by the locked default: wire all, no deletions, so the agent count stays 19. Per agent, added Task to the target command's allowed-tools and a coherent 'subagent_type: linkedin-studio:<name>' delegation at a real point in the command's flow (not a token grep-match).
Wired (agents 1-9 of 11): video-scripter -> video.md (Step 4); content-optimizer -> post.md (Step 7 refinement) + ab-test.md (2a.4 optimized challenger); analytics-interpreter -> report.md (Step 7, report mode) + analyze.md (Step 2, interpret mode); content-planner -> batch.md (Step 2) + pipeline.md (Step 1); trend-spotter -> batch.md (Step 1) + pipeline.md (Step 1); network-builder -> outreach.md (Step 3a); strategy-advisor -> strategy.md (Step 3); voice-trainer -> setup.md (Step 3a); post-feedback-monitor -> calendar.md (publish action, 48h monitor).
Deferred to their dedicated steps: #10 differentiation-checker -> Step 14 (short-form de-AI gate), #11 engagement-coach -> Step 16 (first-hour command). Namespaced subagent_type form requires a session reload before the wired agents are invokable.
Verify: each of the 9 has >=1 invocation in commands/; structural lint 61/61 (counts 19/26/25/6 intact); agent-fixtures 35/35; hook tests 62/62. Three-doc + version reconciliation deferred to Step 21 per the locked plan [skip-docs].
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 3 / Step 12 of the remediation plan (Phase 1 — usable by a non-author).
Fix the discoverability defects the audit flagged:
- skills/linkedin-studio/SKILL.md (the auto-activating router): self-naming 'the
LinkedIn thought leadership plugin' -> 'LinkedIn Studio' (v3.0 rename leftover);
the 'All Agents' table corrected from 14 to the real 19 (added editorial-reviewer,
voice-scrubber, content-reviewer, language-reviewer, fact-reviewer); the 'All
Commands' table completed to 26 (added headless-review, pivot, carousel) so it
routes to newsletter/headless-review/pivot/react.
- commands/onboarding.md: '25 commands' -> '26 commands' (x2); pillar count '3-5
expertise areas' -> '5 expertise areas' (reconciles onboarding's 3-5 with
setup.md's '5 core topics' and the CLAUDE.md '5 core expertise areas' rule).
- commands/linkedin.md: router table + numbered option list gain headless-review
and pivot.
Scope note (operator decision, this session): the plan's Verify grep
'grep -rni "thought leadership" skills/ -> no matches' is broader than the brief's
actual criterion (which targets only the router skill's self-naming). 'thought
leadership' is a plugin-wide DOMAIN term — the '8 Thought Leadership Angles'
framework lives in references/thought-leadership-angles.md and is referenced by ~40
files (glossary, agents, post/video/batch commands). Renaming it only inside skills/
would create cross-file inconsistency; renaming it plugin-wide is a separate
vocabulary migration outside Step 12's discoverability scope. Per operator choice,
the router SELF-NAMING is fixed (brief criterion met) and the 4 remaining skills/
hits (linkedin-content-creation headings + one linkedin-strategy phase cell) are
legitimate domain usage kept consistent with the rest of the tree.
setup.md needed no edit: its pillar number was already '5'; reconciliation was a
one-sided fix in onboarding.
Verify: router no longer says 'thought leadership plugin'; grep -nc 'headless-review'
commands/linkedin.md -> 2; onboarding pillar count '5' matches setup.md; SKILL.md
agent table 19 rows, command table 26 rows; structural lint exit 0 (61 passed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 3 / Step 10 of the remediation plan (Phase 1 — usable by a non-author), per
research/02.
State what the plugin can and cannot do for a PERSONAL profile, dated 'as of
2026-05', without replacing one false claim with another:
- README.md: new 'Boundaries (as of 2026-05)' section. (a) post-level analytics API
EXISTS but is partner-gated (vetted Community Management app + verified org +
Page) — not self-serve; CSV is the practical floor; saves visible natively
(count-only, ~Sept 2025) with no self-serve API pull. (b) auto-publish is
technically POSSIBLE self-serve (w_member_social) but deliberately NOT built — a
design + ToS choice, explicitly 'not an impossibility' (avoids the new false
'cannot auto-publish' claim). (c) dwell internal-only for organic, not exportable.
- commands/calendar.md: the publish action now states plainly it marks a post YOU
posted manually as published — the tool does not post on your behalf.
- commands/report.md: dated the saves/no-self-serve-API note ('as of 2026-05').
- commands/import.md: dated 'Why CSV' note — API is partner-gated, CSV is the floor.
Verify: grep -niE 'cannot auto-publish|only way to (get|access)' README.md
commands/*.md -> no matches; grep -ni 'as of 2026' README.md -> 2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 3 / Step 9 of the remediation plan (Phase 1 — usable by a non-author).
The long-form review layer shipped Norwegian-locked: language-reviewer graded
unconditional Norwegian, voice-scrubber's gold standard was 'approved Norwegian
editions', and the editorial/content craft gates pointed at a 'skrivekontrakt §C2'
that does not ship. A non-Norwegian adopter would get English prose graded against
Norwegian idiom and a gate that depends on an unshipped contract.
- config/edition-state.template.json: add additive 'language' field (top-level,
default 'en') + a _doc entry. Threads into the language-dependent agents.
- agents/language-reviewer.md: new 'Language parameter' section — Norwegian-specific
checks (anglicism->Norwegian idiom, kanselli-stil) apply only when language=='no';
any other value grades that language's equivalents and never flags idiomatic
English as an anglicism. Default 'en'.
- agents/voice-scrubber.md: gold standard reframed to 'approved editions in the
configured language'; the Norwegian-chronicle calibration is the language=='no'
instantiation.
- agents/editorial-reviewer.md + agents/content-reviewer.md: the in-tree checklist
is now the operative, self-contained source of truth; Maskinrommet §C2 is an
optional upstream contract that does NOT ship (available only on the author's
runs). The gates work for an adopter without it.
- commands/newsletter.md: thread 'language' through the Step 6.5 cold-inputs and the
per-reviewer call inputs; the writing contract is now 'if it ships'.
Norwegian remains fully working when language: no (the author's case).
fact-reviewer.md was in the plan's file list but needed no change on inspection:
its F1-F4 checks (claims/quotes/numbers/sources) are language-agnostic; its
'Norwegian' mentions are boundary notes vs language-reviewer, which stay correct.
[skip-docs]: three-doc + version reconciliation is Step 21 (pre-review-gate); these
intermediate Wave commits are not pushed before the /trekreview gate.
Verify: edition-state JSON parses + has top-level language 'en'; language-reviewer
has 'language ==' references and no unconditional-Norwegian assertion; editorial
§C2 reframed to in-tree fallback ('operative source', 'does not ship'); agent
fixtures 35/35 pass; structural lint exit 0 (61 passed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 3 / Step 8 of the remediation plan (Phase 1 — usable by a non-author).
The flagship long-form engine shipped bespoke-as-general: a maintainer-private
absolute series path and a hardcoded 'Maskinrommet' brand in the renderers. Both
are now generalized so a non-author can run the pipeline without inheriting the
author's filesystem or publication identity.
- commands/newsletter.md + config/edition-state.template.json: series-root default
/Users/ktg/repos/maskinrommet/serier -> $HOME/linkedin-series; reconciled the
prose that called the maskinrommet folder 'the default' so it no longer
contradicts the new neutral default. LTL_SERIES_ROOT override contract preserved.
- render/build-linkedin.mjs + render/build-carousel.mjs: brand is now an LTL_BRAND
env-var (empty default -> generic). Samle-post title, carousel footer brand-span,
and cover-eyebrow fallback are de-branded; empty brand renders clean chrome. The
operator sets LTL_BRAND=Maskinrommet in their own env to re-brand (same pattern
as LTL_SERIES_ROOT).
- config/image-credit-caption.template.md: 'Maskinrommet-/serie-badge' -> generic
'serie-badge'.
Out of scope (Step 9): the residual 'Maskinrommet skrivekontrakt §C2/§A'
references in newsletter.md are the writing-contract generalization, handled next.
[skip-docs]: three-doc + version reconciliation is Step 21 (pre-review-gate, per
plan: 'LAST so it captures everything'). These intermediate Wave commits are NOT
pushed before the /trekreview gate, so the three-doc obligation (which governs
pushed changes) is satisfied at Step 21, not per local checkpoint commit.
Verify: grep -rIn '/Users/ktg' config/ commands/ render/ (excl .local) -> no
matches; grep -rn 'Maskinrommet' render/ -> no matches (de-branded); node --check
on both render scripts -> OK; LTL_SERIES_ROOT still present in newsletter.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 2 / Step 7 of the remediation plan (research/02 D2/D4).
The tool parses the LinkedIn analytics CSV export, which has no saves/dwell — and
there is no self-serve API to pull them. Stop implying it tracks them:
- report.md: replace "Saves (10x weight) and expert comments (7-9x) are the
highest-impact signals" with honest wording — saves rank highest in the
engagement ORDER (cite references/algorithm-signals-reference.md, not a restated
coefficient), but are visible only in native LinkedIn post analytics (count-only,
~Sept 2025+) with no self-serve API, so this tool does not auto-track them; dwell
is internal to LinkedIn for organic posts.
- types.ts: document why PostMetrics intentionally omits saves/dwell (no ingest
source) so a future contributor does not "add the missing fields".
- strategy.md: reconcile two saves references (signature-content criterion +
authority scorecard) to say the count is read from native LinkedIn analytics, not
captured by this tool.
No new metric field, no manual-entry feature (operator Q3).
Verify: report.md has no "Saves (10x"/"highest-impact signals" and does say "no
self-serve API"; tsc --noEmit clean; analytics suite 106/106; structural lint
0 failed (report.md cites the reference, so stat-consistency stays green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 2 / Step 6 of the remediation plan.
Organic personal-post A/B tests gather a handful of posts per variant — far below
the volume a significance test needs — so the tool must not imply statistical
significance:
- Rename the results-table "Significant?" column to "Directional?" and define it
as "clears the ~20% minimum-meaningful-difference AND points the same way across
most posts" — a direction to test further, not a significant result.
- Reword the "20% significance rule" to a minimum-meaningful-difference effect-size
heuristic (explicitly NOT statistical significance).
- Replace the "3 = Medium, 5+ = High" confidence ladder with a directional-only
confidence section: treat every result as directional (not significant) given
realistic volume is well under ~50 conversions/variant; name a direction, not a
winner.
The 20% minimum-meaningful-difference threshold itself stays — it is a legitimate
effect-size heuristic; only the significance framing was the false claim.
Verify: no "Significant?"/"20% significance" remain; "directional" present;
structural lint 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 2 / Step 5 of the remediation plan (coupled criticals: voice-leak +
placeholder-detection).
Voice profile (the adopter-default leak):
- Ship a PII-free placeholder at authentic-voice-samples.md carrying a
<!-- VOICE_PLACEHOLDER --> sentinel + neutral default voice principles.
- Migrate the author's real profile to gitignored authentic-voice-samples.local.md
(already matched by *.local.md; added an explicit, commented .gitignore entry so
the intent is unmissable). NO git-history rewrite — the historical file is
attributed authorship, not a secret (per the plan threat model).
- Add authentic-voice-samples.template.md — a clean fill-in template for adopters.
- personalization-score.mjs: detect the sentinel (deterministic) instead of the
unreliable `[Your Name]` heuristic, so the placeholder scores 0 voice points and
a populated profile (sentinel removed) earns the 25.
- Both voice writers replace-not-append on the placeholder: setup.md (merge ->
replace-if-placeholder) and onboarding.md (append -> replace-if-placeholder), so
populating removes the sentinel; updated setup.md's stale heuristic table.
Operator decisions (deviations from plan-literal, approved this session):
- KEEP the plugin.json author name. The plan said scrub author -> neutral/org, but
that contradicts its own LICENSE reasoning (intentional MIT attribution) and all
5 sibling plugins keep author = the author; scrubbing only this one would create
inconsistency for zero security gain (the name is public-by-design). The voice
placeholder fully fixes the adopter-inheritance bug.
- Scrub the stale "January 2026 360Brew" brand from the plugin.json description and
the "360brew" keyword (locked decision: no publishable model name/date). This is
a Wave-1 propagation miss surfaced here because plugin.json was in Step 5's
touch-scope.
Flagged for follow-up (NOT done here — out of Session 2 scope):
- The lint's stat-consistency grep (scripts/test-runner.sh) scans references/,
commands/, skills/, hooks/prompts/, CLAUDE.md, README.md — but NOT
.claude-plugin/plugin.json, which is why the 360Brew brand slipped Wave 1.
Needs a Session-1-scoped lint extension to add plugin.json to the scan set.
- Readers (user-prompt-context.mjs, voice-guardian.md, state-update-reminder.md)
read the tracked .md (placeholder), per the plan. The operator's real voice now
lives in the gitignored .local.md, which nothing reads. To use it, readers + the
voice score should prefer .local.md (matching the user-profile.local.md
precedent). Deferred as a coherence follow-up for operator review.
Test-first: hooks/scripts/__tests__/personalization-score.test.mjs (red on the
placeholder scoring 25 under the old heuristic, green after the sentinel fix). Hook
suite 62/62, structural lint 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wave 2 / Step 4 of the remediation plan (docs/remediation/plan.md).
PRIMARY (the real fresh-clone failure):
- scripts/analytics/node_modules is gitignored, so a fresh clone has neither
tsx nor csv-parse. Surface an idempotent `npm install --silent` prerequisite
at point-of-use in report.md (Step 1b) and import.md (Step 4).
DEVIATION FROM PLAN (correction-in-scope, to satisfy the plan's own Verify gate):
- The plan assumed prepending `npm install` was sufficient. Verified it is NOT:
the commands invoke the CLI with an absolute src/cli.ts path but from the
user's arbitrary CWD, and `node --import tsx` resolves the `tsx` specifier
relative to CWD, not the script. There is no global tsx, so the call still
fails with ERR_MODULE_NOT_FOUND from any CWD other than scripts/analytics.
- Complete fix: invoke the locally-installed tsx by its absolute
node_modules/.bin/tsx path in all CLI calls (report.md x10, import.md x3), so
they resolve from any working directory once the install above has run.
Verified: 0 ERR_MODULE_NOT_FOUND running `report` from /tmp.
SECONDARY (latent correctness / hardening):
- Add findPluginRoot(): walks up to the dir holding .claude-plugin/plugin.json
and anchors getAnalyticsRoot() on it, falling back to the legacy 4-up count.
MEASURED that ../../../../ already resolved to the plugin root from BOTH
src/utils and build/utils (both 4 levels deep), so the plan's "src-vs-build
depth miscalibration" premise was false — this is correct-by-construction
hardening (survives a future source move), not a live-bug fix.
- Reconcile cli.ts usage/help text: `node build/cli.js` -> `node --import tsx
src/cli.ts` (the real runtime).
- Fix report.md troubleshooting: "Verify tsx is available" -> the actual
install command on ERR_MODULE_NOT_FOUND.
Test-first: scripts/analytics/tests/storage-root.test.ts (red on missing
findPluginRoot export, green after). Full suite 106/106, tsc --noEmit clean,
structural lint 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cold, adversarial review package for the long-form pipeline + configurable
per-edition personas. Motivated by Del 4 (Security Champions pivot): the
in-session editor + persona sweep shared the drafting session's framing-bias,
so the shipped version was never independently re-reviewed.
Headless package (9a/9b):
- New Step 6.5 (headless-review) in /linkedin:newsletter, after the persona
sweep, before lock — the independence layer the in-session gates can't be.
- New standalone /linkedin:headless-review command (run in a fresh session for
maximum isolation; reconstructs frozen draft + contract + personas from disk).
- 3 new Opus archetypes, each with a cardinal context-isolation block that
refuses drafting-session framing as "context pollution":
- content-reviewer (argument integrity C1–C5, ≤8 flags)
- language-reviewer (Norwegian language L1–L5, ≤10 flags)
- fact-reviewer (cold re-verification F1–F4, risk-sort + pivot-risk, WebSearch)
- Deliberate redundancy with fact-checker / editorial-reviewer documented so
the pairs are never de-duplicated.
Pivot-reopen (9c):
- New /linkedin:pivot command: logs articles.NN.pivots[], resets currentPhase,
un-locks, marks gates to re-run.
- Pivot-detection gate in Step 8 lock precondition (>20% word-count change or
>2 new sections re-opens cleared gates). Del 4 v8→v11 worked example.
Per-artifact personas (new requirement):
- articles.NN.personas with resolution order (edition-state → series file →
plugin library → interactive). One or more readers configurable per edition.
Schema/docs:
- edition-state.template.json: additive personas[], pivots[], headlessReview,
headless-review phase (16 phases); personaSweep.resonance.wordCount baseline.
- 3 fasit fixtures + 3 structural lint tests (Del 4 worked cases).
- Counts: 24→26 commands, 16→19 agents, 15→16 newsletter phases.
- README + CLAUDE.md (plugin + root) + CHANGELOG synced.
Verification: 35 agent-fixture + 59 hook + 20 render tests green. Backward-
compatible (additive state); reload required before the 3 new agents resolve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BREAKING CHANGE: the marketplace slug, the agent namespace
(linkedin-studio:<agent>), and the runtime state-file path
(~/.claude/linkedin-studio.local.md) all change. Reinstall required;
existing state migrated in place (post metrics, streak, history preserved).
The /linkedin:* commands are unchanged — the command namespace is set
per-command in frontmatter and was always independent of the plugin slug.
Functionality is byte-identical to v2.4.0; this release is pure identity.
- dir + manifests: plugins/linkedin-studio + plugin.json + root marketplace.json
- agent namespace updated in commands/newsletter.md (only functional invoker)
- state path updated in 4 hook scripts + topic-rotation prompt + state template
- catch-all skill dir renamed skills/linkedin-studio (5 functional skills unchanged)
- docs + version bump to 3.0.0 across README badge, CHANGELOG, root README/CLAUDE.md
- historical records (CHANGELOG past entries, docs/ build artifacts,
config-audit v5.0.0 snapshots) intentionally retain the old slug
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Endring 8 from the change spec (Del 4 production, Maskinrommet). The persona
resonance sweep measures reader-response (does it land?); nothing measured prose
craft or narrative architecture (is it well-made?). In Del 4 every persona
reported PASS, yet the editor found 8 fresh editorial points on first reading —
~6/8 craft/architecture blind spots no agent could see. v2.4.0 adds the missing
editor role.
New Step 5.5 (editorial-review) runs between fact-check (Step 5) and the persona
sweep (Step 6): a new editorial-reviewer agent (Opus) judges two axes —
prosa-handverk (em-dash density, verbatim repetition, postulated numbers,
contradictions, versal-tic) + narrativ-arkitektur (concrete instantiation,
theory-anchored hypotheses, series-title symmetry, equal action per addressee,
un-overloaded conclusion). Returns <=10 flags as direction (never copy), each
BLOCK/REWORK/NICE, operator-gated via SendUserFile. Runs before the persona
sweep so the personas measure resonance instead of stumbling on craft noise.
Mirrors the Maskinrommet writing-contract section C2 (bidirectional mirror rule).
- agents/editorial-reviewer.md (NEW, Opus, orange) + fasit fixture
(editorial-reviewer-cases.md: Del 4 v5 gold standard, 8 points -> 2 axes +
severities, 3 BLOCK / 5 REWORK, 6/8 blind spots) + structural lint (7 tests).
- Step 5.5 wired into commands/newsletter.md; pipeline 14 -> 15 phases.
- editorial-review phase + additive editorialReview state in
config/edition-state.template.json; resumption: factcheck-sweep -> Step 5.5,
editorial-review -> Step 6 (spec said fact-check; canonical key is
factcheck-sweep).
- persona-reviewer contract unchanged: editorial-reviewer is supplementary
(one measures craft, one measures response).
- All doc levels synced (plugin + root README/CLAUDE.md, CHANGELOG, plugin.json
2.3.0 -> 2.4.0; agents 15 -> 16). 94 tests green.
Acceptance-criterion #8 (live run on Del 4 v5) delivered as fasit fixture:
a live run needs a session reload (new agent not invokable until then) + read
access to the Del 4 v5 draft in Maskinrommet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>