docs(linkedin-studio): Voyage remediation setup — brief + research + plan (Phase 0-3)

Audit-remediation Voyage project authored end-to-end this session:
- brief.md (reviewer PROCEED; validator pass) — full Phase 0-3 scope, phased,
  with success criteria refined by research
- research/01-03 — high-effort external swarm + Gemini (Topic 1); reconciled the
  external bar and corrected several audit feature-premises (no publishable model
  name/date; saves UI-visible not API-pullable; auto-publish possible-not-built;
  9:16 not mandatory; newsletter notifications deduplicated not triple; CLI crash
  = missing npm install, depth-bug latent)
- plan.md (21 steps, 7 sessions, 5 waves; validator pass; A- 88/100) — plan-critic
  REVISE (3 blockers + majors) addressed; scope-guardian ALIGNED; gemini Pass-2
  folded in 2 blind spots (git-history decision; lint stat-grep sequencing)

Execution is future sessions (one wave each) via /trekexecute, /trekreview as the
release gate. Audit report stays local until the article ships.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-29 19:49:27 +02:00
commit a61b818578
6 changed files with 2082 additions and 0 deletions

View file

@ -0,0 +1,417 @@
---
type: trekbrief
brief_version: "2.1"
created: 2026-05-29
task: "Remediate linkedin-studio from the baseline audit — correctness, honesty, generalization, and the highest-leverage 2026 coverage gaps (full Phase 03 roadmap, phased)"
slug: remediation
project_dir: docs/remediation/
research_topics: 3
research_status: complete
auto_research: false
interview_turns: 3
source: interview
phase_signals:
- phase: research
effort: high
model: opus
- phase: plan
effort: high
model: opus
- phase: execute
effort: high
model: opus
- phase: review
effort: high
model: opus
---
# Task: linkedin-studio baseline-audit remediation
> Generated by `/trekbrief` on 2026-05-29.
> This brief is the contract between requirements and planning. `/trekplan`
> reads it to produce the implementation plan. Every decision in the plan must
> trace back to content in this brief.
>
> **Source of record:** `docs/critical-review-2026-05-29.local.md` (the baseline
> audit — Workflow `wf_8623b3ea-682`, 28 agents + Gemini Deep Research
> triangulation), including its **Operator correction (2026-05-29)** which is
> primary-source and supersedes the cold read. Section references below
> (§3, §3b, §4, §5, §7, §8, §9, §10) point into that file.
## Intent
The baseline audit was a cold, hostile read of the plugin repo with no operator
input. It found a set of **file-reproducible correctness and honesty defects**
that survive a second hostile pass: the analytics CLI crashes on first use on a
fresh clone (`ERR_MODULE_NOT_FOUND`); the algorithm "facts" contradict
themselves across files (a comment is "15x more reach" on one line and "5x" ten
lines later; carousel is "6.6%, highest of all formats" in two files and "1.92%"
in a third; the external-link penalty is "40-50%" in eight files and "25-40%" in
ten); the author's **real** voice profile ships committed and is read
unconditionally, so a fresh adopter who skips setup writes in the author's voice
and is told "Voice ✓ Done"; the only structural lint is dead and always fails
identically whether the plugin is healthy or gutted; the A/B output claims
statistical significance organic personal posts essentially never reach; the
analytics data model measures network-graph metrics while the strategy layer
tells the user to optimize saves/dwell it structurally cannot read; and 11 of 19
agents are never invoked by any command. On top of the correctness layer, the
plugin presents algorithm knowledge with a precision the evidence does not
support, and ships its flagship long-form engine as **bespoke disguised as
general** — hardcoded Norwegian, a maintainer-private absolute series path, and a
"skrivekontrakt §C2" that does not ship.
The operator's correction **refutes the audit's single hardest finding**: the
long-form pipeline HAS run end-to-end — two editions shipped via
`/linkedin:newsletter`, with artifacts living in a separate series repo
(`maskinrommet/serier`), so in-repo archaeology saw nothing. So this work is
**not** "prove the pipeline runs." It is: fix what is file-reproducibly broken,
make the plugin honest about what it knows and what it cannot do, and make it
usable by someone who is not the author. The stakes are trust — the operator
writes long-form regularly from now on, intends to share the plugin actively, and
will publish a Maskinrommet article about it. Every algorithm claim that ships
becomes a public claim, so correctness and honesty are load-bearing, not cosmetic.
## Goal
linkedin-studio passes a does-it-work bar on a fresh clone and is honest about
its boundaries, while keeping its differentiators. Concretely, the end state is:
the analytics CLI runs from any working directory on a fresh clone; the
structural lint reflects the real layout and fails on drift; there is **one**
source-anchored algorithm-signal statement that every command and agent cites,
with no intra-file or cross-file contradictions and no unsourced numeric
precision (the deployed-model name, the January-2026 date, and the 40-60% figure
are downgraded to exactly what current sources support); a generic placeholder
voice profile ships while the author's real one is gitignored; the plugin is
de-Norwegian-locked with a parameterized series path and documented default; the
plugin honestly discloses its boundaries (no self-serve analytics API for
personal profiles, no auto-publish, dwell is not exportable) up front; the
highest-leverage 2026 coverage gaps are closed with **wired, tracked** surfaces
(first-hour/reply loop, short-form de-AI gate, video 9:16 enforcement,
profile-SEO, newsletter distribution, outreach pipeline state); the long-form
stack is **kept** and trimmed for quality only where review-pass overlap is
**measured**, not assumed; and the 11 orphan agents are resolved case-by-case
(wired to a command or deleted). The README's "the version that ships is the
version that's actually been independently reviewed" claim is removed and
replaced with an honest framing. Delivered **phased** per §9: Phase 0
(correctness + honesty) → Phase 1 (usable by a non-author) → Phase 2 (coverage
gaps) → Phase 3 (long-form earn / redundancy measurement).
## Non-Goals
- **Not** rebuilding the plugin or rewriting the long-form engine. Fork-1 decision
is **KEEP** the long-form stack; trim only where it measurably improves quality.
- **Not** proving the long-form pipeline runs end-to-end — refuted by the operator
correction; it has shipped two editions. The audit's "never run" framing, the §2
"Long-form stack: never executed" row, and teardown spine A's "accretion without
dogfooding" premise are **dropped** and must not anchor any work.
- **Not** adding a manual-entry feature for saves/dwell. The saves/dwell decision
is the **honesty-fix only** — downgrade the claims; do not build a measurement
surface for them (operator decision, 2026-05-29).
- **Not** building LinkedIn auto-publish, an analytics-API integration, or any
paid/remote service. Boundaries are to be **disclosed**, not engineered away.
- **Not** any enterprise feature (web dashboard, fleet policy, ticketing) — this is
a solo project; those are fork-and-own.
- **Not** changing the `/linkedin:*` command invocation surface for short-form
unless a fix requires it; the short-form feed engine is the part that works.
- **Not** writing the Maskinrommet article in this work — that is downstream of a
clean, honest plugin.
## Constraints
- **Opus on everything** — all phases, all subagents, all loops (standing
operator default; overrides any model-tiering table). Voyage = Opus always.
- **No hidden costs** — any `/trekplan` run or Workflow with many agents MUST be
cost-warned with explicit operator yes before it runs. The operator is present.
- **Verification duty (the article will publish)** — every external claim that
ships is verified against a current primary or credible source before it is
written; gaps are marked "Not verified" or omitted, never filled with a guess;
no sales language in technical text. Reproduce the audit's key external findings
first-hand (§10 items 24), do not inherit them from the report.
- **Three-doc rule** — any feature change pushed to Forgejo updates all three doc
levels in the same change: plugin `README.md`, plugin `CLAUDE.md`, root
`README.md`.
- **Version sync** — on any version bump, grep the old version and update every
reference (package/manifest, README badges, CHANGELOG, CLAUDE.md, SKILL.md,
STATE.md counts).
- **Hook editing** — edit `hooks/hooks.template.json` + `hooks/prompts/*.md`, then
run `python3 hooks/scripts/compile-hooks.py`; never edit `hooks.json` directly.
- **bash 3.2 + Node-only hooks** — all shell is bash-3.2-compatible; hooks are
Node `.mjs`, cross-platform, zero npm dependencies.
- **Cross-repo `maskinrommet/`** — writing there requires an explicit instruction;
this work touches only the plugin repo.
- **Audit report stays local**`docs/critical-review-2026-05-29.local.md` is not
committed until the article is out.
## Preferences
- Phased delivery following the §9 roadmap (Phase 0 → 1 → 2 → 3); one phase is a
natural plan-step boundary, executed one Voyage session at a time.
- Per execution step, choose the engine deliberately: inline · `Agent` · `Workflow`
(tightly-scoped fan-out for heavy steps only).
- Fix the **substrate first**: `references/algorithm-signals-reference.md` is the
file the contradictions propagate from — correct it once and the agents/commands
inherit the fix.
- Reframe "ritual vs craft" in the long-form critique as **trim-for-quality**, not
justify-existence (the stack is exercised machinery).
- Honesty-reframe of the README "independently reviewed" claim happens **in this
plan**, not as an out-of-band edit now (operator-confirmed).
- Generalize cleanly: parameterize the series path via env-var/config with a
documented, non-private default; keep all voice profiles (author's and any
adopter's) local-only and gitignored.
## Non-Functional Requirements
- **Zero new npm dependencies** in hooks/scanners/scripts (Node built-ins + `node:test`).
- **Fresh-clone clean** — analytics CLI and lint both succeed on a clone with no
prior `npm install` state assumed (CLI surfaces the install step as first-class).
- **No PII in committed files** — the shipped voice profile contains no real name,
avoid-list, or identifying vocabulary; ownership-neutral placeholder markers only.
- **Backward-compatible state** — any state-file change to
`~/.claude/linkedin-studio.local.md` is additive; existing editions/queues keep working.
- **Cross-platform** — clipboard, hooks, and any new scripts run on macOS + Linux + WSL.
## Success Criteria
*Each is falsifiable by a command or a specific observation. Grouped by phase.*
**Phase 0 — correctness + honesty**
- Analytics CLI runs from any CWD on a fresh clone: from a directory other than
the plugin root, `/linkedin:report`'s underlying CLI invocation exits 0 (no
`ERR_MODULE_NOT_FOUND`) — verified by running the documented invocation after a
clean checkout with the install step performed as instructed.
- One magnitude per algorithm effect: `grep -rn` for the comment multiplier,
carousel engagement %, and link-penalty % across `references/` + `agents/` +
`commands/` returns a **single** value (or one labelled range) per effect — no
"15x vs 5x", no "6.6% vs 1.92%", no "40-50% vs 25-40%".
- `references/algorithm-signals-reference.md` carries a **per-claim source +
confidence column**; every agent/command that states a signal cites it rather
than restating a bare number.
- The "360Brew, January 2026" / "40-60% before anyone sees it" claims are
downgraded to sourced direction only: no asserted deployed-model name, no
asserted Jan-2026 switch date, no unsourced reach figure (README + `commands/profile.md`).
- `assets/voice-samples/authentic-voice-samples.md` contains **no real PII**
(no author name, no real avoid-list); a placeholder is detected by
ownership-neutral markers; the real profile lives at a gitignored `*.local.md`;
`setup.md` **overwrites** rather than merges.
- `scripts/test-runner.sh` exits **0** on the healthy repo, globs the real
`agents/` layout, derives `EXPECTED_AGENTS` from `ls agents/` with a
length-equality assertion, and **fails (non-zero)** when an agent file is
added or removed without registration (provable by a temporary add/remove).
- `commands/ab-test.md` has **no** literal `Significant? Yes/No` column; confidence
is capped at "directional" below ~50 samples per variant; the "20% significance
rule" wording is gone.
- Saves/dwell claims are downgraded to honest wording **[refined by research/02]**:
no report or strategy surface tells the user to optimize a signal the data model
cannot populate. Accurate framing: saves ARE visible in native LinkedIn post
analytics (since ~Sept 2025, count-only) but there is **no self-serve API** to pull
them, so the tool does not auto-track them (point the user to the native number);
dwell is internal-only for organic posts. The `report.md` "Saves (10x weight) …
highest-impact" line is reframed accordingly.
**Phase 1 — usable by a non-author**
- The series root is read from an env-var/config with a **documented** default
that is **not** a maintainer-private absolute path (`grep -rn '/Users/ktg'` over
shipped — non-`*.local.*` — files returns 0 hits in long-form config/commands).
- The Norwegian-language review layer is gated/parameterized so it does not grade
English prose against Norwegian rules for a non-Norwegian adopter (language is a
configurable input, not a hardcoded lock).
- README + the relevant commands state the boundaries up front **[refined by
research/02]**, each as plain prose, not buried and **dated ("as of 2026-05")**:
(a) post-level analytics API for personal profiles EXISTS but is partner-gated
(vetted Community Management API + verified org + Page) — not self-serve; CSV is the
practical floor; (b) auto-publish to a personal profile IS technically possible
self-serve (`w_member_social`) but is **deliberately not built** — a design + ToS
choice, not an API impossibility (do NOT write "cannot auto-publish"); (c) dwell not
exportable for organic posts. The calendar/queue/"publish" wording is reconciled so
it never implies the tool auto-posts.
- Discoverability counts reconcile: the auto-activating router skill
`skills/linkedin-studio/SKILL.md` no longer calls it the "thought leadership plugin",
lists the **real** agent count, and routes to `newsletter`/`headless-review`/
`pivot`/`react`; onboarding's "25 commands" and the pillar-count disagreement
(`setup.md` 5 vs `onboarding.md` 35) are aligned to the real numbers.
- README contains **no** "the version that ships is the version that's actually
been independently reviewed" string; an honest framing replaces it.
**Phase 2 — highest-leverage coverage gaps**
- A wired first-hour/reply surface exists, invoked by a command (not orphan prose),
with tracked state: a target list + draft comments + a timestamped first-hour
plan persisted in plugin state.
- A short-form de-AI / differentiation gate fires on short-form creation
(post/quick/react/carousel/video) — provable by a hook/gate or an agent the
command actually invokes (`grep -rl 'subagent_type: linkedin-studio:differentiation-checker' commands/` ≥ 1, or an equivalent wired gate).
- Each of the 11 orphan agents is **either** invoked by ≥1 command
(`grep -rl 'subagent_type: linkedin-studio:<name>' commands/` ≥ 1) **or** deleted;
the agent count in CLAUDE.md / README / SKILL.md equals `ls agents/*.md | wc -l`.
- Video gate is a **quality gate, not a reach-push [refined by research/03]**: MP4
default (warn-only on MOV/AVI) + within-limits; **captions enforced/strongly
recommended** (SRT or native auto-captions); aspect ratio is **guidance, not a hard
gate — 4:5 / 1:1 preferred for broad distribution, 9:16 mobile-only opt-in** (the
"4:5 deprioritized" self-contradiction is fixed toward "4:5 preferred"); **no
"3-second hook" rule** (replaced by "front-load value for muted autoplay") and **no
"video maximizes reach" copy** (per-video reach is declining; documents out-engage
video).
- Profile, newsletter distribution, and outreach gain the §5 surfaces **[newsletter
refined by research/03]** — each at least a wired command surface, not prose-only:
profile-SEO fields; **honest** newsletter distribution (bypasses organic feed ranking
via **one deduplicated** notification per subscriber per edition — NOT "triple
notification"; one-time launch-blast + a **~12K follower floor**; realistic
cold-start floors of 0100 subs in months 13; disclose non-export / no-canonical /
no-read-analytics / per-subscriber decay); outreach tracked contact/pipeline state.
**Phase 3 — long-form earn / redundancy measurement**
- `/linkedin:newsletter` shows a "multi-session, multi-gate, ~N-hour" expectation
banner at the top.
- Long-form review-pass overlap is **measured** (a recorded comparison of what each
reviewer/gate actually catches) and the redundancy is trimmed where the measurement
does not justify it — with the measurement committed as evidence, not an assertion.
The measurement source is an **in-repo fixture edition** by default; reading a
shipped edition from `maskinrommet/serier` requires an explicit operator instruction
per the cross-repo constraint, so the plan must not assume that read.
**Cross-cutting (every phase)**
- All three doc levels updated in the same change (plugin README, plugin CLAUDE.md,
root README); `grep` for the prior version string returns 0 stale hits after any bump.
- The plugin's own lint (`scripts/test-runner.sh`, rebuilt) and any `node --test`
suites pass on the final state.
## Research Plan
*The internal/file-level fixes (analytics-CLI crash, dead lint, voice-leak,
orphan-agent wiring, A/B significance, doc counts) need **no** external research —
they are reproducible from the repo. The fixes that touch **external claims** do:
the audit's external bar is explicitly "thin / re-check before publishing" (§10),
and the operator's verification duty (the article publishes) mandates first-hand
sourcing. Three topics, each feeding specific plan steps.*
### Topic 1: Canonical 2026 LinkedIn algorithm signal statement
- **Why this matters:** This is the substrate fix. Phase-0 steps that reconcile the
contradictory stats (comment multiplier, carousel %, link penalty), reframe the
first-comment advice, downgrade the "360Brew / Jan-2026 / 40-60%" premise, and
widen the "golden hour" depend on knowing what current defensible sources actually
support — magnitude, direction, and confidence per signal. The audit (§3/§3b) shows
the existing numbers are a mix of vendor-blog estimates and self-contradiction; the
reconciled statement every command/agent will cite cannot be authored without this.
- **Research question:** "What does the 2026 LinkedIn feed-ranking system actually
reward — for the comment-vs-reaction weighting, document/carousel engagement rate,
external-link reach effect and the current first-comment-workaround status, the
early-engagement ('golden hour') window incl. delayed/evergreen reinjection, and
the deployed ranking model's verifiable name and deployment date — with a primary
or credible source and a confidence level for each claim?"
- **Suggested invocation:** `/trekresearch --project docs/remediation/ --external "What does the 2026 LinkedIn feed-ranking system actually reward — comment-vs-reaction weighting, document/carousel engagement rate, external-link reach effect and first-comment status, the early-engagement window incl. delayed reinjection, and the deployed ranking model's verifiable name and date — with a source and confidence per claim?"`
- **Required for plan steps:** Phase-0 "reconcile algorithm stats to one sourced
statement", "reframe external-link penalty", "downgrade 360Brew premise"; the
per-claim source/confidence column in `algorithm-signals-reference.md`.
- **Confidence needed:** high
- **Estimated cost:** deep — with contrarian + Gemini triangulation (the audit
already shows vendor-blog noise and two passes disagreeing on the model name).
- **Scope hint:** external
### Topic 2: Personal-profile analytics + auto-publish boundaries (2026)
- **Why this matters:** Phase-1 "state the boundaries honestly" and Phase-2 "saves
honesty-fix" need the current, verifiable status of three things the plugin makes
architectural claims about: whether a personal profile can self-serve a Member Post
Analytics API (§3b #11 marks the plugin's "individuals cannot → CSV only" premise
as *outdated*, not merely a constraint), whether per-post saves are visible in the
LinkedIn UI (the audit cites Sept-2025), and what auto-publish is/isn't possible for
a personal profile via API. Wrong boundary statements would replace one false claim
with another.
- **Research question:** "As of 2026, can a personal LinkedIn profile self-serve
post-level analytics via an API (Member Post Analytics or partner platforms), are
per-post saves visible in the native UI, and can a personal profile auto-publish
posts via any API — and what are the exact access constraints for a solo user
without partner/company-page access?"
- **Suggested invocation:** `/trekresearch --project docs/remediation/ --external "As of 2026, can a personal LinkedIn profile self-serve post-level analytics via an API, are per-post saves visible in the native UI, and can a personal profile auto-publish via any API — with the exact constraints for a solo user without partner/company-page access?"`
- **Required for plan steps:** Phase-1 "honest boundary statements in README +
commands"; Phase-2 "saves/dwell honesty-fix wording"; the §5 scheduling-boundary
disclosure.
- **Confidence needed:** high
- **Estimated cost:** standard — agent swarm.
- **Scope hint:** external
### Topic 3: Coverage-gap feature specs — video, de-AI signal, newsletter distribution
- **Why this matters:** Phase-2 builds new **wired** surfaces, so they need current
specs, not prose. Three inputs: the hard requirements for short-form video that
LinkedIn actually rewards in 2026 (aspect ratio, resolution, hook timing,
caption/SRT), what concretely triggers the templated-AI / engagement-bait down-rank
the de-AI gate must guard against, and the real newsletter-distribution mechanics
(the triple-notification leverage, cadence discipline, realistic cold-start numbers)
the long-form stack currently omits.
- **Research question:** "For LinkedIn in 2026, what are the hard short-form video
requirements the algorithm rewards (aspect ratio, resolution, hook timing,
captions), what specifically triggers the templated-AI / engagement-bait down-rank
signal, and what are the newsletter-distribution mechanics (notification leverage,
cadence, realistic cold-start subscriber numbers) a creator should follow?"
- **Suggested invocation:** `/trekresearch --project docs/remediation/ --external "For LinkedIn in 2026: hard short-form video requirements the algorithm rewards (aspect ratio, resolution, hook timing, captions); what triggers the templated-AI/engagement-bait down-rank; and newsletter-distribution mechanics (notification leverage, cadence, realistic cold-start numbers)?"`
- **Required for plan steps:** Phase-2 "video 9:16 enforcement", "short-form de-AI
gate", "newsletter distribution surface".
- **Confidence needed:** medium
- **Estimated cost:** standard — agent swarm.
- **Scope hint:** external
## Open Questions / Assumptions
- **[ASSUMPTION]** The plan will order work as the §9 phases (0→1→2→3), one phase
per Voyage session, with `/trekreview` as the final release gate. To be confirmed
when `/trekplan` produces the step list.
- **[ASSUMPTION]** Orphan-agent case-by-case decisions (wire vs delete) are made
**in the plan phase**, not now; the operator answered "case by case".
- **[ASSUMPTION]** Versioning shape (single major bump vs phased minor releases) is a
plan-phase decision; not pre-committed here.
- **[OPEN — narrow, from the operator correction]** Whether the v3.1 headless
cold-review layer (Step 6.5) co-ran in a *shipped* edition. Moot for the README
(the "independently reviewed" claim is removed per fork-4), but worth noting so the
honesty-reframe wording is accurate.
- **[ASSUMPTION]** Research Topic 1's "deep" cost (contrarian + Gemini) will be
cost-warned before it runs, per the no-hidden-costs rule.
## Prior Attempts
The baseline audit itself is the prior work: a cold adversarial Workflow
(`wf_8623b3ea-682`, 28 agents) plus an independent Gemini Deep Research
triangulation pass, delivered as `docs/critical-review-2026-05-29.local.md`. After
delivery the operator supplied first-hand corrections that refuted the audit's
flagship "never run" finding and locked four scope decisions (keep long-form +
trim for quality; regular use confirmed; generalize the plugin; remove the README
"independently reviewed" claim). No remediation code has been written yet — this
brief is the first step of the fix. The plugin reached v3.1.0 via six versions in
~48 hours (the audit's "accretion" meta-finding), which is *why* the correctness
and honesty pass is needed before further feature accretion.
## Metadata
- **Created:** 2026-05-29
- **Interview turns:** 3 (scope boundary, orphan-agent disposition, saves/dwell disposition — the four headline forks were pre-locked by the operator correction)
- **Auto-research opted in:** no
- **Source:** trekbrief interview (driven by the locked operator corrections; no full re-interview per the operating model)
---
## How to continue
Manual (default):
```bash
# Run each research topic (order does not matter):
/trekresearch --project docs/remediation/ --external "What does the 2026 LinkedIn feed-ranking system actually reward — comment-vs-reaction weighting, document/carousel engagement rate, external-link reach effect and first-comment status, the early-engagement window incl. delayed reinjection, and the deployed ranking model's verifiable name and date — with a source and confidence per claim?"
/trekresearch --project docs/remediation/ --external "As of 2026, can a personal LinkedIn profile self-serve post-level analytics via an API, are per-post saves visible in the native UI, and can a personal profile auto-publish via any API — with the exact constraints for a solo user without partner/company-page access?"
/trekresearch --project docs/remediation/ --external "For LinkedIn in 2026: hard short-form video requirements the algorithm rewards (aspect ratio, resolution, hook timing, captions); what triggers the templated-AI/engagement-bait down-rank; and newsletter-distribution mechanics (notification leverage, cadence, realistic cold-start numbers)?"
# Then plan:
/trekplan --project docs/remediation/
# Then execute:
/trekexecute --project docs/remediation/
```
Auto (opt-in during `/trekbrief`): research and planning run automatically; only
execution is manual. **Not used here** — per the operating model, `/trekplan` is
invoked separately in the foreground after operator approval, with an explicit
cost warning.

View file

@ -0,0 +1,908 @@
# LinkedIn Studio — Baseline-Audit Remediation
> **Plan quality: A** (88/100, post-revision) — APPROVE_WITH_NOTES
> (plan-critic: REVISE → 3 blockers + actionable majors fixed; scope-guardian: ALIGNED)
>
> Generated by trekplan v5.1.1 on 2026-05-29
> plan_version: 1.7
> Project: `docs/remediation/` · Brief: `docs/remediation/brief.md` · Research: `research/01..03`
## Context
The baseline audit (`docs/critical-review-2026-05-29.local.md`, cold hostile read + Gemini
triangulation) found file-reproducible **correctness and honesty defects** in linkedin-studio,
and a flagship engine **shipped bespoke-as-general** (Norwegian-locked, private series path,
non-shipping contract). The operator's correction refuted the audit's "long-form never ran"
premise (two editions shipped via `/linkedin:newsletter`), so this is **not** "prove the
pipeline runs." It is: fix what is file-reproducibly broken, make the plugin honest about what
it knows and what it cannot do, and make it usable by someone who is not the author. The stakes
are trust — the operator writes long-form regularly, will share the plugin actively, and will
publish a Maskinrommet article about it, so every algorithm claim that ships is a public claim.
Three research briefs (run cold, this session) reconciled the external bar and **corrected
several of the audit's own feature premises**: there is no publishable name/date for the
deployed ranking model; the engagement order is saves > shares > quality-comments > reactions
with dwell + topic-relevance the only officially-named signals; documents (~7%) lead video
(declining); the link effect is correlational (LinkedIn denies intent) and the first-comment
"fix" is contested; **auto-publish IS self-serve-possible** (so "cannot auto-publish" would be
a new false claim); **saves ARE visible in the native UI** (so "untrackable" is stale); a hard
**9:16 video gate is wrong** (4:5/1:1 preferred; captions are the enforceable spec); and the
newsletter **"triple-notification" is deduplicated** (the honest benefit is "bypasses feed
ranking", with a ~12K follower floor). Delivered phased per audit §9.
## Architecture Diagram
```mermaid
graph TD
subgraph "Phase 0 — guard + substrate + leaks"
LINT[test-runner.sh rebuilt<br/>dynamic counts + drift greps]
SUB[algorithm-signals-reference.md<br/>canonical + source/confidence]
CLI[analytics getAnalyticsRoot<br/>anchor on .claude-plugin/]
VOICE[voice placeholder + sentinel<br/>real → gitignored .local.md]
SUB --> CITERS[~40 citing files reconciled]
LINT -.guards.-> CITERS
LINT -.guards.-> VOICE
end
subgraph "Phase 1 — usable by non-author"
PATH[series path default + de-brand render]
LANG[language-lock → configurable]
BOUND[honest dated boundaries]
COUNTS1[discoverability counts + SKILL rename]
end
subgraph "Phase 2 — coverage gaps"
ORPH[11 orphans wire/delete]
DEAI[de-AI: wire differentiation-checker<br/>+ extend voice-guardian]
VID[video gate: captions + aspect guidance]
ENG[first-hour/reply loop + state]
DIST[honest newsletter distribution]
ORPH --> DEAI
ORPH --> ENG
end
subgraph "Phase 3 — long-form earn"
BANNER[newsletter time/effort banner]
OVERLAP[review-pass overlap measured + trimmed]
end
FINAL[version bump + counts + three-doc + CHANGELOG]
CITERS --> FINAL
COUNTS1 --> FINAL
DIST --> FINAL
OVERLAP --> FINAL
LINT -.asserts.-> FINAL
```
## Codebase Analysis
- **Tech stack:** Markdown-first Claude Code plugin (commands/agents/references as `.md` with
YAML frontmatter) + Node.js ESM (`.mjs`) hooks (zero npm deps, `node:test`) + one Python
hook-compiler + one bash 3.2 structural lint + a TypeScript analytics CLI (`scripts/analytics/`,
run via `tsx`). 214 source files (medium).
- **Key patterns:** commands invoke agents ONLY via `Task` with `subagent_type:
linkedin-studio:<name>` (namespaced; bare fails); hooks compiled from
`hooks.template.json` + `prompts/*.md` via `compile-hooks.py` (never edit `hooks.json`);
content gates are advisory (always exit 0) via `content-gatekeeper.mjs` + `isLinkedInContent`;
reference docs cite algorithm facts by path but **restate numbers inline** (so a substrate fix
only propagates if citers are converted to cite-not-restate); state mutations via pure
functions in `state-updater.mjs`; agents Sonnet except the 6 Opus long-form gates + 1 Haiku
(`post-feedback-monitor`, the lone deviation).
- **Relevant files:** `scripts/test-runner.sh` (dead lint); `references/algorithm-signals-reference.md`
(the substrate, cited by 16 files); `scripts/analytics/src/utils/storage.ts:17-22`
(`getAnalyticsRoot` depth bug); `assets/voice-samples/authentic-voice-samples.md` +
`hooks/scripts/personalization-score.mjs:23` + `.claude-plugin/plugin.json:6` (voice/PII);
`commands/ab-test.md:301-317`; `commands/report.md`/`import.md` (CLI invocation);
`config/edition-state.template.json:4` + `commands/newsletter.md:36,138` + `render/build-linkedin.mjs:350`
+ `render/build-carousel.mjs` (series path + brand); `agents/language-reviewer.md` (lang lock);
the 11 orphan agents; `commands/video.md` + `references/linkedin-formats.md` (aspect contradiction);
`skills/*/SKILL.md` + `commands/onboarding.md`/`setup.md`/`linkedin.md` (counts); `README.md:120,128`.
- **Reusable code:** `hooks/scripts/state-updater.mjs` (add pure mutation fns for new tracked
state); `hooks/prompts/voice-guardian.md` (already has AI-pattern detection — extend, don't
duplicate); `agents/differentiation-checker.md` (the orphan to wire as the de-AI gate);
`agents/__tests__/*-fixture.test.mjs` (shape-test pattern); `scripts/analytics/tests/storage.test.ts`
(tmpdir + env-override pattern for the CWD-fix test); `agents/content-reviewer.md` (Opus
gate-agent template, if any new agent is needed); `compile-hooks.py --check` (drift guard to
wire into the lint).
- **Recent git activity:** solo project; six versions in ~48h (the audit's accretion finding) —
v3.1.0 added the cold-review trio. No CI runs the lint.
## Research Sources
| Topic | Source(s) | Key findings | Confidence |
|-------|-----------|--------------|------------|
| Algorithm signals (research/01) | LinkedIn Eng blog, arXiv 2501.16450, Socialinsider 1.3M, Ordinal 900K, Entrepreneur (Lorenzetti), van der Blom 1.8M + Gemini | No publishable model name/date; order saves>shares>comments>reactions; dwell+topic-relevance the only named signals; docs ~7% top, video declining; link ~38% correlational, intent disputed; golden window 6090 min; AI-slop down-rank officially confirmed | high (direction) / medium (magnitude) |
| Analytics + publish boundaries (research/02) | Microsoft Learn (li-lms-2026-05), LinkedIn Help, Marcus Noble, GitHub #35 | Analytics API exists but partner-gated (not self-serve); **auto-publish IS self-serve via `w_member_social`** (clipboard-stop is a choice/ToS, not a wall); **saves visible in UI since ~Sept 2025** + API POST_SAVE (gated); dwell internal-only for organic | high |
| Coverage-gap specs (research/03) | LinkedIn Help, Socialinsider, aspectratiocalculator, LinkedIn newsletter FAQ, The Science Marketer | 9:16 not a clean win (4:5/1:1 preferred, desktop crop); captions the enforceable spec; "3-sec hook" folklore; video reach 36% YoY; newsletter notifications **deduplicated** (not triple); one-time launch invite → ~12K floor; non-exportable lock-in | high (mechanics) / medium (cold-start) |
## Implementation Plan
> **Ordering rationale (from risk-assessor):** the lint is rebuilt FIRST because it is the
> tripwire every later step relies on (it already fails 29 checks against the real layout, so
> nothing is guarded today). The substrate stat file is reconciled SECOND so all citers inherit
> one source. Voice-leak + placeholder-detection are sequenced TOGETHER (coupled criticals).
> The version/counts/three-doc reconciliation is LAST so it captures everything.
### Step 1: Rebuild the dead structural lint
- **Files:** `scripts/test-runner.sh`
- **Changes:** Replace the stale hardcoded `EXPECTED_AGENTS` (14), `EXPECTED_COMMANDS` (lists 4 deleted commands, uses wrong `commands/linkedin:NAME.md` layout), the `skills/<name>.md` path check (real: `skills/<name>/SKILL.md`), the `personalization-scorer` reference, the fabricated `auto_discover` plugin.json field, and the missing `docs/DEVELOPMENT-LOG.md` assert. Derive counts dynamically: `AGENTS=$(ls agents/*.md | wc -l)` with a length-equality assert against the CLAUDE.md "Telling" block (19/26/25/6/9); glob `commands/*.md`, `references/*.md`, `skills/*/SKILL.md`; assert each command/agent has `name:`/`description:` frontmatter; call `python3 hooks/scripts/compile-hooks.py --check` for hook drift. bash 3.2-safe (plain arrays, no `declare -A`). **Scope note (gemini Pass-2): this step rebuilds the STRUCTURAL lint only (counts/layout/frontmatter/hook-drift) — all of which are already broken today, so the lint can go green immediately and guard Steps 421. The stat-consistency grep is added to the lint in Step 3 (after reconciliation makes it pass — adding it here would fail this step's own Verify, since the contradictions still exist) and the version-consistency grep in Step 21. This avoids the chicken-and-egg the lint-first ordering otherwise risks.**
- **Reuses:** existing `pass()/fail()/warn()` + `PLUGIN_ROOT` skeleton in `test-runner.sh`; `compile-hooks.py --check` (already works).
- **Test first:**
- File: manual red/green (bash validator, not node:test)
- Verifies: exits 0 on healthy repo; exits 1 when an agent file is removed
- Pattern: the existing section structure in `scripts/test-runner.sh`
- **Verify:** `bash scripts/test-runner.sh; echo "exit=$?"` → expected: `exit=0`; then prove the registration guard bites with a self-restoring one-liner: `git mv agents/trend-spotter.md /tmp/x; bash scripts/test-runner.sh; rc=$?; git mv /tmp/x agents/trend-spotter.md; echo "removed-agent exit=$rc"``removed-agent exit=1` (restore runs regardless of the lint's `set -e`).
- **On failure:** revert — `git checkout -- scripts/test-runner.sh`
- **Checkpoint:** `git commit -m "fix(linkedin-studio): rebuild dead structural lint to real v3.1 layout"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- scripts/test-runner.sh
min_file_count: 1
commit_message_pattern: "^fix\\(linkedin-studio\\): rebuild dead structural lint"
bash_syntax_check:
- scripts/test-runner.sh
forbidden_paths: []
must_contain:
- path: scripts/test-runner.sh
pattern: "ls agents"
```
### Step 2: Rebuild the algorithm-signals substrate (canonical, sourced)
- **Files:** `references/algorithm-signals-reference.md`
- **Changes:** Make this the single source of truth. Add a per-claim **Source + Confidence** column to the signal tables (house style: keep the `| Signal | Weight | … |` table genre, widen rows; keep the closing `*Sources:*` footer as bibliography). Apply research/01: (a) engagement order saves > shares > quality-comments > reactions, comment ≈ 2x like (medium), drop the bare "15x/5x" framing → state as ordering + sourced; (b) reconcile the intra-file carousel contradiction (`:72` 1.92% PDF vs `:73` 6.60% multi-image) → documents/carousels top format ~7% (Socialinsider, company-page per-impression), note 1.92% was a personal-profile baseline; (c) link effect ~38% correlational 2026 (band ~1960%), LinkedIn denies intent, value-first > location; (d) golden window 6090 min + evergreen resurfacing (drop "2472h" precision); (e) the model: "an LLM relevance-ranking system is live in 2026" — **no name, no date**; (f) drop the "40-60% off-topic" figure, keep "profile/topic alignment is a ranking input"; (g) dwell + topic-relevance flagged as the only officially-named signals; (h) buzzwords = editorial guidance, not a reach mechanic.
- **Reuses:** existing table format; `references/longform-quality-rules.md` provenance-blockquote style; research/01 §Recommendation as the source-of-truth content.
- **Test first:**
- File: n/a (content doc) — verification is the Step 1 lint's stat-consistency grep
- Verifies: no intra-file contradiction; every numeric claim has a Source/Confidence cell
- Pattern: research/01 reconciliation table
- **Verify:** `grep -nE '1\.92%|15x more reach|January 2026|360Brew|-40-60%' references/algorithm-signals-reference.md` → expected: no matches (all downgraded); `grep -c 'Confidence' references/algorithm-signals-reference.md` → ≥ 1.
- **On failure:** revert — `git checkout -- references/algorithm-signals-reference.md`
- **Checkpoint:** `git commit -m "fix(linkedin-studio): reconcile algorithm-signals to one sourced statement"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- references/algorithm-signals-reference.md
min_file_count: 1
commit_message_pattern: "^fix\\(linkedin-studio\\): reconcile algorithm-signals"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: references/algorithm-signals-reference.md
pattern: "Confidence"
```
### Step 3: Propagate the reconciled numbers to all citers
- **Files:** ENUMERATE AT EXECUTION by running the two greps below first, then edit every hit. Known set (from exploration + plan-critic Blocker 2): `commands/carousel.md`, `commands/profile.md`, `commands/react.md`, `commands/analyze.md`, `commands/monetize.md`, `commands/audit.md`, `commands/strategy.md`, `commands/linkedin.md`, `references/linkedin-formats.md`, `references/linkedin-visual-style.md`, `references/glossary.md`, `references/first-comment-strategy.md`, `references/troubleshooting-guide.md`, `references/linkedin-monetization-strategies.md`, `references/engagement-frameworks.md`, `agents/content-optimizer.md`, `hooks/prompts/content-quality-gate.md`, `hooks/prompts/topic-rotation-gate.md`, `CLAUDE.md`, `README.md`, `skills/linkedin-studio/SKILL.md`, `skills/linkedin-analytics/SKILL.md`, `assets/templates/carousel-templates.md`
- **Changes:** First enumerate the real citer set: `grep -rlnE '40-50%|25-40%|6\.6%|1\.92%|15x more reach|January 2026|360Brew|-40-60%' references/ commands/ agents/ skills/ hooks/prompts/ CLAUDE.md README.md` — edit EVERY hit (do not work from a stale hand-list; plan-critic Blocker 2 found `audit.md`/`strategy.md`/`linkedin.md`/`topic-rotation-gate.md` were missing). Replace every restated number with the reconciled value AND a citation to `algorithm-signals-reference.md` (cite-not-restate). Carousel 6.6%/1.92% → "documents/carousels top format (~7%); see algorithm-signals-reference". Link penalty 40-50% / 25-40% → one correlational statement. 360Brew / "January 2026" / "40-60%" → sourced direction only. **Note (plan-critic Blocker 3): `content-quality-gate.md` and `topic-rotation-gate.md` are loaded at RUNTIME by `content-gatekeeper.mjs` — editing their prose does NOT require `compile-hooks.py` and does NOT change `hooks.json`. No recompile in this step.** **Then (gemini Pass-2) add the stat-consistency grep to `scripts/test-runner.sh`** — the external-link penalty % and carousel % must each appear as ONE value across `references/ commands/ skills/ hooks/prompts/` (fail if ≥2 distinct magnitudes; exclude commented lines). Adding it HERE, after reconciliation, means the lint goes green instead of failing its own gate.
- **Reuses:** Step 2's canonical file as the cite target; the Step 1 lint skeleton.
- **Test first:**
- File: n/a — the Step 1 lint stat-consistency grep is the regression check
- Verifies: one magnitude per effect across the tree
- Pattern: the lint's penalty-% grep
- **Verify:** `grep -rnE '40-50%|25-40%|1\.92%' references/ commands/ skills/ hooks/prompts/ CLAUDE.md README.md | grep -v algorithm-signals-reference` → no matches; `grep -rnE 'January 2026|360Brew' commands/ README.md skills/ hooks/prompts/ CLAUDE.md` → no matches (or only inside a sourced "research model, not deployed" note); spot-check 3 random citers actually cite the reference (cite-not-restate, not just a consistent bare number); `bash scripts/test-runner.sh` → exit 0 (the newly-added stat-grep is green).
- **On failure:** revert — `git checkout -- <enumerated files> scripts/test-runner.sh`
- **Checkpoint:** `git commit -m "fix(linkedin-studio): propagate reconciled algorithm numbers, cite-not-restate"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/carousel.md
- commands/profile.md
- commands/strategy.md
- commands/audit.md
- scripts/test-runner.sh
min_file_count: 5
commit_message_pattern: "^fix\\(linkedin-studio\\): propagate reconciled algorithm numbers"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/carousel.md
pattern: "algorithm-signals-reference"
```
### Step 4: Fix the analytics CLI root-resolution + surface install
- **Files:** `scripts/analytics/src/utils/storage.ts`, `commands/report.md`, `commands/import.md`, `scripts/analytics/src/cli.ts`
- **Changes:** **PRIMARY fix (the actual fresh-clone crash):** `tsx` + `csv-parse` are absent on a fresh clone (gitignored `node_modules`), so `node --import tsx` throws `ERR_MODULE_NOT_FOUND` — surface the install at point-of-use in `report.md`/`import.md`: prepend `cd "${CLAUDE_PLUGIN_ROOT}/scripts/analytics" && npm install --silent` (idempotent) before the `node --import tsx` invocation, and fix the troubleshooting line "Verify tsx is available" → the actual install command. Reconcile `cli.ts` usage text (`node build/cli.js` → the real `tsx src/cli.ts` runtime). **SECONDARY fix (latent correctness bug — `getAnalyticsRoot()` depth):** the `../../../../` in storage.ts:17-22 is calibrated for `build/utils/` but runs under tsx from `src/utils/`, so the *fallback* root is wrong. The shipped commands mask it by always passing `ANALYTICS_ROOT="${CLAUDE_PLUGIN_ROOT}/assets/analytics"` (so it is latent, NOT the crash cause — per plan-critic Blocker 1), but it is wrong for any direct/test invocation. Anchor it on the dir containing `.claude-plugin/plugin.json`. Keep the `ANALYTICS_ROOT` override as the test seam.
- **Reuses:** existing `ANALYTICS_ROOT` env handling in storage.ts; `scripts/analytics/tests/storage.test.ts` tmpdir/afterEach pattern.
- **Test first:**
- File: `scripts/analytics/tests/storage-root.test.ts` (new)
- Verifies: default root (no env) anchors on the plugin dir, NOT `scripts/analytics/assets`; `ANALYTICS_ROOT` override returns the resolved env path (red against current code, green after fix)
- Pattern: `scripts/analytics/tests/storage.test.ts`
- **Verify:** install first, then run from a foreign CWD — `cd scripts/analytics && npm install --silent && cd /tmp && node --import tsx "${CLAUDE_PLUGIN_ROOT:-$OLDPWD}/scripts/analytics/src/cli.ts" report 2>&1 | head` → no `ERR_MODULE_NOT_FOUND`; `cd scripts/analytics && npm install --silent && npm test 2>&1 | tail -3` → storage-root test passes.
- **On failure:** revert — `git checkout -- scripts/analytics/ commands/report.md commands/import.md`
- **Checkpoint:** `git commit -m "fix(linkedin-studio): anchor analytics root on plugin marker + surface npm install"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- scripts/analytics/src/utils/storage.ts
- scripts/analytics/tests/storage-root.test.ts
- commands/report.md
- commands/import.md
min_file_count: 4
commit_message_pattern: "^fix\\(linkedin-studio\\): anchor analytics root"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: scripts/analytics/src/utils/storage.ts
pattern: "plugin.json"
```
### Step 5: Voice-leak + placeholder-detection (coupled criticals)
- **Files:** `assets/voice-samples/authentic-voice-samples.template.md` (new), `assets/voice-samples/authentic-voice-samples.md` (→ becomes placeholder), `hooks/scripts/personalization-score.mjs`, `commands/setup.md`, `commands/onboarding.md`, `.claude-plugin/plugin.json`, `.gitignore`, plus the exact-filename readers (`hooks/scripts/user-prompt-context.mjs`, `hooks/prompts/voice-guardian.md`, `hooks/prompts/state-update-reminder.md`)
- **Changes:** Ship a PII-free placeholder at `authentic-voice-samples.md` carrying an explicit sentinel (e.g. `<!-- VOICE_PLACEHOLDER -->`); move the author's real profile to a gitignored `authentic-voice-samples.local.md` and add that glob to `.gitignore`; add a `.template.md` for adopters. Fix `personalization-score.mjs:23` to detect the sentinel (not the `[Your Name]` heuristic) so the placeholder scores 0 voice points. **Both voice writers must replace-not-append (plan-critic Major 1):** fix `setup.md:99` (merge→overwrite) AND `commands/onboarding.md:142` (append→overwrite) so a populated profile removes the sentinel — otherwise the sentinel survives below appended content and the score stays 0 after the user fills it in. Also update the stale detection-heuristic table at `setup.md:28` ("Check for `[Your Name]` or <50 lines") to describe the sentinel. Scrub the author name from `.claude-plugin/plugin.json:6` (author field → neutral/org). **PII NFR scope (plan-critic Major 2):** the leak is the voice profile + `plugin.json` author field; the author name in `LICENSE` is the legally-required MIT copyright holder and is an intentional, correct exception (do NOT scrub it). Verify the ~15 readers tolerate the placeholder (most read a dir glob; the exact-filename readers keep working since the filename persists). **Git-history decision (gemini Pass-2 Decision 3): `.gitignore` does NOT remove the already-committed real profile from past commits — it stays retrievable from history.** Decision (proportionate to the threat model): this is the **author's own attributed open-source plugin** — his name is already public by design in `LICENSE` (MIT copyright), the README, and his Forgejo account, so the historical voice file is *attributed authorship, not a leaked secret*. The bug being fixed is the **adopter-default** (a fork inheriting the author's voice + being told "Voice ✓"), which the placeholder+gitignore-going-forward fully resolves. **Do NOT force-push a history rewrite** (`git-filter-repo`) — it is disruptive for an open-source plugin with existing clones/forks and is disproportionate for non-secret attributed content. Record this as a conscious decision in the commit body (so it is a documented choice, not an oversight). *If the real `.local.md` ever contains genuine secrets (it should not — it is styling), that would change the calculus.*
- **Reuses:** the `<!-- ... -->` sentinel convention; existing dir-glob reads.
- **Test first:**
- File: `hooks/scripts/__tests__/personalization-score.test.mjs` (new)
- Verifies: placeholder (with sentinel) scores 0 voice points; a real-looking profile (no sentinel, >50 lines) scores the voice points
- Pattern: `hooks/scripts/__tests__/state-updater.test.mjs`
- **Verify:** `grep -rIn "Kjell Tore" assets/voice-samples/authentic-voice-samples.md .claude-plugin/plugin.json` → no matches; `git check-ignore assets/voice-samples/authentic-voice-samples.local.md` → prints the path; `node --test hooks/scripts/__tests__/personalization-score.test.mjs` → passes.
- **On failure:** revert — `git checkout -- assets/voice-samples/ hooks/scripts/personalization-score.mjs commands/setup.md .claude-plugin/plugin.json .gitignore` and restore the real file from the local copy.
- **Checkpoint:** `git commit -m "fix(linkedin-studio): ship placeholder voice profile, gitignore real, sentinel detection"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- assets/voice-samples/authentic-voice-samples.md
- assets/voice-samples/authentic-voice-samples.template.md
- hooks/scripts/personalization-score.mjs
- hooks/scripts/__tests__/personalization-score.test.mjs
- .gitignore
min_file_count: 5
commit_message_pattern: "^fix\\(linkedin-studio\\): ship placeholder voice profile"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: hooks/scripts/personalization-score.mjs
pattern: "VOICE_PLACEHOLDER"
```
### Step 6: Fix the A/B significance claim
- **Files:** `commands/ab-test.md`
- **Changes:** Remove the literal `Significant? Yes/No` column (`:301-306`); rename to `Directional?`; cap confidence at "directional only" below ~50 conversions/variant; drop the "20% significance rule" wording (`:277,310-313`) and the "3 = Medium, 5+ = High" ladder (`:315-317`) — replace with a plain "organic personal-post volume rarely reaches statistical significance; treat results as directional."
- **Reuses:** existing ab-test.md structure.
- **Test first:**
- File: n/a (content doc)
- Verifies: no "Significant?" column, no "20% significance rule"
- Pattern: grep check
- **Verify:** `grep -nE 'Significant\?|20% significance' commands/ab-test.md` → no matches; `grep -c 'irectional' commands/ab-test.md` → ≥ 1.
- **On failure:** revert — `git checkout -- commands/ab-test.md`
- **Checkpoint:** `git commit -m "fix(linkedin-studio): downgrade A/B significance claim to directional"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/ab-test.md
min_file_count: 1
commit_message_pattern: "^fix\\(linkedin-studio\\): downgrade A/B significance"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/ab-test.md
pattern: "irectional"
```
### Step 7: Saves/dwell honesty-fix
- **Files:** `commands/report.md`, `scripts/analytics/src/models/types.ts`, `commands/strategy.md`
- **Changes:** Apply research/02 D2/D4. Reframe `report.md:223` ("Saves (10x weight) … highest-impact") → honest dated wording: "saves are visible in your native LinkedIn post analytics (since ~Sept 2025, count-only) but there is no self-serve API to pull them, so this tool does not auto-track them — read them in LinkedIn directly; dwell is internal-only for organic posts." Add a code comment in `types.ts` documenting why `saves`/`dwell` are intentionally absent (no self-serve source). Reconcile any strategy-layer copy that tells the user to "optimize saves" the tool can't read. **No** new metric field, **no** manual-entry feature (operator Q3).
- **Reuses:** research/02 D2 wording; existing report.md structure.
- **Test first:**
- File: n/a (content/comment)
- Verifies: no claim the tool measures saves/dwell
- Pattern: grep check
- **Verify:** `grep -nE 'Saves \(10x|highest-impact signals' commands/report.md` → no matches; `grep -n 'no self-serve API' commands/report.md` → ≥ 1 match.
- **On failure:** revert — `git checkout -- commands/report.md scripts/analytics/src/models/types.ts commands/strategy.md`
- **Checkpoint:** `git commit -m "fix(linkedin-studio): honest saves/dwell wording, no false tracking claim"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/report.md
- scripts/analytics/src/models/types.ts
min_file_count: 2
commit_message_pattern: "^fix\\(linkedin-studio\\): honest saves/dwell wording"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/report.md
pattern: "no self-serve API"
```
### Step 8: Parameterize the series path + de-brand render output
- **Files:** `config/edition-state.template.json`, `commands/newsletter.md`, `render/build-linkedin.mjs`, `render/build-carousel.mjs`, `config/image-credit-caption.template.md`
- **Changes:** Change the `${LTL_SERIES_ROOT:-…}` default from the private `/Users/ktg/repos/maskinrommet/serier` to a documented neutral default (e.g. `${LTL_SERIES_ROOT:-$HOME/linkedin-series}`), and scrub the hardcoded private path from `edition-state.template.json:4` prose. De-brand the render output: make the "Maskinrommet" footer/eyebrow/title strings in `build-linkedin.mjs:350` + `build-carousel.mjs:7,232,282` configurable (env var or config field, defaulting to a neutral/empty brand). Preserve the env-var + explicit-path-arg contract.
- **Reuses:** existing `LTL_SERIES_ROOT` env mechanism; config-field pattern.
- **Test first:**
- File: n/a (config/render) — verification is the Phase-1 grep
- Verifies: no `/Users/ktg` in shipped files; brand configurable
- Pattern: grep check
- **Verify:** `grep -rIn '/Users/ktg' config/ commands/ render/ | grep -v '.local'` → no matches; `grep -rn 'Maskinrommet' render/` → only inside a configurable default, not a hardcoded literal.
- **On failure:** revert — `git checkout -- config/ commands/newsletter.md render/`
- **Checkpoint:** `git commit -m "feat(linkedin-studio): parameterize series path + de-brand render output"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- config/edition-state.template.json
- commands/newsletter.md
- render/build-linkedin.mjs
min_file_count: 3
commit_message_pattern: "^feat\\(linkedin-studio\\): parameterize series path"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/newsletter.md
pattern: "LTL_SERIES_ROOT"
```
### Step 9: Parameterize the Norwegian language-lock
- **Files:** `agents/language-reviewer.md`, `agents/voice-scrubber.md`, `agents/content-reviewer.md`, `agents/fact-reviewer.md`, `agents/editorial-reviewer.md`, `commands/newsletter.md`, `config/edition-state.template.json`
- **Changes:** Make the review language a configurable input rather than a hardcoded Norwegian contract. Add a `language` field to edition-state (additive, default e.g. `en`), thread it into the long-form agent prompts so `language-reviewer` grades against the configured language's rules (Norwegian anglicism/kanselli checks fire only when `language: no`), and `voice-scrubber`'s "gold standard" references the configured language's approved editions. Keep Norwegian fully working when configured. **Also resolve the "skrivekontrakt §C2 does not ship" generalization defect (plan-critic minor 2):** make the §C2 reference in the craft-gate agents (`editorial-reviewer`, `content-reviewer`) non-blocking for a non-author — point to the in-tree fallback checklist the audit noted exists, so the gate works without the unshipped Maskinrommet contract.
- **Reuses:** the additive edition-state schema; the namespaced agent-invocation contract; the in-tree fallback craft checklist.
- **Test first:**
- File: n/a (agent prose) — fixture-shape test if a fixture changes
- Verifies: agents read a `language` input; Norwegian path intact; §C2 has an in-tree fallback
- Pattern: `agents/__tests__/language-reviewer-fixture.test.mjs` if present
- **Verify:** `grep -ni 'language' config/edition-state.template.json` → ≥ 1 (positive: the field exists and is threaded — confirm a `language ==`/`language:` reference appears in `agents/language-reviewer.md`); `grep -niE 'unconditional|always Norwegian|norsk draft' agents/language-reviewer.md` → no unconditional-Norwegian assertion remains; `grep -ni 'C2' agents/editorial-reviewer.md` → reference now points to an in-tree fallback, not only the unshipped contract.
- **On failure:** revert — `git checkout -- agents/ commands/newsletter.md config/edition-state.template.json`
- **Checkpoint:** `git commit -m "feat(linkedin-studio): make long-form review language configurable"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- agents/language-reviewer.md
- config/edition-state.template.json
min_file_count: 2
commit_message_pattern: "^feat\\(linkedin-studio\\): make long-form review language configurable"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: config/edition-state.template.json
pattern: "language"
```
### Step 10: Honest, dated boundary statements
- **Files:** `README.md`, `commands/report.md`, `commands/calendar.md`, `commands/import.md`
- **Changes:** Apply research/02. Add a dated ("as of 2026-05") boundaries section: (a) post-level analytics API exists but is partner-gated (verified org + LinkedIn Page) — not self-serve; CSV is the practical floor; (b) **auto-publish to a personal profile is technically possible self-serve but deliberately not built** (OAuth/token overhead + LinkedIn Terms on automated posting) — a choice, NOT "cannot"; (c) dwell internal-only for organic. Reconcile the `calendar.md` "publish action" / queue wording so it never implies the tool auto-posts (it marks a manually-posted item as published).
- **Reuses:** research/02 §Recommendation wording.
- **Test first:**
- File: n/a (docs)
- Verifies: no "cannot auto-publish" / no "CSV is the only way"; dated
- Pattern: grep check
- **Verify:** `grep -niE 'cannot auto-publish|only way to (get|access)' README.md commands/*.md` → no matches; `grep -ni 'as of 2026' README.md` → ≥ 1.
- **On failure:** revert — `git checkout -- README.md commands/report.md commands/calendar.md commands/import.md`
- **Checkpoint:** `git commit -m "docs(linkedin-studio): honest dated API/auto-publish/analytics boundaries"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- README.md
- commands/calendar.md
min_file_count: 2
commit_message_pattern: "^docs\\(linkedin-studio\\): honest dated"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: README.md
pattern: "as of 2026"
```
### Step 11: Remove the README "independently reviewed" claim + honest reframe
- **Files:** `README.md`
- **Changes:** Remove the "the version that ships is the version that's actually been independently reviewed" string (~L128) and the matching pipeline-diagram "independent re-read" line (~L120). Replace with an honest framing: the long-form pipeline runs cold/headless review gates before lock (describe the capability), without claiming every shipped edition was independently re-reviewed. Locate by content, not line number (lines have drifted).
- **Reuses:** existing README pipeline section.
- **Test first:**
- File: n/a (docs)
- Verifies: the exact claim string is gone
- Pattern: grep check
- **Verify:** `grep -ni 'independently reviewed' README.md` → no matches.
- **On failure:** revert — `git checkout -- README.md`
- **Checkpoint:** `git commit -m "docs(linkedin-studio): remove independently-reviewed claim, honest reframe"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- README.md
min_file_count: 1
commit_message_pattern: "^docs\\(linkedin-studio\\): remove independently-reviewed claim"
bash_syntax_check: []
forbidden_paths: []
must_contain: []
```
### Step 12: Reconcile discoverability surfaces
- **Files:** `skills/linkedin-studio/SKILL.md`, `skills/linkedin-analytics/SKILL.md`, `skills/linkedin-content-creation/SKILL.md`, `skills/linkedin-strategy/SKILL.md`, `skills/linkedin-networking/SKILL.md`, `skills/linkedin-voice/SKILL.md`, `commands/onboarding.md`, `commands/setup.md`, `commands/linkedin.md`
- **Changes:** In `skills/linkedin-studio/SKILL.md`: rename "thought leadership plugin" → "LinkedIn Studio"; correct the agent table to the real count and route to `newsletter`/`headless-review`/`pivot`/`react`. Fix `onboarding.md` "25 commands" → 26; reconcile the pillar-count disagreement (`onboarding.md` 3-5 vs `setup.md` 5) to one number; add `headless-review`/`pivot` to the `linkedin.md` router tables + option list. (Final exact counts are set in Step 21; here, fix names/routing/contradictions.)
- **Reuses:** existing SKILL.md table format.
- **Test first:**
- File: n/a (docs) — Step 1 lint asserts agent/command counts
- Verifies: no "thought leadership"; router lists all commands
- Pattern: grep check
- **Verify:** `grep -rni 'thought leadership' skills/` → no matches; `grep -nc 'headless-review' commands/linkedin.md` → ≥ 1; pillar count consistent: `grep -rnE 'pillars|expertise areas' commands/onboarding.md commands/setup.md` shows one consistent number.
- **On failure:** revert — `git checkout -- skills/ commands/onboarding.md commands/setup.md commands/linkedin.md`
- **Checkpoint:** `git commit -m "docs(linkedin-studio): reconcile discoverability surfaces + skill naming"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- skills/linkedin-studio/SKILL.md
- commands/onboarding.md
- commands/linkedin.md
min_file_count: 3
commit_message_pattern: "^docs\\(linkedin-studio\\): reconcile discoverability"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/linkedin.md
pattern: "headless-review"
```
### Step 13: Resolve the 11 orphan agents (wire-or-delete, case-by-case)
- **Files:** `commands/video.md`, `commands/post.md`, `commands/ab-test.md`, `commands/calendar.md`, `commands/report.md`, `commands/analyze.md`, `commands/batch.md`, `commands/pipeline.md`, `commands/strategy.md`, `commands/outreach.md`, `commands/setup.md`, plus `CLAUDE.md`
- **Changes:** **DEFAULT: wire all 11 (no deletions) — each has a clear home, so the agent count stays 19** (plan-critic Major 3: the dispositions are fixed here, not deferred). Per agent, add `Task` to the command's `allowed-tools` AND a `subagent_type: linkedin-studio:<name>` call. The 11 orphans + disposition:
| # | Orphan agent | Disposition | Target command |
|---|---|---|---|
| 1 | `video-scripter` | wire | `video.md` (already says "delegate") |
| 2 | `content-optimizer` | wire | `post.md` + `ab-test.md` |
| 3 | `analytics-interpreter` | wire | `report.md` + `analyze.md` |
| 4 | `content-planner` | wire | `batch.md` + `pipeline.md` (already hold `Task`) |
| 5 | `trend-spotter` | wire | `batch.md` + `pipeline.md` |
| 6 | `network-builder` | wire | `outreach.md` |
| 7 | `strategy-advisor` | wire | `strategy.md` |
| 8 | `voice-trainer` | wire | `setup.md` (voice-profile building) |
| 9 | `post-feedback-monitor` | wire | `calendar.md` (publish action / 48h monitor) |
| 10 | `differentiation-checker` | wire (Step 14) | post/quick/react/carousel/video |
| 11 | `engagement-coach` | wire (Step 16) | `firsthour.md` |
Agents 1011 are wired in their dedicated steps; this step wires 19. If a wiring proves genuinely awkward at execution, the fallback is delete-that-agent (document it + decrement the count in Step 21) — but the default is wire-all → 19 agents. Use the namespaced `linkedin-studio:<name>` form; note the session-reload requirement.
- **Reuses:** the namespaced `subagent_type` contract; existing `Task` grants in batch/pipeline.
- **Test first:**
- File: n/a — Step 1 lint asserts agent count == `ls agents/*.md`
- Verifies: each remaining orphan is invoked ≥1; deleted ones removed from counts
- Pattern: the grep success-criterion
- **Verify:** for each agent NOT deleted: `grep -rl "subagent_type: linkedin-studio:<name>" commands/` → ≥1; `bash scripts/test-runner.sh` → exit 0 (counts consistent).
- **On failure:** revert — `git checkout -- commands/ agents/ CLAUDE.md` (and `git restore` any deleted agent)
- **Checkpoint:** `git commit -m "refactor(linkedin-studio): wire or delete 11 orphan agents (case-by-case)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/video.md
- CLAUDE.md
min_file_count: 2
commit_message_pattern: "^refactor\\(linkedin-studio\\): wire or delete 11 orphan agents"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/video.md
pattern: "subagent_type: linkedin-studio:video-scripter"
```
### Step 14: Short-form de-AI / differentiation gate
- **Files:** `commands/post.md`, `commands/quick.md`, `commands/react.md`, `commands/carousel.md`, `commands/video.md`, `hooks/prompts/voice-guardian.md`, `hooks/hooks.json` (regenerated)
- **Changes:** Wire the existing orphan `differentiation-checker` into the five short-form creation commands (add `Task` to `allowed-tools` + a `subagent_type: linkedin-studio:differentiation-checker` call). EXTEND (do not duplicate) `hooks/prompts/voice-guardian.md`'s existing AI-pattern section with the LinkedIn-named signals from research/01 D8 + research/03 D4 (personal substance, original thinking, concrete specifics, genuine voice; soft engagement-bait check: block mechanical-response CTAs, allow genuine questions). **No recompile (plan-critic Blocker 3): `voice-guardian.md` is loaded at runtime by `content-gatekeeper.mjs` — extending its prose changes nothing in `hooks.json`; `compile-hooks.py` is only needed when ADDING a new hook entry to the template, which this step does not do.** (No new agent — reuse the orphan; avoids the voice-guardian overlap the architecture-mapper flagged.)
- **Reuses:** `agents/differentiation-checker.md` (the orphan); `hooks/prompts/voice-guardian.md` (existing AI detection, runtime-loaded).
- **Test first:**
- File: `hooks/scripts/__tests__/linkedin-content-filter.test.mjs` (new) — verifies `isLinkedInContent` fires on the short-form content paths the gate guards
- Verifies: gate scope correct; differentiation-checker wired
- Pattern: `hooks/scripts/__tests__/state-updater.test.mjs`
- **Verify:** `grep -rl "subagent_type: linkedin-studio:differentiation-checker" commands/` → ≥1; `node --test hooks/scripts/__tests__/linkedin-content-filter.test.mjs` → passes; `python3 hooks/scripts/compile-hooks.py --check` → no drift.
- **On failure:** revert — `git checkout -- commands/ hooks/`
- **Checkpoint:** `git commit -m "feat(linkedin-studio): short-form de-AI gate via differentiation-checker + voice-guardian"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/post.md
- hooks/prompts/voice-guardian.md
- hooks/scripts/__tests__/linkedin-content-filter.test.mjs
min_file_count: 3
commit_message_pattern: "^feat\\(linkedin-studio\\): short-form de-AI gate"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/post.md
pattern: "differentiation-checker"
```
### Step 15: Video quality gate (captions + aspect guidance, not 9:16-mandatory)
- **Files:** `commands/video.md`, `references/linkedin-formats.md`, `references/algorithm-signals-reference.md`
- **Changes:** Apply research/03 D1-D3. In `video.md`: MP4 default (warn-only on MOV/AVI), within official upload limits; **enforce/strongly-recommend captions** (SRT or native auto-captions, labelled best-practice not "required"); aspect ratio as **guidance — 4:5 / 1:1 preferred for broad distribution, 9:16 mobile-only opt-in**. Fix the contradiction in `linkedin-formats.md` (the "4:5 deprioritized" line → "4:5 preferred"; remove "3-second hook" → "front-load value for muted autoplay"). Remove the "Vertical 9:16 gets distribution boost" + any "video maximizes reach" copy in `algorithm-signals-reference.md:75`/`video.md:198`; add a one-line "per-video reach declining; documents out-engage video" note.
- **Reuses:** research/03 §Recommendation; the orphan `video-scripter` (wired in Step 13).
- **Test first:**
- File: n/a (content) — grep check
- Verifies: no "9:16 required", no "3-second hook"; captions present
- Pattern: grep check
- **Verify:** `grep -niE 'must be 9:16|9:16 \(1080|3-second hook' commands/video.md references/linkedin-formats.md` → no matches; `grep -ni 'captions' commands/video.md` → ≥1; `grep -ni 'deprioritized' references/linkedin-formats.md` → no 4:5-deprioritized line.
- **On failure:** revert — `git checkout -- commands/video.md references/linkedin-formats.md references/algorithm-signals-reference.md`
- **Checkpoint:** `git commit -m "feat(linkedin-studio): video quality gate (captions + aspect guidance, drop 9:16 mandate)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/video.md
- references/linkedin-formats.md
min_file_count: 2
commit_message_pattern: "^feat\\(linkedin-studio\\): video quality gate"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/video.md
pattern: "4:5"
```
### Step 16: First-hour / reply-loop command with tracked state
- **Files:** `commands/firsthour.md` (new), `config/state-file.template.md`, `hooks/scripts/state-updater.mjs`, `hooks/scripts/__tests__/state-updater.test.mjs`, `CLAUDE.md`
- **Changes:** Add a wired first-hour/reply-loop command that invokes `engagement-coach` (+ `post-feedback-monitor`) with tracked state: a target list, draft comments, and a timestamped first-hour plan persisted in `~/.claude/linkedin-studio.local.md`. Add additive scalar fields / a non-`R`-initial section to `config/state-file.template.md` and a pure mutation function in `state-updater.mjs` mirroring `updatePostTracking`. Register the command in CLAUDE.md.
- **Reuses:** `commands/react.md` structure; `agents/engagement-coach.md`; `state-updater.mjs` `updatePostTracking` pattern; the additive-state contract.
- **Test first:**
- File: `hooks/scripts/__tests__/state-updater.test.mjs` (extend) — verifies the new mutation fn is additive (missing field → graceful default)
- Verifies: first-hour state round-trips; existing fields untouched
- Pattern: existing state-updater tests
- **Verify:** `grep -rl "subagent_type: linkedin-studio:engagement-coach" commands/` → ≥1; `node --test hooks/scripts/__tests__/state-updater.test.mjs` → passes; `bash scripts/test-runner.sh` → exit 0 (command count updated).
- **On failure:** revert — `git checkout -- commands/firsthour.md config/state-file.template.md hooks/scripts/ CLAUDE.md`
- **Checkpoint:** `git commit -m "feat(linkedin-studio): first-hour/reply-loop command with tracked state"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/firsthour.md
- config/state-file.template.md
- hooks/scripts/state-updater.mjs
min_file_count: 3
commit_message_pattern: "^feat\\(linkedin-studio\\): first-hour/reply-loop command"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/firsthour.md
pattern: "engagement-coach"
```
### Step 17: Honest newsletter-distribution + profile-SEO + outreach pipeline surfaces
- **Files:** `commands/newsletter.md`, `commands/profile.md`, `commands/outreach.md`, `references/linkedin-growth-playbook-2025-2026.md`
- **Changes:** Apply research/03 D5. Newsletter distribution surface = the **honest** version: "bypasses organic feed ranking via ONE deduplicated notification per subscriber per edition (NOT triple)"; one-time launch-blast + a **~12K follower floor** (frame: wait until you can spend the blast); realistic cold-start floors (0100 subs months 13); disclose non-export / no-canonical / no-read-analytics / per-subscriber decay. Add profile-SEO fields to `profile.md` (headline-as-search-field, per-section keyword targets). Add tracked contact/pipeline state to `outreach.md` (or reference the state added in Step 16's pattern).
- **Reuses:** research/03 D5; the state pattern from Step 16.
- **Test first:**
- File: n/a (content) — grep check
- Verifies: no "triple notification"; follower floor present
- Pattern: grep check
- **Verify:** `grep -niE 'triple.notification' commands/newsletter.md` → no matches; `grep -niE 'deduplicated|follower floor|1[-]?2K|bypasses.*feed' commands/newsletter.md` → ≥1; `grep -ni 'headline' commands/profile.md` → ≥1.
- **On failure:** revert — `git checkout -- commands/newsletter.md commands/profile.md commands/outreach.md references/linkedin-growth-playbook-2025-2026.md`
- **Checkpoint:** `git commit -m "feat(linkedin-studio): honest newsletter distribution + profile-SEO + outreach pipeline"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/newsletter.md
- commands/profile.md
min_file_count: 2
commit_message_pattern: "^feat\\(linkedin-studio\\): honest newsletter distribution"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/newsletter.md
pattern: "deduplicated"
```
### Step 18: Promote post-feedback-monitor off Haiku
- **Files:** `agents/post-feedback-monitor.md`, `CLAUDE.md`
- **Changes:** Change `model: haiku``model: opus` (the lone non-Opus/Sonnet deviation; human-facing real-time coaching; contradicts the standing Opus-default). Update the model column in the CLAUDE.md agent table.
- **Reuses:** existing agent frontmatter.
- **Test first:**
- File: n/a (frontmatter) — grep check
- Verifies: no Haiku agent remains
- Pattern: grep check
- **Verify:** `grep -rl 'model: haiku' agents/` → no matches; `grep -n 'post-feedback-monitor' CLAUDE.md` shows Opus.
- **On failure:** revert — `git checkout -- agents/post-feedback-monitor.md CLAUDE.md`
- **Checkpoint:** `git commit -m "fix(linkedin-studio): promote post-feedback-monitor to Opus (Opus-default)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- agents/post-feedback-monitor.md
- CLAUDE.md
min_file_count: 2
commit_message_pattern: "^fix\\(linkedin-studio\\): promote post-feedback-monitor to Opus"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: agents/post-feedback-monitor.md
pattern: "model: opus"
```
### Step 19: Newsletter multi-session/effort banner
- **Files:** `commands/newsletter.md`
- **Changes:** Add a banner at the top of the newsletter command: "This is a multi-session, multi-gate, ~N-hour process (16 phases)." Set realistic expectations before the user starts.
- **Reuses:** existing newsletter.md header.
- **Test first:**
- File: n/a (content) — grep check
- Verifies: banner present
- Pattern: grep check
- **Verify:** `grep -niE 'multi-session|multi-gate|16 phases|~[0-9].*hour' commands/newsletter.md` → ≥1 near the top.
- **On failure:** revert — `git checkout -- commands/newsletter.md`
- **Checkpoint:** `git commit -m "docs(linkedin-studio): add multi-session/effort banner to newsletter"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- commands/newsletter.md
min_file_count: 1
commit_message_pattern: "^docs\\(linkedin-studio\\): add multi-session/effort banner"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: commands/newsletter.md
pattern: "multi-session"
```
### Step 20: Measure long-form review-pass overlap + trim
- **Files:** `docs/remediation/overlap-measurement.md` (new, committed evidence), and any agent/command trimmed as a result (e.g. `commands/newsletter.md`, an agent merged/removed)
- **Changes:** On an **in-repo fixture edition** (NOT a read of `maskinrommet/serier` — cross-repo needs explicit instruction), run the long-form review agents and record what each catches in a committed comparison table (per the operator's "trim-for-quality, measured-not-assumed" decision). Trim redundancy ONLY where the measurement shows it (merge/remove a gate that catches nothing the others don't); if the measurement justifies the redundancy, record that and keep it. Commit the measurement as evidence.
- **Reuses:** the existing `agents/fixtures/*-cases.md` fasit edition as the fixture; the long-form review agents.
- **Test first:**
- File: n/a (measurement artifact)
- Verifies: measurement committed; any trim is justified by it
- Pattern: n/a
- **Verify:** `test -f docs/remediation/overlap-measurement.md` → exists; the file contains a per-reviewer catch table; if any gate was removed, `bash scripts/test-runner.sh` → exit 0 (counts consistent).
- **On failure:** skip — if the fixture is insufficient to measure, record "measurement inconclusive; redundancy retained pending a real edition" and do not trim.
- **Checkpoint:** `git commit -m "docs(linkedin-studio): measure long-form review-pass overlap, trim where unjustified"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/remediation/overlap-measurement.md
min_file_count: 1
commit_message_pattern: "^docs\\(linkedin-studio\\): measure long-form review-pass overlap"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: docs/remediation/overlap-measurement.md
pattern: "catch"
```
### Step 21: Version bump + counts + three-doc + CHANGELOG reconciliation
- **Files:** `.claude-plugin/plugin.json`, `README.md`, `CLAUDE.md`, `CHANGELOG.md`, `../../README.md` (root marketplace), `../../.claude-plugin/marketplace.json`, `STATE.md`
- **Changes:** Bump the version → **4.0.0** (breaking: reinstall for new state/wired agents). Counts are now determinable (plan-critic Major 7): Step 13 wires all 11 orphans with **no deletions → 19 agents**; Step 16 adds `firsthour`**27 commands** (26 + 1); **25 reference docs · 6 skills · 9 hooks · 16 newsletter phases**. (If execution deletes any orphan per Step 13's documented fallback, decrement here — but the default is the fixed set above.) **Always recompute from `ls` at execution as the source of truth** (`ls agents/*.md | wc -l`, etc.) and reconcile to that. Update the three doc levels in the same change (plugin README, plugin CLAUDE.md, root README), the README badges, CHANGELOG (Keep-a-Changelog entry summarizing Phase 03), and the marketplace.json. `grep` the old version → 0 stale. The Step 1 lint asserts version + count consistency.
- **Reuses:** the version-sync memory (grep old version, update all); the lint's version/count asserts.
- **Test first:**
- File: n/a — Step 1 lint is the consistency check
- Verifies: version consistent everywhere; counts match `ls`
- Pattern: the lint version-grep
- **Verify:** `grep -rn "3\\.1\\.0" --include=*.json --include=*.md . | grep -v CHANGELOG | grep -v docs/remediation` → no stale hits; `bash scripts/test-runner.sh` → exit 0; counts in README == CLAUDE.md == root README.
- **On failure:** revert — `git checkout -- .claude-plugin/ README.md CLAUDE.md CHANGELOG.md ../../README.md ../../.claude-plugin/marketplace.json STATE.md`
- **Checkpoint:** `git commit -m "release(linkedin-studio): v4.0.0 — audit remediation (Phase 0-3), counts + three-doc sync"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- .claude-plugin/plugin.json
- README.md
- CLAUDE.md
- CHANGELOG.md
min_file_count: 4
commit_message_pattern: "^release\\(linkedin-studio\\): v4\\.0\\.0"
bash_syntax_check: []
forbidden_paths: []
must_contain:
- path: CHANGELOG.md
pattern: "4.0.0"
```
## Alternatives Considered
| Approach | Pros | Cons | Why rejected |
|----------|------|------|--------------|
| Build features as the audit sketched them (enforce 9:16; "triple-notification" surface; "cannot auto-publish" boundary; saves untrackable) | Less research; matches the audit | Research refuted all four premises — would ship NEW false claims, the exact disease being treated | Rejected — built on corrected premises instead |
| New Opus de-AI agent (per convention-scanner) | Matches the 6-gate Opus cluster | `voice-guardian.md` already does AI-detection + the brief's success-criterion greps `differentiation-checker` → a new agent duplicates logic and misses the criterion | Rejected — wire the existing orphan + extend voice-guardian |
| Single mega-release vs phased | One reinstall | Huge unreviewable diff; harder to bisect | Phased per §9, one Voyage session per phase, lint guards each |
| Reconcile stats by editing each file independently | Simpler per-file | 40 files restate numbers → guaranteed to miss one | Rejected — substrate-first + enumerate-by-grep + cite-not-restate; the lint stat-grep guards against *contradiction* (it cannot prove every citer cites-vs-restates, so completeness is also spot-checked at Step 3) |
## Test Strategy
- **Framework:** `node:test` (`.test.mjs` / `.test.ts`, glob form `node --test path/*.test.mjs` — Node 25 directory mode is broken); bash for the structural lint; fixture-shape tests for agents.
- **Existing patterns:** `hooks/scripts/__tests__/state-updater.test.mjs` (pure-module), `scripts/analytics/tests/storage.test.ts` (tmpdir + env override), `agents/__tests__/*-fixture.test.mjs` (shape-only, never self-certify live output).
- **New tests in this plan:** `scripts/analytics/tests/storage-root.test.ts` (CWD-fix, red-first), `hooks/scripts/__tests__/personalization-score.test.mjs` (placeholder sentinel), `hooks/scripts/__tests__/linkedin-content-filter.test.mjs` (gate scope), state-updater extension (first-hour additive state). The rebuilt `test-runner.sh` is the cross-cutting drift gate.
### Tests to write
| Type | File | Verifies | Model test |
|------|------|----------|------------|
| Unit | `scripts/analytics/tests/storage-root.test.ts` | default root anchors on plugin marker; env override | `scripts/analytics/tests/storage.test.ts` |
| Unit | `hooks/scripts/__tests__/personalization-score.test.mjs` | placeholder→0 voice pts; real→pts | `state-updater.test.mjs` |
| Unit | `hooks/scripts/__tests__/linkedin-content-filter.test.mjs` | gate fires on short-form content paths only | `state-updater.test.mjs` |
| Lint | `scripts/test-runner.sh` | exit 0 healthy; exit 1 on agent add/remove | existing sections |
## Risks and Mitigations
| Priority | Risk | Location | Impact | Mitigation |
|----------|------|----------|--------|------------|
| Critical | Voice move breaks ~15 readers if not atomic | `assets/voice-samples/` + 15 readers | content commands break | Step 5 lands placeholder + sentinel-detector + all readers in one commit |
| Critical | Placeholder mis-detected → false "Voice ✓" | `personalization-score.mjs:23` | adopter writes in stock voice, told done | sentinel check (not `[Your Name]`); test both cases |
| Critical | Stat reconciliation misses a file | ~40 citers | contradiction persists | substrate-first + cite-not-restate + Step 1 lint stat-grep proves completeness |
| High | Lint unguarded until rebuilt | `scripts/test-runner.sh` | later steps ship unguarded | Step 1 is FIRST |
| High | CWD bug writes data to wrong dir even with deps | `storage.ts:17-22` | analytics silently wrong | anchor on `.claude-plugin/`; red-first test |
| High | Orphan wire uses bare type / no reload | `commands/*` | Task fails at runtime | namespaced `linkedin-studio:<name>`; note reload; lint asserts count |
| Medium | Hook edit without recompile → stale runtime | `hooks/hooks.json` | gate doesn't fire | always run `compile-hooks.py`; lint wires `--check` |
| Medium | State change non-additive → orphans 2 shipped editions | `state-updater.mjs`, templates | editions break | additive scalar/non-`R` section only; preserve `extractField||default` |
| Medium | Series-default change mis-routes new editions | `newsletter.md`, `edition-state.template.json` | wrong write path | keep `${LTL_SERIES_ROOT:-…}` override; neutral default; shipped editions are outside the repo |
| Low | Auto-publish boundary re-states a new false claim | `README.md` | dishonest again | research/02 wording: "possible, deliberately not built" — never "cannot" |
## Assumptions
| # | Assumption | Why unverifiable | Impact if wrong |
|---|-----------|-----------------|-----------------|
| 1 | Final command count after Steps 13/16 = 26 ± deletions + 1 new (`firsthour`) | depends on per-agent wire/delete decisions made at execution | counts in Step 21 adjust; lint catches drift |
| 2 | The in-repo fasit fixture is rich enough to measure review-pass overlap (Step 20) | the fixture is Del-4-scoped, not a full edition | Step 20 on-failure: record "inconclusive, redundancy retained" |
| 3 | ToS permits documenting auto-publish as "possible but not built" | LinkedIn Terms language not re-read first-hand | finalize-time check before Step 10 wording locks |
| 4 | Each phase = one Voyage `/trekcontinue` session | execution cadence | adjust session grouping in the Execution Strategy |
## Verification
*(Per-step manifests verify each step during execution. These are the end-to-end integration checks.)*
- [ ] `bash scripts/test-runner.sh; echo $?``0` (lint green on final state)
- [ ] `grep -rnE '40-50%|25-40%|1\.92%|360Brew|January 2026|-40-60%' references/ commands/ skills/ hooks/prompts/ CLAUDE.md README.md | grep -v algorithm-signals-reference` → no stale contradiction
- [ ] `grep -rIn '/Users/ktg' config/ commands/ render/ agents/ | grep -v '.local'` → no private path in shipped files
- [ ] `grep -rIn 'Kjell Tore' assets/ .claude-plugin/plugin.json` → no PII in shipped files
- [ ] `grep -rni 'independently reviewed\|thought leadership\|cannot auto-publish\|triple.notification' README.md skills/ commands/` → no matches
- [ ] from `/tmp`: analytics CLI runs without `ERR_MODULE_NOT_FOUND` and writes to `<plugin>/assets/analytics`
- [ ] for each non-deleted orphan: `grep -rl "subagent_type: linkedin-studio:<name>" commands/` → ≥1
- [ ] `node --test hooks/scripts/__tests__/*.test.mjs` and `cd scripts/analytics && npm test` → all pass
- [ ] `grep -rn "3\.1\.0"` (excluding CHANGELOG + docs/remediation) → no stale version
- [ ] counts identical across plugin README, plugin CLAUDE.md, root README
## Estimated Scope
- **Files to modify:** ~5560 (heavy on references/ + commands/ propagation in Step 3)
- **Files to create:** ~7 (placeholder template, 3 test files, firsthour command, overlap-measurement, voice .template)
- **Complexity:** high (breadth + coupled criticals + 4 corrected feature premises), but each step is small and lint-guarded
## Execution Strategy
*21 steps grouped into 7 sessions across 5 waves. One phase ≈ one Voyage `/trekcontinue` session.*
### Session 1: Guard + substrate (Phase 0a)
- **Steps:** 1, 2, 3
- **Wave:** 1
- **Depends on:** none
- **Scope fence:** Touch: `scripts/test-runner.sh`, `references/`, the Step-3 citers, `hooks/prompts/content-quality-gate.md`. Never touch: `scripts/analytics/`, `assets/voice-samples/`.
### Session 2: CLI + voice + correctness (Phase 0b)
- **Steps:** 4, 5, 6, 7
- **Wave:** 2
- **Depends on:** Session 1 (lint must guard)
- **Scope fence:** Touch: `scripts/analytics/`, `assets/voice-samples/`, `personalization-score.mjs`, `commands/ab-test.md`, `commands/report.md`, `commands/setup.md`, `.claude-plugin/plugin.json`. Never touch: `references/algorithm-signals-reference.md`.
### Session 3: Generalize (Phase 1a)
- **Steps:** 8, 9
- **Wave:** 3
- **Depends on:** Session 1
- **Scope fence:** Touch: `config/`, `render/`, the long-form agents, `commands/newsletter.md`. Never touch: short-form commands.
### Session 4: Honesty + discoverability (Phase 1b)
- **Steps:** 10, 11, 12
- **Wave:** 3 (parallel with Session 3 — disjoint files)
- **Depends on:** Session 1
- **Scope fence:** Touch: `README.md`, `skills/`, `commands/onboarding.md`/`setup.md`/`linkedin.md`/`calendar.md`/`report.md`/`import.md`. Never touch: `config/`, `render/`, `agents/`.
### Session 5: Orphans + gates (Phase 2a)
- **Steps:** 13, 14, 15, 18
- **Wave:** 4
- **Depends on:** Sessions 14
- **Scope fence:** Touch: `commands/` (short-form + video), `hooks/prompts/voice-guardian.md`, `agents/` (wire/delete + post-feedback-monitor), `references/linkedin-formats.md`. Never touch: `README.md` counts (Step 21 owns).
### Session 6: Feature surfaces (Phase 2b)
- **Steps:** 16, 17
- **Wave:** 4 (parallel with Session 5 — mostly disjoint; coordinate `commands/newsletter.md`/`profile.md`)
- **Depends on:** Sessions 14
- **Scope fence:** Touch: `commands/firsthour.md`, `config/state-file.template.md`, `state-updater.mjs`, `commands/newsletter.md`/`profile.md`/`outreach.md`. Never touch: short-form gate files.
### Session 7: Long-form earn + release (Phase 3 + cross-cutting)
- **Steps:** 19, 20, 21
- **Wave:** 5
- **Depends on:** all prior
- **Scope fence:** Touch: `commands/newsletter.md`, `docs/remediation/overlap-measurement.md`, all version/count/doc files. Never touch: anything not yet committed by earlier sessions.
### Execution Order
- **Wave 1:** Session 1
- **Wave 2:** Session 2 (after Wave 1)
- **Wave 3:** Sessions 3 + 4 (parallel, after Wave 1)
- **Wave 4:** Sessions 5 + 6 (parallel, after Waves 23)
- **Wave 5:** Session 7 (after all)
### Grouping rules applied
- Steps sharing files → same session (the algorithm-stat propagation is all in Session 1; newsletter touches coordinated across Sessions 6/7).
- Independent modules → separate sessions (CLI vs voice vs generalize).
- The lint (Step 1) gates everything → Wave 1 alone.
- Release reconciliation (Step 21) → last, captures all prior counts.
## Plan Quality Score
| Dimension | Weight | Score | Notes |
|-----------|--------|-------|-------|
| Structural integrity | 0.15 | 90 | dependency-ordered; lint-first; release-last; Step 13/21 count-determinism fixed |
| Step quality | 0.20 | 86 | real paths+lines; Step 4 re-diagnosed, Step 3 enumerate-by-grep; doc-heavy Step 3/13 inherently broad |
| Coverage completeness | 0.20 | 90 | every brief criterion mapped + in Verification (scope-guardian ALIGNED); missing 360Brew citers added |
| Specification quality | 0.15 | 88 | concrete greps; manifests on all steps; "decide at execution" removed from Step 13 |
| Risk & pre-mortem | 0.15 | 90 | coupled criticals sequenced; second voice-writer + analytics-misdiagnosis now caught |
| Headless readiness | 0.10 | 88 | On-failure + Checkpoint per step; Step 13 dispositions fixed; Step 20 honest on-failure |
| Manifest quality | 0.05 | 86 | all steps have manifests; false `hooks.json` entry removed from Step 3 |
| **Weighted total** | **1.00** | **88** | **Grade: A (post-revision)** |
**Adversarial review:**
- **Plan critic:** REVISE → addressed. Initial pass found 3 blockers + 7 majors (Grade C, 73); all 3 blockers and the actionable majors fixed in the Revisions below. The three blockers were genuine and load-bearing (analytics misdiagnosis, incomplete stat-propagation file-list, false hook-recompile premise) — exactly the value an independent pass adds.
- **Scope guardian:** ALIGNED — all 21 brief success criteria map to a step and appear in Verification; all four refined-by-research criteria honored (video, newsletter, boundaries, saves); all non-goals respected; every cited path verified to exist. 2 low-risk creep items (Step 18 Opus-promotion, Step 8 de-branding), both justified by Constraints/Intent.
## Revisions
*Added by adversarial review (Phase 9). Plan-critic REVISE → addressed.*
| # | Finding | Severity | Resolution |
|---|---------|----------|------------|
| 1 | Step 4 misdiagnosed the analytics crash: `getAnalyticsRoot()` is `__dirname`-based (CWD-independent) and bypassed by the commands' explicit `ANALYTICS_ROOT` override; the real fresh-clone crash is missing `tsx`/`csv-parse` | blocker | Step 4 reframed: PRIMARY = `npm install` surfacing (the actual crash); the depth-anchor is now a SECONDARY latent-correctness fix; Verify installs before running |
| 2 | Step 3's Verify greps all of `commands/` for 360Brew/penalty but the file-list omitted `audit.md`/`strategy.md`/`linkedin.md`/`topic-rotation-gate.md` → step fails its own gate; propagation incomplete | blocker | Step 3 now enumerates the citer set by grep at execution (not a stale hand-list) + added the four missing files; Verify excludes the substrate file from the no-match grep |
| 3 | Step 3 & 14 claimed editing a runtime-loaded prompt requires `compile-hooks.py` + regenerates `hooks.json` — false (those prompts are read at runtime; only ADDING a template entry needs recompile) | blocker | Removed the recompile/`hooks.json` claims from Step 3 and Step 14 (+ dropped `hooks.json` from Step 3's manifest) |
| 4 | Step 5 not atomic: `onboarding.md:142` APPENDS the user's voice below the placeholder → sentinel survives, score stays 0 after population; `setup.md:28` detection table left stale | major | Step 5 now fixes BOTH writers (setup.md:99 + onboarding.md:142 → overwrite) and the setup.md:28 table |
| 5 | "No-PII" NFR ambiguous re: `LICENSE` / historical docs | major | Step 5 clarified: `LICENSE` author name is the required MIT copyright holder (intentional, not scrubbed); the leak scope is the voice profile + `plugin.json` author field |
| 6 | Step 13 deferred 3 agents' wire/delete "to execution"; 11 orphans never enumerated; non-determinism propagated to Step 21 counts | major | Step 13 now has an explicit 11-row disposition table (DEFAULT: wire all → 19 agents, no deletions); Step 21 counts are consequently determinable (19 agents · 27 commands) |
| 7 | Step 4 Verify ran `npm test` without `npm install` first → fails on the fresh-clone scenario | major | Verify now installs first |
| 8 | Step 1 over-claimed the stat-grep "proves propagation completeness" (it only detects contradiction); restore step not failure-safe | major→minor | Softened the Alternatives claim (guards contradiction; completeness spot-checked) + made the Step 1 Verify restore self-running regardless of `set -e` |
| 9 | "skrivekontrakt §C2 does not ship" neither fixed nor dropped; Step 9 verify weak | minor | Step 9 now makes the §C2 reference fall back to an in-tree checklist + adds a positive language-threading assertion |
| — | `commands/report.md` edited in Steps 7/10/13 | minor (note) | Execution note: later edits to `report.md` must preserve the Step 7 saves-honesty wording (the cross-cutting Verify greps for it) |
## Adversarial Pass 2 (gemini-bridge, v5.1.1 high-effort)
An independent Gemini Deep Research pass (~16 min, 24 sources) stress-tested the five
load-bearing decisions. It surfaced **two genuine blind spots the local reviewers and the
author shared**, both now folded in, plus three points already substantially covered.
**Folded in (real):**
1. **Git history ≠ gitignore (Decision 3).** `.gitignore` does not remove the already-committed
real voice profile from past commits. → Step 5 now records an explicit, proportionate
decision: this is the author's own *attributed* open-source plugin (name already public in
LICENSE/README/Forgejo), so the historical file is attributed authorship, not a leaked
secret; the bug is the adopter-default, which placeholder+gitignore fixes; **no force-push
history rewrite** (disproportionate + disruptive for an open-source plugin with forks).
2. **Lint stat-grep chicken-and-egg (Decision 1).** Putting the stat-consistency grep in the
Step-1 lint would make Step 1 fail its own Verify (contradictions persist until Step 3). →
Step 1 now rebuilds the STRUCTURAL lint only (already-broken, goes green immediately); the
stat-grep moves to Step 3 (after reconciliation), the version-grep to Step 21.
**Already covered (Gemini's caveats map to existing plan content):**
3. **Downgrade-to-direction still asserts a mechanism (Decision 2)** → the per-claim
**Source + Confidence column** (Step 2) IS the "Unverified/Illustrative" labeling Gemini
prescribes; honest `low`/`unverified` confidence values satisfy the integrity concern.
First-party benchmarking is out of scope (operator runs no benchmarks).
4. **Prompt-bloat / single-responsibility on the de-AI gate (Decision 4)** → the PRIMARY de-AI
mechanism is already an **isolated agent** (`differentiation-checker`, invoked via `Task` =
fresh context window — exactly Gemini's "isolated subagent" prescription); the
`voice-guardian` touch is a minimal prose extension to an existing runtime prompt, kept small.
5. **i18n scope (Decision 5)** → the goal is **removing the Norwegian lock so one other operator
can run it in their language**, NOT a multi-language-output i18n framework. Step 9 keeps the
lighter touch (language as a configurable input to the review agents) and avoids
"configuration as the new hardcoding" by defaulting cleanly; full i18n/pseudo-localization is
deliberately out of scope (no second-language *output* requirement).
**Verdict:** Pass 2 strengthened the plan on two real axes (history decision + lint sequencing)
without expanding scope. No finding overturned the approach; the phasing and decisions hold.

View file

@ -0,0 +1,310 @@
---
type: trekresearch-brief
created: 2026-05-29
question: "What does the 2026 LinkedIn feed-ranking system actually reward — comment-vs-reaction weighting, document/carousel engagement rate, external-link reach effect and first-comment status, the early-engagement window incl. delayed reinjection, and the deployed ranking model's verifiable name and date — with a source and confidence per claim?"
confidence: 0.82
dimensions: 8
mcp_servers_used: [tavily, gemini-deep-research]
local_agents_used: []
external_agents_used: [docs-researcher, community-researcher, security-researcher, contrarian-researcher, gemini-bridge]
---
# 2026 LinkedIn Feed-Ranking — Canonical Signal Statement
> Generated by trekresearch (high-effort swarm: 4 external + Gemini) on 2026-05-29.
> Topic 1 of 3 for the linkedin-studio remediation. This is the **substrate**: the
> Phase-0 fixes that reconcile the plugin's contradictory algorithm stats consume it.
## Research Question
What does the 2026 LinkedIn feed-ranking system actually reward — comment-vs-reaction
weighting, document/carousel engagement rate, external-link reach effect and the
current first-comment-workaround status, the early-engagement ("golden hour") window
incl. delayed/evergreen reinjection, and the deployed ranking model's verifiable name
and deployment date — with a primary or credible source and a confidence level per claim?
## Executive Summary
The plugin's algorithm "facts" are **directionally right but numerically indefensible**:
every specific magnitude it states (comment "15x", carousel "6.6%"/"1.92%", link
"40-50%"/"25-40%", a clean "40-60% before distribution", "360Brew, January 2026") is
either third-party-only, self-contradictory, conflated across denominators, or — for the
model name/date — **not establishable from any primary source.** What IS defensible and
high-confidence: an LLM-based relevance-ranking system is live in 2026; the engagement
hierarchy is **saves > shares > quality comments > reactions** with **dwell-time a
top-tier signal** (the only two signals LinkedIn officially confirms by name are *dwell
time* and *topic/interest relevance*); documents/carousels are the #1 format; body links
reduce reach (magnitude contested, ~1960% across studies, LinkedIn denies it is
*intentional*); the early window is **6090 min** (90 is the 2026 consensus); and — the
single best-supported actionable finding — **LinkedIn now officially suppresses generic
AI "slop"** (named executive, May 2026), which directly justifies a short-form de-AI gate.
**Key caveat:** treat every number as directional and per-account-testable; encode
*ordering + sourced direction*, never hard coefficients. (Overall confidence 0.82 — high
on direction, medium on magnitude.)
## Dimensions
### D1. Deployed ranking model — name & date — Confidence: high (on the negative claim)
**External findings:**
- The arXiv paper *"360Brew: A Decoder-only Foundation Model…"* (2501.16450) is dated
**2025-01-27**, self-labels as a **"research pre-production model" (V1.0, 150B params)**
claiming *offline* parity only, and was **withdrawn 2025-08-23** (submitter lacked
license rights). It is neither a deployment announcement nor a clean citable artifact.
[arXiv 2501.16450]
- LinkedIn's own 2026 communications describe a live LLM-based feed system but the
**production name is not reliably establishable**: the docs + contrarian agents both
read a LinkedIn Engineering post ("Generative Recommender / GR", attributed to Hristo
Danchev, 2026-03-12); the independent Gemini pass **flagged a third-party citation of
that same post as possibly fabricated** (Danchev's verifiable authorship is on AWS
OpenSearch work). So even the "GR" name carries a provenance question.
- "January 2026" as a deployment date appears in **no** primary source; it is third-party
extrapolation from the paper's Jan-**2025** date.
**Contradictions:** docs/contrarian treat the GR engineering blog as primary; Gemini
casts doubt on its provenance. **Conservative resolution:** assert neither name nor date.
An LLM relevance-ranking system is live (high confidence); its *deployed name* and
*go-live date* are **not publishable as fact**.
### D2. Comment vs reaction weighting + saves/dwell hierarchy — Confidence: high (ordering) / medium (magnitude)
**External findings:**
- "Comment = 15x a like" is **unverified folklore** — no primary source; meet-lea labels
it "industry estimate, original source unclear." Sources span 2x15x with no anchor.
AuthoredUp's NLP-quality-scored analysis puts the real comment-vs-like effect **~2x**.
[authoredup.com/blog/linkedin-algorithm; meet-lea]
- Convergent across AuthoredUp + Vertebrae + van der Blom (1.8M): **a save ≈ 5x a like,
≈ 2x a comment** — saves are the top signal (and a follow-graph signal: saving a post
gives the author's next post ~80% feed-appearance odds). The plugin's stray "5x" is the
**saves** number mis-assigned to comments.
- **Officially confirmed (the only two named):** *dwell time* is a ranking signal
(LinkedIn Eng "Understanding feed dwell time" 2020; "Leveraging Dwell Time" /
Auto-Normalized-Long-Dwell model 2024); LinkedIn describes active (like/comment/share)
vs passive (click/skip/long-dwell) tasks but **assigns no weights**. [linkedin.com/blog/engineering/feed/leveraging-dwell-time-to-improve-member-experiences-on-the-linkedin-feed]
**Resolution (for the canonical statement):** order is **saves > shares > quality
comments > reactions/likes**, with **dwell-time top-tier**; comment ≈ 2x like
(quality-weighted, single-vendor). Drop "15x" and the comment-"5x" entirely.
### D3. Document/carousel engagement rate — Confidence: high (format rank) / medium (number)
**External findings:**
- Three independent large-N studies agree documents/carousels are **#1**: Socialinsider
(1.3M) native document **7.00%** (multi-image 6.80%), Buffer (2M) carousel **21.77%**
median, Metricool (673K) **49.52%**. The 7 vs 21.77 vs 49.52 spread is a
**denominator/methodology artifact**, not disagreement about the winner.
[socialinsider.io/social-media-benchmarks/linkedin; buffer.com/resources/data-best-content-format-social-media/; metricool.com/linkedin-trends/]
- The "6.6%" is a **stale 2024 multi-image** figure (now ~6.45% multi-image / ~7.00%
document) — and LinkedIn removed native carousels Dec 2023, so "carousel" = PDF document
post; the multi-image↔document conflation is real.
- **The plugin's "1.92%" is NOT a carousel rate** — it matches the **personal-profile
per-post baseline** (Metricool personal 2.60% / company 1.74%; AuthoredUp 2.102.67%).
The plugin mixed a format benchmark with a personal-profile baseline.
**Resolution:** documents/carousels = top format (high confidence). For a number use
**~7% (Socialinsider, conservative, company-page per-impression)**; never present 1.92%
as a carousel figure; state the format-vs-account-type distinction.
### D4. External-link reach effect + first-comment status — Confidence: medium (effect) / low (intent, first-comment)
**External findings:**
- A body-link reach reduction is real and observational. The most rigorous source
(Ordinal, 900K posts, Mann-Whitney p<0.001) shows it **changed over time: 5% (2023) →
35% (2024) → 42% (2025) → ~38% (2026 YTD)**, 37-month avg 26.5%. van der Blom reports a
milder **~18.8% median**; DigitalApplied/Gemini cite **~60%**. So the plugin's "40-50%"
≈ the 2024-25 peak and "25-40%" ≈ the long-run average — **both partial views of one
moving number.** [tryordinal.com/blog/linkedin-link-penalty-study]
- **LinkedIn denies an *intentional* penalty** (Sr. Director Product, reported Aug 2025):
no penalty "if the post leads with value"; the effect is engagement-driven, not a flat
tax. The observed reach gap is real **regardless of intent**. [threads.com/@mattnavarra/post/DOWa_61Cown/]
- First-comment workaround is **genuinely contested**: Ordinal data leans "still
net-positive but reduced (~5 to 10%)"; multiple 2026 blogs claim it's now detected as
"bridge behavior" and throttled — but that claim is **practitioner-only, no large-N
backing.** The one officially-confirmed principle: what gets limited is
**off-platform-funnel intent + thin standalone value**, *regardless of link location*.
**Resolution:** state it as a **correlational reach reduction (~38% in 2026, contested
band ~1960%, LinkedIn disputes intent)**, not a hard penalty. Reframe first-comment as
**neither a magic fix nor a confirmed penalty** — lead with standalone value; native
formats are the durable answer. Drop the precise % from the enforcing hook.
### D5. Early-engagement window + evergreen reinjection — Confidence: high (60-90 min) / low (24-72h timing)
**External findings:**
- 2026 consensus has widened from "strict 60 min" to **6090 min** (90 is van der Blom's
current figure), with the **first 1530 min** the highest-leverage sub-window and ~70%
of reach decided in it. [buffer.com/resources/linkedin-algorithm/; expandi.io/blog/best-time-to-post-on-linkedin/]
- Evergreen resurfacing is **real in direction** (the 2026 relevance model resurfaces
strong-save / high-dwell posts days-to-weeks later on viewer intent; AuthoredUp: posts
now live 23 weeks vs days) — but **no large-N source confirms a specific "2472h
reinjection" rule**; it is intent-driven and irregular.
**Resolution:** "**6090 min golden window; first 1530 min highest-leverage**"; describe
evergreen as "**can resurface days-to-weeks later on intent-match**", not a fixed 2472h
second wave. The plugin both over-indexes the strict first hour AND omits evergreen — fix
both.
### D6. Profile/topic relevance as a ranking input — Confidence: high (signal) / none (the 40-60% figure)
**External findings:**
- **Officially confirmed (qualitatively):** topic/interest relevance drives distribution,
including beyond your network — Tim Jurka (Head of Feed AI, 2025-08-11): "Exceptional
content may even be distributed broadly … to members interested in the type of content
you post, even if they don't follow you." 2026 comms add an Interest Picker + "relevant
to your interests, not a popularity contest." [linkedin.com/pulse/how-does-linkedin-feed-work-tim-jurka-oxraf]
- **No primary source** states any **40-60% reach reduction** for off-topic content, nor
a discrete "validation-before-distribution gate" with a number. That figure is
third-party.
**Resolution:** keep "profile/topic alignment is a real ranking input" (sourced
direction); **drop the "40-60% before anyone sees it" figure** entirely.
### D7. Buzzword penalty — Confidence: high (that it is NOT a measured ranking mechanic)
**External findings:**
- **No primary source** ties specific words to a measured reach penalty. Evidence is
either editorial/clarity advice (Inc.) or unmeasured vendor assertion (linkboost
"LLMs throttle corporate speak"). A semantic-relevance ranker *may* indirectly favor
specific over generic phrasing — inferred, not confirmed. [inc.com/...buzzwords; linkboost.co/blog]
**Resolution:** keep buzzword-avoidance as **editorial guidance**, not a "reduces reach"
ranking claim. (The plugin already enforces a buzzword list via a hook — keep the list,
fix the *justification*.)
### D8. AI-content down-rank — Confidence: high (officially confirmed) — *the build-justifying finding*
**External findings:**
- **Officially confirmed, named executive:** LinkedIn VP & Executive Editor Laura
Lorenzetti (2026-05-19) confirmed an active program targeting (1) generic AI-written
posts/comments, (2) automation tools, (3) attention-bait video. Mechanism: ML models
trained on thousands of human-annotated posts distinguish "original thinking" from
"posts lacking substance"; **low-quality-flagged posts are reach-suppressed (reportedly
down to first-degree connections), not deleted.** [entrepreneur.com/business-news/linkedin-is-fighting-back-against-ai-slop-and-ai-comments]
- Corroborated: Jobanputra (Feed) — "we actively detect and limit the reach of spammy or
low-quality content, including bot-generated posts." Originality.ai (8,795 posts):
likely-AI posts saw **45% less engagement** (correlational). [prdaily.com/...guardians-of-the-feed; originality.ai/blog/ai-content-published-linkedin]
- Also officially confirmed and relevant: **engagement-pod crackdown** (VP Product
Gyanda Sachdeva, 2026-02-16 — auto-comments demoted out of "Most Relevant", scoped to
own network, repeat offenders restricted). [socialmediatoday.com/news/linkedin-outlines-more-measures-to-combat-engagement-pods/812290/]
**Resolution:** **build the short-form de-AI / differentiation gate** — it targets an
officially-confirmed suppression surface. Enforce the signals LinkedIn *named* (personal
substance, original thinking, concrete specifics, genuine voice), not an unverified SEO
"tell-list."
## External Knowledge
### Best Practice (official / primary)
Only two ranking signals are officially named: **dwell time** and **topic/interest
relevance**. LinkedIn officially **denies an intentional link penalty** and officially
**confirms an AI-slop down-rank** + **engagement-pod enforcement**. Everything else
(coefficients, multipliers, windows) is third-party.
### Alternatives / contrarian
The contrarian pass refuted 6 of 7 plugin claims **on magnitude/naming, not direction**:
the strategic advice (favor native formats, prompt quality comments, write with
substance, expect link posts to underperform, post when the audience is active) survives;
the specific numbers and the "360Brew, Jan 2026" branding do not. Two need **outright
correction**: the model name/date, and the "no analytics API → CSV only" premise (see D9
in Topic 2 — Member Post Analytics API launched 2025-07-08).
### Known issues
Numbers rot: every magnitude is observational and moves year-to-year (link penalty
5%→42%→38%; carousel 6.6%→6.45%). A fabricated citation ("Hristo Danchev / Mar-12-2026")
is actively circulating — do not propagate any single named-source deployment claim
without first-hand re-verification.
## Gemini Second Opinion
Independent ~22-min deep-research pass (27 grounding sources). Agreements with the swarm:
360Brew is a Jan-**2025** pre-production paper, not a confirmed 2026 production system;
saves/dwell primacy; carousel #1 with methodology-driven rate spread; 90-min window;
**per-post Saves ARE visible in the native UI for your own posts**; a Member Post
Analytics API exists but is gated behind Community Management API approval (not
self-serve). Unique contribution: independently flagged the "Hristo Danchev / March 2026
engineering post" citation as likely **fabricated**, which is *why* this brief refuses to
publish any deployed-model name even though two of the swarm agents cited "GR."
## Synthesis
Three insights emerge only from triangulation:
1. **The plugin's contradictions are mostly denominator/era artifacts, not errors of
fact.** "40-50% vs 25-40%" = the same link number at peak vs average; "6.6% vs 1.92%"
= a format benchmark vs a personal-profile baseline; "15x vs 5x" = a folklore comment
figure vs the real *saves* figure mis-assigned. The fix is therefore **one canonical
statement that names the era, the denominator, and the account type** — not a hunt for
"the right number." This is the single most important design instruction for Phase 0.2.
2. **Encode ordering + officially-named signals, not coefficients.** The only durable,
defensible spine is: *dwell + topic-relevance are the two officially-named signals;
saves > shares > quality-comments > reactions is the engagement order; documents are
the top format.* Every coefficient must carry a source + confidence + "directional,
test per account" caveat. A `references/algorithm-signals-reference.md` rebuilt around
*named signals + ordering + per-claim source column* makes the contradictions
structurally impossible to reintroduce.
3. **The two highest-confidence findings each map to a Phase-2 build decision.** The
officially-confirmed **AI-slop down-rank** justifies the **short-form de-AI gate**
(D8); the officially-confirmed **link-intent principle** (value-first, location-
secondary) rewrites the link advice (D4). Both are now grounded in *named-executive*
sources, not vendor blogs — the strongest evidence in the whole pass.
## Open Questions
- **Deployed model name/date** — unresolvable from open sources and partly contaminated
by a fabricated citation. *Carry as: do not assert; state "an LLM relevance model is
live in 2026" only.* No further research will likely fix this before publication.
- **Link-penalty exact magnitude & first-comment status** — genuinely contested
(~1960%; first-comment net-positive vs detected). *Carry as a range + "test per
account"; do not hard-code.*
- **Member Post Analytics API self-serve depth** — answered enough here to act, but is the
primary subject of **Topic 2** (verify gating + saves-UI before writing boundary prose).
## Recommendation
For the Phase-0 "reconcile to one sourced statement" step, adopt this canonical spine and
make every command/agent cite it:
1. **Model:** "An LLM-based relevance-ranking system is live on LinkedIn in 2026." **No
name, no date.** Remove "360Brew" and "January 2026" from CLAUDE.md/README/profile.
2. **Signals (officially named):** dwell time; topic/interest relevance. **Engagement
order:** saves > shares > quality comments > reactions; likes ≈ 1x baseline. No
coefficients without a source column; comment ≈ 2x like is the most defensible single
figure (medium).
3. **Format:** documents/carousels are the top organic format (~7%, Socialinsider,
company-page per-impression). Delete the 1.92% carousel claim (it's a personal-profile
baseline). Native video #2 and *declining*.
4. **Links:** correlational reach reduction (~38% in 2026; contested ~1960%); LinkedIn
denies intentional penalty; value-first matters more than link location; first-comment
is a hedge, not a fix. Soften the enforcing hook from a hard % mechanic.
5. **Timing:** 6090 min early window (first 1530 min highest-leverage); add evergreen
resurfacing (days-to-weeks, intent-driven); drop the strict-60-min fixation and the
"2472h reinjection" precision.
6. **Profile/topic:** real ranking input (keep); **drop the 40-60% figure.**
7. **Buzzwords:** editorial guidance only (keep the list, fix the "reduces reach" claim).
8. **Build the de-AI gate** (D8, officially-confirmed surface) and **reframe link advice
around intent** (D4). Both are Phase-2 builds with named-executive backing.
## Sources
| # | Source | Type | Quality | Used in |
|---|--------|------|---------|---------|
| 1 | [arXiv 2501.16450 — 360Brew (withdrawn 2025-08-23)](https://arxiv.org/abs/2501.16450) | official | high | D1 |
| 2 | [LinkedIn Eng — Engineering the next-gen Feed (provenance contested)](https://www.linkedin.com/blog/engineering/feed/engineering-the-next-generation-of-linkedins-feed) | official(?) | low | D1 |
| 3 | [LinkedIn Eng — Leveraging Dwell Time (2024-10-01)](https://www.linkedin.com/blog/engineering/feed/leveraging-dwell-time-to-improve-member-experiences-on-the-linkedin-feed) | official | high | D2 |
| 4 | [Tim Jurka — How Does the LinkedIn Feed Work? (2025-08-11)](https://www.linkedin.com/pulse/how-does-linkedin-feed-work-tim-jurka-oxraf) | official | high | D6 |
| 5 | [AuthoredUp — LinkedIn Algorithm (621K posts)](https://authoredup.com/blog/linkedin-algorithm) | community | medium | D2, D3, D5 |
| 6 | [Socialinsider — LinkedIn benchmarks (1.3M)](https://www.socialinsider.io/social-media-benchmarks/linkedin) | community | medium | D3 |
| 7 | [Buffer — Best Content Format (2M+)](https://buffer.com/resources/data-best-content-format-social-media/) | community | medium | D3 |
| 8 | [Metricool — 2026 LinkedIn study (673K)](https://metricool.com/linkedin-trends/) | community | medium | D3 |
| 9 | [Ordinal — Link Penalty Study (900K, p<0.001)](https://www.tryordinal.com/blog/linkedin-link-penalty-study) | community | medium-high | D4 |
| 10 | [Threads/Matt Navarra — LinkedIn denies intentional link penalty](https://www.threads.com/@mattnavarra/post/DOWa_61Cown/) | official (relayed) | medium | D4 |
| 11 | [Entrepreneur — LinkedIn fights AI slop (Lorenzetti, 2026-05-19)](https://www.entrepreneur.com/business-news/linkedin-is-fighting-back-against-ai-slop-and-ai-comments) | official (reported) | high | D8 |
| 12 | [PR Daily — Guardians of the Feed (Jobanputra)](https://www.prdaily.com/what-works-and-doesnt-on-linkedin-according-to-guardians-of-the-feed/) | official (reported) | medium-high | D4, D8 |
| 13 | [Social Media Today — engagement-pod crackdown (Sachdeva, 2026-02-16)](https://www.socialmediatoday.com/news/linkedin-outlines-more-measures-to-combat-engagement-pods/812290/) | official (reported) | high | D8 |
| 14 | [Originality.ai — AI content on LinkedIn (45% gap)](https://originality.ai/blog/ai-content-published-linkedin) | community | medium | D8 |
| 15 | [van der Blom — Algorithm Insights 2025 (1.8M)](https://www.scribd.com/document/984921783/Algorithm-Insights-Report-2025-chapter-1-Richard-Van-der-Blom) | community | medium | D2, D4, D5 |
| 16 | [meet-lea — LinkedIn Algorithm Explained 2026](https://meet-lea.com/en/blog/linkedin-algorithm-explained) | community | low-medium | D2 |
| 17 | [Microsoft Learn — Member Post Statistics API](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/members/post-statistics?view=li-lms-2025-11) | official | high | D2/Topic-2 |
| 18 | [Inc. — buzzwords to scrub](https://www.inc.com/amy-george/14-buzzwords-to-scrub-from-your-linkedin-page-right-now.html) | community | low | D7 |

View file

@ -0,0 +1,218 @@
---
type: trekresearch-brief
created: 2026-05-29
question: "As of 2026, can a personal LinkedIn profile self-serve post-level analytics via an API, are per-post saves visible in the native UI, and can a personal profile auto-publish via any API — with the exact constraints for a solo user without partner/company-page access?"
confidence: 0.86
dimensions: 4
mcp_servers_used: [tavily]
local_agents_used: []
external_agents_used: [docs-researcher, community-researcher, contrarian-researcher]
---
# Personal-Profile Analytics + Auto-Publish Boundaries (2026)
> Generated by trekresearch (standard external swarm: docs + community + contrarian; no
> Gemini — scoped to Topic 1) on 2026-05-29. Topic 2 of 3 for the linkedin-studio
> remediation. Feeds Phase-1 honest boundary statements + the Phase-2 saves honesty-fix.
> **Primary-sourced from Microsoft Learn (LinkedIn's canonical dev docs).**
## Research Question
As of 2026, can a personal LinkedIn profile self-serve post-level analytics via an API,
are per-post saves visible in the native UI, and can a personal profile auto-publish via
any API — with the exact constraints for a solo user without partner/company-page access?
## Executive Summary
**The audit's own boundary assumptions are partly wrong, and fixing them naively would
replace one false claim with another.** Three findings, all primary-sourced and
high-confidence: (1) a personal profile **CAN auto-publish self-serve**`w_member_social`
is an Open Permission via the free "Share on LinkedIn" product, publishing immediately at
~150 posts/member/day; the plugin's clipboard-stop is a **design + Terms-of-Service
choice, not an API impossibility**. (2) Per-post **saves ARE visible** in native post
analytics (rolled out ~Sept 2025, count-only, no saver identity) **and** exposed via the
API's `POST_SAVE` metric (since version li-lms-2026-04) — so "saves aren't trackable" is
**stale/false**; the honest line is "visible in the UI, not self-serve via API." (3)
Post-level analytics via API **exist** (`memberCreatorPostAnalytics` / `r_member_postAnalytics`)
but sit behind the **vetted Community Management API** (verified organization + associated
LinkedIn Page + use-case review) — **not self-serve for a solo creator**; CSV export is the
practical floor, not the only technical path. **Key caveat:** every boundary statement
must be *dated* ("as of 2026-05") — this surface changed fast (saves went UI→API inside
~12 months).
## Dimensions
### D1. Post-level analytics API for personal profiles — Confidence: high
**External findings:**
- The endpoint exists: `GET /rest/memberCreatorPostAnalytics` ("Member Post Statistics"),
permission `r_member_postAnalytics` (versions ≥ li-lms-202506). Finders `q=entity`
(one post) and `q=me` (aggregated). Metrics: `IMPRESSION`, `MEMBERS_REACHED`, `RESHARE`,
`REACTION`, `COMMENT` (since 2025-06) and — added **li-lms-2026-04**`POST_SAVE`,
`POST_SEND`, `LINK_CLICKS`, `PREMIUM_CTA_CLICKS`, `FOLLOWER_GAINED_FROM_CONTENT`,
`PROFILE_VIEW_FROM_CONTENT`. (Video metrics are a separate endpoint
`memberCreatorVideoAnalytics`; follower count is `memberFollowersCount` /
`r_member_profileAnalytics`.) [learn.microsoft.com/.../members/post-statistics?view=li-lms-2026-05]
- **The access gate is the crux:** `r_member_postAnalytics` is listed **exclusively under
the Community Management API** (a "Vetted Product") — never in the consumer Open
Permissions. Community Management approval requires an approved use case, **verified
organization**, verified domain, **an app verified by a LinkedIn Page associated with
the same organization**, and (Standard tier) a privacy policy + screencast review.
Dev-tier rate limits: 500 calls/app/24h, 100/member/24h. [learn.microsoft.com/.../increasing-access; .../community-management-app-review]
- The 2025 "LinkedIn opened Member Post Analytics to individuals" headline is true **only
through approved partner platforms** (Metricool/Buffer/Hootsuite-class) that hold the
approval — not by a creator calling the API directly. LinkedIn is also *tightening*:
the read scope `r_member_social` (Member Post Management) is flatly **closed** — "not
accepting access requests at this time due to resource constraints."
**Contradictions:** "CSV is the only way" (plugin/audit) is wrong at the *capability*
level (an API exists) but right at the *practical* level for a solo dev (the API is
org-vetted). **Resolution:** state "exists but partner-gated; not self-serve; CSV is the
practical floor for a solo creator," not "no API exists."
### D2. Per-post saves visibility — Confidence: high
**External findings:**
- **Native UI:** the official LinkedIn Help page on post analytics lists, under Social
Engagement: Reactions, Comments, Reposts, **Saves** ("number of times members saved
your post"), Sends — rolled out ~**Sept 2025**. **Count only, never saver identity.**
Rollout was phased and historical backfill limited (older posts may show no saves).
[linkedin.com/help/linkedin/answer/a516971; socialmediatoday.com/news/linkedin-adds-save-and-send-data-to-content-insights/759828/]
- **API:** `POST_SAVE` exposed via `memberCreatorPostAnalytics` from li-lms-2026-04 — but
behind the same Community-Management gate as D1 (not self-serve).
**Resolution (sharpens the Q3 honesty-fix):** the honest downgrade is **NOT** "saves
can't be tracked." It is: *"saves are visible in your native LinkedIn post analytics
(since Sept 2025, count-only) but there is no self-serve API to pull them, so this tool
does not auto-ingest them — read them in LinkedIn directly."* The operator's decision (no
manual-entry feature) stands and is defensible: the number is human-readable but
programmatically out of reach + would be hand-typed and instantly stale.
### D3. Auto-publish from a personal profile — Confidence: high (capability) / the ToS line is the real boundary
**External findings:**
- **A personal profile CAN auto-publish, self-serve.** `w_member_social` is an **Open
Permission** (no approval), granted by adding the free **"Share on LinkedIn"** product
(filed under `.../integrations/self-serve/`). `POST /v2/ugcPosts` (or `/rest/posts`)
with `lifecycleState: PUBLISHED` publishes **immediately, no human-in-the-loop**. Rate
limit ~150 requests/member/day. Self-serve content types: text, image, video,
article/URL share. [learn.microsoft.com/.../share-on-linkedin; .../getting-access]
- **Genuine limitations:** organic **carousels (ad-style) are NOT available** via API for
a personal profile (sponsored only) — use **MultiImage** for swipeable images; **member
document/PDF** posts and the modern `/rest/posts` *member* path are doc-ambiguous (don't
promise without testing). You also **cannot self-serve read your own posts back**
(`r_member_social` is restricted/closed) — capture the post URN from the publish
response header instead.
- **Operational friction (real, first-hand):** 3-legged OAuth (one interactive auth);
60-day access tokens / ~365-day refresh tokens that invalidate on revoke **or scope
change** (`invalid_grant` footgun); a common `403 me.GET.NO_VERSION` trap (use
`/userinfo` `sub`, not `/v2/me`); a placeholder company-page link required at app setup
even for personal-only posting; "outdated docs everywhere." [marcusnoble.co.uk/2025-02-02-posting-to-linkedin-via-the-api/; github.com/linkedin-developers/linkedin-api-js-client/issues/35]
- **The real boundary is ToS, not capability:** `w_member_social` being "open" does not
mean automated/scheduled posting is within LinkedIn's API Terms. The legitimate basis
for the plugin's clipboard-stop is **(a) per-user OAuth + token-refresh operational
overhead and (b) LinkedIn's Terms posture on automated posting** — not "the API can't
do it."
**Resolution:** the plugin must **not** write "cannot auto-publish." Honest framing:
*"auto-publish to a personal profile is technically possible and self-serve (Share on
LinkedIn / `w_member_social`); the plugin deliberately stops at the clipboard — OAuth +
60-day-token overhead and LinkedIn's Terms on automated posting make human-in-the-loop
the safer default. A choice, not a wall."* (Verify current Platform/Marketing API Terms
language on automated personal posting before finalizing the wording.)
### D4. Dwell time exportability — Confidence: medium-high (organic boundary holds, with carve-outs)
**External findings:**
- For **organic personal posts**, there is **no creator-facing dwell metric** — not in the
UI Help metric list, not in `memberCreatorPostAnalytics`. Dwell is an internal ranking
signal only. [linkedin.com/help/linkedin/answer/a516971]
- **Carve-outs (so the claim isn't falsifiable):** (1) **video** posts expose **Watch
time / Average watch time** (UI + member video API) — a real per-post depth metric, just
not for non-video; (2) `averageDwellTime` exists in the **ads** `adAnalytics` API (paid
campaigns only, methodology improved ~+25% in a 2026 change).
**Resolution:** "for organic posts, dwell is internal-only — no creator number; video
Watch time is the closest creator-visible depth proxy; a true dwell field exists but only
for paid ads." Do not say "LinkedIn has no dwell metric anywhere."
## External Knowledge
### Best Practice (official / primary)
Microsoft Learn (li-lms-2026-05) is authoritative. The self-serve set for a solo dev is
exactly three Open Permissions: `profile`, `email`, `w_member_social`. **Everything
analytics** is Community-Management-vetted. This single fact drives every honest boundary
statement.
### Known issues
The surface moves fast and docs lag the API (saves went invisible→UI→API in ~12 months;
`r_member_social` went from program to closed). Any boundary statement must be **dated**.
Practitioner reality adds friction (token fragility, the `/me` 403 trap) that makes
"feasible" ≠ "frictionless."
## Synthesis
1. **The audit's "honest scheduling boundary" finding (§5) was itself half-wrong.** It
assumed the plugin "stops at the clipboard because it can't auto-publish." The truth:
it *can* (self-serve), and the honest disclosure is about the **design + ToS choice**,
not an impossibility. Phase 1's boundary prose must encode the *choice*, or it ships a
brand-new false claim — exactly the failure mode this whole remediation exists to kill.
2. **The saves honesty-fix is a wording fix, not a "we can't" fix.** Saves are now
visible in the UI. The accurate downgrade points the user to LinkedIn's own analytics
for the number while being honest that the tool can't pull it. This keeps the operator's
"no manual-entry" decision *and* avoids a stale "saves aren't trackable" claim.
3. **There is a latent, deliberately-deferred capability here.** Auto-publish is buildable
self-serve. It is explicitly a **Non-Goal** of this remediation (operator: disclose
boundaries, don't engineer them away). Worth a one-line "possible but intentionally not
built (ToS + token overhead)" note so a future maintainer doesn't rediscover it as a
"missing feature."
## Open Questions
- **Exact ToS language on automated/scheduled personal posting** — the load-bearing reason
for the clipboard-stop. *Carry as: verify current Platform/Marketing API Terms before
finalizing the boundary wording.* (Not a blocker for the plan; a finalize-time check.)
- **Member document/PDF + `/rest/posts` member-author path** — doc-ambiguous. Irrelevant
unless a future version builds publishing; flag, don't resolve.
## Recommendation
For Phase 1 (honest boundaries) and Phase 2 (saves honesty-fix), write these **dated**
statements:
1. **Analytics API:** "A post-level analytics API for personal posts exists, but it's
behind LinkedIn's vetted Community Management API (verified organization + LinkedIn
Page + use-case review) — not self-serve for an individual. For a solo creator, CSV
export is the practical path. (As of 2026-05.)"
2. **Saves:** "Saves are visible in your native LinkedIn post analytics (since ~Sept 2025,
count-only). There's no self-serve API to pull them, so this tool doesn't track saves
automatically — read them in LinkedIn directly." Remove any "saves (10x weight) are the
highest-impact signal" claim presented *inside a report the tool populates*; reframe as
strategy guidance pointing to the native number.
3. **Auto-publish:** "Auto-publishing to a personal profile is technically possible
(self-serve `w_member_social`); this tool deliberately stops at the clipboard — OAuth +
token-refresh overhead and LinkedIn's Terms on automated posting. A choice, not a
limitation. (As of 2026-05.)" Reconcile the calendar/queue/"publish action" wording so
it never implies the tool auto-posts.
4. **Dwell:** "Dwell time is an internal ranking signal — no creator-facing number for
organic posts (video Watch time is the closest proxy)."
## Sources
| # | Source | Type | Quality | Used in |
|---|--------|------|---------|---------|
| 1 | [Member Post Statistics API (li-lms-2026-05)](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/members/post-statistics?view=li-lms-2026-05) | official | high | D1, D2 |
| 2 | [Increasing Access — permissions table](https://learn.microsoft.com/en-us/linkedin/marketing/increasing-access?view=li-lms-2026-05) | official | high | D1 |
| 3 | [Community Management App Review](https://learn.microsoft.com/en-us/linkedin/marketing/community-management-app-review?view=li-lms-2026-05) | official | high | D1 |
| 4 | [Community Management Overview / FAQ (r_member_social closed)](https://learn.microsoft.com/en-us/linkedin/marketing/community-management/community-management-overview?view=li-lms-2026-05) | official | high | D1 |
| 5 | [Share on LinkedIn (self-serve)](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/share-on-linkedin) | official | high | D3 |
| 6 | [Getting Access — Open Permissions](https://learn.microsoft.com/en-us/linkedin/shared/authentication/getting-access) | official | high | D3 |
| 7 | [Post analytics for your content — LinkedIn Help (Saves listed)](https://www.linkedin.com/help/linkedin/answer/a516971) | official | high | D2, D4 |
| 8 | [Social Media Today — LinkedIn adds Save/Send data (Sept 2025)](https://www.socialmediatoday.com/news/linkedin-adds-save-and-send-data-to-content-insights/759828/) | community | medium-high | D2 |
| 9 | [Refresh Tokens with OAuth 2.0](https://learn.microsoft.com/en-us/linkedin/shared/authentication/programmatic-refresh-tokens) | official | high | D3 |
| 10 | [Marcus Noble — Posting to LinkedIn via the API (first-hand)](https://marcusnoble.co.uk/2025-02-02-posting-to-linkedin-via-the-api/) | community | medium-high | D3 |
| 11 | [GitHub — linkedin-api-js-client #35 (403 /me trap)](https://github.com/linkedin-developers/linkedin-api-js-client/issues/35) | community | medium | D3 |
| 12 | [Recent Marketing API Changes (ads averageDwellTime)](https://learn.microsoft.com/en-us/linkedin/marketing/integrations/recent-changes?view=li-lms-2026-01) | official | high | D4 |
| 13 | [ConnectSafely — LinkedIn API guide 2026 (approval reality)](https://connectsafely.ai/articles/linkedin-api-complete-guide-2026) | community | low-medium | D1 |

View file

@ -0,0 +1,225 @@
---
type: trekresearch-brief
created: 2026-05-29
question: "For LinkedIn in 2026: hard short-form video requirements the algorithm rewards (aspect ratio, resolution, hook timing, captions); what triggers the templated-AI/engagement-bait down-rank; and newsletter-distribution mechanics (notification leverage, cadence, realistic cold-start numbers)?"
confidence: 0.80
dimensions: 5
mcp_servers_used: [tavily]
local_agents_used: []
external_agents_used: [docs-researcher, community-researcher, contrarian-researcher]
---
# Coverage-Gap Feature Specs — Video · De-AI · Newsletter Distribution (2026)
> Generated by trekresearch (standard external swarm: docs + community + contrarian)
> on 2026-05-29. Topic 3 of 3 for the linkedin-studio remediation. Feeds the Phase-2
> video gate, short-form de-AI gate, and newsletter-distribution surface.
> **The de-AI signal is covered in depth in Topic 1 (D8); this brief carries the
> video + newsletter specs and a short de-AI cross-reference.**
## Research Question
For LinkedIn in 2026: hard short-form video requirements the algorithm rewards (aspect
ratio, resolution, hook timing, captions); what triggers the templated-AI / engagement-bait
down-rank; and newsletter-distribution mechanics (notification leverage, cadence, realistic
cold-start numbers)?
## Executive Summary
**Three of the audit's proposed coverage-gap features rest on wrong premises, and building
them naively would bake new false claims into the plugin.** (1) A **hard 9:16 video gate is
wrong** — LinkedIn's audience is desktop-heavy; 9:16 is mobile-only and **crops on desktop**;
the broad-distribution picks are **4:5 / 1:1**, with 9:16 a mobile-only opt-in. The plugin's
internal contradiction ("4:5 preferred" vs "deprioritized") resolves toward **"4:5 preferred."**
The "3-second hook" is **imported TikTok folklore**, not a LinkedIn rule. The one video spec
**safe to enforce is captions** (8085% watch muted; +12% watch-time per LinkedIn; caption
text is indexed) — but framed as best-practice, not "LinkedIn requires SRT." (2) **Native
video reach is DOWN ~36% YoY** (per-video views, Socialinsider 1.3M); the "video is king
2026" narrative is hype from a benchmark misread — so the video gate must be a **quality
gate for users who choose video**, never copy that positions video as top-reach. (3) The
newsletter **"triple-notification" framing is oversold** — LinkedIn's own FAQ says the three
channels are **deduplicated** (one notification per event); the real benefit is
"**bypasses organic feed ranking**," not three guaranteed touchpoints; and the **one-time
launch invite** makes a sub-~12K-follower newsletter premature. **Key caveat:** aspect-ratio
performance has **no rigorous comparative study** — encode as heuristic, never a hard gate;
date every claim ("as of 2026-05"). (Confidence 0.80 — high on captions / video-decline /
newsletter mechanics; medium on aspect-ratio + cold-start ranges.)
## Dimensions
### D1. Video upload specs (official, enforceable) — Confidence: high
**External findings (LinkedIn Help, primary):**
- Aspect ratio accepted **1:2.42.4:1**; resolution **256×1444096×2304**; duration **min
3s desktop / 2s mobile, max 15min desktop / 10min mobile**; file **75KB5GB**; **1060fps**;
**192Kbps30Mbps**; **MP4 the safe default**. [linkedin.com/help/linkedin/answer/a548372; .../a1311816]
- **Official-source conflict:** the member troubleshooting page lists MOV/AVI as *supported*;
the Pages spec says LinkedIn "no longer supports AVI, QuickTime, or MOV." **Resolution for
a gate:** enforce MP4 as the safe default; **warn-only** (not block) on MOV/AVI.
### D2. Aspect ratio — 9:16 is NOT a clean win — Confidence: medium (no rigorous data)
**External findings:**
- LinkedIn runs an official **full-screen vertical video experience** (video tab + carousel)
that **vertical (≈9:16) fills uncropped**; landscape is letterboxed. BUT you **cannot
directly post into** that surface — placement is algorithmic. [linkedin.com/help/linkedin/answer/a6290168]
- The contrarian + spec-guide consensus: for the **main feed** on a **desktop-heavy
professional audience**, **4:5 and 1:1** are the broad-distribution picks; **9:16 is
delivered mobile-only and crops to 1:1 on desktop** — a real degradation. The only official
numeric 9:16 target (720×1280 rec / 1080×1920 max) is scoped to **ads**, not organic.
[aspectratiocalculator.com/linkedin-aspect-ratios/; LinkedIn recommendation post]
- **No peer-reviewed or official study compares 4:5 vs 9:16 vs 1:1 engagement.** The
"vertical takes more feed real estate → more engagement" claim is uncorroborated heuristic.
**Resolution:** the plugin's contradiction resolves toward **"4:5 / 1:1 preferred for broad
distribution; 9:16 mobile-only opt-in / for the video tab."** Fix the file that calls 4:5
"deprioritized." **Do not build a hard 9:16 gate** — make aspect ratio guidance, not enforcement.
### D3. Hook timing + captions — Confidence: high (captions) / low (3-sec hook)
**External findings:**
- The **"3-second hook" is cross-platform folklore** (TikTok/Reels), absent from
LinkedIn-specific algorithm analyses; LinkedIn's only official "3 seconds" is the *minimum
video length*. The real LinkedIn-native reason the opening matters is **muted autoplay**.
[dataslayer.ai/blog/linkedin-algorithm-february-2026-whats-working-now; sproutsocial.com/insights/linkedin-video/]
- **Captions are the one spec safe to enforce:** ~8085% watch muted; LinkedIn's own data
~**+12% watch-time** with captions; **caption text is indexed for search/discovery** and
factored into distribution. Both **SRT upload and native auto-captions** are first-party
(auto-captions in 10 languages, opt-in, reviewable). [opus.pro/blog/linkedin-video-caption-subtitle-best-practices; linkedin.com/help/linkedin/answer/a1327025; .../a552177]
**Resolution:** enforce a **captions** quality gate (BLOCK is defensible at 80% muted),
accept SRT OR auto-captions, label it "best-practice / algorithmic signal" (not "required").
Replace any "3-second hook" rule with **"front-load value for muted autoplay."**
### D4. De-AI / engagement-bait down-rank — Confidence: high (cross-ref Topic 1 D8)
**External findings (see Topic 1 D8 for full sourcing):**
- **Officially confirmed:** LinkedIn VP Laura Lorenzetti (2026-05-19) — active program
suppressing generic AI posts/comments + automation + attention-bait video; mechanism is
**reach-suppression (down to first-degree), not deletion**, via ML trained on human-
annotated "original thinking vs lacking substance." Engagement-pod crackdown officially
confirmed too (Sachdeva, 2026-02-16). [entrepreneur.com/business-news/linkedin-is-fighting-back-against-ai-slop-and-ai-comments]
- **Engagement bait** ("Comment YES", "Like for Part 2") → post-level throttle; genuine
open questions are **not** penalized. The line is *real answer* vs *reflexive token*.
**Resolution:** **build the short-form de-AI gate** targeting the signals LinkedIn *named*
(personal substance, original thinking, concrete specifics, genuine voice) — not an
unverified SEO "tell-list." Add a soft engagement-bait check (block mechanical-response CTAs,
allow genuine questions).
### D5. Newsletter distribution mechanics — Confidence: high (mechanics) / medium (cold-start)
**External findings:**
- **Official, solid:** all members can create newsletters (**max 5, 2-week cooldown**); on
first edition LinkedIn **auto-invites all connections/followers to subscribe** (and on each
new follow thereafter); editions are **also posted to the feed** + resurface via engagement
+ appear in interest/trending sections; **lowest-friction subscribe** (no typing — LinkedIn
has the email). [linkedin.com/help/linkedin/answer/a517925; .../a522525; .../a517914]
- **"Triple-notification" is OVERSOLD / contested:** LinkedIn's FAQ states the in-app / push /
email channels are **deduplicated** — "if you receive an in-app or push notification, you
should NOT expect to also receive an email for the same." So it is **one notification per
event via the subscriber's preferred channel**, not three guaranteed touchpoints. Delivery
failures are reported; one creator measured **~23% click vs 810% on their own email list.**
The defensible benefit is **"bypasses organic feed ranking,"** not "triple notification."
- **One-time launch invite → follower floor:** the mass "invite all followers" fires **once,
at any size** — launching sub-~12K followers permanently spends the blast on a tiny base.
Practitioner floor: **500+ min, 3,000+ ideal.** Defensible plugin gate: ~**12K floor**
(aligns with the existing ~1K `/monetize`/`/outreach` unlock).
- **Realistic cold-start (don't inflate):** true zero-audience start ≈ **0100 subs months
13**; viral "0→9K/7-days" and "0→10K" case studies **were NOT cold starts** (leveraged
existing audiences / 12-month grinds). Cadence: **weekly common among top performers;
biweekly a safe default for original analysis.**
- **Honest downsides to disclose:** subscribers are **non-exportable** (platform lock-in — lose
them all if LinkedIn kills the feature); LinkedIn **outranks your own site** for the same
article (no canonical) — harmful if building an owned property; **no read/open/unsubscribe
analytics**; per-subscriber reach **decays** as the list grows.
**Resolution:** the newsletter-distribution surface must teach the **honest** version:
notification-bypass-of-feed (with the dedup caveat), the one-time-launch-blast + follower
floor, realistic cold-start floors, and the lock-in/no-canonical/no-analytics downsides — NOT
the audit's "triple-notification leverage" framing.
## External Knowledge
### Best Practice (official)
Enforceable/teachable from LinkedIn's own docs: video upload limits; captions (SRT + auto);
the full-screen vertical experience exists but isn't directly postable; newsletter mechanics
(5-max/2-week cooldown, auto-invite, feed resurfacing, dedup notifications). Everything about
*reach magnitude* (video, aspect ratio, newsletter click rates) is practitioner-sourced.
### Alternatives / contrarian
The "video is king 2026" and "triple-notification leverage" narratives are **both refuted**
— the first by a benchmark misread (views 36% YoY per video; +36% is the platform aggregate),
the second by LinkedIn's own dedup FAQ. Documents/carousels lead video on engagement (~7% vs
~6%). Build features on the corrected premises.
### Known issues
Aspect-ratio guidance has no rigorous data — heuristic only. Newsletter data is non-portable.
The MOV/AVI support conflict is unresolved in LinkedIn's own docs. Date every claim.
## Synthesis
1. **"Close the coverage gaps" ≠ "build what the audit sketched."** The audit named the
gaps correctly (no video enforcement, no de-AI gate, thin newsletter distribution) but
sketched two of the fixes on hype: a 9:16 gate and "triple-notification leverage." The
research says build a **captions/aspect-guidance** video gate (not 9:16-mandatory) and an
**honest** newsletter surface (bypass-feed + caveats), not the sketched versions. This is
the same disease the whole remediation treats — features must rest on verified premises.
2. **The de-AI gate is the highest-confidence Phase-2 build** (D4 + Topic 1 D8): officially
confirmed, named-executive, with a stated mechanism. It is also the **single most robustly
triangulated 2026 down-rank signal** the audit flagged as unguarded on short-form. Prioritize it.
3. **The newsletter follower-floor reconciles two findings cleanly:** the one-time launch
blast + the existing ~1K `/monetize`/`/outreach` unlock → gate the newsletter behind a
~12K floor framed as "wait until you can spend the launch blast well." Below it, the
plugin should steer the user to short-form/document content to *build* the base.
## Open Questions
- **Newsletter email-delivery behavior** — genuinely contested (FAQ dedup vs third-party
"always emailed"). *Carry as: state the dedup behavior from the FAQ; note delivery is not
guaranteed; a one-edition live test would resolve it.* Not a blocker.
- **Aspect-ratio engagement** — no rigorous data exists. *Carry as heuristic; never a hard gate.*
- **Dedicated vertical video feed as a primary surface** — if LinkedIn promotes it, the 9:16
calculus could flip. *Re-check before finalizing the video gate copy.*
## Recommendation
For Phase 2:
1. **Video gate = quality gate, not reach-push.** Enforce MP4 + within-limits (warn on
MOV/AVI); **enforce/strongly-recommend captions** (SRT or auto); make aspect ratio
**guidance — 4:5 / 1:1 preferred, 9:16 mobile-only opt-in**; fix the "4:5 deprioritized"
contradiction toward "4:5 preferred"; drop any "3-second hook" rule and any "video
maximizes reach" copy. Add a one-line note that per-video reach is declining and
documents out-engage video.
2. **Build the short-form de-AI gate** (highest-confidence build) on LinkedIn's named signals
(substance / original thinking / specifics / voice) + a soft engagement-bait check.
3. **Newsletter-distribution surface = the honest version:** "bypasses organic feed ranking
(one deduplicated notification per subscriber per edition)"; one-time launch-blast +
**~12K follower floor**; realistic cold-start floors (0100 subs months 13); disclose
non-export/no-canonical/no-analytics/per-subscriber-decay. Steer sub-floor users to build
the base first.
## Sources
| # | Source | Type | Quality | Used in |
|---|--------|------|---------|---------|
| 1 | [Video sharing troubleshooting (specs)](https://www.linkedin.com/help/linkedin/answer/a548372) | official | high | D1 |
| 2 | [Video specs for Pages (MOV/AVI conflict)](https://www.linkedin.com/help/linkedin/answer/a1311816) | official | high | D1 |
| 3 | [Full-screen vertical video](https://www.linkedin.com/help/linkedin/answer/a6290168) | official | high | D2 |
| 4 | [Aspect Ratio Calculator — LinkedIn 2026](https://www.aspectratiocalculator.com/linkedin-aspect-ratios/) | community | medium | D2 |
| 5 | [Add Closed Captions (SRT)](https://www.linkedin.com/help/linkedin/answer/a552177/add-closed-captions-to-videos-on-linkedin) | official | high | D3 |
| 6 | [Auto captions for videos](https://www.linkedin.com/help/linkedin/answer/a1327025) | official | high | D3 |
| 7 | [OpusClip — caption best practices (80% muted, +12%)](https://www.opus.pro/blog/linkedin-video-caption-subtitle-best-practices) | community | medium | D3 |
| 8 | [Socialinsider 2026 benchmarks (video 36% YoY)](https://www.socialinsider.io/social-media-benchmarks/linkedin) | community | medium-high | D2, D5 |
| 9 | [Omni Lab — video views down 36% YoY](https://www.omnilabconsulting.com/blog/linkedin-video-views-down-36-yoy) | community | medium | D2 |
| 10 | [Entrepreneur — LinkedIn fights AI slop (Lorenzetti)](https://www.entrepreneur.com/business-news/linkedin-is-fighting-back-against-ai-slop-and-ai-comments) | official (reported) | high | D4 |
| 11 | [Manage a newsletter (5-max, cooldown, auto-invite)](https://www.linkedin.com/help/linkedin/answer/a517925) | official | high | D5 |
| 12 | [Newsletters overview (triple notification wording)](https://www.linkedin.com/help/linkedin/answer/a522525) | official | high | D5 |
| 13 | [Newsletters FAQ (dedup; resurfacing)](https://www.linkedin.com/help/linkedin/answer/a517914) | official | high | D5 |
| 14 | [The Lime One — follower floor for newsletter](https://thelime.one/blog/how-many-followers-do-you-need-for-linkedin-newsletter) | community | medium | D5 |
| 15 | [The Science Marketer — newsletter pros/cons (lock-in, click rates)](https://thesciencemarketer.com/p/linkedin-newsletter-pros-cons) | community | medium | D5 |
| 16 | [InfluenceFlow — newsletter cold-start ranges 2026](https://influenceflow.io/resources/linkedin-newsletter-strategy-complete-guide-to-building-an-engaged-subscriber-base-in-2026/) | community | low-medium | D5 |
| 17 | [dataslayer — LinkedIn algorithm Feb 2026](https://www.dataslayer.ai/blog/linkedin-algorithm-february-2026-whats-working-now) | community | low-medium | D3 |