Compare commits

...

18 commits

Author SHA1 Message Date
d71941072d docs(linkedin-studio): SECURITY.md Supported versions section (AAA+ D-axis, ORDRE D4-B)
Adds the missing "## Supported versions" section — the one AAA+ D4
gap this repo had among 17 artefact repos. States the actual verified
numbers (plugin.json 0.8.0, tags v0.4.0-v0.8.0, single pre-1.0 line)
and the support level (latest 0.x only, no backports), not just a
version table.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015fkBTBLLH3ZZdCBLBC2WEC
2026-08-21 10:31:39 +02:00
66fd00c005 docs(linkedin-studio): SECURITY.md contact info + drop GitHub reference (AAA+ C-axis, ORDRE 36)
Repo is on Forgejo, not GitHub — the disclosure path had no email/URL and
pointed to a feature that doesn't exist here. Adds security@fromaitochitta.com,
the https:// canonical-repo URL, and an explicit acknowledge/fix/disclosure
timeline. Existing prose (Security Considerations / User Data / Dependencies)
kept as-is.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpHNahaWWf1gX1FoAxuBFV
2026-08-16 21:15:28 +02:00
7e5133c8a6 docs(linkedin-studio): README Table of Contents (AAA+ B-axis, ORDRE 32)
13-item flat TOC over all H2 sections, placed after intro/badges before
first content section. Anchors validated mechanically (dead-link check
against actual H2 headings); no version/tag/catalog-ref changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JyMgMvo5Jgq8GFYhSQ7JMy
2026-08-16 16:18:01 +02:00
978601447f docs(linkedin-studio): D11 GOVERNANCE.md consolidation + VERIFY-MISSING test command
D11: remove the local GOVERNANCE.md copy (byte-identical to repo-standard's
canonical, verified md5 736fc9d6a...), repoint both README links (line 7 +
table row 255) to the absolute canonical URL, same commit per org-ops's
LINK-INTERNAL-MISSING guidance.

VERIFY-MISSING: document the actual working test command in a README
fenced block. A bare `node --test` also auto-discovers the .test.ts files
under scripts/*/tests/ and fails on them (no tsx loader at root) — scoped
the documented command to the node:test suite (289 tests, 63 suites, all
passing) and noted the TypeScript sub-packages run their own npm test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lr3gjZmAuV1CmYu8UygWUt
2026-08-13 20:26:44 +02:00
3290e8fb7d feat(linkedin-studio): RE-R3f — /linkedin:trends --headless (MR-F5) [skip-docs]
Unattended discovery: the full poll->score->capture->brief loop can
now fire with no operator present (Sunday-morning discovery), gated
behind a new Step 0.5 contract. Fails fast instead of asking when the
profile is missing, skips triage unconditionally (candidates stay
pending), and refuses --demand --headless rather than silently
degrading.

Trigger mechanism is Claude Code's own Desktop Scheduled Tasks, not a
hand-rolled cron wrapper -- verified against Claude Code's headless
docs that a cron + `claude -p` wrapper loses observability/session-
resumption/error-recovery versus the first-party mechanism. The
fallback `claude -p` recipe is documented (README) for portability,
not built as repo code. No new source/test file; no schema change;
counts unchanged (30/20/29).

This opens the research-engine's own 2026-06-24 re-evaluation gate
against v1.0.0 product maturity for slice (e) -- surfaced to the
operator explicitly rather than built around or silently deferred.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2qHoS4FrkabuD1bCg8Nr2
2026-08-10 20:57:34 +02:00
63745ddcab docs(linkedin-studio): RE-R3f brief + plan — unattended AI discovery (headless, slice e)
Tracked slice docs for MR-F5: the poll->score->capture->brief loop
made safe to fire with no operator present. Records the 2026-06-24
re-evaluation gate against v1.0.0 maturity and the operator's explicit
decision to open it (2026-08-10), plus the Desktop Scheduled Tasks vs.
hand-rolled cron design decision.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2qHoS4FrkabuD1bCg8Nr2
2026-08-10 20:57:20 +02:00
24ac8dbe02 docs(linkedin-studio): D12 disclosure — self-contained line at README:7, drop cross-repo link
Org-ops D12 (2026-08-01) requires the plugin-class disclosure to name
generator, process, and ownership basis (Anthropic Consumer Terms §4)
inline, with no link — the GOVERNANCE.md anchor form doesn't carry the
ownership basis and pointed at a tag-pinned commit anyway.
2026-08-09 09:58:49 +02:00
83b054aad4 docs(linkedin-studio): repo-standard BADGE-COUNT — 7 → 5 badges (behold katalog-speilede akser)
Fjerner platform- og reference_docs-badgene. De tre stat-badgene katalogens
check-versions.mjs speiler per akse (commands/agents/hooks) beholdes, sammen med
version (intern konsistenssjekk mot plugin.json) og license.

README-H1-funnet er vurdert som bevisst: menneskelesbar tittel er husmønsteret
på tvers av marketplace-repoene.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F3zUvokC7zniU71RYQPJZ
2026-08-04 11:45:12 +02:00
51bb5a6fb2 docs(linkedin-studio): repo-standard-gate — README-forsteskjerm, Install-blokk, doede referanser [skip-docs]
Kjort mot repo-standard v0.1.1 (klasse: plugin). Fikset alle ekte ERROR:

- README-DESC: apningslinjen er na identisk med forge-beskrivelsen
  (README == forge). Taglinen flyttet en linje ned.
- HEADING-MISSING x3: `### Installation` under Quick Start promotert til
  toppniva `## Install` og flyttet opp foran «Two Engines» (agent-lesbar,
  forutsigbar posisjon); `### Prerequisites` -> `## Requirements`;
  `## Boundaries (as of 2026-07)` -> `## Non-goals` (dato beholdt i
  ingressen); nytt `## Changelog` med peker til CHANGELOG.md.
- Doed referanse (SKIP LINK-OUTSIDE-REPO): `../../README.md#ai-generated-
  code-disclosure` var monorepo-arv og peker ingensteds etter polyrepo-
  migreringen — na GOVERNANCE.md#can-i-trust-this i eget repo.
- docs/voyage-build/brief.md: to lenker med `./` som ikke ble oppdatert da
  fila ble flyttet ned en mappe — na `../`.

Star igjen (ikke repo-defekter):
- ERROR LINK-INTERNAL-MISSING README.md:255 `references/` + WARN samme kode
  scripts/trends/README.md:8 `../specifics-bank`: begge er sporede
  KATALOGER. Gaten bygger sitt fasit-sett fra `git ls-files`, som aldri
  emitterer katalognavn, sa en katalog-lenke kan pr. konstruksjon ikke
  resolve. Klassifiserer-defekt i repo-standard, ikke her — lenkene er
  korrekte og beholdes. Varslet repo-standard via coord.
- WARN README-H1: tittelen er bevisst produktnavn, ikke drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015F3zUvokC7zniU71RYQPJZ
2026-08-03 21:54:08 +02:00
bc47c1842a fix(linkedin-studio): N24.6 — brain-restposter (slugify-translitterering + okf_version til rot-frontmatter) [skip-docs]
To restposter fra N24.5s FUNN-liste, begge i scripts/brain, i én berøring.

1. slugify() var tapsgivende for alt annet enn engelsk: hver ikke-ASCII-bokstav
   falt i [^a-z0-9]-kjøringen og ble en bindestrek. «Løkkene» ble «l-kkene», og
   verre: ø og å ble BEGGE «-», så «møte» og «måte» kollapset til én id.
   Nå: NFD + strip av kombinerende tegn (å, é, ñ), så en eksplisitt tabell for de
   ikke-dekomponerbare (æ→ae, ø→o, œ→oe, ß→ss, đ/ð→d, þ→th, ł→l).
   REKKEFØLGEN: translitterering kjører FØR kollapsen, så kollapsen forblir siste
   gate. Presist om hva det beviser — utdata-whitelisten [a-z0-9-] bæres av
   kollapsen selv, ikke av rekkefølgen; dagens tabell produserer bare [a-z], så
   rekkefølgen ville ikke brutt whitelisten i dag. Den er en invariant for
   FREMTIDIGE tabelloppføringer, og den er nå pinnet av test. Det er whitelisten
   ingestion-guardens tall hviler på (0/81 %-escapes, 0/81 bilde-URL-er,
   rapportert til guard-eieren som strukturelt); repo-vid grep bekrefter at
   scripts/brain/src/id.ts er den ENESTE slugify-implementasjonen i repoet, så
   attribusjonen holder.

   ID-STABILITET (den åpne beslutningen, avgjort på bevis, ikke skjønn):
   endre in-place — ingen versjonering av slugifieren, ingen migrering.
   - Ingen mintede ider finnes på disk: $DATA/brain og $DATA/ingest inneholder
     bare tomme kataloger (opprettet 23.06, null filer — initBrain lager kataloger
     OG filer i ett kall, så brain-en er aldri blitt genuint initialisert her).
   - Alle 27 profile-field-labels den shippede malen minter er ASCII (målt ved å
     kjøre extractFields' faktiske regler mot malen) ⇒ endringen er et BEVIST
     no-op for profil-laget. Fem golden-ider pinner det.
   - observed-ider (consolidate) mintes fra brukerskrevne nøkler og ville endret
     seg — men ingenting er persistert, så migrasjonsflaten er null rader.
   Ærlig avgrensning: dette KRYMPER kollisjonsklassen, det lukker den ikke —
   «møte» og «mote» møtes fortsatt, som de må for at stabiliteten over case og
   whitespace skal holde. Og en label uten latinske tegn i det hele tatt
   («日本語») slugger fortsatt til tom streng, så to slike minter samme id —
   pre-eksisterende, ikke innført her, men det hører hjemme ved siden av
   «krymper, lukker ikke» framfor å stå uskrevet. Adopter-forbehold: repoet er offentlig, men brain init
   er ikke session-start-wiret (SB-S2 eier det), så en persistert brain krever en
   eksplisitt invokasjon.

2. okf_version: 0.1 lå som BRØDTEKST i rot-index.md. Kanonisk plassering er
   frontmatter-blokken (OKF-form spec §6, v0.3) — upstreams ene utskårne unntak
   fra «index-filer har ingen frontmatter», og unntaket er oppregnet til ÉN
   nøkkel, så ingenting annet blir med (okf_layout blir i brødtekst per §12).
   Verdien flytter, den bumpes ikke: vi blir på 0.1.

VERIFISERING
- TDD: 6 røde først, så grønt. Brain 134 → 142 (+8), floor 127 → 142.
- Mutasjonstestet mot de FAKTISKE kildefilene, ikke bare self-tester:
  okf_version tilbake til brødtekst → 3 røde · fjern translitterering → 1 rød ·
  flytt translitterering til ETTER kollapsen → 1 rød · restaurert → 142/0.
- De to okf-testene dette erstattet var VAKUØSE: /^okf_version:/m matcher en
  frontmatter-linje like gjerne som en brødtekstlinje, og frontmatterType() leser
  type:, så den ga null med eller uten blokk. Begge gikk grønt på begge
  plasseringer — nøyaktig gjeldsklassen N24.5 feide. Meldingen «index.md carries
  NO frontmatter» ble usann og er skrevet om.
- Delt gate (katalogen, read-only): node catalog/scripts/okf-check.mjs <bundle>
  → exit 0, «OK: valid OKF bundle», okf_version 0.1. Bevist at verdien leses FRA
  frontmatter: fjern blokken → «MISSING». check-okf-parity-signaturen er
  conceptCount|untyped|okfVersion|okfVersionAccepted — plassering inngår ikke, så
  det å migrere først splitter ingen paritet (okr skriver fortsatt brødtekst).
- Ti suiter grønne: test-runner 303/0 · trends 300 · analytics 202 · hooks 191 ·
  brain 142 · editions 72 · render 63 · specifics-bank 45 · tests 35 ·
  contract-gate 33.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EgUSPs7vDejiHr7iZw8xUx
2026-08-01 20:05:54 +02:00
e38f71f418 fix(linkedin-studio): N24.5 — mekanisk gjeldssveip (plugin.json, CHANGELOG-dublett, de-niche, KTG, Step 9-nøkler) [skip-docs]
Fem grep-lokaliserte gjeldsposter fra STATE FUNN, pluss to anti-erosjons-lints
som gjør to av dem terminale i stedet for gjenoppdaget en gang til.

1. .claude-plugin/plugin.json description — v4.0.0-arvet release-notat med
   historiske tellinger («→ 19 agents», «→ 27 commands», «→ 29 commands»)
   erstattet av en beskrivelse av hva pluginen ER. Dette er teksten /plugin
   viser. Ingen tellinger i teksten: de drifter, og CHANGELOG.md eier historikken.

2. CHANGELOG hadde TO «## [0.6.0]»-overskrifter (2026-07-17 = ekte, 2026-02-07 i
   halen). Halens datert om til «## [0.6.0-legacy]». Sjette gangs gjenoppdagelse
   — derfor lint, se punkt 6.

3. De-niche: 7 «thought leader»-treff utenfor commands/ rettet — README (mot
   kommandoens egen frontmatter-ordlyd), skills/linkedin-networking (2:
   FUNN-listen navnga bare :9, men :185 var samme defekt), analytics-interpreter,
   analytics-tools-guide, collaborations-guide, linkedin-visual-style.
   BEVISST BEHOLDT: de 5 blocklist-linjene, de to negative eksemplene
   (opportunity-generation, det ironiske sitatet i video-strategy-guide), og
   network-builder:562 — et WebSearch-uttrykk som treffer hvordan ANDRE
   merker seg selv, allerede paret med «Creator».

4. Hardkodet «KTG» i generisk operatør-tiltale fjernet. FUNN navnga tre linjer i
   language-reviewer; klassen var 21 linjer over 4 agenter, newsletter.md,
   edition-state-templaten og specifics-bank sitt renderte NN-kilder.md-artefakt.
   BEVISST BEHOLDT som proveniens: CHANGELOG, tests/fixtures/* (de ER KTGs
   faktiske redaktørrunde, kalibreringsfasiten), adopter-notatene som ærlig
   flagger KTG-interne verktøy (contract-gate, specifics-bank), «KTG
   cover-directive» og «answers KTG's question» — attribusjon, ikke tiltale.

5. Step 9 persisterte tre nøkler templaten aldri deklarerte: randsoneGate (N9),
   hookForge + personaSweep.deling (N24). Nå deklarert i BÅDE artikkel-shapen og
   _doc. Additivt, schemaVersion forblir 1. _doc.hookForge bærer N22-invarianten
   videre: en variant har målt lengde / krok-type / kroppsspor og ALDRI en
   predikert engagement-score.

6. To nye ubetingede lints (Section 16aa), begge mutasjonstestet mot de FAKTISKE
   filene, ikke bare mot self-testen:
   - CHANGELOG-dublettvakt (+ in-memory non-vakuitets-self-test som skiller en
     ekte dublett fra en bevisst «-legacy»-overskrift)
   - Step 9-nøkkeldeklarasjon (shape + _doc)
   Mutasjon 1 (gjeninnfør dubletten) → rødt. Mutasjon 2 (fjern hookForge fra
   shapen) → rødt. Mutasjon 3 (fjern _doc.randsoneGate, fila fortsatt gyldig
   JSON) → rødt. Restaurert → grønt.

Ti suiter grønne: test-runner 300 → 303 (floor 281 → 284) · trends 300 ·
analytics 202 · hooks 191 · brain 134 · editions 72 · render 63 ·
specifics-bank 45 · tests 35 · contract-gate 33.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017vMLTF7J13jJmgqwwJyivt
2026-07-31 21:46:35 +02:00
77426135a5 feat(linkedin-studio): N24 — promo-post-motor (hook-forge + shareability-gate) [skip-docs]
Step 9 (post-lock distribution gate) får to tillegg, begge scoped til utgave-promo:

1. Hook-forge (E#7): 5-8 krok-varianter for delingsteksten. 110-140-båndet
   håndheves PER VARIANT og MÅLES (perl -CSD; awk length/wc -c teller bytes og
   lyver på æ/ø/å). Utenfor båndet = diskvalifisert, ikke rangert lavere.
   Settet må spenne minst tre krok-typer, ellers er det omskrivinger, ikke valg.
   Hver variant må spores til en påstand kroppen faktisk gjør (ingen bait).
   Ingen predicted-engagement score — et tall som påstår å vite hva som sprer
   seg er oppfunnet (N22-defektklassen); operatøren rangerer.
   Valgt variant går GJENNOM randsone-gaten, aldri utenom.

2. Shareability-gate (E#8): konverter-modus får en ANDRE binær — deling.
   «Hvem deler dette til en kollega, og hvorfor?» + RESHARE/SAVE/SEND.
   Svaret må navngi en rolle og et avsender-motiv; «nyttig for alle i tech»
   er NEI. Blokkerende: to mislykkede omramminger => operatørvalg (pivot eller
   registrert foldIn), aldri en stille passering.

Ti suiter grønne: test-runner 300/0 (287->300, +13 ubetingede; floor 268->281)
· trends 300 · analytics 202 · hooks 191 · brain 134 · editions 72 · render 63
· specifics-bank 45 · tests 35 · contract-gate 33. Begge nye compound-predikater
mutasjonstestet mot faktisk fil (fjernet literal => rød).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvtDSvQDUmjd5bXdAxUu6D
2026-07-31 21:22:49 +02:00
af80150019 feat(linkedin-studio): N23 — positioning-brief-motor (generering + konsum) [skip-docs]
Persistert posisjonering som alle drafts bindes mot. Innplassert i
/linkedin:setup (Step 3g) etter operatør-friksjonskriteriet — setup eier
allerede ${DATA}/profile/ og har menyen; telling forblir 30.

- config/positioning-brief.template.md (NY): fire seksjoner (Who I am ·
  Who this is for · What I hold · How I differ), hver med _Evidence:_-linje.
- setup.md Step 3g: DELEGERER til linkedin-studio:strategy-advisor.
  Brain-konsum-blokken forblir i én kopi (N21-lærdommen), lint-håndhevet.
- strategy-advisor.md: distillasjonen, kildepresedens, anti-flatteri
  (What I hold må bære påstander en leser kan være UENIG i) og
  evidensgulv ([insufficient evidence] framfor plausibel oppfinnelse).
- Distillat, ikke andre profil: briefen REFERERER user-profile.md framfor
  å gjenta den. Operatørens deklarasjon og daterte anker slår maskinens
  inferens; motstrid rapporteres som spenning, aldri stille anvendt.
- Konsum: post/quick/batch + newsletter Step 1 + differentiation-checker
  (scorer originalitet mot How I differ, ikke generisk nyhet).
- Regenerering er operatør-gatet: explicit go + seksjonsvis diff + aldri
  stille overskriving av operatørens egne redigeringer.

Lint: Section 16y, 17 nye ubetingede sjekker (self-test 1 akseptert /
4 underspesifiserte avvist). Floor 251 → 268. Alle ti suiter grønne:
test-runner 270→287/0 · trends 300 · analytics 202 · hooks 191 · brain 134
· editions 72 · render 63 · specifics-bank 45 · tests 35 · contract-gate 33.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbwkH8phPsgv9xFd8Qbq7E
2026-07-31 21:01:55 +02:00
8c91c409cd fix(linkedin-studio): N22 — ærlighetsscrub (fabrikkerte benchmarks, Velocity Score, 15+-terskelen) [skip-docs]
Tre defektklasser av samme slag: tall som ser sourcet ut, men ikke er det.

Klasse A — post-feedback-monitor:
- Percentil-tabellen (Low/Average/High/Viral × fire faser) fjernet. Ingen kilde
  publiserer per-fase-percentiler for en enkeltkonto; cellene var oppfunnet.
  Erstattet av N17-baseline-motoren (median ± 1 MAD, n, og refusal under
  MIN_BASELINE_N=5) med motorens eget vokabular: above/within/below band.
- Velocity Score fjernet i sin helhet, inkl. fase-multiplikatorene (5,0x/3,0x/
  1,5x/1,0x/0,5x). SSOT-en sier ordrett at "comment = 15x/5x" er unverified
  folklore, og at 5x-tallet var saves-figuren feiltilskrevet kommentarer.
  Erstattet av rå tellinger + engagement rate slik csv-parser.ts definerer den.
- Output-malen har nå en eksplisitt refusal-gren. Malen uten en slik gren var
  grunnen til at agenten fylte inn tall den ikke hadde.
- To folklore-multiplikatorer i Principles ("5x the impact", "worth 15 likes").

Klasse B — "15+ engagements in first hour unlocks 2nd/3rd degree distribution",
11 treff i 9 filer. SSOT-en sier "Directional, not a fixed threshold". Påstanden
overlevde både hardening-gaten og kald-review (log.md:1099 sjekket ~70%-
misattribusjonen, ikke terskelen).

Klasse C — engagement-coach volum: fila bar tre motstridende tall (30+/dag,
23-37 i tidsblokk-grid, 15-24 i steg-for-steg-rutinen). Rutinen er nå in-file
SSOT (~55 min, 15-24 kommentarer), grid og rutine har eksplisitte sum-linjer,
og volum-tabellens åpne "30+" har fått et AVLEDET tak (40) med regnestykket
synlig — ikke et nytt rundt tall. Uverifiserbar superlativ ("110K followers,
#2 global creator") fjernet.

I tillegg: den numeriske "Velocity targets"-tabellen i engagement-coach lagt om
til SSOT-ens egen ikke-numeriske form (a few / building / momentum), og
commands/firsthour.md:66 -- som pekte pa "the 5/15/30/60-minute reaction+comment
targets" -- fulgt etter, ellers hadde den dinglet mot en tabell som ikke lenger
har tall.

docs/hardening/log.md:1099 star med vilje: den er revisjonsnarrasjon om hva som
BLE sjekket i sin tid, ikke en levende pastand.

Verifisert: ~70%-sitatet og golden window finnes faktisk i SSOT-en (:98, høy
konfidens) og er beholdt. Alle ti suiter grønne, floors uendret.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Pwb1oWLqKB2oBJHSoWNcy
2026-07-31 18:35:25 +02:00
885526738c fix(linkedin-studio): N21 — scaffold-bånd-redesign + ferskhet-rester (newsletter-guide, first-comment) [skip-docs]
Del 1 — scaffold-bånd (KTG-beslutning: behold målbåndet, utvid komponentene).
Komponentsummen var 960–1 640 mot målbåndet 1 200–1 800: en skjelett-konform
draft kunne lande under gulvet og nådde aldri taket. Nye bånd — Context
250–350, Insight 550–850, Implication 250–350 (Hook 110–140 og CTA 50–100
uendret) — summerer til 1 210–1 790, altså INNI 1 200–1 800. Skjelett-konform
er nå gate-konform per konstruksjon.

Målbåndet 1 200–1 800 er uendret overalt; hooks/prompts/content-quality-gate.md
(den kanoniske gaten) er ikke rørt. Alternativet — å heve taket per AuthoredUp
D-4 — ville krevd 25 filer / 40 linjer inkl. gate-prompten, begge skills,
quality-scorecard, config-malen og brain-fiksturen.

Skjelettet fantes i seks kopier, alle rettet på KTG-go: commands/post.md,
commands/batch.md, commands/pipeline.md, references/engagement-frameworks.md,
agents/content-optimizer.md, skills/linkedin-content-creation/SKILL.md. De tre
siste lå utenfor planens scope, men to av dem var internt selvmotsigende på én
linje (overskrift 1 200–1 800 over komponenter som summerte til 960–1 640), og
engagement-frameworks.md er nettopp fila post.md/pipeline.md instruerer
modellen om å LESE for strukturen.

Nabofunn tatt med på KTG-go: post.md:95 ga «Personal stories → 1 000–1 400»,
som motsa post.md:136s egen gate. Kald-review R2a MAJOR — nå 1 200–1 800.

D-4 inn i kanonfila: ny «Post length»-seksjon i
references/algorithm-signals-reference.md med AuthoredUp-optimumet 1 301–2 500
(372 126 poster, sep 2025–feb 2026), merket single-vendor/ett vindu, med
eksplisitt note om at datapunktet gjør det shippede taket konservativt — ikke
feil — og at de to AuthoredUp-N-ene i fila (621K vs 372K) er ulike studier.
Kilde verifisert mot primærkilden, ikke overført fra planen.

Del 2 — ferskhet-rester (begge påstander verifisert mot LinkedIn Help):
- D-6 newsletter-strategy-guide.md: «5 000+ følgere» framstilt som terskel er
  feil — «All LinkedIn members have access to create a newsletter on LinkedIn»
  (a517914). Omskrevet til redaksjonell modenhetsvurdering (også i
  Mistakes-tabellen og Bottom Line). E-post er ikke garantert: LinkedIn
  de-dupliserer på tvers av kanaler — «if you receive an in-app or push
  notification, you should not expect to also receive an email for the same
  notification» (a517914). Bringer referansefila i tråd med newsletter.md:2384.
  Fjernet samtidig den ukildede «Algorithm favors newsletters from established
  creators» i den omskrevne blokka.
- D-7 first-comment-strategy.md: «pinned by default» er uverifisert og feil —
  pinning er en eksplisitt forfatterhandling (a524166), og standard
  kommentarsortering er algoritmisk. Lagt til «What this file does not claim»-
  avsnitt som speiler kanonfilas «contested»/low-confidence-epistemikk.

Scope 3 (KTG-go, amend): references/engagement-frameworks.md har FIRE
skjeletter, ikke ett. To til brot gulvet i malbandet — Data-Driven Post
(1 050-1 400) og Contrarian Post (1 060-1 410) — og er lagt om til samme
komponentprofil som Standard, sum 1 210-1 790. Narrative Arc (1 350-1 500) la
allerede inni og star urort; alle tre har na en eksplisitt sum-linje. Fila er
den post.md:104 sender modellen til for «story structures», sa a sertifisere
den som fikset med to odelagte skjeletter igjen ville vaert usant.

SUPERSEDED og ikke gjeninnført: gammel B §S6 Del 2 pkt 1 (first-comment
−5/−10 %-tall, pods-eskalering, 360Brew-fotnote).

Verifisering: bånd-summen ligger inni målbåndet i alle seks kopier av
standard-skjelettet og i alle fire skjelettene i engagement-frameworks.md
(grep-bevis, 0 gjenværende 200-300/400-800) · D-6/D-7 omformulert (0 treff på
«5,000+ followers» / «pinned by default» / «inbox + email») · alle ti suiter
grønne, alle floors uendret: test-runner 270/0 (269 assertions >= floor 251) ·
trends 300/0 · analytics 202/0 · hooks 191/0 · brain 134/0 · editions 72/0 ·
render 63/0 · specifics-bank 45/0 · tests 35/0 · contract-gate 33/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhF1H7ctT5Fk8KkoCQpe5n
2026-07-31 18:08:53 +02:00
e7276cf8bb fix(linkedin-studio): N20 — CLAUDE_PLUGIN_ROOT-normalisering av load-bearing ref-stier [skip-docs]
81 load-bearing stier i 16 kommandofiler pinnet til ${CLAUDE_PLUGIN_ROOT}/.
Rene prefikser: 81 inn / 81 ut, ingen linje lagt til eller fjernet.

FASIT-VERIFISERING (premiss-sjekk mot BASELINE, ikke bare HEAD): planens
68 linjer / 17 filer er RIKTIG mot sin egen baseline 882f6ee. HEAD ga 73/18 —
altså ekte kode-drift siden baseline (newsletter.md 11→15, calendar.md 0→1),
ikke en telle-feil som i N19.

SCOPE UTVIDET ETTER GO: planens grep (references|hooks/scripts|scripts) har en
blindsone — skills/ assets/ config/ render/ agents/ commands/ er samme defekt-
klasse og allerede anerkjent som pin-verdig av mønsteret (audit.md:24 og
competitive.md:22 pinner skills/, batch.md:214 og pipeline.md:211 pinner
assets/). Under planens scope kunne dens EGEN verifisering ikke passere:
quick.md:229-230 ville stått upinnet rett over en pinnet linje, og setup.md
(3 load-bearing Read, 0 pins) var ikke i settet i det hele tatt.
Full defektklasse: 117 linjer / 19 filer → 81 pinnet, 36 bevisst urørt.

KLASSIFISERINGSREGEL (utledet av mønsteret, ikke oppfunnet):
- PIN = stien skal åpnes/kjøres/skrives — Read/Reference/See-pekere,
  ref-fil-lister, Bash/node/npm, edit-mål.
- PROSA = stien står som sitat bak en påstand, eller som beskrivelse av
  oppførsel/plassering. Ingen inline «(see …)» er pinnet noe sted i repoet
  (0 av 25 filer) — den grensen er arvet, ikke satt her.

36 BEVISST URØRTE, tre klasser:
1. Inline sitat/attribusjon (17): carousel.md:37 · pipeline.md:70,75 ·
   newsletter.md:999,1082,1211,1433 · report.md:273 · video.md:69 ·
   react.md:115 · calendar.md:87 · profile.md:28,34 · firsthour.md:112 ·
   monetize.md:343,492 · outreach.md:922
2. Beskrivelse av oppførsel/plassering (10): newsletter.md:50,1097,1205,1694,
   1744,1944 · report.md:45 · linkedin.md:218 · import.md:121,200
3. newsletter.md fase-tabellens Tooling-kolonne (9): :100,102,105,106,111,112,
   113,115,116 — tabellen er et register, og hvert steg pinner sitt eget
   faktiske kall i brødteksten.

S13-LINTEN (M0 data-dir): alle assets/-stiene som ble pinnet står eksplisitt i
lintens NEGATIVE13-unntaksliste, og lintens egen kommentar (test-runner.sh:552)
sier at shipped read-only assets SKAL bære ${CLAUDE_PLUGIN_ROOT}.

VERIFISERING: 76 unike pinnede stier hentet ut og resolvet med cwd=/tmp —
75 finnes. Den ene som ikke gjør det, config/personas.local.md, er en
gitignored valgfri fallback («else personas.template.md») som var pinnet på
newsletter.md:205 før denne endringen. Re-grep: gjenstående 36 er utelukkende
prosa, listet over.

Alle ti suiter grønne, alle floors uendret: test-runner 270/0 (269 assertions
>= floor 251) · trends 300/0 · analytics 202/0 · hooks 191/0 · brain 134/0 ·
editions 72/0 · render 63/0 · specifics-bank 45/0 · tests 35/0 ·
contract-gate 33/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M31cw43gqiSSDrKzcwUi7L
2026-07-31 17:21:22 +02:00
ab091121d9 fix(linkedin-studio): N19 — terminologi-sveip (8 treff) + monetize scorecard/scope [skip-docs]
Terminologi (memory no-thought-leadership-phrase, plugin-is-domain-general):
erstatter "thought leader(s)" med nøytral, kontekstvalgt ordbruk i de 8
load-bearing treffene — competitive.md:4,17,31,34 · monetize.md:4 ·
linkedin.md:145 · strategy.md:371 · outreach.md:229.

Fasit-korreksjon: planens "14 treff / 6 gjenværende" var en telle-feil.
`git grep` mot planens egen baseline 882f6ee gir 15 — settet er uendret
siden da, kun linkedin.md (118→145) og post.md (134→142) har flyttet seg.
Riktig regnestykke er 15 = 8 fiks + 7 urørte.

De 7 urørte er alle bevisste negative eksempler og står med vilje:
- pipeline.md:79, post.md:142, react.md:131 — buzzword-blocklisten i
  pre-publish-sjekklisten (Content Quality Rule #4; frasen ER regelen)
- carousel.md:123, video.md:129 — samme blocklist sitert i caption-steget
- profile.md:79 — "thought leader"/"guru"/"ninja" som headline-antimønster
- profile.md:101 — en dårlig headline vist som avskrekkende eksempel

Monetize:
- Audience Size summerte 40 mot /25 (5+10+15+5+5). Follower-tiersene er nå
  ett gjensidig utelukkende valg (pick ONE), så maks blir 15+5+5 = 25.
  Alle fire dimensjoner summerer nå eksakt 25 → TOTAL /100 er ekte.
- Beskrivelsens "Works from 1K+ followers" motsa Stage 1 (0–1K, :92).
  Staged formulering i stedet, konsistent med linkedin.md-gatingregelen
  ("work at any follower count, value compounds") og CLAUDE.md-tabellens
  "unlocks at ~1K followers".

Verifisert: re-grep gir nøyaktig 7 gjenværende, alle på ikke-rør-lista.
ASCII-boksen måler 60 tegn på hver linje. Alle ti suiter grønne —
test-runner 270/0 (269 assertions >= floor 251) · analytics 202/0 ·
trends 300/0 · hooks 191/0 · brain 134/0 · editions 72/0 · render 63/0 ·
specifics-bank 45/0 · tests 35/0 · contract-gate 33/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PbMAQ3MR3TvDtAznxRNbR8
2026-07-31 15:38:10 +02:00
b5a7125e19 docs(linkedin-studio): legg N10 inn i [0.8.0] + presiser SC1-bevisets rekkevidde
To rettelser i release-notatene for v0.8.0, funnet i etterkontroll ETTER at
taggen var pushet. Taggen flyttes IKKE - v0.8.0 peker fortsatt paa f98446a.
Aa re-peke en pushet tag paa en offentlig remote er verre enn hullet den
skulle lukke.

1. N10 (0810631, konfigurerbart maalnivaa-spenn / C-8) manglet. Releasen
   inneholder 12 N-oekter; notatene foerte 11. Verifisert med enumerering i
   begge retninger: N-numre i [0.8.0] == N-numre i git log 0b34218..f98446a.
   CHANGELOG-dekning er en TELLING, og en telling skal enumereres mot ground
   truth foer release - ikke leses ut av en seksjon som ser komplett ut.

2. "proven end-to-end" om SC1-kjeden er myknet til det som faktisk ble gjort:
   CLI-lagene kjoert ekte, kommandostegene simulert, fordi plugin-kommando- og
   agentflater resolver konsument-side og ikke i utviklingsrepoet. STATE bar
   forbeholdet hele tiden; den offentlige teksten gjorde det ikke.

test-runner 270/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
2026-07-27 20:42:34 +02:00
66 changed files with 1694 additions and 526 deletions

View file

@ -1,7 +1,7 @@
{
"name": "linkedin-studio",
"version": "0.8.0",
"description": "LinkedIn Studio — full-spectrum LinkedIn content engine: feed posts, carousels, video scripts, and long-form newsletter editions, with the 2026 relevance-ranking model baked in. v4.0.0 is an audit-remediation release (Voyage Phase 03): every user-facing claim is made honest or removed, all 11 previously-orphaned agents are wired (→ 19 agents), a `/linkedin:firsthour` post-publish command is added (→ 27 commands), the algorithm-signal claims are reconciled to one sourced statement (no unpublishable model name or date), short-form de-AI and video quality gates are added, and the structure lint is rebuilt to guard the real layout plus version/count/stat consistency. Breaking: the newly-wired agents register only on reinstall/reload, and this consolidates the v3.0.0 identity break (slug, agent namespace `linkedin-studio:<agent>`, state-file path `~/.claude/linkedin-studio.local.md`). v3.1.0 added the cold adversarial review package (`/linkedin:headless-review` + Step 6.5 + `/linkedin:pivot` + per-artifact personas); the `/linkedin:*` commands are unchanged. v4.1.0 adds a journey layer: two guided front-doors (`/linkedin:create`, `/linkedin:measure`) plus a router re-tiered into five journeys (Start · Create · Engage · Measure · Grow), with the 27 existing commands kept as the execution tier (→ 29 commands; additive, reload registers the two new commands).",
"description": "LinkedIn Studio — full-spectrum LinkedIn content engine: feed posts, carousels, video scripts, and long-form newsletter editions, with the 2026 relevance-ranking model baked in. Commands are organized into five journeys (Start · Create · Engage · Measure · Grow) over an execution tier of atomic commands, backed by agents for drafting, review, analytics and strategy, and by hooks that gate content quality, voice drift and topic rotation as you write. The long-form pipeline is a multi-phase orchestrator with gates before prose (skeleton + spine), a deterministic contract-gate, fact-check, editorial review, and a cold adversarial review package run on a frozen draft — all before lock. Analytics import, weekly reports and A/B tests feed back into creation through a do-next contract, so a measurement changes the next piece instead of dying in chat. Domain-general by design: expertise areas, reader personas, publishing slots, brand tokens and language rules come from your own config — none of it is hardcoded. Version history is in CHANGELOG.md.",
"author": {
"name": "Kjell Tore Guttormsen"
},

View file

@ -7,9 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added — unattended discovery, `--headless` (RE-R3f / MR-F5)
- **`/linkedin:trends --headless`** — the full poll → score → capture → brief loop, safe to fire
with no operator present, so Sunday-morning discovery can run on a schedule. Fails fast instead of
asking for pillars when the profile is missing; skips triage unconditionally (candidates stay
`pending` for the next attended session); refuses `--demand --headless` rather than silently
degrading to supply-side discovery. Recommended trigger is Claude Code's own Desktop Scheduled
Tasks; a fallback hand-rolled `claude -p` recipe is documented (`scripts/trends/README.md`), not
built as repo code. No new command, agent, or store field — a command-contract change plus docs.
- **Gate note:** this opens the research-engine's own 2026-06-24 decision that AI-capture autonomy
(slice e) sits behind a re-evaluation gate against v1.0.0 product maturity. The conflict was
surfaced to the operator explicitly and the gate was opened deliberately for the production need
(Maskinrommet feedback register, MR-F5), not silently bypassed. See `docs/research-engine/
brief-re-r3f.md`.
## [0.8.0] - 2026-07-27
**The production line, closed.** 0.7.0 built the bridge from discovery to a scheduled edition; 0.8.0 makes the whole line hold together and, for the first time, feed back into itself. A candidate now travels id-traceably from capture to a scheduled slot with **no manual re-entry anywhere in the chain** (proven end-to-end before this release). A measurement now changes the next piece instead of dying in chat. A correction made once is enforced from then on. Voice is bound *before* prose rather than scrubbed after. The series remembers what it has already spent. And the engine finally knows your own normal well enough to **refuse a verdict** when the data is too thin — the answer that keeps everything above it honest.
**The production line, closed.** 0.7.0 built the bridge from discovery to a scheduled edition; 0.8.0 makes the whole line hold together and, for the first time, feed back into itself. A candidate now travels id-traceably from capture to a scheduled slot with **no manual re-entry anywhere in the chain** — exercised before this release with the CLI layers run for real and the command steps simulated, since plugin command and agent surfaces resolve consumer-side rather than in the development repo. A measurement now changes the next piece instead of dying in chat. A correction made once is enforced from then on. Voice is bound *before* prose rather than scrubbed after. The series remembers what it has already spent. And the engine finally knows your own normal well enough to **refuse a verdict** when the data is too thin — the answer that keeps everything above it honest.
### Added — demand-sweep «innenfra og ut» (N7.5 / MR-F9)
@ -36,6 +51,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **WebFetch on both fact agents (C-7)**`fact-checker` and `fact-reviewer` gain `WebFetch`: quote precision (F2) and exact-figure attribution (F3) require the verbatim source text in context, not a search snippet.
- test-runner 146 → 154 (Section 16p: 8 unconditional randsone-gate greps + self-test; anti-erosion floor 129 → 137). Domain-general; no KTG-specific values hardcoded.
### Added — target level becomes a configurable axis, not a hardcoded reader (N10 / C-8)
- **The hardcoded primary reader is gone**`references/longform-quality-rules.md` judged every edition against "the non-technical line manager", one specific reader baked into a domain-general plugin. It now refers to **the user's own configured target-level span**, so "practically usable at every level" is measured against the levels *they* write for.
- **A mandatory secondary persona at the opposite end of the span**`config/personas.template.md`'s "primary trumps" rule let a secondary-persona NO be waved through as "signal, not failure". A secondary at the far end of the span is now required, and dismissing its NO demands an **explicit ceiling justification**. The seed library also gained a technician-end persona (a solutions developer / AI engineer): it shipped three *manager* personas and no technician, which quietly biased every gate toward one end of the very span it was supposed to test.
- **`targetLevel` stops being a phantom field** — the plan assumed the N7 bridge already carried it into the edition state; it did not (it existed only in the trend store and a free-text mapping). Two files beyond the planned scope close the loop: an additive `articles.NN.targetLevel` slot in `config/edition-state.template.json` (default `null`, **no schemaVersion bump** — same pattern as `sourceTrendId` and `language`), and `/linkedin:newsletter` persisting the resolved level at the Step 1.5 checkpoint. `persona-reviewer` reads it and places the personas on the span, falling back to persona expertise when the field is `null`.
- test-runner 154 → 163 (Section 16q: 9 unconditional `.md`/`.json` greps + non-vacuity self-test; anti-erosion floor 137 → 146). A vacuity bug in the new edition-state check was caught and tightened during the session — it grepped for the *word* `targetLevel`, which the `sourceTrendId` doc-string already contained, so it now pins the slot literal. (`0810631`)
### Added — series-level protection: make the retread visible before prose (N11 / C-5)
- **`scripts/editions/` — series distillate (new package)** — each locked edition leaves behind the narrative units it spent (anecdotes told, arguments argued, hooks used) in `<serie>/linkedin/series-distillate.json`, beside `edition-state.json` in the **series root** — series-scoped state where the series' other state already lives, so it travels with the series and needs no slug→path map. `distil-append` (Step 8 lock) folds an edition in; a re-lock after `/linkedin:pivot` **replaces** its entry rather than duplicating it.
@ -544,7 +566,7 @@ Public release for open-source marketplace. All runtime bugs fixed, documentatio
- **Orphaned files** — deleted outdated docs/commands-reference.md
- **BACKLOG.md and DEVELOPMENT-LOG.md** — gitignored (internal development files)
## [0.6.0] - 2026-02-07
## [0.6.0-legacy] - 2026-02-07
### Note
First formal version. Previously unversioned.

View file

@ -12,6 +12,7 @@ Full-spectrum LinkedIn content engine — short-form feed posts, carousels, vide
- **Post queue:** `assets/drafts/queue.json` (managed by `hooks/scripts/queue-manager.mjs`)
- **Analytics:** CLI `scripts/analytics/` (TypeScript, needs `tsx` + `npm install`); data `assets/analytics/` (gitignored)
- **Specifics-bank:** `scripts/specifics-bank/` (TypeScript, needs `tsx` + `npm install`) — deterministic, topic-tagged store of the operator's lived specifics (real numbers, named cases, held opinions) in the per-user data dir; elicited/bound by `/linkedin:newsletter` Step 1.5 so drafts draw from real inventory, never invented filler. Consumption is logged back at Step 8 lock (`record-usage``usedIn`), so cross-edition re-use of the same material is visible rather than silently encouraged by the dedupe
- **Positioning brief (N23):** `${DATA}/profile/positioning-brief.md` (template `config/positioning-brief.template.md`) — the stance-and-difference layer every drafting surface reads before it writes: **Who I am · Who this is for · What I hold · How I differ**, each carrying an `_Evidence:_` line. It is a **distillate, not a second profile**: `profile/user-profile.md` stays the operator's *declared* identity and the brief **references** it rather than restating it — two files asserting the same fact is how positioning forks. Generated by `/linkedin:setup` **Step 3g**, which *delegates* to `linkedin-studio:strategy-advisor` (the SB-S3a brain readers live there in one copy; the command never copies the block). Precedence is declaration-over-inference: the operations anchor and the declared profile outrank the distillation, and a contradiction is reported as a tension, never silently applied. Consumed by `post`/`quick`/`batch`, `newsletter` Step 1 and `differentiation-checker` (which scores originality against **How I differ** rather than generic novelty). **Regeneration is operator-gated** — explicit go, section-by-section diff, never a silent overwrite of the operator's own edits; a section without evidence is marked `[insufficient evidence]`, never filled with a plausible invention. Absent file ⇒ every surface proceeds silently
- **Series distillate:** `scripts/editions/` (TypeScript, needs `tsx` + `npm install`) — series-level memory: each locked edition's spent narrative units (anecdotes/arguments/hooks) in `<serie>/linkedin/series-distillate.json`; written at `/linkedin:newsletter` Step 8 (`distil-append`), checked against the next skeleton at Step 2.5 (`distil-check`, advisory). Deterministic character-trigram similarity — AI extracts, code compares
- **Editions register:** `scripts/editions/` (same package) — one row per edition in production in the per-user data dir (`editions/register.json`): series, edition, path, current phase, next action, slot, `startedAt`/`completedAt`. Each of the 17 canonical `/linkedin:newsletter` phase transitions runs `register-upsert --edition-state <path>`, which appends `articles.NN.phaseLog` (lead-time telemetry, additive — schemaVersion stays 1) **and** mirrors the row in one call; Step 10 runs `register-complete`. `register-list` is the work-in-progress view. The register is a **mirror** — deterministic resumption reads `edition-state.json` only, and a lost register is rebuilt by the next transition
- **Publishing slots:** `hooks/scripts/slots.mjs` — the weekly production grid. Slots are **operator config** in the per-user data dir (`profile/publishing-slots.json`, schema + opt-in template in `config/publishing-slots.template.json`); absent config ⇒ every slot surface stays silent, and the plugin hardcodes no publishing times. Coverage of a slot counts the short-form queue (`scheduled`/`published`) **and** editions-register rows claiming that date, so long-form and short-form cannot be double-booked. Zero-dep (SessionStart may not spawn tsx) and imported identically by the hook and by the commands (`queue-manager.mjs` pattern), so session-start's vacancy warning and `/linkedin:newsletter` Step 10's slot default are the same computation
@ -57,7 +58,7 @@ All content commands auto-copy output to clipboard via `clipboard-helper.mjs`; i
| `/linkedin:post` | Full post creation (10-15 min) |
| `/linkedin:quick` | 5-minute quick post (3-line formula) + 8 post-type templates |
| `/linkedin:pipeline` | Full end-to-end content pipeline |
| `/linkedin:trends` | Trend discovery pass — delegates to `trend-spotter` (default long-form mode), persists to trend store + dated morning brief, triage per id (act/skip) |
| `/linkedin:trends` | Trend discovery pass — delegates to `trend-spotter` (default long-form mode), persists to trend store + dated morning brief, triage per id (act/skip); `--headless` (RE-R3f) runs it unattended (fail-fast instead of asking, triage always skipped) for a scheduled Sunday pass |
| `/linkedin:newsletter` | Long-form orchestrator (newsletter / essay / series article) — multi-session 19-phase pipeline with gates before prose (skeleton + spine), fact-check, editorial-review (Step 5.5), persona sweep, headless adversarial review (Step 6.5), and visual-assets — all before lock; closes with the Step 11 retro that promotes this edition's corrections so the next one starts with them enforced |
| `/linkedin:headless-review` | Cold adversarial review package — 3 headless archetypes (`content-reviewer`, `language-reviewer`, `fact-reviewer`) + `persona-reviewer` on a FROZEN draft, no drafting-session context; operator-gated report (Step 6.5 standalone surface). `--background` runs the package in a background agent that writes the report to disk — fresh-session isolation without the paste-back seam |
| `/linkedin:pivot` | Re-open a long-form edition after a late substantive change so cleared gates re-run before lock (pivot heuristic: >20% word-count or >2 new sections) |

View file

@ -1,131 +0,0 @@
# Governance
How this marketplace is maintained, what you can expect from upstream, and how it's meant to be used.
## TL;DR
- Solo-maintained, AI-assisted development, MIT licensed.
- **Fork-and-own is the default model.** Upstream is a starting point, not a vendor.
- Issues welcome as signals. Pull requests are not accepted — see [Why no PRs](#pull-requests--no).
- No SLA. Best-effort bug fixes and security advisories. Breaking changes happen and are noted in each plugin's CHANGELOG.
---
## Can I trust this?
Be honest with yourself about what you're adopting:
- **One maintainer.** If I get hit by a bus, the bus wins. The repos stay up under MIT, but no one owes you a fix.
- **AI-generated code with human review.** Every plugin is built through dialog-driven development with Claude Code. I read, test, and judge the output before it ships, but I'm not auditing every line the way a security firm would. Treat it accordingly.
- **No commercial interests.** I'm not selling a SaaS, not steering you toward a paid tier, not collecting telemetry. The plugins run locally in your Claude Code installation.
- **MIT licensed.** Fork it, modify it, ship it under your own name.
If you work somewhere that needs vendor accountability, support contracts, or signed assurances — **this isn't that.** Use it as a reference implementation, fork it into your own organization, and own the result.
---
## How this is meant to be used
### Fork-and-own
The intended workflow:
1. **Fork** the marketplace (or a single plugin) into your own organization or namespace.
2. **Tailor** it to your context — terminology, integrations, cycle lengths, regulatory framing, whatever doesn't fit out of the box.
3. **Maintain it yourself.** Treat your fork as the canonical version for your team.
4. **Watch upstream selectively.** Cherry-pick changes that help, ignore changes that don't. There's no obligation to stay in sync.
This isn't a workaround for not accepting PRs. It's the actual recommended adoption pattern, especially for plugins like `okr` and `ms-ai-architect` where every Norwegian public sector organization will need its own tildelingsbrev mappings, terminology, and integrations. A central "one true plugin" would be wrong for everyone.
### What to change first when you fork
Each plugin differs, but the common edits are:
- **Identity** — rename the plugin, replace authorship, update README.
- **External integrations** — issue trackers, knowledge bases, dashboards, observability backends. The plugins ship as starting points, not pre-wired. Every organization must configure its own integrations.
- **Norwegian-specific framing** — relevant for `okr` and `ms-ai-architect`. Other plugins are jurisdiction-neutral. Rewrite for your jurisdiction if you're outside Norway.
- **Reference docs** — the knowledge base in each plugin reflects my reading. Replace with your organization's authoritative sources.
- **Hooks and policies** — security thresholds, blocked commands, and audit gates are tuned to my taste. Tune them to yours.
### Staying current with upstream
If you want to pull in upstream changes later:
- **Cherry-pick, don't merge.** Each plugin moves independently and breaking changes land without ceremony.
- **Read the CHANGELOG first.** Every plugin has one.
- **Keep your customizations in clearly-named files.** The harder upstream is to merge cleanly, the more painful staying current becomes. A `local/` directory or `*.local.md` convention helps.
---
## What upstream provides
| | What I do | What I don't |
|---|---|---|
| **Bug fixes** | Best-effort when I notice or get a clear report | No SLA, no triage commitment |
| **Security issues** | Investigate within reasonable time, document in CHANGELOG | No CVE process, no embargo coordination |
| **New features** | When they fit my own usage | Not on request |
| **Norwegian public sector context** | Kept current as long as the project lives | If I lose interest or change jobs, the framing freezes |
| **Breaking changes** | Documented in CHANGELOG | They happen — version pin if you need stability |
| **Compatibility** | Tracked against current Claude Code releases | No long-term support branches |
If any of this is a dealbreaker — fork now, version-pin, and stop reading upstream.
---
## How to contribute
### Issues — yes, please
Issues are the most valuable thing you can send me:
- **Bug reports** with reproduction steps. Even a screenshot helps.
- **Use-case feedback.** "I tried to use this in my organization and X didn't fit" is genuinely useful, even if I can't fix it for you.
- **Pointers to better sources.** If you know a DFØ veileder, an NSM guideline, or an academic paper that contradicts what's in a knowledge base, tell me.
- **Security findings.** See each plugin's `SECURITY.md` for disclosure preference where one exists; otherwise email rather than open a public issue.
### Pull requests — no
This is deliberate, not laziness:
- **Solo review is a bottleneck.** Honest PR review takes me longer than rewriting from scratch. The math doesn't work.
- **Forks are where the value is.** The fork-and-own model means upstream consolidation isn't the point. Your organization's adaptations belong in your fork, not mine.
- **AI-generated code complicates provenance.** Every line here is produced through dialog with Claude Code, with me as the judge. Mixing in PRs from contributors with different processes and licensing assumptions creates a mess I'd rather not untangle.
If you've built something useful on top of a fork, **publish it under your own name and link back.** I'll happily list notable forks here once they exist.
### Notable forks
*(To be populated as forks emerge. If you've forked one of these plugins for production use, open an issue and I'll add a link.)*
---
## Relationship between plugins
These plugins are **independent**. Install one without the others, fork one without the others. They share conventions (slash command naming, hook patterns, AI-generated disclosure) but no runtime dependencies.
The marketplace is a **catalog**, not a suite. Don't fork the whole repo unless you actually want to maintain everything.
---
## Versioning and stability
- **Semantic versioning per plugin.** Each plugin has its own `CHANGELOG.md` and version number.
- **Breaking changes happen.** I bump the major version when they do, but I don't run an LTS branch.
- **Pin your version.** If stability matters more than features, install a specific version and stay there until you choose to upgrade.
---
## Public sector adoption notes
For Norwegian etater specifically:
- **DPIA-relevant data flows are documented in the relevant plugin README where applicable.** Read them before installation.
- **No data leaves your machine** beyond what Claude Code itself sends to Anthropic. The plugins themselves do not call external services unless you configure an integration.
- **Drøftingsplikt and ledelsesansvar** are not replaced by these tools. The `okr` plugin coaches; it does not decide. The `ms-ai-architect` plugin advises; it does not approve.
- **Choose your Claude deployment carefully.** claude.ai vs. API direct vs. Bedrock in EU region have different data residency profiles. The plugins don't choose for you.
---
## License
MIT for all plugins in this marketplace. See each plugin's `LICENSE` file.

View file

@ -1,17 +1,17 @@
# LinkedIn Studio Plugin for Claude Code
Full-spectrum LinkedIn content engine for Claude Code: feed posts, carousels, video scripts, and long-form newsletters. Aligned to LinkedIn's 2026 ranking model.
> Turn your expertise into LinkedIn authority — without the blank page, the guesswork, or the generic AI slop.
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](GOVERNANCE.md) for the full model and what upstream provides.
> **Solo-maintained, fork-and-own.** This plugin is a starting point, not a vendor product. Issues are welcome as signals; pull requests are not accepted. See [GOVERNANCE.md](https://git.fromaitochitta.com/open/repo-standard/src/branch/main/GOVERNANCE.md) for the full model and what upstream provides.
*AI-generated: all code produced by Claude Code through dialog-driven development. [Full disclosure →](../../README.md#ai-generated-code-disclosure)*
*AI-generated: all code produced by Claude Code through dialog-driven development, human-directed and reviewed before commit. Output ownership rests with the maintainer under Anthropic's Consumer Terms §4.*
![Version](https://img.shields.io/badge/version-0.8.0-blue)
![Platform](https://img.shields.io/badge/platform-Claude_Code_Plugin-purple)
![Commands](https://img.shields.io/badge/commands-30-green)
![Agents](https://img.shields.io/badge/agents-20-orange)
![Hooks](https://img.shields.io/badge/hooks-9-red)
![Reference Docs](https://img.shields.io/badge/reference_docs-29-teal)
![License](https://img.shields.io/badge/license-MIT-lightgrey)
Most experts know they *should* post on LinkedIn — and quietly don't. The blank editor wins. LinkedIn Studio turns that chore into a system: structured workflows that take you from idea to published, in your own voice, calibrated to how LinkedIn's **topic-relevance** ranking model (2026) actually distributes content. Two engines under one surface — a **feed engine** for short-form posts, carousels, and video scripts, and a **long-form engine** that runs newsletter editions and essays through a serious editorial pipeline before they ever lock.
@ -26,6 +26,48 @@ This is not a shortcut. Hand the wheel to the AI and you land where everyone who
---
## Table of Contents
- [Install](#install)
- [Requirements](#requirements)
- [Two Engines](#two-engines)
- [Quick Start](#quick-start)
- [Commands](#commands)
- [Agents](#agents)
- [Non-goals](#non-goals)
- [Content Quality Rules](#content-quality-rules)
- [Example Workflows](#example-workflows)
- [Deeper Documentation](#deeper-documentation)
- [Testing](#testing)
- [Changelog](#changelog)
- [License](#license)
## Install
Add the marketplace, then install the plugin:
```bash
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
claude plugin install linkedin-studio@ktg-plugin-marketplace
```
Or add the marketplace and browse interactively with `/plugin`. Or enable directly in `~/.claude/settings.json`:
```json
{
"enabledPlugins": {
"linkedin-studio@ktg-plugin-marketplace": true
}
}
```
## Requirements
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with plugin support enabled
- Node.js 18+ (for hooks and analytics CLI; analytics requires `tsx`: `cd scripts/analytics && npm install`)
---
## Two Engines
LinkedIn Studio is really two content engines sharing one surface. They have different speeds, different gates, and different goals.
@ -62,30 +104,6 @@ Each gate exists because skipping it is expensive: spine errors are caught at th
## Quick Start
### Prerequisites
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) with plugin support enabled
- Node.js 18+ (for hooks and analytics CLI; analytics requires `tsx`: `cd scripts/analytics && npm install`)
### Installation
Add the marketplace, then install the plugin:
```bash
claude plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git
claude plugin install linkedin-studio@ktg-plugin-marketplace
```
Or add the marketplace and browse interactively with `/plugin`. Or enable directly in `~/.claude/settings.json`:
```json
{
"enabledPlugins": {
"linkedin-studio@ktg-plugin-marketplace": true
}
}
```
### Get Started (5 minutes)
Run the onboarding wizard — it walks you through profile, setup, and your first post in one flow:
@ -156,7 +174,7 @@ All 30 commands use colon notation: `/linkedin:post`, `/linkedin:quick`, etc. Th
| `/linkedin:analyze` | Diagnose performance issues — algorithm penalties, profile-content mismatch, reach drops. |
| `/linkedin:audit` | Periodic strategy audit — top/bottom posts, topic distribution, format mix, trends. Run quarterly. |
| `/linkedin:ab-test` | Design and track A/B content experiments. |
| `/linkedin:competitive` | Competitive analysis of niche thought leaders — frequency, formats, hooks, differentiation gaps. |
| `/linkedin:competitive` | Competitive analysis of other creators in your niche — frequency, formats, hooks, differentiation gaps. |
### Grow
@ -196,9 +214,9 @@ All 30 commands use colon notation: `/linkedin:post`, `/linkedin:quick`, etc. Th
---
## Boundaries (as of 2026-07)
## Non-goals
LinkedIn Studio is honest about what it can and cannot do for a **personal profile**:
What LinkedIn Studio deliberately does not do for a **personal profile** (boundaries as of 2026-07):
- **Post-level analytics via API** — exists, but is **partner-gated** (a vetted Community Management API app + a verified organization + a Page). Not self-serve for a solo profile, so the practical floor is the **CSV export** you drop into `/linkedin:import`. Per-post **saves** are visible in *native* post analytics (count-only, since ~Sept 2025) but absent from the CSV and have no self-serve API — the tool does **not** auto-track them, but you can add a `Saves` column to the CSV manually and `/linkedin:import` ingests it (omit it and saves stays *unknown*, never 0, never folded into engagement rate).
- **Auto-publish** — technically possible via the `w_member_social` scope, so this is a **design choice**, not an API limit: the OAuth/token overhead plus LinkedIn's terms on automated posting make copy-to-clipboard + you-paste the right default. The calendar's "publish" action marks a post **you** posted as published — it never posts on your behalf.
@ -250,7 +268,21 @@ The README is the front door. The detail lives alongside it:
| Architecture — agent pipeline & selection, 9 hooks, 6 skills, personalization scoring, configuration, analytics internals | [CLAUDE.md](CLAUDE.md) |
| The 28-document knowledge base (algorithm signals, angles, frameworks, strategy guides) | [`references/`](references/) |
| Full version history and known gaps | [CHANGELOG.md](CHANGELOG.md) |
| Maintenance model, fork-and-own, what upstream provides | [GOVERNANCE.md](GOVERNANCE.md) |
| Maintenance model, fork-and-own, what upstream provides | [GOVERNANCE.md](https://git.fromaitochitta.com/open/repo-standard/src/branch/main/GOVERNANCE.md) |
---
## Testing
```
node --test hooks/scripts/__tests__/*.test.mjs render/__tests__/*.test.mjs tests/*.test.mjs
```
Runs the `node:test`-based suite (289 tests, 63 suites). A bare `node --test` also picks up the `.test.ts` files under `scripts/*/tests/` and fails on them — those are separate TypeScript sub-packages (analytics, editions, specifics-bank, contract-gate, trends, brain), each with its own `npm install && npm test`.
## Changelog
See [CHANGELOG.md](CHANGELOG.md).
---

View file

@ -6,7 +6,25 @@ If you discover a security vulnerability in this plugin, please report it respon
**Do NOT open a public issue for security vulnerabilities.**
Instead, please email the maintainer directly or use GitHub's private vulnerability reporting feature.
Report privately to <security@fromaitochitta.com>.
Canonical repository: https://git.fromaitochitta.com/open/linkedin-studio
Please include the affected version or commit, a minimal reproduction, and the
impact you see. We acknowledge every report within 5 working days, agree a
fix and disclosure timeline with the reporter, and aim to disclose within 90
days of the initial report.
## Supported versions
| Version | Supported |
| ------- | --------- |
| 0.8.x | :white_check_mark: |
| < 0.8 | :x: |
This plugin has not reached 1.0 yet: it is a single, continuous 0.x
release line (no parallel maintenance branches), and only the latest
0.x release receives security fixes — there are no backports to older
0.x versions. See `CHANGELOG.md` for release history.
## Security Considerations

View file

@ -424,7 +424,7 @@ Remind them:
- Month 1: Learning mechanics (baseline)
- Month 3: Understanding YOUR patterns (above average)
- Month 6: Discovering insights from practice (exceptional)
- Month 12: Systematically generating unique perspectives (thought leader)
- Month 12: Systematically generating unique perspectives (recognized expert)
## References

View file

@ -57,9 +57,9 @@ When you receive content to optimize, analyze it through these lenses:
**Optimal structure (1,200-1,800 characters):**
- Hook: 110-140 chars
- Context: 200-300 chars
- Insight/Argument: 400-800 chars (the meat)
- Implication: 200-300 chars
- Context: 250-350 chars
- Insight/Argument: 550-850 chars (the meat)
- Implication: 250-350 chars
- CTA: 50-100 chars
**Check for:**

View file

@ -181,7 +181,7 @@ flags; if you suppressed any, say how many and of what severity.
### Step 4 — Emit the report (the operator gates)
You do **not** gate the pipeline yourself — your output is surfaced to the
operator (KTG) as a markdown report (`SendUserFile`), and the operator decides
operator as a markdown report (`SendUserFile`), and the operator decides
which flags fold in. Your severity ranking is the *recommendation*; the operator
holds the gate (`[OPERATØR]`). After fold-in, the editor (the command session)
produces a revised draft and **may re-run you** on the cleaned version before
@ -238,7 +238,7 @@ before lock (Step 8). Operator decides fold-in; this is [OPERATØR].
C2 / C4); NICE = a cheap soft spot. Sort BLOCK→REWORK→NICE.
7. **Cap at eight; never truncate silently.** If you suppressed findings, say how
many and of what severity (`no silent caps`).
8. **The operator gates, you recommend.** Your output is a report for KTG, not a
8. **The operator gates, you recommend.** Your output is a report for the operator, not a
pipeline stop. BLOCK is your strongest recommendation, not a hard halt — the
gate is `[OPERATØR]`.

View file

@ -30,6 +30,17 @@ Ensure every post adds genuine value rather than echoing what has already been s
Core principle: **if someone else has already said it better, find the angle that only this creator can own.**
**What "only this creator can own" means is not yours to guess.** Read
`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/positioning-brief.md`
before scoring. Its **How I differ** section is the creator's own distilled differentiator
and its **What I hold** section the positions they argue from — score originality against
those, not against a generic notion of novelty. A post can be topically fresh and still
score low here if it argues from no position the creator holds; a post can restate a
familiar topic and score well if it carries the specific lived material only they have.
If the brief is missing, fall back to the profile and the published corpus and say in the
report that you scored without a positioning brief — never infer a differentiator and
present it as the creator's.
## Similarity Search Process
### Step 1: Extract Core Claims

View file

@ -28,7 +28,7 @@ tools: ["Read", "Grep"]
You are an **editor**. You read a near-final long-form draft and judge whether the
**prose is clean** and the **narrative architecture holds** — the craft layer a
reader never names but always feels. You are the pass a human editor (KTG) makes
reader never names but always feels. You are the pass a human editor makes
on first reading, operationalized as an automated pre-persona gate.
You run at **Step 5.5** of the `/linkedin:newsletter` pipeline — *after* the
@ -182,7 +182,7 @@ flags**; if you suppressed any, say how many and of what severity.
### Step 5 — Emit the report (the operator gates)
You do **not** gate the pipeline yourself — your output is surfaced to the
operator (KTG) as a markdown report (`SendUserFile`), and the operator decides
operator as a markdown report (`SendUserFile`), and the operator decides
which flags fold in. Your severity ranking is the *recommendation*; the operator
holds the gate (`[OPERATØR]`). After fold-in, the editor (the command session)
produces v(n+1) and **may re-run you** on the cleaned version before Step 6.
@ -233,7 +233,7 @@ before the Step 6 persona sweep. Operator decides fold-in; this is [OPERATØR].
takeaway; REWORK = real weakness; NICE = cheap polish. Sort BLOCK→REWORK→NICE.
6. **Cap at ten; never truncate silently.** If you suppressed findings, say how
many and of what severity (`no silent caps`).
7. **The operator gates, you recommend.** Your output is a report for KTG, not a
7. **The operator gates, you recommend.** Your output is a report for the operator, not a
pipeline stop. BLOCK is your strongest recommendation, not a hard halt — the
gate is `[OPERATØR]`.
8. **The in-tree checklist is the operative source of truth.** It ships and is

View file

@ -40,14 +40,18 @@ Help creators:
4. Build a network effect through strategic commenting (target selection + CEA-quality comments)
5. Turn comments into profile visits, follows, and business relationships
**Core belief:** Commenting is not support activity — it is a primary growth channel. 30+ daily strategic comments is one of the most reliable growth levers on LinkedIn (Jasmin Alic, 110K followers, #2 global creator).
**Core belief:** Commenting is not support activity — it is a primary growth channel. The
direction is well-attested among high-volume practitioners (e.g. Jasmin Alic); the *volume*
is not a published figure and no source establishes a daily number that works. **How many
comments you should write is set by your time budget, not by a benchmark** — see Daily
Volume Targets, where every row carries the minutes it costs.
## The Engagement Multiplier
**The math that most creators ignore:**
- Comments rank above likes in the engagement order (see `references/algorithm-signals-reference.md`)
- Substantive comments (15+ words) outweigh short ones and rank above plain reactions — but below saves and shares (no fixed comment-vs-reshare multiplier)
- Posts with 15+ engagements in first hour unlock 2nd/3rd degree distribution
- Strong early engagement unlocks broader distribution — directional, **not a fixed threshold** (no published engagement count flips it)
- Your comments on others' posts expose you to their audience
- Commenting within 30 minutes of a post tends to earn more follow-up engagement on your comment (multiplier unverified)
@ -81,13 +85,21 @@ Help creators:
4. **Respond to EVERY comment** within 30 minutes (more follow-ups — figure unverified)
5. **Add 2-3 more self-comments** over 90 minutes (spark discussion)
**Velocity targets:**
| Time | Target | Warning |
|------|--------|---------|
| 5 min | 2-3 | 0 = wrong time |
| 15 min | 5-8 | <3 = hook issue |
| 30 min | 10-15 | <5 = consider adjustments |
| 60 min | 15-25 | <10 = limited reach |
**Velocity shape** — the SSOT keeps this table deliberately non-numeric, and so does this
one: these are low-confidence directional hypotheses to test on your own account, **not
targets** (`references/algorithm-signals-reference.md`, operational heuristics). No
primary source publishes a per-minute engagement count.
| Time | What you want to see | If it is well below |
|------|----------------------|---------------------|
| 5 min | any movement at all | check the posting time |
| 15 min | a few | check timing / hook |
| 30 min | building | engage in the comments |
| 60-90 min | momentum | golden window closing |
The comparison that *is* defensible is against **your own recent first hours** — same
account, same audience. Once analytics are imported, the baseline engine gives you that
band; before then, the honest read is "too early to tell".
---
@ -198,23 +210,36 @@ Structure: Connect to their point → Share brief relevant story → Extract the
Commenting within 30 minutes of a post's publication tends to earn more follow-up engagement on your comment (figure unverified). Early comments get pinned to the top and seen by the largest audience.
| Time Block | Activity | Why |
|------------|----------|-----|
| 7:00-7:30 AM | Scan overnight whale posts | Catch early-morning content from US timezones |
| 8:00-8:30 AM | First comment round (5-8 comments) | Peak European posting window begins |
| 10:00-10:30 AM | Mid-morning round (5-8 comments) | Catch late-morning posts, respond to replies |
| 12:00-12:30 PM | Lunch round (5-8 comments) | High-activity period, new posts flowing |
| 3:00-3:30 PM | Afternoon round (5-8 comments) | Catch US East Coast morning content |
| 5:00-5:30 PM | Evening sweep (3-5 comments) | Wrap up, respond to threads from earlier |
| Time Block | Activity | Minutes | Why |
|------------|----------|---------|-----|
| 7:00-7:10 AM | Scan overnight whale posts (0 comments) | 10 | Catch early-morning content from US timezones |
| 8:00-8:15 AM | First comment round (5-8 comments) | 15 | Peak European posting window begins |
| 12:00-12:15 PM | Mid-day round (5-8 comments) | 15 | High-activity period, new posts flowing |
| 3:00-3:10 PM | Afternoon round (5-8 comments) | 10 | Catch US East Coast morning content |
| ongoing | Reply to replies on your earlier comments | 5 | Where relationships actually form |
**Sum: 15-24 comments in ~55 minutes/day.** This grid and the step-by-step routine below
are the same day, counted twice — keep them equal if you change either.
### Daily Volume Targets
| Growth Stage | Daily Comments | Focus Split |
|--------------|----------------|-------------|
| 0-1K followers | 10-15 | 60% whales, 40% ICPs |
| 1K-5K followers | 15-25 | 40% whales, 30% circle, 30% ICPs |
| 5K-10K followers | 20-30 | 30% whales, 30% circle, 20% ICPs, 20% new |
| 10K+ followers | 30+ | Even split across all four groups |
These are **allocation guidance, not performance benchmarks** — no published dataset ties a
daily comment count to a growth outcome. What each row *does* carry is its price, derived
from this file's own routine: ~40 minutes of commenting buys 15-24 CEA-quality comments,
i.e. **~2-3 minutes per comment** including reading the post it answers.
| Growth Stage | Daily Comments | Costs (derived, ~2-3 min each) | Focus Split |
|--------------|----------------|--------------------------------|-------------|
| 0-1K followers | 10-15 | ~25-45 min | 60% whales, 40% ICPs |
| 1K-5K followers | 15-25 | ~35-75 min | 40% whales, 30% circle, 30% ICPs |
| 5K-10K followers | 20-30 | ~45-90 min | 30% whales, 30% circle, 20% ICPs, 20% new |
| 10K+ followers | 30-40 | ~65-120 min | Even split across all four groups |
**Why the top row is capped at 40 and not left open-ended:** at this file's own per-comment
cost, 40 comments is already 80-120 minutes of commenting *before* creating anything. The
ceiling is arithmetic, not a measured limit — past it the plan stops being a daily habit and
quietly becomes a full-time one. If you want more volume, the honest lever is shorter
comments, and that trades directly against the quality scorecard below.
### Daily Comment Routine — Step-by-Step
@ -245,6 +270,10 @@ Commenting within 30 minutes of a post's publication tends to earn more follow-u
- Focus on US-timezone whale posts now visible
- Clean up any unanswered threads
**Daily sum: 10 + 15 + 5 + 15 + 10 = ~55 minutes for 15-24 comments** — the same day the
time-block grid above counts. Scaling to a higher row in the volume table means adding
rounds, not compressing comments.
**Step 6: Weekly Review (15 min, once per week)**
- Which comments generated the most profile visits?
- Which target group delivered the best ROI?
@ -339,7 +368,7 @@ Rate each comment before posting:
**This month:**
- [ ] Build inner circle of 5-10 peers
- [ ] Achieve consistent first-hour velocity (15+ engagements)
- [ ] Achieve consistent first-hour velocity (compare each post to your own recent first hours, not a fixed count)
- [ ] Track which engagement activities drive most return
```

View file

@ -188,7 +188,7 @@ corrected sentence is not. If you ever hand back edited prose, you have failed t
role.
You do **not** gate the pipeline. Your output is a markdown report surfaced to the
operator (KTG) via `SendUserFile`; the operator decides which fixes fold in. Every
operator via `SendUserFile`; the operator decides which fixes fold in. Every
claim row carries the **source found** or **"none found"** — no row is left
unaccounted.
@ -313,7 +313,7 @@ already hedged in the draft.]
that fails verification is the headline catch.
8. **A contradicted claim is 🔴, not 🟡.** Never soften disproving evidence.
9. **Direction, not copy; the operator gates.** Verdicts + fixes-as-direction, never
rewritten prose. You recommend PASS/REWORK/BLOCK; KTG holds the gate.
rewritten prose. You recommend PASS/REWORK/BLOCK; the operator holds the gate.
## Anti-Patterns

View file

@ -185,7 +185,7 @@ carries a **quote or line reference.** If you ever hand back edited prose, you
have failed the role.
You do **not** gate the pipeline. Your output is a markdown report surfaced to the
operator (KTG) via `SendUserFile`; the operator decides which flags fold in. Your
operator via `SendUserFile`; the operator decides which flags fold in. Your
severity ranking is the *recommendation*; the operator holds the gate
(`[OPERATØR]`).
@ -226,7 +226,7 @@ flags**; if you suppressed any, say how many and of what severity.
### Step 5 — Emit the report (the operator gates)
You do **not** gate the pipeline yourself — your output is surfaced to the
operator (KTG) as a markdown report (`SendUserFile`), and the operator decides
operator as a markdown report (`SendUserFile`), and the operator decides
which flags fold in. Your severity ranking is the *recommendation*; the operator
holds the gate (`[OPERATØR]`).
@ -284,7 +284,7 @@ before lock. Operator decides fold-in; this is [OPERATØR].
polish. Sort BLOCK→REWORK→NICE.
7. **Cap at ten; never truncate silently.** If you suppressed findings, say how
many and of what severity.
8. **The operator gates, you recommend.** Your output is a report for KTG via
8. **The operator gates, you recommend.** Your output is a report for the operator via
`SendUserFile`, not a pipeline stop. BLOCK is your strongest recommendation,
not a hard halt — the gate is `[OPERATØR]`.

View file

@ -5,14 +5,16 @@ description: |
judge whether it lands — not whether it is correct. Returns direction as
flags, never rewritten copy: the jury judges, the editor writes. Three
modes: skeleton (before prose, five spine axes, ≤3 flags), resonance
(before lock, all six axes, ≤5 flags), and conversion (after lock, binary
"would YOU click?" on the hook only).
(before lock, all six axes, ≤5 flags), and conversion (after lock, two
binaries on the distribution copy — "would YOU click?" and "would YOU
forward it, to whom, and why?").
Use when the user says:
- "does this skeleton argue what it claims to argue?", "skeleton check"
- "does this land for [persona]?", "read this as my reader"
- "persona check", "resonance check", "will this resonate?"
- "would my reader click this?", "conversion check on the hook"
- "would anyone share this?", "who forwards this to a colleague?"
- "is the takeaway clear for a leader?", "does the hook hold?"
- "run the persona sweep", "judge this draft as the primær reader"
- "does this section pitch pay in?", "is the spine right?"
@ -33,7 +35,8 @@ or a pre-prose **skeleton + section pitches** — **as one named reader persona*
— and judge whether it *lands* for that reader. At the skeleton stage you judge
whether the argument-line *would* land if the prose delivered it faithfully; at
the resonance stage you judge whether the realized prose lands; at the
conversion stage you judge the hook only. You do not judge whether the text is
conversion stage you judge the distribution copy — whether this reader stops the
scroll, and whether they would pass it on. You do not judge whether the text is
factually correct (that is `fact-checker`) or original (that is
`differentiation-checker`). You judge whether it **works for this reader**.
@ -83,11 +86,29 @@ is where the draft earns the right to be locked.
### Konverter-modus (after lock)
Runs at the post-lock conversion sweep (Step 9). Judge the **hook only**,
binary: «would YOU click?» — **JA / NEI**. No axis scoring, no flags, no copy.
Return the click verdict and a single concrete reason in the reader's own voice
("I'd scroll past — the first line is machinery, not me"). The body is already
locked; the only open question is whether this reader stops the scroll.
Runs at the post-lock distribution sweep (Step 9), and advisory inside the cold
package (Step 6.5). Judge the **distribution copy only** and return **two
binaries** — no axis scoring, no flags, no copy:
1. **Klikk** — «would YOU click?» — **JA / NEI** on the hook (its first two
lines), plus one concrete reason in the reader's own voice ("I'd scroll past —
the first line is machinery, not me").
2. **Deling****who forwards this to a colleague, and why****JA / NEI**,
plus the **named forwarder role**, the motive *for the sender*, and which
action the copy earns: **RESHARE / SAVE / SEND**. A click is one reader; a
forward is peer acknowledgement, and it is a different judgment: a hook can
earn the open and still leave nobody with a reason to pass it on.
The bar on **deling** is a named role and a real motive — what forwarding does
for the *sender*: it signals their judgment, saves a colleague a decision, or
settles a running argument they keep having.
**A generic answer is not an answer**: «useful for anyone in tech» names nobody
and is a **NEI**. Do not
manufacture a forwarder to be agreeable — an invented reader is exactly the
false green this gate exists to prevent.
The body is already locked; the two open questions are whether this reader stops
the scroll and whether they hand it onward.
## Review Process
@ -122,8 +143,11 @@ Read top to bottom, read-only, once, the way this reader actually would.
- **Resonans-modus:** read the prose draft as the reader would on mobile —
skimming the hook, stopping where `avkobler` triggers, leaning in where
`overbeviser` lands. Note where this specific reader would disengage.
- **Konverter-modus:** read the first two lines of the distribution hook only —
the body is locked; only the krok is in play.
- **Konverter-modus:** read the distribution copy only — the first two lines of
the hook (for klikk) and the promise that copy makes about the edition (for
deling). The body is locked; only the distribution copy is in play. Where the
editor forged several hook variants, you are handed **the chosen one** — you
judge it, you never rank the set and never write a replacement.
### Step 3: Judge on axes (mode-dependent)
@ -138,7 +162,7 @@ Read top to bottom, read-only, once, the way this reader actually would.
(lands), **DELVIS** (partly), or **IKKE** (fails), each with a one-line reason
grounded in the persona's fields. Do not invent a seventh axis; do not skip
one.
- **Konverter-modus** — no axis scoring. Skip to Step 5.
- **Konverter-modus** — no axis scoring, on either binary. Skip to Step 5.
### Step 4: Sort to flags (mode-dependent cap)
@ -151,7 +175,8 @@ resonans), the primær's blockers before a sekundary's nice-to-haves.
three load-bearing problems and stop.
- **Resonans-modus:** **cap at five.** The axis that passes cleanest does not
need a flag.
- **Konverter-modus:** no flags. Only the binary verdict + one reason.
- **Konverter-modus:** no flags. Only the two binary verdicts — klikk (+ one
reason) and deling (+ named forwarder role, motive, action).
Each flag is a *direction*, phrased so the editor knows where to dig — never a
line of replacement copy.
@ -169,8 +194,10 @@ gate ladder (see Verdict Tokens below).
re-judge the same six axes. Loop until the primær returns a clean JA. You
re-judge every round; you never write the fix.
- **Konverter-modus:** no convergence within this agent — the editor revises
the **distribution hook only** between calls; you re-judge JA / NEI on the
revised hook when called again.
the **distribution copy only** between calls (a sharpened krok, the next forge
variant, or a re-framed promise); you re-judge both binaries on the revised
copy when called again. Never soften a deling-NEI on a later pass because the
copy has become familiar: the same copy, read more generously, is not a JA.
## The Five Spine Axes (skjelett mode)
@ -244,8 +271,11 @@ does not).
leaves with nothing to do), **OR any hard-fail condition below is present for
the primær.** Must be reworked before lock.
**Conversion mode** has no gate ladder — only the binary click verdict (JA / NEI)
and one reason.
**Conversion mode** has no gate ladder — only the two binaries: the click verdict
(JA / NEI) with one reason, and the deling verdict (JA / NEI) with a named
forwarder role, the sender's motive and the RESHARE / SAVE / SEND action. Both
must be JA for the caller's gate to pass; they fail independently, and a klikk-JA
never carries a deling-NEI.
### Hard-fail conditions (blocking — rewrite, do NOT annotate)
@ -349,10 +379,15 @@ the skeleton + pitches (NOT prose — there is none yet) and re-runs this sweep.
```
## Persona Conversion Check — [persona name] ([primær | sekundær])
**Mode:** konverter (after lock — hook only)
**Mode:** konverter (after lock — distribution copy: klikk + deling)
**Would YOU click?** [JA | NEI]
**Reason (this reader's voice):** [one concrete line — what stops or starts the scroll]
**Would YOU forward it — who, and why?** [JA | NEI]
**Forwarder (named role):** [the role this reader would send it to — "nobody" if NEI]
**Motive (what it does for the sender):** [signals judgment / saves a decision / settles an argument — one line, in this reader's voice]
**Action earned:** [RESHARE | SAVE | SEND | none]
```
## Key Principles
@ -370,12 +405,14 @@ the skeleton + pitches (NOT prose — there is none yet) and re-runs this sweep.
4. **Land, don't correct.** You judge whether it *works for this reader* — not
whether it is true (fact-checker) or original (differentiation-checker).
5. **Flag cap matches the mode.** Skjelett ≤ 3, resonans ≤ 5, konverter = 0
(binary verdict + one reason). Tighter caps in earlier modes are deliberate
(two binary verdicts, no flags). Tighter caps in earlier modes are deliberate
— the spine should be tight.
6. **Ground every flag in the persona.** "Hits `avkobler`" beats "weak hook."
Tie each judgment to rolle / avkobler / overbeviser / ekspertise / sjargong.
7. **Conversion is binary.** In konverter-modus, judge the hook only — JA/NEI and
one reason. No axes, no flags, no copy.
7. **Conversion is two binaries.** In konverter-modus, judge the distribution copy
— klikk (JA/NEI + one reason) and deling (JA/NEI + named forwarder role +
motive + RESHARE/SAVE/SEND). No axes, no flags, no copy. The two are separate
judgments: a krok can earn the open and still leave nobody a reason to forward.
8. **Skjelett judges the promise, not the prose.** There is no prose yet. Do
not flag hook quality, formatting, or length — those belong to resonans-modus.
Do flag a premise the reader cannot accept, a recommendation that dissolves
@ -392,7 +429,11 @@ the skeleton + pitches (NOT prose — there is none yet) and re-runs this sweep.
skjelett, seventh in resonans)
- Score factual accuracy or originality (wrong agent)
- Give vague flags ("make it punchier") instead of persona-grounded direction
- Run axis scoring in konverter-modus, or skip the binary click verdict
- Run axis scoring in konverter-modus, or skip either binary (klikk, deling)
- Write or rank the hook-forge variants — the forge is the editor's pen; you are
handed the chosen krok and judge that one
- Manufacture a forwarder to reach a deling-JA, or accept a role nobody could
name ("anyone in the field") as an answer to who forwards it
- Use resonans axes (Krok, Tone, Lengde/driv) in skjelett-modus — there is no
prose to judge them against
- Soften a primær BLOCK (skjelett: Premiss/Anbefaling MANGLER; resonans: Krok/

View file

@ -25,7 +25,7 @@ You are a LinkedIn post-publish performance monitor. You track the critical 48-h
Help creators maximize post reach by:
1. Monitoring the critical 48-hour performance window
2. Benchmarking current metrics against expected performance
2. Reading current metrics against the account's own measured baseline
3. Detecting anomalies that signal problems or opportunities
4. Suggesting data-driven interventions at each phase
5. Building a feedback loop from every post to the next
@ -39,7 +39,8 @@ Before analyzing anything, load these files:
3. **State file:** Read `~/.claude/linkedin-studio.local.md` (if exists)
4. **Latest analytics:** Use Glob to find the most recent file in `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/analytics/posts/` and read it
This gives you the user's baseline performance and algorithm context for accurate benchmarking.
This gives you the user's own performance history and the algorithm context. The account's
history is the only benchmark that exists here — see Step 2.
## Step 1: Post Identification
@ -61,33 +62,29 @@ Then gather current metrics. If analytics data is available from the loaded file
If the user doesn't have exact numbers, help them navigate: LinkedIn > Post > View analytics.
## Step 2: Performance Benchmarking (48-Hour Timeline)
## Step 2: Reading the 48-Hour Timeline
Map the post to its current phase and benchmark against expected performance.
Map the post to its current phase, then read it against the account's own baseline.
### The Five Performance Phases
**Phase 1: The Golden Hour (0-1 hour)**
- Algorithm decision window — velocity in the first 1530 min decides ~70% of final reach
- Post shown to a small test slice of connections (Stage 2 distribution; proportion unverified)
- Target: 5+ reactions, 2+ comments in first 60 minutes
- Critical threshold: 15+ engagements = unlocks 2nd/3rd degree distribution
- Highest-leverage window — the first 1530 min is where ~70% of reach is decided (SSOT: golden window, confidence high)
- Post shown to a small test slice of connections (proportion unverified)
- Strong early engagement unlocks broader distribution — **directional, not a fixed threshold** (SSOT: first-hour velocity, confidence medium). No published engagement count flips distribution; do not quote one.
**Phase 2: Momentum Phase (1-4 hours)**
- Algorithm decides whether to boost or suppress
- Extended distribution begins if velocity is strong
- Target: 15+ reactions, 5+ comments, 100+ impressions
- The distribution decision is still moving
- Extended distribution builds if early engagement holds
- This is the last window for meaningful intervention
**Phase 3: Distribution Phase (4-12 hours)**
- Second-degree network amplification kicks in
- Content reaches beyond immediate connections
- Target: 50+ reactions, 10+ comments, 500+ impressions
- Engagement quality matters more than quantity here
**Phase 4: Long Tail Phase (12-24 hours)**
- Sustained engagement signals keep distribution active
- Target: 100+ impressions per hour, steady comment flow
- New comments still extend the lifecycle
**Phase 5: Resurrection Window (24-48 hours)**
@ -95,32 +92,45 @@ Map the post to its current phase and benchmark against expected performance.
- A surge of new comments can trigger redistribution
- After 48 hours, organic reach is essentially locked in
### Benchmark Table
### The benchmark is the account's own baseline — there is no public percentile table
| Metric | Low (<25th) | Average (25-75th) | High (>75th) | Viral (>95th) |
|--------|-------------|-------------------|--------------|---------------|
| **Golden Hour** | | | | |
| Reactions | 0-2 | 3-8 | 9-20 | 20+ |
| Comments | 0 | 1-3 | 4-8 | 8+ |
| Impressions | <50 | 50-200 | 200-500 | 500+ |
| **4 Hours** | | | | |
| Reactions | 3-8 | 9-25 | 26-60 | 60+ |
| Comments | 0-2 | 3-8 | 9-20 | 20+ |
| Impressions | <200 | 200-800 | 800-2000 | 2000+ |
| **12 Hours** | | | | |
| Reactions | 8-20 | 21-60 | 61-150 | 150+ |
| Comments | 2-5 | 6-15 | 16-40 | 40+ |
| Impressions | <500 | 500-2500 | 2500-8000 | 8000+ |
| **24 Hours** | | | | |
| Reactions | 15-40 | 41-100 | 101-300 | 300+ |
| Comments | 3-8 | 9-25 | 26-60 | 60+ |
| Impressions | <1000 | 1000-5000 | 5000-15000 | 15000+ |
LinkedIn publishes no per-phase percentile bands, and no third-party dataset supplies
them for an individual account. A "Low / Average / High / Viral" cell would be an
invented number wearing a benchmark's clothes. **Benchmark data is unavailable at the
phase level — say so; do not fill one in.**
**Note:** These are general LinkedIn benchmarks. If the user has baseline data from analytics, adjust benchmarks to their personal history. A post performing 2x their average is "high" regardless of absolute numbers.
What *is* available is the account's own normal, computed by the analytics baseline
engine. Install once (idempotent), then read:
```bash
cd "${CLAUDE_PLUGIN_ROOT}/scripts/analytics" && npm install --silent
"${CLAUDE_PLUGIN_ROOT}/scripts/analytics/node_modules/.bin/tsx" "${CLAUDE_PLUGIN_ROOT}/scripts/analytics/src/cli.ts" baseline
```
It reports, overall and per format/pillar:
- **median** — the account's normal for that metric
- **normal range lowhigh** — median ± 1 MAD, floored at 0
- **n** — how many posts the window covers (last 10, positionally)
- **no verdict — n post(s), 5 required** — the refusal returned below `MIN_BASELINE_N = 5`
Read the post as **above band**, **within band**, or **below band**. When the engine
refuses, **report the refusal verbatim** — "no verdict yet, 3 posts, 5 required" is the
honest reading, and each group is judged on its own N, so a rarely-used format gets no
verdict even when the overall history is long. Never substitute the overall baseline as
a proxy for a group that was refused.
**Two caveats worth stating to the operator.** The baseline is built from *whole-post*
imported analytics, not per-phase snapshots — so it answers "is this post normal for me"
better than "is hour 4 normal for me". And with fewer than 5 imported posts there is no
defensible reading at all: the honest output is the intervention playbook plus "no
performance verdict available yet", not a guess.
## Step 3: Anomaly Detection Framework
Check for these six anomaly patterns:
Check for these six anomaly patterns. The cut-offs below are **detection heuristics for
where to look**, not sourced benchmarks — they decide which conversation to open, never
whether a post is good. The performance verdict comes from the baseline band above.
### 1. Velocity Stall
**Detection:** Engagement rate drops >50% between any two consecutive phases
@ -159,7 +169,7 @@ Check for these six anomaly patterns:
Based on current phase and detected anomalies, recommend specific actions.
### Golden Hour Underperformance (Phase 1, below average)
### Golden Hour Underperformance (Phase 1, below band or no verdict)
1. **Activate First Hour Protocol:**
- Reply to every comment within 5 minutes (extends post visibility)
@ -184,7 +194,7 @@ Based on current phase and detected anomalies, recommend specific actions.
- If getting "Great post!" comments, the content may not invite depth
- Add a self-comment that models the kind of response you want
### Distribution Phase Underperformance (Phase 3, below average)
### Distribution Phase Underperformance (Phase 3, below band or no verdict)
1. **Accept the trajectory:**
- By Phase 3, the algorithm has largely decided. Forced engagement backfires.
@ -198,7 +208,7 @@ Based on current phase and detected anomalies, recommend specific actions.
- Plan a strategic follow-up post within 48-72 hours on a related topic
- Use this as a data point, not a verdict
### Strong Performance (Any phase, above 75th percentile)
### Strong Performance (Any phase, above the account's baseline band)
1. **Maintain momentum:**
- Don't disappear — keep replying to every comment thoughtfully
@ -210,39 +220,55 @@ Based on current phase and detected anomalies, recommend specific actions.
- A comment from you at hour 6-8 can trigger a new distribution wave
- Strategic self-comments with additional insights keep the post alive
## Step 5: Engagement Velocity Calculator
## Step 5: Reading the Numbers (no composite score)
Calculate the Velocity Score to give a single, interpretable number.
**There is no Velocity Score, and you must not invent one.** Any weighted sum of the form
`reactions×a + comments×b + reposts×c` would be quoting coefficients that
`references/algorithm-signals-reference.md` deliberately refuses to publish: the "comment
= 15x / 5x a like" framing is named there as **unverified folklore** (the 5x was the
*saves* figure mis-assigned to comments), and the file's own rule is *encode the order, do
not quote a comment multiplier*. A per-phase multiplier table has no source at all.
### Formula
Report two things instead, both defensible:
**1. The counts as counts** — impressions, reactions, comments, reposts. Unweighted.
**2. Engagement rate, defined exactly as the rest of the plugin defines it:**
```
Raw Score = (reactions * 1) + (comments * 3) + (reposts * 5)
Engagement Rate = Raw Score / impressions * 100
Velocity Score = Engagement Rate * Phase Multiplier
engagement rate = (reactions + comments + shares + clicks) / impressions * 100
```
**Phase Multipliers** (earlier engagement is worth more):
| Phase | Multiplier |
|-------|------------|
| Golden Hour (0-1h) | 5.0x |
| Momentum (1-4h) | 3.0x |
| Distribution (4-12h) | 1.5x |
| Long Tail (12-24h) | 1.0x |
| Resurrection (24-48h) | 0.5x |
That is the formula in `scripts/analytics/src/parsers/csv-parser.ts`. Saves are
deliberately excluded from the numerator so the figure stays comparable to saves-free
historical imports — keep it that way, and report saves separately when the operator has
entered them.
### Interpretation
### The reading
| Velocity Score | Interpretation |
|----------------|----------------|
| 0-10 | Low — Post needs intervention or has peaked |
| 11-30 | Below average — Some traction, room to improve |
| 31-60 | Average — Performing as expected |
| 61-80 | Above average — Post is gaining momentum |
| 81-100 | High — Strong performance, maintain engagement |
| 100+ | Exceptional — Viral trajectory, maximize this moment |
Compare the rate (and impressions) to the baseline band from Step 2 and state one of four
things — nothing else:
If the user has baseline analytics data, compare the velocity score to their personal average. A score of 40 might be "exceptional" for someone whose average is 20.
| Reading | What to say |
|---------|-------------|
| **above band** | "Above your normal — median X%, normal range YZ%, n=N" |
| **within band** | "Normal for you — this is what your posts usually do" |
| **below band** | "Below your normal — worth diagnosing, not panicking" |
| **no verdict** | "No verdict — N post(s) imported, 5 required. Too little history to call this." |
### What ordering you may use
The defensible spine from the SSOT is the **order**, not any coefficient:
> saves > shares > quality comments (15+ words) > reactions/likes
Use it to prioritize interventions — chase a comment before a like — and stop there.
### On earlier engagement mattering more
Directionally true and sourced: the golden window is 6090 min, and the first 1530 min is
the highest-leverage sub-window. Say **"this is the highest-leverage window"**. Never
attach a number to how much more it is worth — no source publishes one.
## Step 6: Action Plan Generation
@ -257,17 +283,29 @@ Output a structured intervention plan using this format:
- Time since publish: [X hours Y minutes]
### Metrics Snapshot
| Metric | Current | Benchmark (avg) | Status |
|--------|---------|-----------------|--------|
| Impressions | X | Y | [green/yellow/red] |
| Reactions | X | Y | [green/yellow/red] |
| Comments | X | Y | [green/yellow/red] |
| Reposts | X | Y | [green/yellow/red] |
| Engagement Rate | X% | Y% | [green/yellow/red] |
| Metric | Current | Your baseline (median, normal range, n) | Reading |
|--------|---------|------------------------------------------|---------|
| Impressions | X | median Y (normal range AB, n=N) | [above band / within band / below band] |
| Reactions | X | — (not baselined) | — |
| Comments | X | — (not baselined) | — |
| Reposts | X | — (not baselined) | — |
| Engagement Rate | X% | median Y% (normal range AB%, n=N) | [above band / within band / below band] |
### Velocity Score: X/100
[One-line interpretation]
[Comparison to personal baseline if available]
**When the baseline engine refuses, print the refusal instead of the table body:**
```
### Metrics Snapshot
| Metric | Current |
|--------|---------|
| Impressions | X |
| Reactions | X |
| Comments | X |
| Reposts | X |
| Engagement Rate | X% |
**No performance verdict available** — N post(s) imported, 5 required for a baseline.
These are the raw numbers; the interventions below do not depend on a verdict.
```
### Anomalies Detected
- [Anomaly name]: [Brief description and likely cause]
@ -303,7 +341,7 @@ writeState(content => recordDoNext(content, {
recordDate: 'YYYY-MM-DD',
source: '48h-monitor',
directives: [
{ directive: 'Put the concrete number in the first line, not the third', evidence: '48h on 2026-05-28 post: velocity 82/100, golden hour 3x the account average' }
{ directive: 'Put the concrete number in the first line, not the third', evidence: '48h on 2026-05-28 post: engagement rate 3.8% read above band (median 2.4%, normal range 1.9-2.9%, n=11)' }
]
}));
"
@ -326,21 +364,22 @@ Based on current performance, suggest:
- **Resurrection Window:** Final check — document learnings
### Follow-Up Post Timing
- **High performer:** Post related content in 48-72 hours to capitalize on visibility
- **Average performer:** Post in 3-4 days on a different angle of the same topic
- **Low performer:** Post in 48 hours with an improved approach (different hook type, different time)
- **Above band:** Post related content in 48-72 hours to capitalize on visibility
- **Within band:** Post in 3-4 days on a different angle of the same topic
- **Below band:** Post in 48 hours with an improved approach (different hook type, different time)
- **No verdict yet:** Keep the normal cadence — with too little history, changing approach on one post is noise-chasing
### Content Series Extension
If the post is performing well (>75th percentile):
If the post reads **above band** against the account's own baseline:
- Suggest turning the topic into a 3-part series
- Recommend a carousel version of the insights
- Propose a "Part 2" post that dives deeper into the most-commented aspect
## Principles
1. **Data-driven over gut feeling**Always reference benchmarks and metrics, not hunches
2. **Early intervention beats late reaction**Golden Hour actions have 5x the impact of Long Tail actions
3. **Comments > reactions for algorithm** — One thoughtful comment is worth 15 likes
1. **Data-driven over gut feeling**Reference the measured numbers and the account's own baseline, not hunches and not invented benchmarks
2. **Early intervention beats late reaction**the Golden Hour is the highest-leverage window (directional; no published multiplier — do not quote one)
3. **Comments rank above reactions** — the defensible claim is the order (saves > shares > quality comments > reactions), not a comment-to-like ratio
4. **Don't game the system** — Authentic engagement only. Pods and bait are detected and penalized
5. **Accept underperformance gracefully** — Not every post will be a hit. Learn and iterate.
6. **Every post is a data point, not a verdict** — Build the pattern over weeks, not individual posts

View file

@ -75,6 +75,33 @@ The operations centre is the user-authored other half of the second brain (the b
**Graceful absence:** If `brain/operations.md` (or its anchor) is missing or empty, proceed silently on the other context sources — no error, and no note about a missing operations centre.
### Distilling the positioning brief
Invoked by `/linkedin:setup` Step 3g. You produce the **positioning brief** — the distilled answer to the four questions a draft cannot answer for itself — and write it to:
```
${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/positioning-brief.md
```
Shape it on `${CLAUDE_PLUGIN_ROOT}/config/positioning-brief.template.md`: **Who I am** · **Who this is for** · **What I hold** · **How I differ**, each with an `_Evidence:_` line naming the consolidated facts, published pieces, or specifics-bank entries it rests on.
**Sources, in precedence order:**
1. `brain/operations.md` — the dated `## Who I am now` anchor (user-declared direction)
2. `profile/user-profile.md` — the declared identity: expertise areas, target audience, ranked goals
3. `brain/profile.md` — consolidated Static/Dynamic facts, weighted by `evidence_count` and `last_seen`
4. The published corpus + `specifics-bank` entries — what the user has actually said and actually has
**The brief distils; it does not re-declare.** Where `user-profile.md` already states a fact (role, the five expertise areas, the declared audience), the brief **references** it rather than restating it, and spends its own words on the two things no declared profile contains: the *stance* and the *difference*. **Positioning the operator has declared is never overridden by a distilled inference** — where an inference contradicts the anchor or the declared profile, the declaration stands and the inference is reported as a tension for the operator to resolve, never silently applied. This is the frozen-past-self guard extended from facts to positioning.
**Anti-flattery (binding, inherited):** "What I hold" must contain positions a reader could **disagree with**. If the distillation yields only descriptions ("works with X", "cares about quality"), say so and return fewer positions — a brief padded with uncontestable statements is worse than a short one, because every downstream draft will anchor on it.
**Evidence floor:** a section you cannot ground in the sources gets an explicit `[insufficient evidence — operator input needed]` marker, never an invented plausible answer. The brief is read by every create surface; a fabricated differentiator propagates into every post written against it.
**Regeneration is operator-gated.** When a brief already exists you **never overwrite** it without an **explicit go**: present a section-by-section **diff** of the proposed distillation against the current file (including any operator edits), and write only what the operator approves. The operator's own edits outrank your distillation by default.
**Graceful absence:** if `brain/profile.md` and the published corpus are both empty there is **no brief yet** — say that the second brain has nothing to distil and point to `/linkedin:setup` voice-samples and profile steps first. Never scaffold a brief out of the template's placeholders.
### New Creator Advantage Detection
From the state file, extract `first_post_date`. Calculate the creator window status:

View file

@ -19,9 +19,9 @@ You are a LinkedIn performance analyst. Help the user diagnose why their content
## Load Context
Read these reference files:
- `references/troubleshooting-guide.md` - Failure patterns and solutions
- `references/algorithm-signals-reference.md` - Algorithm mechanics
- `skills/linkedin-studio/SKILL.md` - User's profile and goals
- `${CLAUDE_PLUGIN_ROOT}/references/troubleshooting-guide.md` - Failure patterns and solutions
- `${CLAUDE_PLUGIN_ROOT}/references/algorithm-signals-reference.md` - Algorithm mechanics
- `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` - User's profile and goals
## Step 1: Diagnose the Problem
@ -117,7 +117,7 @@ Based on their answer, ask relevant follow-up questions:
## Step 3: Apply Diagnostic Framework
Based on `references/troubleshooting-guide.md`, diagnose the pattern:
Based on `${CLAUDE_PLUGIN_ROOT}/references/troubleshooting-guide.md`, diagnose the pattern:
### Pattern: Good Content, Low Reach
@ -312,6 +312,6 @@ Consider creating a new account if:
## Reference Files
- `references/troubleshooting-guide.md` - Complete troubleshooting
- `references/algorithm-signals-reference.md` - Algorithm mechanics
- `references/growth-roadmaps.md` - Stall points and fixes
- `${CLAUDE_PLUGIN_ROOT}/references/troubleshooting-guide.md` - Complete troubleshooting
- `${CLAUDE_PLUGIN_ROOT}/references/algorithm-signals-reference.md` - Algorithm mechanics
- `${CLAUDE_PLUGIN_ROOT}/references/growth-roadmaps.md` - Stall points and fixes

View file

@ -31,6 +31,7 @@ Load state and personalization:
- Read `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` for profile and preferences
- Check `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/plans/` for existing weekly plan
- Read `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/templates/my-post-templates.md` for proven templates — vary templates across the batch for format diversity
- Read `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/positioning-brief.md` — the week's posts must add up to one position, not five unrelated takes. Use **What I hold** to keep the batch arguing from the same stance, and **How I differ** as the check on the plan as a whole: if the week could have been produced by any account in the niche, revise the plan before writing. Absent file: proceed silently
If a plan exists for this week, use it as the foundation. If not, create one first.
@ -44,7 +45,7 @@ Use AskUserQuestion:
3. **Use existing plan** — Follow the weekly plan already created
4. **Mix it up** — Diverse topics across pillars
If they choose a theme, help them identify 3-5 unique angles from `references/content-angles.md`. For timely angles, delegate to the `trend-spotter` agent — invoke it via `Task` with `subagent_type: linkedin-studio:trend-spotter` (foreground, from this command layer) — to surface trending topics and score their relevance against the user's pillars.
If they choose a theme, help them identify 3-5 unique angles from `${CLAUDE_PLUGIN_ROOT}/references/content-angles.md`. For timely angles, delegate to the `trend-spotter` agent — invoke it via `Task` with `subagent_type: linkedin-studio:trend-spotter` (foreground, from this command layer) — to surface trending topics and score their relevance against the user's pillars.
## Step 2: Plan the Batch (with Scheduling)
@ -90,11 +91,13 @@ For each post in the batch:
### 3a. Draft
Follow the standard structure:
- Hook: 110-140 characters
- Context: 200-300 characters
- Insight: 400-800 characters
- Implication: 200-300 characters
- Context: 250-350 characters
- Insight: 550-850 characters
- Implication: 250-350 characters
- CTA: 50-100 characters
Sum: 1,210-1,790 characters — inside the standard 1,2001,800 band checked in 3b.
### 3b. Quick Quality Check
- Character count in the band for this post's format (standard 1,2001,800 · quick 150500 · carousel/video caption per that format) — not just "in range"
- Hook works standalone

View file

@ -200,7 +200,7 @@ First Hour:
- [ ] Respond to comments within 5 minutes
- [ ] Add value in every response (not just "thanks!")
- [ ] Ask follow-up questions to deepen conversation
- [ ] Target: 15+ engagements in first 60 minutes
- [ ] Keep the first 60 minutes active (early engagement unlocks broader distribution — directional, no fixed threshold)
- [ ] Check back at 30-min and 60-min marks
48-Hour Check-In:

View file

@ -29,7 +29,7 @@ You are a LinkedIn carousel content specialist. Create high-engagement carousel
## Step 1: Choose Template
Read `assets/templates/carousel-templates.md` for the 5 templates.
Read `${CLAUDE_PLUGIN_ROOT}/assets/templates/carousel-templates.md` for the 5 templates.
Present the options:

View file

@ -1,7 +1,7 @@
---
name: linkedin:competitive
description: |
Competitive analysis of other LinkedIn thought leaders in your niche. Analyzes posting
Competitive analysis of other LinkedIn creators in your niche. Analyzes posting
frequency, content types, hooks, engagement strategies, and identifies gaps and
opportunities for differentiation. Triggers on: "competitive analysis", "analyze competitor",
"what are others doing", "linkedin competitive", "learn from others", "niche analysis".
@ -14,7 +14,7 @@ allowed-tools:
# LinkedIn Competitive Analysis
You are a LinkedIn competitive intelligence analyst. Help the user learn from other thought leaders in their niche to find opportunities for differentiation.
You are a LinkedIn competitive intelligence analyst. Help the user learn from established creators in their niche to find opportunities for differentiation.
## Step 0: Load Context
@ -28,10 +28,10 @@ Ask the user to provide 3-5 LinkedIn profiles to analyze:
Use AskUserQuestion:
1. I have specific profiles to analyze
2. Help me find thought leaders in my niche
2. Help me find leading voices in my niche
3. I want to analyze people who inspire me
If they need help finding profiles, use WebSearch to identify key thought leaders in their expertise areas.
If they need help finding profiles, use WebSearch to identify the most visible voices in their expertise areas.
For each profile, note:
- Name and headline

View file

@ -63,8 +63,11 @@ Show, in this order:
1. **Timeline** (anchored to the real publish time) — what to do at each mark.
2. **Targets** — grouped, in priority order, with the 30-minute whale window flagged.
3. **Draft comments** — self-comments first, then the CEA replies, each labelled.
4. **Velocity checkpoints** — the 5/15/30/60-minute reaction+comment targets, with the
"below this = hook/timing issue" warnings, so the user can self-diagnose mid-window.
4. **Velocity checkpoints** — the 5/15/30/6090-minute shape (any movement → a few →
building → momentum) with its "if it is well below" diagnostics, so the user can
self-diagnose mid-window. These are directional checkpoints, **not numeric targets**
no source publishes a per-minute engagement count, and the comparison that holds is
against the account's own recent first hours.
Auto-copy the self-comments + draft replies to clipboard silently (so they're one paste away):
@ -117,5 +120,5 @@ delayed spike) with the post-feedback monitor — invoke it via `Task` with
## Reference Files
- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/authentic-voice-samples.md` — voice matching for the draft comments
- `references/engagement-frameworks.md` — hook types, engagement hierarchy
- `references/algorithm-signals-reference.md` — first-hour weighting, signal order, timing data
- `${CLAUDE_PLUGIN_ROOT}/references/engagement-frameworks.md` — hook types, engagement hierarchy
- `${CLAUDE_PLUGIN_ROOT}/references/algorithm-signals-reference.md` — first-hour weighting, signal order, timing data

View file

@ -171,7 +171,7 @@ contains ONLY the cold-contract inputs. Map `--type` to archetypes:
| `language` | `linkedin-studio:language-reviewer` | — | frozen draft + contract |
| `fact` | `linkedin-studio:fact-reviewer` | — | frozen draft + contract |
| `persona-resonance` | `linkedin-studio:persona-reviewer` | `mode: resonans`, one call **per active persona** | frozen draft + persona block |
| `persona-conversion` | `linkedin-studio:persona-reviewer` | `mode: konverter`, **primær only** (hook only) | distribution hook / first two lines |
| `persona-conversion` | `linkedin-studio:persona-reviewer` | `mode: konverter`, **primær only** (klikk + deling) | distribution copy: hook / first two lines + the promise it makes |
`all` = every row above (resonance fans out one call per active persona;
conversion runs the primær). **Cold-prompt template** for each call:
@ -215,7 +215,7 @@ yourself or pick winners between reviewers — surface them, the operator gates.
| # | F-kat | 🔴/🟡/🟢 | Påstand | Kilde / retning |
… + Pivot-risk: <claims that look freshly added, or "none">
### persona-resonance — <per persona: JA/NEI + ≤5 flags>
### persona-conversion — <primær JA/NEI on the hook>
### persona-conversion — <primær klikk JA/NEI on the hook + deling JA/NEI (forwarder role, motive, RESHARE/SAVE/SEND)>
## Cross-archetype signal
- BLOCK / 🔴 total: <N> REWORK total: <N> primær resonance: JA/NEI primær conversion: JA/NEI

View file

@ -21,7 +21,7 @@ You are a LinkedIn analytics data import assistant. Guide the user through impor
## Reference
For data format details and directory structure, see `assets/analytics/README.md`.
For data format details and directory structure, see `${CLAUDE_PLUGIN_ROOT}/assets/analytics/README.md`.
> **Why CSV (as of 2026-07).** Post-level analytics via LinkedIn's API is
> partner-gated (vetted Community Management app + verified org + Page) and **not
@ -32,7 +32,7 @@ For data format details and directory structure, see `assets/analytics/README.md
> percentages, and is absent from the CSV, with export status unverified; dwell is
> internal-only for organic posts. Saves and the reach share are therefore
> **optional manual columns** the user adds to the CSV (`Saves`,
> `Out-of-network` / `In-network`) — see `assets/analytics/README.md` for the
> `Out-of-network` / `In-network`) — see `${CLAUDE_PLUGIN_ROOT}/assets/analytics/README.md` for the
> entry rules, and the README boundaries for the full data boundary.
## Step 1: Check for CSV Files in Exports Directory

View file

@ -142,7 +142,7 @@ first 48 hours, detects anomalies, and advises real-time interventions.
|---------|---------|-----------|
| `/linkedin:strategy` | **Front-door.** Phase roadmap, trajectory, authority + signature content | Any phase |
| `/linkedin:profile` | profile/topic-relevance optimization checklist | Any phase |
| `/linkedin:competitive` | Competitive analysis of other thought leaders | Any phase |
| `/linkedin:competitive` | Competitive analysis of other creators in your niche | Any phase |
| `/linkedin:monetize` | Monetization strategy (lead magnets, consulting funnel, pricing) | ~1K followers |
| `/linkedin:outreach` | Collaborations and speaking opportunities (CFPs, partner pitches) | ~1K followers |
@ -229,4 +229,4 @@ If the user's intent is clear from context:
## Reference
For full skill documentation, see:
- `skills/linkedin-studio/SKILL.md` - Complete skill with personalization settings
- `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` - Complete skill with personalization settings

View file

@ -1,9 +1,10 @@
---
name: linkedin:monetize
description: |
Monetization strategy for LinkedIn thought leaders. Assesses readiness with scoring,
Monetization strategy for LinkedIn creators. Assesses readiness with scoring,
creates lead magnets with templates, optimizes CTAs with A/B testing, plans funnel content,
and tracks consulting inquiries. Works from 1K+ followers with stage-specific action plans.
and tracks consulting inquiries. Stage-specific action plans from 0-1K followers onward;
the payoff compounds from ~1K.
Triggers on: "monetize", "make money from linkedin", "linkedin revenue", "lead generation",
"consulting pipeline", "linkedin monetize", "pricing strategy", "lead magnet".
allowed-tools:
@ -49,9 +50,8 @@ Score each dimension 0-25 (total /100):
╠══════════════════════════════════════════════════════════╣
║ ║
║ Audience Size: /25 ║
║ ├─ [ ] 1K+ followers (+5) ║
║ ├─ [ ] 5K+ followers (+10) ║
║ ├─ [ ] 10K+ followers (+15) ║
║ ├─ [ ] Follower tier (pick ONE): ║
║ │ 1K+ (+5) · 5K+ (+10) · 10K+ (+15) ║
║ ├─ [ ] Followers in target niche (+5) ║
║ └─ [ ] Growing 5%+ monthly (+5) ║
║ ║

View file

@ -207,7 +207,7 @@ the edition left off before doing anything.
4. **None / insufficient** — Step 1 will **define personas interactively**.
Exactly one persona is the **primær**. The resolved set feeds BOTH the Step 6
resonance sweep AND the Step 6.5 headless package; see
`config/personas.template.md` → "Per-artifact personas".
`${CLAUDE_PLUGIN_ROOT}/config/personas.template.md` → "Per-artifact personas".
6. **Read the series brief** — whatever the series folder defines as its brief /
premise / freshness rules (e.g. `<serie>/brief.md`, or the resolved brief
recorded in `edition-state.json`). This anchors angle and scope.
@ -341,6 +341,16 @@ Establish the edition brief with **at most ~3 calibration questions**. Infer
everything you can from Step 0 (series brief, STATE.md, prior edition); only ask
what genuinely changes the work.
**Read the positioning brief first** —
`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/positioning-brief.md`.
A long-form edition is the format where a missing position shows most: 1,500 words of
competent survey that takes no stance is the failure mode. Use **What I hold** to fix
what this edition *argues* (not merely covers) and **How I differ** as the brief-level
test — if the edition as briefed could carry another author's byline unchanged, the
angle is not yet an angle. Where the brief and the operator's stated brief for THIS
edition disagree, the operator wins; note the tension in one line rather than quietly
steering to the file. Absent file: proceed silently on the other sources.
> **Trend intake (optional bridge — N7).** If this edition is being started from a
> `/linkedin:trends` candidate — the operator supplies a **trend id** (shown in the
> trends brief and via `CLI list --json`, where
@ -421,7 +431,7 @@ Settle these dimensions (most should come from context, not questions):
`articles.NN.personas` (each entry: name, tier, the five fields, source) at the
**Step 1.5 checkpoint** (the first machine-state write; Step 2 then adds the
research notes) — it is then stable across sessions and is the single source
every later sweep reads. See `config/personas.template.md` →
every later sweep reads. See `${CLAUDE_PLUGIN_ROOT}/config/personas.template.md` →
"Per-artifact personas".
- **Key points** — the 24 load-bearing claims the edition must make.
- **Tone** — respected-peer vs. teaching-down; calibrated to the primær.
@ -633,7 +643,7 @@ Next: Step 2 — Research (scoped by the binding: fill ekstern/unresolved, verif
1. **Decompose** the binding's `ekstern`/`unresolved` slots + unverified numbers
(step 0) into 25 *scoped, orthogonal* research sub-questions. Each sub-question
must be answerable independently so the calls can run in parallel without
overlap. Reuse the multi-source synthesis discipline from `commands/react.md`
overlap. Reuse the multi-source synthesis discipline from `${CLAUDE_PLUGIN_ROOT}/commands/react.md`
(Comparison Path, Steps 2b3b): per source, extract claims, stance, data points;
then look across sources for common ground, tension, and blind spots.
@ -867,7 +877,7 @@ Next: Step 2.5 — Skeleton + section pitch (operator + persona gate BEFORE pros
`<serie>/NN-skjelett.md`, and **`mode: skjelett`** (the before-prose mode —
five spine axes, ≤3 flags as direction, HOLDER/TVILER/MANGLER scoring).
This is NOT resonans mode (Step 6 — that runs on full prose) and NOT
konverter mode (Step 9 — that judges the hook only).
konverter mode (Step 9 — that judges the distribution copy: klikk + deling).
8. **Collect skjelett verdicts and gate.** Each call returns per-axis flags
(HOLDER/TVILER/MANGLER), ≤3 direction-only flags, a section-pitch check
@ -1083,7 +1093,7 @@ turning-points the spine already named.
only afterward.)
3. **Expand with the `content-repurposer` muscle.** Reuse
`agents/content-repurposer.md` (its article→long-form conversion discipline)
`${CLAUDE_PLUGIN_ROOT}/agents/content-repurposer.md` (its article→long-form conversion discipline)
for individual section expansions — invoke it via `Task`
(`subagent_type: linkedin-studio:content-repurposer`) when useful, *from this
command layer* (foreground, principle 4). The command owns assembly and
@ -1212,7 +1222,7 @@ mechanical noise.
> which is the §A/quality reference applied by reasoning.
> **Adopter note.** The gate is a KTG-internal tool: it needs the contract-gate deps
> (`scripts/contract-gate/node_modules` — run `npm install` there once) and, for
> (`${CLAUDE_PLUGIN_ROOT}/scripts/contract-gate/node_modules` — run `npm install` there once) and, for
> ratify, the maskinrommet contract. If either is absent (a plain adopter ships
> neither), this Step is a **no-op** and the §B/§C1 discipline falls back to the Step 4
> quality-pass reasoning. It never blocks an adopter.
@ -1416,19 +1426,19 @@ and `persona-reviewer` never flags em-dash density (that is this step).
grep-able, the architecture checks need a read.
3. **Surface the report to the operator (`SendUserFile` — the Endring-5 pattern).**
The flags are surfaced to KTG as a **markdown report**, the same operator-gate
The flags are surfaced to the operator as a **markdown report**, the same operator-gate
shape the visual-assets step (7.5) uses for candidates and Steps 2.5/3a use for
annotation:
1. Write the agent's report to `<serie>/NN-editorial-review.md` (NN = the same
zero-padded edition number; new suffix, a first-class artifact alongside
`NN-skjelett.md`).
2. `SendUserFile` it (else a markdown `file://` link) so KTG can read the flags
2. `SendUserFile` it (else a markdown `file://` link) so the operator can read the flags
sorted BLOCK → REWORK → NICE and **decide which fold in**.
3. **KTG gates.** The agent's severity ranking is a *recommendation*; the
3. **The operator gates.** The agent's severity ranking is a *recommendation*; the
operator holds the gate. A BLOCK is the agent's strongest "must fix before
Step 6", not an automatic pipeline halt. `[OPERATØR]`
4. **Fold in the approved flags by tightening, → v(n+1).** Fold the flags KTG
4. **Fold in the approved flags by tightening, → v(n+1).** Fold the flags the operator
approved into `<serie>/NN-utkast.md` **by tightening** (rule 6 of
`references/longform-quality-rules.md` — close the gap, hold the length flat;
never expand to paper over a craft defect). The result is the next draft
@ -1448,7 +1458,7 @@ and `persona-reviewer` never flags em-dash density (that is this step).
fold-in was substantive (especially any BLOCK), re-run the agent on v(n+1) to
confirm the flags cleared and no new craft defect was introduced by the edit.
This loop is cheap and is the point of the gate — every craft round saved here
is a KTG round saved at first reading.
is an operator round saved at first reading.
6. **Persist + checkpoint state.** Once the editorial pass is folded in (and any
re-run confirms clean):
@ -1496,7 +1506,7 @@ reopening locked texts — the biggest single process error of the series (plan
1. **Load the active personas** chosen in Step 1, with exactly one marked
**primær**. Each persona's five fields (rolle, avkobler, overbeviser,
ekspertise, sjargong) come from `config/personas.local.md` (or the template).
ekspertise, sjargong) come from `${CLAUDE_PLUGIN_ROOT}/config/personas.local.md` (or the template).
2. **Fan out one `persona-reviewer` call per persona, in parallel** — issue them
in a SINGLE message (multiple `Task` tool-uses, `subagent_type:
@ -1583,7 +1593,7 @@ in-session pass hid. `content-reviewer` is genuinely new (argument integrity,
which no in-session gate measures). Do NOT collapse the pairs.
**Procedure** (this is the same package the standalone `/linkedin:headless-review`
command runs — see `commands/headless-review.md` for the full cold contract):
command runs — see `${CLAUDE_PLUGIN_ROOT}/commands/headless-review.md` for the full cold contract):
1. **Freeze the draft.** Snapshot the persona-passed `NN-utkast.md` so the
reviewers judge a stable artifact and the report names exactly what was read:
@ -1596,7 +1606,7 @@ command runs — see `commands/headless-review.md` for the full cold contract):
default `en` — tells `language-reviewer` and `voice-scrubber` which language's
rules to grade against; Norwegian-specific checks fire only when `language: no`),
the writing contract *if it ships* (`<serie>/../../docs/skrivekontrakt.md`
plugin mirror → `references/longform-quality-rules.md`; absent for an adopter,
plugin mirror → `${CLAUDE_PLUGIN_ROOT}/references/longform-quality-rules.md`; absent for an adopter,
the craft agents' in-tree checklists are self-contained), and the active
personas (`articles.NN.personas`, primær identified). Nothing else.
@ -1607,7 +1617,7 @@ command runs — see `commands/headless-review.md` for the full cold contract):
- `linkedin-studio:language-reviewer` — language quality (L1L5; grades against `edition-state.language`, Norwegian-specific rules when `language: no`)
- `linkedin-studio:fact-reviewer` — cold re-verification (F1F4, 🔴/🟡/🟢, incl. pivot premises)
- `linkedin-studio:persona-reviewer` `mode: resonans` — **one call per active persona**
- `linkedin-studio:persona-reviewer` `mode: konverter`**primær only** (hook)
- `linkedin-studio:persona-reviewer` `mode: konverter`**primær only** (hook + share; advisory here, blocking at Step 9)
Each call's prompt carries ONLY the cold-contract inputs (frozen draft path,
`language`, contract path if it ships, persona for the persona modes) + the
@ -1636,10 +1646,10 @@ command runs — see `commands/headless-review.md` for the full cold contract):
with a cross-archetype signal line. **Mark ⚑ converged** any passage two
independent cold reviewers flag — independent agreement with no shared session
is the package's strongest signal. `SendUserFile` it (else a `file://` link)
so KTG decides which flags fold in. You do not resolve flags or pick winners;
so the operator decides which flags fold in. You do not resolve flags or pick winners;
the operator gates. `[OPERATØR]`
6. **Fold in by tightening, → v(n+1).** Fold the flags KTG approved into
6. **Fold in by tightening, → v(n+1).** Fold the flags the operator approved into
`NN-utkast.md` **by tightening** (rule 6 — close the gap, hold the length flat).
The editor (this session) holds the pen; never paste a reviewer's direction as
copy. If the fold-in was substantive, re-run the affected archetype on v(n+1).
@ -2051,18 +2061,22 @@ Next: Step 9 — Hook / conversion gate (post-lock).
The locked edition still has to earn the feed. Before it ships, the
**feed-facing surfaces** the reader actually meets — the **delingstekst hook**,
the **samle post**, the **carousel slide text**, and the **edition title**
face two final gates: a **randsone fact + language gate** (so nothing false or
clumsy reaches the feed in the most-read, most-requoted copy — C-4/C-9) and the
**hook conversion binary** (would the primær persona, scrolling, actually stop
and open this?). Both operate on **distribution copy only** — the post-lock
sweep, distinct from the pre-lock resonance sweep (Step 6), never the locked
body. Until N9 only the hook's click-worth was judged; the hook's *truth and
language*, the samle post, the carousel slides and the title reached the feed
ungated.
face a **hook-forge** (the krok competes against alternatives instead of shipping
the one the draft happened to end on) and three gates: a **randsone fact +
language gate** (so nothing false or clumsy reaches the feed in the most-read,
most-requoted copy — C-4/C-9), the **hook conversion binary** (would the primær
persona, scrolling, actually stop and open this?), and the **shareability gate**
(would that reader pass it on — and to whom?). All operate on **distribution copy
only** — the post-lock sweep, distinct from the pre-lock resonance sweep (Step 6),
never the locked body. Until N9 only the hook's click-worth was judged; the hook's
*truth and language*, the samle post, the carousel slides and the title reached
the feed ungated — and until N24 the hook shipped unchallenged and nobody asked
who would forward the edition.
> **Order assertion (enforced).** This gate (Step 9) runs AFTER lock (Step 8) — it
> operates on the **distribution copy** of an already-locked edition and must never
> reopen the locked body. Both the randsone fact/language gate and the hook binary
> reopen the locked body. The hook-forge, the randsone fact/language gate, the hook
> binary and the shareability gate all
> revise only distribution surfaces (delingstekst, samle, carousel text,
> distribution title) and re-render via Step 8's `build-linkedin.mjs` /
> `build-carousel.mjs` — never the edition text. A defect that traces into the
@ -2091,7 +2105,38 @@ lock invariant without breaking phase order.
- the **edition title** — the edition's own headline (its distribution / feed
title), never strength-gated before now.
2. **Randsone fact + language gate (scoped — C-4/C-9).** Run two scoped reviewers
2. **Hook-forge — forge the alternatives before gating one (E#7).** The hook that
exists is the one the draft happened to end on; it has never competed against
anything. Forge **five to eight hook variants** for the delingstekst krok, then
let the operator choose — the gates below judge the chosen krok, not the accident.
- **Measure the band, per variant.** Each variant's opening line must land in
**110140 characters** (the mobile "…see more" cutoff — the hook rule the
short-form surfaces already hold). Measure it, never estimate it:
`perl -CSD -ne 'chomp; printf "%d\n", length($_)'`. Norwegian æ/ø/å are
multibyte, so `awk length` and `wc -c` count BYTES and over-report. A variant
outside the band is **disqualified, not ranked lower** — the cutoff is
physical, not a preference, and it is enforced per variant, never on the set.
- **Span the hook types.** Name each variant's type from
`${CLAUDE_PLUGIN_ROOT}/references/engagement-frameworks.md`. The surviving set
must cover **at least three distinct hook types** — otherwise the forge
produced rewordings of one idea and the operator has no real choice.
- **Trace every variant to the locked body.** A variant must rest on
**a claim the locked body actually makes**. One that promises what the
edition does not deliver is disqualified — it is bait, and the randsone fact
gate below would 🔴 it anyway.
- **Carry only what can be checked.** Each variant ships with its measured
length, its hook type and its body-trace — and
**no predicted-engagement score**. A number claiming to know which krok will
travel is invented; the ranking is the operator's, on variants the plugin can
honestly describe.
- **Fewer than five survivors → forge again.** Presenting three survivors as
"the set" hides that the band or the body constrained the forge.
- The operator picks one via `AskUserQuestion` — the gate that already exists
here. The chosen variant becomes the delingstekst hook and goes
**through the randsone gate, never around it**: the same fact, language and
length rules as any other distribution copy. `[GATE]`
3. **Randsone fact + language gate (scoped — C-4/C-9).** Run two scoped reviewers
over *only* these surfaces (never the locked body), issued from THIS command
layer in the foreground, `subagent_type` namespaced:
- `linkedin-studio:fact-reviewer`**F1** (verifiable claims) + **F3** (number
@ -2104,7 +2149,7 @@ lock invariant without breaking phase order.
Pass each reviewer only the four surfaces + the sources they cite + `language`.
This is a **scoped mini-pass**, not a re-review of the article. `[GATE]`
3. **Title-strength gate (C-9).** The edition's own title was never strength-gated —
4. **Title-strength gate (C-9).** The edition's own title was never strength-gated —
only the hook's two lines were. Judge the **title-strength** of the **edition
title** here: does it earn the open on its own, or is it a generic label? The
strongest candidate becomes the distribution title. The verdict is **binding on
@ -2112,7 +2157,7 @@ lock invariant without breaking phase order.
canonical (locked) title** — changing that is a body edit → STOP, run
`/linkedin:pivot`; never edit the locked title in place.
4. **Resolve the randsone gate.**
5. **Resolve the randsone gate.**
- **Fact 🔴 / L-BLOCK on a distribution surface** → revise that surface **by
tightening** (hook, samle, carousel text, or distribution title — all
distribution copy), **re-render** the affected artifact (POST.html via Step 8's
@ -2122,32 +2167,63 @@ lock invariant without breaking phase order.
pivot: STOP, run `/linkedin:pivot`. The randsone gate never reopens the locked
body in place — the same invariant as the conversion loop. `[GATE]`
5. **Run the hook conversion binary** (`subagent_type:
linkedin-studio:persona-reviewer`, **`mode: konverter`** — the after-lock,
hook-only mode, NOT resonans) for the **primær** persona only. The agent returns
a single binary verdict, **JA / NEI**, on «would YOU click?» — no axis scoring,
no flags, no rewritten copy (the jury judges, the editor writes).
- **JA** → the hook converts. Proceed to Step 10.
6. **Run the distribution binaries** (`subagent_type:
linkedin-studio:persona-reviewer`, **`mode: konverter`** — the after-lock
distribution mode, NOT resonans) for the **primær** persona only, on the chosen
forge variant. The agent returns **two binary verdicts****klikk** («would YOU
click?») and **deling** (item 7) — no axis scoring, no flags, no rewritten copy
(the jury judges, the editor writes; the forge is the editor's pen).
- **JA** → the hook converts. Proceed to the shareability gate.
- **NEI** → revise the **delingstekst hook only** (sharpen the krok by tightening,
body frozen), **re-render POST.html** via `build-linkedin.mjs`, and re-run the
conversion check. Loop until JA. `[GATE]`
or take the next forge variant — body frozen), **re-render POST.html** via
`build-linkedin.mjs`, and re-run the conversion check. Loop until JA. `[GATE]`
6. **Persist.** Record the randsone-gate result in `edition-state.json`
7. **Shareability gate — «hvem deler dette til en kollega, og hvorfor?» (E#8).**
A click is one reader; a forward is peer acknowledgement, and it is the signal
the edition was written to earn. The `konverter` call's second binary is the
**deling verdict**: judged by the primær persona on the same distribution copy —
**who forwards this to a colleague, and why** — plus which action the edition
actually earns from that reader: **RESHARE / SAVE / SEND**.
- **A plausible answer names a role and a motive.** It must name a role the
reader could actually point at, and what forwarding does *for the sender*
signals their judgment, saves a colleague a decision, settles a running
argument. «Useful for anyone in tech» names nobody: that is a **NEI**.
- **NEI → revise the distribution copy** (the promise may simply be framed for a
reader nobody forwards to), re-render, re-check. Body frozen, as everywhere in
this step.
- **Two failed re-framings → stop; do not lower the bar.** If the honest reason
is that the edition offers nothing a reader would pass on, that is a
body-level finding, not a copy problem: put it to the operator as a choice —
run `/linkedin:pivot`, or ship and record it as an `articles.NN.foldIns` row
for the Step 11 retro. There is **never a silent pass**: this gate is resolved
by a fix or by a recorded operator decision, never by re-reading the same copy
more generously. `[GATE]`
8. **Persist.** Record the forge in `edition-state.json`
`articles.NN.hookForge` (each variant with its measured length, hook type,
body-trace and disqualification reason if any, plus which one the operator
chose), the randsone-gate result in
`articles.NN.randsoneGate` (per-surface fact/language verdicts + title-strength +
what was revised) and the conversion verdict in
`articles.NN.personaSweep.conversion`; set `currentPhase: "hook-conversion-gate"`.
what was revised), the click verdict in
`articles.NN.personaSweep.conversion` and the share verdict in
`articles.NN.personaSweep.deling` (verdict + the named forwarder role + the
RESHARE/SAVE/SEND action, or the recorded operator decision);
set `currentPhase: "hook-conversion-gate"`.
**↳ Phase transition** — run the protocol (Step 0) for `hook-conversion-gate`,
`--next "Step 10 — scheduling"`.
```
Randsone + conversion gate (post-lock, distribution copy only).
Forge + randsone + conversion + shareability gate (post-lock, distribution copy only).
- Hook-forge: <N> forged / <N> disqualified (band <N>, body-trace <N>) · types spanned: <N> → chose #<k> (<measured> chars, <type>)
- Surfaces gated: delingstekst hook · samle post · carousel slide text · edition title
- Fact (F1/F3): <N> 🔴 → resolved (or pivot if it traces to the body)
- Language (L1/L2): <N> BLOCK → tightened
- Title-strength: <verdict> (distribution title sharpened; canonical-title weakness → pivot)
- Conversion (primær, konverter): JA (else: NEI — revise hook, re-render, re-check)
- Deling (primær, konverter): JA — <role> sends it to <role> because <motive> · action: RESHARE|SAVE|SEND
(else: NEI — reframe the copy; twice failed → operator decision, never a silent pass)
- Body: untouched (locked in Step 8)
Gate: [PASS — randsone clean + JA] (else loop on distribution copy, or pivot)
Gate: [PASS — randsone clean + klikk JA + deling JA] (else loop on distribution copy, or pivot)
Next: Step 10 — Scheduling.
```
@ -2269,7 +2345,7 @@ time. Step 11 empties that queue into permanent, enforced homes.
- **`mechanical-block` / `mechanical-warn`** — provable by string, regex, or
count with ~zero false positives. **JA →** promote **atomically**: add the
rule to `scripts/contract-gate/src/rules.ts`, the matching row to the
rule to `${CLAUDE_PLUGIN_ROOT}/scripts/contract-gate/src/rules.ts`, the matching row to the
writing contract, and the manifest row that binds them; set the fold-in's
`decision: "promoted"` and `ruleId`. Then prove it:

View file

@ -226,7 +226,7 @@ TOTAL: /25
Search strategies:
By topic:
- "[your niche] linkedin thought leader"
- "top [your niche] voices linkedin"
- "[topic] expert linkedin"
- "[industry] creator linkedin"

View file

@ -28,7 +28,7 @@ Load persistent state and personalization:
- Read `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` for profile and preferences
- Check `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/` for voice matching
- Read `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/templates/my-post-templates.md` for proven post templates — use these in Step 2 (Draft)
- Read `assets/frameworks/framework-template.md` if the topic involves a framework or methodology
- Read `${CLAUDE_PLUGIN_ROOT}/assets/frameworks/framework-template.md` if the topic involves a framework or methodology
Display status:
```
@ -52,14 +52,15 @@ To situate the post in the broader plan — does it fill a content-mix gap or re
Once topic is chosen, create the draft:
1. **Select angle** — Auto-select the strongest angle from `references/content-angles.md` based on topic and user's expertise. Present ONE recommended angle with reasoning. Do NOT use AskUserQuestion — just proceed. If user disagrees, offer alternatives.
1. **Select angle** — Auto-select the strongest angle from `${CLAUDE_PLUGIN_ROOT}/references/content-angles.md` based on topic and user's expertise. Present ONE recommended angle with reasoning. Do NOT use AskUserQuestion — just proceed. If user disagrees, offer alternatives.
2. **Infer format** — Default to text post. Only mention carousel/video as a note if particularly well-suited.
3. **Write draft** — Following the structure:
- Hook: 110-140 characters
- Context: 200-300 characters
- Insight: 400-800 characters
- Implication: 200-300 characters
- Context: 250-350 characters
- Insight: 550-850 characters
- Implication: 250-350 characters
- CTA: 50-100 characters
- Sum: 1,210-1,790 characters — inside the 1,200-1,800 band gated in Step 3.
Reference `${CLAUDE_PLUGIN_ROOT}/references/engagement-frameworks.md` for hooks and CTAs.
@ -166,7 +167,7 @@ First Hour Engagement Plan:
- [ ] Respond to comments within 5 minutes
- [ ] Add value in every response (not just "thanks!")
- [ ] Ask follow-up questions to deepen conversation
- [ ] Target: 15+ engagements in first 60 minutes
- [ ] Keep the first 60 minutes active (early engagement unlocks broader distribution — directional, no fixed threshold)
- [ ] Check back at 30-min and 60-min marks
```

View file

@ -24,7 +24,7 @@ You are a LinkedIn content creator. Guide the user through creating a high-quali
First, load persistent state and personalization:
- Read `~/.claude/linkedin-studio.local.md` for posting state (streak, weekly progress, recent topics)
- Read `skills/linkedin-studio/SKILL.md` for user profile, voice settings, and preferences
- Read `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` for user profile, voice settings, and preferences
**Read `## Do-Next Directives` in the state file and apply them to this draft.** This is what the
last measurement pass (`/linkedin:report`, `/linkedin:analyze`, an A/B Adopt verdict, the 48h
@ -45,8 +45,9 @@ Check weekly progress:
Check for existing assets:
- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/` - Match the user's natural voice
- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/positioning-brief.md` - What the user stands for and how they differ. Write the angle AGAINST this: the "How I differ" section is the test for whether this post could have been written by anyone else on the topic. Absent file: proceed silently (nothing to anchor on yet).
- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/examples/high-engagement-posts.md` - Study past successful posts and replicable patterns
- `assets/frameworks/framework-template.md` - Reference user's documented frameworks for framework posts
- `${CLAUDE_PLUGIN_ROOT}/assets/frameworks/framework-template.md` - Reference user's documented frameworks for framework posts
- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/templates/my-post-templates.md` - User's proven post templates with success rates. **Prefer these over generic structures.**
## Step 1: Understand the Input
@ -71,7 +72,7 @@ If they provide a URL, use WebFetch to extract the content first.
## Step 2: Select Content Angle
Read `references/content-angles.md` for the 8 universal angles.
Read `${CLAUDE_PLUGIN_ROOT}/references/content-angles.md` for the 8 universal angles.
**Industry-specific angles:** If `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/user-profile.md` exists and has an `industry` field, check the "Industry Angle Variants" section in `content-angles.md` for the matching industry table. Use the industry-specific starter questions and example hooks to generate more targeted angle suggestions.
@ -92,7 +93,7 @@ Infer format automatically based on content type — do NOT ask the user to choo
| Content Type | Auto-Selected Format |
|--------------|---------------------|
| Data/research | Medium text post (1,200-1,800 chars) |
| Personal stories | Medium text post (1,000-1,400 chars) |
| Personal stories | Medium text post (1,200-1,800 chars) |
| Quick insights | Redirect to `/linkedin:quick` |
| Frameworks/processes | Medium text post (note: "This could also work as a carousel — run `/linkedin:carousel` if you'd prefer that format.") |
| Opinions/takes | Text-only medium post |
@ -101,19 +102,23 @@ Proceed with standard text post format by default. Only mention carousel or othe
## Step 4: Structure and Write
Read `references/engagement-frameworks.md` for hook types, story structures, and CTAs.
Read `${CLAUDE_PLUGIN_ROOT}/references/engagement-frameworks.md` for hook types, story structures, and CTAs.
Use the Standard Post Structure:
1. **Hook (110-140 chars):** Grab attention, create curiosity gap
2. **Context (200-300 chars):** Set up why this matters
3. **Insight/Argument (400-800 chars):** Main point with evidence
4. **Implication (200-300 chars):** What this means for readers
2. **Context (250-350 chars):** Set up why this matters
3. **Insight/Argument (550-850 chars):** Main point with evidence
4. **Implication (250-350 chars):** What this means for readers
5. **CTA (50-100 chars):** Engagement prompt
The components sum to 1,210-1,790 characters — inside the 1,200-1,800 standard band
the Step 5 checklist gates against, so a draft that respects each component band
passes the length gate by construction.
### Hook Rules
Reference `assets/quick-post-resources.md` for hooks bank.
Reference `${CLAUDE_PLUGIN_ROOT}/assets/quick-post-resources.md` for hooks bank.
- Frontload value - most interesting part first
- Be specific with numbers and details
@ -130,7 +135,7 @@ Match the user's voice profile from SKILL.md:
## Step 5: Quality Check
Before presenting, verify against `assets/checklists/quality-scorecard.md`:
Before presenting, verify against `${CLAUDE_PLUGIN_ROOT}/assets/checklists/quality-scorecard.md`:
- [ ] Hook works in first 110-140 characters
- [ ] Character count: 1,200-1,800 (optimal range)
@ -190,7 +195,7 @@ Before they post, remind them:
- Respond to every comment immediately
- Add value in responses (not just "thanks")
- Ask follow-up questions to deepen conversation
- Target: 15+ engagements in first hour
- Keep the first hour active — early engagement unlocks broader distribution (directional, no fixed threshold)
**State Update:**
After the post is finalized, update state deterministically:
@ -210,9 +215,9 @@ Replace placeholders with actual post data. This replaces manual YAML editing.
## Reference Files
- `references/content-angles.md` - 8 universal angles
- `references/engagement-frameworks.md` - Hooks, structure, CTAs
- `references/linkedin-formats.md` - Format specifications
- `references/algorithm-signals-reference.md` - Algorithm mechanics
- `assets/quick-post-resources.md` - Hooks and CTAs bank
- `assets/checklists/quality-scorecard.md` - Pre-publish check
- `${CLAUDE_PLUGIN_ROOT}/references/content-angles.md` - 8 universal angles
- `${CLAUDE_PLUGIN_ROOT}/references/engagement-frameworks.md` - Hooks, structure, CTAs
- `${CLAUDE_PLUGIN_ROOT}/references/linkedin-formats.md` - Format specifications
- `${CLAUDE_PLUGIN_ROOT}/references/algorithm-signals-reference.md` - Algorithm mechanics
- `${CLAUDE_PLUGIN_ROOT}/assets/quick-post-resources.md` - Hooks and CTAs bank
- `${CLAUDE_PLUGIN_ROOT}/assets/checklists/quality-scorecard.md` - Pre-publish check

View file

@ -19,7 +19,7 @@ You are a LinkedIn profile optimization specialist. Help the user optimize their
## Critical Context: Profile/Topic Relevance
Read `references/algorithm-signals-reference.md` for algorithm mechanics.
Read `${CLAUDE_PLUGIN_ROOT}/references/algorithm-signals-reference.md` for algorithm mechanics.
**The Fundamental Shift:**
- **In the older feed model:** Post something → a slice of your network sees it → the algorithm tracks engagement to decide wider reach
@ -239,6 +239,6 @@ If the answer is no, fix the profile FIRST before posting.
## Reference Files
- `references/algorithm-signals-reference.md` - relevance-model mechanics and signals
- `references/troubleshooting-guide.md` - Recovery if reach is already down
- `skills/linkedin-studio/SKILL.md` - User's expertise areas and topics
- `${CLAUDE_PLUGIN_ROOT}/references/algorithm-signals-reference.md` - relevance-model mechanics and signals
- `${CLAUDE_PLUGIN_ROOT}/references/troubleshooting-guide.md` - Recovery if reach is already down
- `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` - User's expertise areas and topics

View file

@ -32,12 +32,18 @@ Read `~/.claude/linkedin-studio.local.md` for:
Apply the ones that fit and name the applied directive in one line with the draft. Never edit
or clear the section (each source manages its own rows; rows older than 60 days expire).
Read `skills/linkedin-studio/SKILL.md` for:
Read `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` for:
- User's voice profile and tone preferences
- Core expertise areas (for topical alignment)
- Phrases they commonly use
Read `assets/quick-post-resources.md` for:
Read `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/positioning-brief.md` for:
- **What I hold** — the positions to argue from, so a 5-minute post still says something the
user actually believes rather than something generically true
- **How I differ** — the one-line test before you present the draft: could any other account
in this niche have posted this? Absent file: proceed silently.
Read `${CLAUDE_PLUGIN_ROOT}/assets/quick-post-resources.md` for:
- Hooks bank
- CTAs bank
- Quality checklist
@ -61,7 +67,7 @@ Only ask if truly ambiguous (no discernible intent). Otherwise, state: "This rea
These 8 post types ARE the template library. Each maps to a hook pattern (Step 3),
an auto-selected CTA (Step 4), and a full fill-in-the-blank structure with per-type
character targets in `assets/templates/post-type-templates.md`. If the user explicitly
character targets in `${CLAUDE_PLUGIN_ROOT}/assets/templates/post-type-templates.md`. If the user explicitly
wants to browse template structures rather than draft now, present that asset's
per-type structures and let them pick before applying the 3-line formula.
@ -85,7 +91,7 @@ Use this structure for all quick posts:
## Step 3: Select Hook Pattern
Based on post type, use appropriate hook from `assets/quick-post-resources.md`:
Based on post type, use appropriate hook from `${CLAUDE_PLUGIN_ROOT}/assets/quick-post-resources.md`:
### Reaction Post
- "[Industry event/news - state what happened]"
@ -133,7 +139,7 @@ Auto-select the best CTA based on post type — do NOT ask:
| CURATION | "Worth a read — what's your take?" |
| ONE-LINER | "Agree or disagree?" |
Reference `assets/quick-post-resources.md` for additional CTA options if the default doesn't fit.
Reference `${CLAUDE_PLUGIN_ROOT}/assets/quick-post-resources.md` for additional CTA options if the default doesn't fit.
## Step 5: Write and Check
@ -189,7 +195,7 @@ Replace placeholders with actual post data. This replaces manual YAML editing.
## Common Mistakes to Avoid
From `assets/quick-post-resources.md`:
From `${CLAUDE_PLUGIN_ROOT}/assets/quick-post-resources.md`:
1. **Too many ideas** - Pick ONE. Save others for separate posts.
2. **Burying the hook** - Lead with the most interesting element.
@ -226,6 +232,6 @@ If during creation you realize:
## Reference Files
- `assets/quick-post-resources.md` - Hooks and CTAs bank
- `assets/templates/post-type-templates.md` - Template examples
- `references/engagement-frameworks.md` - Hook psychology
- `${CLAUDE_PLUGIN_ROOT}/assets/quick-post-resources.md` - Hooks and CTAs bank
- `${CLAUDE_PLUGIN_ROOT}/assets/templates/post-type-templates.md` - Template examples
- `${CLAUDE_PLUGIN_ROOT}/references/engagement-frameworks.md` - Hook psychology

View file

@ -272,6 +272,6 @@ Same as Step 8 — run `state-updater.mjs` with actual post data.
## Reference Files
- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/authentic-voice-samples.md` — Voice matching
- `references/content-angles.md` — 8 universal angles
- `references/engagement-frameworks.md` — Hooks, structure, CTAs
- `assets/checklists/quality-scorecard.md` — Pre-publish check
- `${CLAUDE_PLUGIN_ROOT}/references/content-angles.md` — 8 universal angles
- `${CLAUDE_PLUGIN_ROOT}/references/engagement-frameworks.md` — Hooks, structure, CTAs
- `${CLAUDE_PLUGIN_ROOT}/assets/checklists/quality-scorecard.md` — Pre-publish check

View file

@ -21,7 +21,7 @@ You are a LinkedIn analytics performance reporter. Generate actionable weekly pe
## Reference
For data format details and directory structure, see `assets/analytics/README.md`.
For data format details and directory structure, see `${CLAUDE_PLUGIN_ROOT}/assets/analytics/README.md`.
## Step 1: Check for Imported Data
@ -247,7 +247,7 @@ Automatically flag these conditions based on the report data and trend analysis:
- 🔴 Format stagnation: Same format used >80% of posts (algorithm favors format variety — see algorithm-signals-reference)
- 🟡 Posting time drift: Publishing outside optimal window (Tue-Thu, 7-9 AM CET for Nordic audience — see posting time windows reference)
- 🟡 Hook length violation: Posts with hooks >140 chars underperforming (>140 chars truncated on mobile "see more")
- 🟢 Engagement velocity improving: First-hour engagement trending up (15+ engagements in first hour unlocks 2nd/3rd degree distribution)
- 🟢 Engagement velocity improving: First-hour engagement trending up against the account's own recent first hours (early engagement unlocks broader distribution — directional, no fixed threshold)
**Surface the alerts the report already computed.** The weekly-report JSON's `alerts[]`
is generated by the CLI itself — intra-week anomaly detection across the week's posts
@ -275,7 +275,7 @@ Your engagement rate has dropped from 4.2% to 2.8% over the last 3 weeks.
🟢 **Positive: New impression record**
Your post on [topic] achieved 12,500 impressions — a personal best!
**Action:** Analyze what made this post succeed. Consider a follow-up post.
**Reference:** First-hour velocity of 15+ engagements unlocks broader distribution.
**Reference:** Strong first-hour velocity unlocks broader distribution — directional, with no published threshold count.
🟡 **Warning: Format stagnation detected**
80%+ of your recent posts are text-only. Documents/carousels are the top organic format and tend to reach further (no reliable multiplier).

View file

@ -67,7 +67,7 @@ Use AskUserQuestion:
**What would you like to set up?**
Options (always show all 7):
Options (always show all 8):
1. **Voice samples** -- Paste 3-5 of your best posts so I can analyze your writing voice
2. **Case study** -- Walk through a guided interview to document a real case study
@ -75,7 +75,8 @@ Options (always show all 7):
4. **Post analysis** -- Add your high-engagement posts with metrics for pattern analysis
5. **Demographics** -- Guide you through LinkedIn Analytics to capture audience demographics
6. **User profile** -- Set up your personalization profile (name, expertise, goals, voice)
7. **Show score details** -- See detailed breakdown of what's missing in each category
7. **Positioning brief** -- Distil what you stand for and how you differ, so every draft is written against it
8. **Show score details** -- See detailed breakdown of what's missing in each category
Based on their answer, run the corresponding sub-workflow below.
@ -128,7 +129,7 @@ Conduct a 6-question interview:
5. **What were the results?** -- Quantitative and qualitative outcomes
6. **What's the key takeaway?** -- The non-obvious lesson others can apply
After the interview, read `assets/case-studies/case-study-template.md` for structure reference, then create a new file:
After the interview, read `${CLAUDE_PLUGIN_ROOT}/assets/case-studies/case-study-template.md` for structure reference, then create a new file:
**Filename:** `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/case-studies/[slug].md` (derive slug from the challenge topic, e.g., `ai-procurement-transformation.md`)
@ -182,7 +183,7 @@ Conduct a 5-question interview:
4. **How does someone apply it?** -- Step-by-step or decision process
5. **What makes this different from standard approaches?** -- Your unique angle
After the interview, read `assets/frameworks/framework-template.md` for structure reference, then create:
After the interview, read `${CLAUDE_PLUGIN_ROOT}/assets/frameworks/framework-template.md` for structure reference, then create:
**Filename:** `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/frameworks/[slug].md` (e.g., `ai-maturity-model.md`)
@ -341,11 +342,34 @@ Guide through each section of the profile:
- Record exactly what they name. If they have none, that's fine — WebSearch + WebFetch are the always-available floor.
- This populates the **Research Tooling** section of the profile; the trend/research engine routes to a declared MCP first and falls back to the floor. Do not invent MCP names — store only what the user declares.
8. Read `config/user-profile.template.md` for structure
8. Read `${CLAUDE_PLUGIN_ROOT}/config/user-profile.template.md` for structure
9. Write the completed profile to `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/user-profile.md`
**Important:** This file is gitignored (`.local.md` pattern), so personal data stays private.
## Step 3g: Positioning Brief
**Goal:** Distil `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/positioning-brief.md` — the stance-and-difference layer every drafting surface reads before it writes.
This step **delegates**. Do not read the second brain yourself and do not restate its consumption rules here: invoke the agent that already owns them.
1. **Check what exists first.** Read the current `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/profile/positioning-brief.md` if present, and note whether it carries operator edits (`## Provenance` → "Operator edits since generation").
2. **Invoke the distiller** via `Task` with `subagent_type: linkedin-studio:strategy-advisor`, asking it to distil the positioning brief. It reads `brain/profile.md`, `brain/operations.md`, `profile/user-profile.md` and the published corpus, and shapes the result on `${CLAUDE_PLUGIN_ROOT}/config/positioning-brief.template.md`.
3. **First run (no brief exists):** show the four distilled sections, ask for corrections, then write the file.
4. **Regeneration (a brief already exists):** this is **operator-gated**.
- Present a **diff**, section by section, of the proposed distillation against the current file.
- Ask for an **explicit go** per section — "keep mine / take the new one / merge".
- **Never overwrite** the existing brief wholesale, and never discard an operator edit because the distillation disagrees with it. Positioning is the operator's own; the machine's read of it is a proposal.
5. **Thin evidence is reported, not filled.** Sections the agent marks `[insufficient evidence — operator input needed]` stay marked. Offer to interview the operator for that section instead — an invented differentiator would propagate into every post written against the brief.
6. **If there is no brain yet** the agent will say there is **no brief yet** — point the user at options 1 (voice samples) and 6 (user profile) first, and exit this step without writing a placeholder file.
**Important:** This file lives in the per-user data dir, survives reinstalls, and is read by `/linkedin:post`, `/linkedin:quick`, `/linkedin:batch`, `/linkedin:newsletter` Step 1 and the `differentiation-checker`.
## Step 4: Recalculate Score
After completing any sub-workflow:

View file

@ -24,10 +24,10 @@ You are a LinkedIn growth strategist. Help the user create a personalized growth
## Load Context
Read these files:
- `references/growth-roadmaps.md` - Detailed phase roadmaps
- `references/linkedin-growth-playbook-2025-2026.md` - Comprehensive tactics
- `references/trajectory-strategy-adjustments.md` - Trajectory-based strategy adjustments
- `skills/linkedin-studio/SKILL.md` - User's goals and context
- `${CLAUDE_PLUGIN_ROOT}/references/growth-roadmaps.md` - Detailed phase roadmaps
- `${CLAUDE_PLUGIN_ROOT}/references/linkedin-growth-playbook-2025-2026.md` - Comprehensive tactics
- `${CLAUDE_PLUGIN_ROOT}/references/trajectory-strategy-adjustments.md` - Trajectory-based strategy adjustments
- `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` - User's goals and context
## Step 0.5: Auto-Detect from State
@ -262,7 +262,7 @@ From the state file, compare current growth rate vs `growth_rate_needed`:
### Present Trajectory Overlay
Using `references/trajectory-strategy-adjustments.md` as the source, show how the phase strategy should be adjusted:
Using `${CLAUDE_PLUGIN_ROOT}/references/trajectory-strategy-adjustments.md` as the source, show how the phase strategy should be adjusted:
```markdown
### Trajectory Adjustment: [STATUS]
@ -368,7 +368,7 @@ Use AskUserQuestion to pick which derivatives to create.
**Network authority:**
- [ ] Connected with key people in niche
- [ ] Engaging with other thought leaders
- [ ] Engaging with recognized experts in the field
- [ ] Tagged or mentioned by others
- [ ] Invited to contribute/speak
@ -513,10 +513,10 @@ Remind them of the long-term view:
## Reference Files
- `references/growth-roadmaps.md` - Detailed phase roadmaps
- `references/linkedin-growth-playbook-2025-2026.md` - Comprehensive tactics
- `references/trajectory-strategy-adjustments.md` - Trajectory-based strategy adjustments
- `references/algorithm-signals-reference.md` - topic-relevance signals (authority audit)
- `references/collaborations-guide.md` - Partnership strategies
- `references/opportunity-generation.md` - Business development
- `references/newsletter-strategy-guide.md` - For 5K+ followers
- `${CLAUDE_PLUGIN_ROOT}/references/growth-roadmaps.md` - Detailed phase roadmaps
- `${CLAUDE_PLUGIN_ROOT}/references/linkedin-growth-playbook-2025-2026.md` - Comprehensive tactics
- `${CLAUDE_PLUGIN_ROOT}/references/trajectory-strategy-adjustments.md` - Trajectory-based strategy adjustments
- `${CLAUDE_PLUGIN_ROOT}/references/algorithm-signals-reference.md` - topic-relevance signals (authority audit)
- `${CLAUDE_PLUGIN_ROOT}/references/collaborations-guide.md` - Partnership strategies
- `${CLAUDE_PLUGIN_ROOT}/references/opportunity-generation.md` - Business development
- `${CLAUDE_PLUGIN_ROOT}/references/newsletter-strategy-guide.md` - For 5K+ followers

View file

@ -38,6 +38,7 @@ All flags are optional, given after the command name:
| `--brief-only` | Skip the discovery poll entirely; render the brief from the existing store | off |
| `--demand` | Run the **demand-sweep** (`demand-spotter`, «innenfra og ut») instead of supply-side discovery: poll where readers ASK, fill the reader fields, render the §4 arc map | off |
| `--dry-run` | Poll + score, but persist nothing: no capture, no brief, no status writes, no last-run marker | off |
| `--headless` | Unattended run (RE-R3f, slice e): never block on operator input. See **Step 0.5** below. | off |
Note the mode inversion deliberately: the **agent's** own default is kortform, this
**command's** default is long-form. That is why Step 2 must always pass the mode explicitly.
@ -47,11 +48,44 @@ Note the mode inversion deliberately: the **agent's** own default is kortform, t
arc map, not the pillar brief). The two are complementary: run discovery to find *what happened*,
then `--demand` on a chosen theme to see *what the reader is stuck on*.
## Step 0.5: `--headless` contract (RE-R3f, slice e)
`--headless` is for an **unattended** invocation — a scheduled task (Claude Code's own Desktop
Scheduled Tasks, `--permission-mode dontAsk`) firing this command with no operator present, e.g. a
Sunday-morning discovery pass. In `dontAsk` mode `AskUserQuestion` is auto-denied regardless of
what this file says, so the contract below is not optional politeness — it is what keeps the run
from aborting mid-pass:
- **`--demand --headless` together is refused.** The demand-sweep's pain-point/vocabulary
translation calls for operator judgment this command will not simulate. Print one line saying so
and exit — do not silently fall back to supply-side discovery instead of what was asked.
- **Step 1 never asks.** If `${DATA}/profile/user-profile.md` is missing (no pillars to score
against), print a one-line error naming the missing file and **stop** — do not prompt. The
source-list fallback (shipped defaults when `${DATA}/trends/sources.md` is absent) is unaffected;
that path already needs no operator input.
- **Step 5 (Triage) is skipped unconditionally.** Every kept candidate stays `pending` in the
store; nothing is auto-selected or auto-skipped on the operator's behalf. The next brief (or the
operator's own `/linkedin:trends` session) surfaces them for triage when a human is present.
- **Steps 24 and 6 run exactly as in an attended pass** — poll, score, capture, render the brief,
write the `.last-run` marker. `--headless` changes *who can be asked*, never what gets persisted.
- **Do not ground success in this command's own exit code or prose.** Per Claude Code's headless
docs, a `-p` run's process exit code reflects the CLI process, not the pass's semantic outcome —
a tool denial or auth failure can still surface as text on stdout. The caller (the scheduled
task's own log, or the operator reviewing Monday) verifies success against the same artifacts
Step 3 already checks: `CLI status --json`'s capture delta and today's
`${DATA}/trends/morning-brief/<date>.md`. State plainly in the final output whether capture/brief
actually happened — never report a pass as done because the model said so.
Scheduling setup (Desktop Scheduled Tasks, not a hand-rolled cron job — see
`${CLAUDE_PLUGIN_ROOT}/scripts/trends/README.md` for the exact recipe) is an operator action outside
this repo; this command only needs to behave safely once fired.
## Step 1: Load context
1. **Pillars:** Read `${DATA}/profile/user-profile.md` and extract the content pillars /
expertise areas. If the file does not exist, ask the user for their pillars before
proceeding (one question, comma-separated answer).
expertise areas. If the file does not exist: on `--headless`, print a one-line error naming
the missing file and stop (**Step 0.5** — never ask unattended); otherwise ask the user for
their pillars before proceeding (one question, comma-separated answer).
2. **Source list:** Resolve which list this pass will use — `${DATA}/trends/sources.md` if it
exists, otherwise the shipped defaults `${CLAUDE_PLUGIN_ROOT}/config/trends-sources.template.md`.
Tell the user which one applies. Do not read research-tooling or route research yourself —
@ -60,6 +94,9 @@ then `--demand` on a chosen theme to see *what the reader is stuck on*.
## Step 2D: Run the demand-sweep (only on `--demand`)
**If `--demand` and `--headless` are both set:** refuse (**Step 0.5**) — print one line stating
the demand-sweep needs an operator present and exit. Do not run either pass.
**If `--demand`:** this replaces Steps 24 (the supply-side discovery pass). Delegate to the
demand-spotter agent — invoke it via `Task` with `subagent_type: linkedin-studio:demand-spotter`
(foreground). The prompt MUST state explicitly:
@ -128,9 +165,13 @@ Include each candidate's store id (shown in the brief and via `CLI list --json`)
step below resolves per id. On `--dry-run`, present the same list but say clearly that nothing
was persisted and there are no store ids to triage.
## Step 5: Triage (skip on `--dry-run`)
## Step 5: Triage (skip on `--dry-run` or `--headless`)
Resolve the top of the queue now instead of leaving it as homework. For the candidates in the
**On `--headless`:** skip this step unconditionally (**Step 0.5** — `AskUserQuestion` is
auto-denied in an unattended run regardless). Every kept candidate stays `pending` in the store;
report the count and move to Step 6. Do not attempt to select or skip on the operator's behalf.
Otherwise, resolve the top of the queue now instead of leaving it as homework. For the candidates in the
top bands (Immediate + High; cap at 8), use AskUserQuestion — one question per candidate, up
to 4 candidates per call, options:

View file

@ -23,7 +23,7 @@ You are a LinkedIn video scripting assistant. Guide the user through creating a
First, load persistent state and personalization:
- Read `~/.claude/linkedin-studio.local.md` for posting state (streak, weekly progress, recent topics)
- Read `skills/linkedin-studio/SKILL.md` for user profile, voice settings, and preferences
- Read `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` for user profile, voice settings, and preferences
Check state for topic planning:
- Compare intended topic against "Recent Posts" in state file
@ -35,14 +35,14 @@ Check weekly progress:
- If `posts_this_week == weekly_goal - 1`, note: "This video will hit your weekly goal."
Load video-specific references:
- Read `references/video-strategy-guide.md` for script templates, pacing, and production guidance
- Read `references/linkedin-formats.md` (Video Content Deep Dive section) for algorithm data and technical specs
- Read `${CLAUDE_PLUGIN_ROOT}/references/video-strategy-guide.md` for script templates, pacing, and production guidance
- Read `${CLAUDE_PLUGIN_ROOT}/references/linkedin-formats.md` (Video Content Deep Dive section) for algorithm data and technical specs
Check for existing assets:
- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/voice-samples/` — Match the user's natural voice (read before scripting)
- `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/examples/high-engagement-posts.md` — Study successful patterns
**Graceful degradation (fresh adopter):** if `voice-samples/` is empty or absent, don't block — fall back to the voice settings in `skills/linkedin-studio/SKILL.md` (or neutral defaults), note that the script uses a default voice, and proceed. The voice-guardian is suppressed under 5 samples, so there is no hard gate to satisfy.
**Graceful degradation (fresh adopter):** if `voice-samples/` is empty or absent, don't block — fall back to the voice settings in `${CLAUDE_PLUGIN_ROOT}/skills/linkedin-studio/SKILL.md` (or neutral defaults), note that the script uses a default voice, and proceed. The voice-guardian is suppressed under 5 samples, so there is no hard gate to satisfy.
## Step 1: Choose Video Type
@ -73,7 +73,7 @@ Default recommendation: **60 seconds** is the 2026 sweet spot — shorter videos
Follow the same flow as `/linkedin:post`:
1. Ask what they want the video to be about (if not already clear)
2. Read `references/content-angles.md` for the 8 universal angles
2. Read `${CLAUDE_PLUGIN_ROOT}/references/content-angles.md` for the 8 universal angles
3. Present 2-3 angle options via AskUserQuestion
4. Verify topic doesn't duplicate recent posts (check state file)
5. Confirm topic aligns with user's 5 core expertise areas
@ -83,7 +83,7 @@ Follow the same flow as `/linkedin:post`:
Delegate script generation to the `video-scripter` agent — invoke it via `Task` with `subagent_type: linkedin-studio:video-scripter` (foreground, from this command layer). The agent will:
1. Calculate word budget based on selected length (duration × 2.5 wps)
2. Select the appropriate script template from `references/video-strategy-guide.md`
2. Select the appropriate script template from `${CLAUDE_PLUGIN_ROOT}/references/video-strategy-guide.md`
3. Write the full script with:
- Timing markers (`[0:00-0:03]`, etc.)
- Visual cues (`[CAM:]`, `[SCREEN:]`, `[SLIDE:]`, `[TEXT:]`)
@ -232,9 +232,9 @@ Replace placeholders with actual post data. This replaces manual YAML editing.
## Reference Files
- `references/video-strategy-guide.md` — Script templates, pacing, production
- `references/linkedin-formats.md` — Video specs, algorithm, technical requirements
- `references/engagement-frameworks.md` — Hook types, CTAs
- `references/content-angles.md` — 8 universal angles
- `references/algorithm-signals-reference.md` — Algorithm mechanics
- `assets/checklists/quality-scorecard.md` — Pre-publish check
- `${CLAUDE_PLUGIN_ROOT}/references/video-strategy-guide.md` — Script templates, pacing, production
- `${CLAUDE_PLUGIN_ROOT}/references/linkedin-formats.md` — Video specs, algorithm, technical requirements
- `${CLAUDE_PLUGIN_ROOT}/references/engagement-frameworks.md` — Hook types, CTAs
- `${CLAUDE_PLUGIN_ROOT}/references/content-angles.md` — 8 universal angles
- `${CLAUDE_PLUGIN_ROOT}/references/algorithm-signals-reference.md` — Algorithm mechanics
- `${CLAUDE_PLUGIN_ROOT}/assets/checklists/quality-scorecard.md` — Pre-publish check

View file

@ -21,7 +21,7 @@
"annotation — optional annotatable review HTML for a manual pass (Step 7)",
"visual-assets — cover (+ optional inline figures) or carousel deck: brief → generate → operator-gate → approve, BEFORE lock so build-linkedin.mjs picks them up (Step 7.5)",
"lock-delivery — LOCK → POST.html all-in-one-place deliverable (Step 8)",
"hook-conversion-gate — persona gate on distribution text post-lock: would YOU click? (Step 9)",
"hook-conversion-gate — persona gate on distribution text post-lock: hook-forge, would YOU click?, and who forwards this and why (Step 9)",
"scheduling — register edition in plugin queue/state for native LinkedIn scheduling (Step 10)",
"retro — close the loop (≤5 min, AFTER scheduling): promote the pending articles.NN.foldIns with an explicit operator JA/NEI (mechanical → a contract-gate rule; judgment → the language-rules file / craft checklist), summarize the MEASURED effort from articles.NN.phaseLog, and ask ONE process-friction question. The corrections made in this edition become enforced before the next one starts (Step 11)"
],
@ -31,9 +31,12 @@
"visualAssets": "Per-article visual-asset record written by Step 7.5 (visual-assets phase). Runs BEFORE lock because render/build-linkedin.mjs picks up linkedin/NN/cover.png + the edition-config credit/caption when it builds POST.html — generating images after lock would force a re-render. Shape: { format: \"standard\" | \"carousel\"; cover: { brief, route, candidates[], approved, status }; figures: [ { id, brief, placement, status } ]; carousel: null | { source, pdf, status } }. format \"standard\" = cover + optional inline figures (cover.png is mandatory per the KTG cover-directive); format \"carousel\" = typografisk deck via render/build-carousel.mjs instead of cover+inline (cover/figures stay empty). route: \"mcp-image\" (default, via mcp__mcp-image__generate_image) | \"external\" (DALL·E / Midjourney / photographer → linkedin/NN/cover-raw.png). status ladder: pending → briefed → generated → approved. candidates[] holds the cover-v<N>-kandidat.png attempts; approved is the fixed approved name (\"cover.png\") once the operator-gate passes. figures[].id = \"fig1\"..; placement = section reference in NN-utkast.md (figures are referenced in the draft via ![alt](linkedin/NN/figN.png) and uploaded manually in the LinkedIn editor — build-linkedin.mjs does NOT embed them). Naming convention: cover.png (approved, fixed — what build-linkedin.mjs reads) | cover-v<N>-kandidat.png (attempts) | cover-raw.png (optional external pre-edit source) | fig<N>.png (inline). credit + caption are recorded in <serie>/linkedin/image-credit-caption.md and flow into edition-config.json coverCredit + captions[NN].",
"livedSpecifics": "Per-article lived-specifics binding (Fix #2 slice 2, kilde-så-draft). A slot-map binding each LOAD-BEARING key-point (or, later, section) of this edition to either a real specific from the specifics-bank OR an explicit escape decision — so a draft starts grounded in the operator's real material instead of inventing plausible filler. Resolved at Step 1.5 (lived-specifics extraction, BETWEEN brief (Step 1) and research (Step 2); wired in slice 3) via a guided interview that REFUSES vague answers. THE GATE (drømme-spec G4, «vaghet avvises», enforced at the Step 2.5 skeleton-gate): every slot must resolve to \"specific\" (grounded), \"abstrakt\" (deliberately abstract here, with a rationale), or \"ekstern\" (backed by research, not lived material) — a slot left \"unresolved\" is the vagueness that BLOCKs. Numbers stay guilty-until-checked: a slot backed by an UNVERIFIED number is allowed but WARNED (regel 6/7), so Step 2 research verifies it before the draft asserts it. The deterministic validator + the rendered NN-kilder.md artifact live in scripts/specifics-bank (binding.ts/kilder.ts; `node --import tsx src/cli.ts validate-binding|render-kilder --edition <edition-state.json>`). Shape: { slots: [ { slotId, kind: \"key-point\" | \"section\", label, binding: ({ type: \"specific\", specificId } | { type: \"abstrakt\", rationale } | { type: \"ekstern\", source? } | { type: \"unresolved\" }) } ], status: \"pending\" | \"bound\" }. Default { slots: [], status: \"pending\" }: populated on first Step 1.5. Companion artifact: <serie>/linkedin/NN-kilder.md, rendered from this + the bank (regenerated, never hand-edited). This is the per-edition consumer of the global specifics-bank.",
"personas": "Per-article resolved reader-persona set (input config), written/confirmed in Step 1. This makes personas configurable PER ARTIFACT, not just from one global plugin library: Step 1 resolves them in order — (1) already present here → use as-is; (2) <serie>/linkedin/personas.md (per-series file) → load; (3) plugin config/personas.local.md (or personas.template.md) library → select a subset; (4) none/insufficient → DEFINE interactively via AskUserQuestion. Exactly one entry has tier \"primær\" (the rest \"sekundær\"); «primær trumfer» on conflict. This set feeds BOTH the in-session sweep (Step 6) and the headless package (Step 6.5 / persona-reviewer). Each entry: { name, tier: \"primær\" | \"sekundær\", rolle, avkobler, overbeviser, ekspertise, sjargong, source: \"edition-state\" | \"series-file\" | \"plugin-library\" | \"interactive\" }. Default []: resolved on first Step 1.",
"personaSweep": "Per-article reader-persona verdicts — one key per sweep point, all written by persona-reviewer. skeleton: the sweep on the five-line skeleton BEFORE prose (Step 2.5), so a premise that does not land is caught while a rewrite is still cheap. resonance: the pre-lock sweep on the full draft (Step 6), including the recorded wordCount that /linkedin:pivot's >20 % heuristic compares a later draft against. conversion: the post-lock «would YOU click?» verdict on the chosen hook (Step 9, primær persona, konverter mode). deling: the post-lock shareability verdict (N24, Step 9 — the second binary next to conversion): «who forwards this to a colleague, and why», plus which action the edition actually earns — RESHARE | SAVE | SEND. A plausible answer must name a ROLE the reader could point at AND a sender-motive (signals their judgment / saves a colleague a decision / settles a running argument); «useful for anyone in tech» names nobody and is a NEI. Shape of deling: { verdict: \"JA\" | \"NEI\", role, motive, action: \"RESHARE\" | \"SAVE\" | \"SEND\", operatorDecision? }. Two failed re-framings → a recorded operator decision (/linkedin:pivot, or ship + an articles.NN.foldIns row for the Step 11 retro) — there is never a silent pass, and the gate is never resolved by re-reading the same copy more generously. All four keys additive-optional (default null, NO schemaVersion bump — an edition produced before the step that writes one simply has none).",
"hookForge": "Per-article hook-forge record written by Step 9 (hook-conversion-gate, N24). Additive-optional (default null, NO schemaVersion bump — same pattern as phaseLog/retro; an edition produced before N24 simply has none). The hook competes against alternatives instead of shipping the one the draft happened to end on: 58 variants are forged for the distribution copy (the body stays locked, as everywhere in this step). The 110140-character band is enforced PER VARIANT and MEASURED — `perl -CSD -ne 'chomp; printf \"%d\\n\", length($_)'`, never `awk length`/`wc -c`, which count BYTES and over-report on æ/ø/å. A variant outside the band is DISQUALIFIED, not ranked lower (a cutoff, not a preference). The surviving set must span ≥3 hook types, or it is a set of rewrites and the operator has no real choice; every variant must trace to a claim the body actually makes (no bait hook — an unearned promise is what the randsone fact gate exists to catch). Fewer than five survivors ⇒ forge again. Shape: { variants: [ { text, measuredChars, hookType, bodyTrace, disqualified: null | \"band\" | \"body-trace\" } ], typesSpanned, chosen, chosenAt }. NO PREDICTED SCORE: a variant carries only its measured length, its hook type and its body trace. A predicted-engagement ranking would be a fabricated composite — exactly the class removed in N22 — and a ranked list only LOOKS more helpful. The ranking is the operator's; the plugin does not rank what it cannot measure.",
"randsoneGate": "Per-article randsone-gate record written by Step 9 (hook-conversion-gate, N9). Additive-optional (default null, NO schemaVersion bump). The «randsone» is the distribution copy AROUND a locked edition — delingstekst hook, samle post, carousel slide text, edition title — text that ships with the edition but never passed the body's fact and language gates. Two scoped reviewers judge it (fact-reviewer F1/F3, language-reviewer L1/L2), plus a title-strength verdict on the distribution title. The gate NEVER reopens the locked body: a 🔴 that traces to the body (or a canonical-title weakness) is a pivot finding — STOP and run /linkedin:pivot; everything else is fixed in the distribution copy and re-checked. The chosen hook-forge variant goes THROUGH this gate, never around it. Shape: { surfaces: [ { surface, factVerdict, languageVerdict, revised } ], titleStrength, pivotTriggered, resolvedAt }.",
"headlessReview": "Per-article headless-review record written by Step 6.5 (headless-review phase). Runs AFTER the in-session persona sweep (Step 6) and BEFORE lock (Step 8), on a FROZEN snapshot of the publish-ready (or pivoted) draft, fanned out from the command layer (foreground) or invoked standalone via /linkedin:headless-review in a fresh/cold session. Five archetypes judge independently with NO drafting-session context: content-reviewer (argument integrity), language-reviewer (Norwegian language), fact-reviewer (cold re-verification incl. claims a late pivot bolted on), persona-reviewer mode=resonans (per active persona), persona-reviewer mode=konverter (primær, hook only). The consolidated report is surfaced to the operator via SendUserFile; the operator decides which flags fold in. Shape: { frozenDraft, reviewers: { content, language, fact, personaResonance, personaConversion } (each { reportPath, summary, status }), consolidatedReport, foldedIn, waived, status }. status ladder: pending → run → folded. null until Step 6.5 runs. This is the adversarial-independence companion to the in-session gates (editorialReview, personaSweep, factcheckLog) — deliberately redundant: a cold reader catches what the framing-biased in-session pass missed.",
"pivots": "Per-article pivot log (Endring 9c). A pivot is a substantive change to a draft AFTER a gate had already cleared — e.g. a new argument anchor / section added late (the Del 4 Security Champions case: +~530 words, 2 new sections, +42 %). Each /linkedin:pivot invocation appends one entry and moves currentPhase back so the cleared gates (Steps 56.5) re-run on the pivoted version before lock. Heuristic (documented, checked at the Step 8 lock precondition): if the current draft's word count differs > 20 % from the version that last cleared Step 6, OR it has > 2 new sections, a pivot-reopen is suggested/required. Each entry: { timestamp, reason, fromPhase, toPhase, wordCountBefore, wordCountAfter, deltaPct, newSections, gatesToRerun: [phase…] }. Default [].",
"foldIns": "Per-article accumulation queue (slice 2 of fix #1 — «rettelser fester seg»). Each correction KTG makes during an edition that is NOT yet a contract rule is captured here, then routed by the JA-promoter (maskinrommet/docs/skrivekontrakt.md §E) to a permanent home so it never has to be re-discovered: a MECHANICAL correction → a rules.ts gate rule (BLOCK/WARN) + a §B-row/§C1/§C2-box + a §E-manifest row (the contract-gate `ratify` check then asserts rules.ts ↔ §E-manifest stay in bijection); a JUDGMENT correction → a §C2-box only (stays with editorial-reviewer, no gate rule). Capture is per-article (provenance = which article surfaced it); promotion is series/contract-wide. Each entry: { id, date (ISO-8601), correction (what KTG corrected, near-verbatim), trigger (where/why it surfaced), classification: \"mechanical-block\" | \"mechanical-warn\" | \"judgment\" | null (set at the classify step), decision: \"pending\" | \"promoted\" | \"rejected\" (the JA-promoter outcome), ruleId: <rules.ts id> | null (set on promote for mechanical), note?: where a judgment/rejected fold-in landed }. Default []. Rejected fold-ins are kept for traceability, never deleted.",
"foldIns": "Per-article accumulation queue (slice 2 of fix #1 — «rettelser fester seg»). Each correction the operator makes during an edition that is NOT yet a contract rule is captured here, then routed by the JA-promoter (maskinrommet/docs/skrivekontrakt.md §E) to a permanent home so it never has to be re-discovered: a MECHANICAL correction → a rules.ts gate rule (BLOCK/WARN) + a §B-row/§C1/§C2-box + a §E-manifest row (the contract-gate `ratify` check then asserts rules.ts ↔ §E-manifest stay in bijection); a JUDGMENT correction → a §C2-box only (stays with editorial-reviewer, no gate rule). Capture is per-article (provenance = which article surfaced it); promotion is series/contract-wide. Each entry: { id, date (ISO-8601), correction (what the operator corrected, near-verbatim), trigger (where/why it surfaced), classification: \"mechanical-block\" | \"mechanical-warn\" | \"judgment\" | null (set at the classify step), decision: \"pending\" | \"promoted\" | \"rejected\" (the JA-promoter outcome), ruleId: <rules.ts id> | null (set on promote for mechanical), note?: where a judgment/rejected fold-in landed }. Default []. Rejected fold-ins are kept for traceability, never deleted.",
"retro": "Per-article retro record written by Step 11 (retro phase), the step that makes a correction STICK. Additive-optional (default null, NO schemaVersion bump — same pattern as phaseLog/sourceTrendId/targetLevel; an edition produced before N14 simply has none). Runs AFTER scheduling (Step 10) because the retro is about the NEXT edition, not this one's delivery. Three jobs, all recorded here: (1) fold-in promotion — every articles.NN.foldIns row with decision \"pending\" gets an explicit operator JA/NEI; JA on a mechanical correction promotes it atomically to a scripts/contract-gate rule (rules.ts + contract row + manifest row, `--ratify` green or the promotion does not count), JA on a judgment correction appends it to the user language-rules file (${LINKEDIN_STUDIO_DATA}/language-rules/<lang>.md) or the craft checklist; NEI marks the row \"rejected\" and KEEPS it. (2) effort summary — read from the MEASURED articles.NN.phaseLog (N12), never re-estimated: number of transitions, phases that recurred (a re-run gate), and the elapsed lead time. (3) ONE process-friction question, whose answer is handed BACK to the operator — the plugin never writes the operator's own notes/register. Shape: { promoted, rejected, ruleIds: [<rules.ts id>…], languageRulesAppended, effort: { transitions, recurredPhases: [phase…], leadTimeDays }, frictionNoted: boolean, completedAt }. The full loop (capture → classify → promote → enforce) is documented domain-generally in references/fold-in-loop.md.",
"language": "Review language for this series/edition (additive, default \"en\"). Threads into the long-form review agents so they grade against THIS language's rules: language-reviewer applies Norwegian-specific checks (anglicism→Norwegian idiom, «kanselli-stil») only when language == \"no\"; voice-scrubber's gold standard is the approved editions IN this language; any other value → the agents apply that language's equivalents and never grade prose against Norwegian idiom. \"no\" = Norwegian (the author's case). Resolved at Step 1 / load-context and passed to the language-dependent agents.",
"sourceTrendId": "Per-article provenance link to the trend this edition was started from (N7 trend→newsletter bridge, MR-F3). Additive-optional (default null, NO schemaVersion bump — an edition started manually never sets it, and Step 10 reads its absence as \"no source trend\"). Set at the Step 1.5 checkpoint when Step 1's trend-intake read a candidate from the trends store (scripts/trends) to prefill the brief (angle / targetLevel / key-points / source-URLs). Its ONE runtime consumer is Step 10 (scheduling): when the article reaches scheduling AND sourceTrendId is set, the command flips that trend's store status to \"acted\" (trends CLI `act --id <sourceTrendId>`), closing the discovery→production loop deterministically instead of by hand. Value = the trend's store id (normalized title+url, as shown in the /linkedin:trends brief and `CLI list --json`).",
@ -65,7 +68,8 @@
"personaSweep": {
"skeleton": null,
"resonance": null,
"conversion": null
"conversion": null,
"deling": null
},
"headlessReview": {
"frozenDraft": null,
@ -93,6 +97,8 @@
"figures": [],
"carousel": null
},
"hookForge": null,
"randsoneGate": null,
"pivots": [],
"foldIns": [],
"retro": null,

View file

@ -35,7 +35,8 @@ trim, or extend them per series.
NO is **not** accepted: the text is revised until the primær reaches a clean YES.
- **Two sweep modes** (same `persona-reviewer` agent): resonance mode (Step 6,
BEFORE lock — «does the point land for this reader?») and conversion mode
(Step 9, after lock — binary «would YOU click?» on the hook only).
(Step 9, after lock — two binaries on the distribution copy: «would YOU click?»
and «who forwards this to a colleague, and why?»).
### Per-artifact personas (one or more personas per edition)

View file

@ -0,0 +1,71 @@
# Positioning Brief
> **What this file is.** A distillate — the second brain's evidence-weighted read of your
> positioning, written by `/linkedin:setup` (Step 3g) and consumed by every drafting
> surface. It answers four questions a draft cannot answer for itself: who is writing,
> who it is for, what they actually hold, and why it is not interchangeable with the
> other twenty posts on the topic that week.
>
> **What this file is not.** It **does not restate** `user-profile.md`. That file is your
> *declared* identity — name, role, the five expertise areas, the target audience, the
> ranked goals — filled in by you and authoritative as such. This file is the *distilled*
> layer above it: the stance and the difference, inferred from what you have actually
> published and consolidated into `brain/profile.md`. When the two disagree, the declared
> profile and your dated `## Who I am now` anchor win, and the inference is the thing that
> gets corrected. Two files asserting the same fact is how positioning quietly forks.
>
> **Editing.** This file is yours to edit directly. Regeneration never overwrites it
> silently: `/linkedin:setup` shows a diff and asks for an explicit go first.
---
## Who I am
[One or two sentences. The working identity a reader would recognise across your posts —
not your job title. Distilled from `brain/profile.md` Static facts and the operations
anchor; grounded in what you have published, not in what you aspire to.]
_Evidence:_ [which consolidated facts or published pieces support this]
---
## Who this is for
[The reader you are actually writing to — the one whose problem your posts solve. Narrower
than the declared target audience if the published record says so; say which, and why.]
_Evidence:_ [engagement patterns, recurring commenters, the questions you actually get]
---
## What I hold
[The two to four positions you argue from — the opinions a reader could disagree with. A
position that no one could disagree with is a description, not a stance, and does not
belong here.]
1. [Position — stated as a claim, not a topic]
2. [Position]
3. [Position]
_Evidence:_ [posts or editions where you took this position explicitly]
---
## How I differ
[Why this account and not the generic account on the same topic. Concrete: the lived
material only you have — the named cases, the real numbers, the work you actually did.
"More practical" and "more authentic" are not differentiators; the specific thing you can
say that a competitor cannot is.]
_Evidence:_ [specifics-bank entries, case studies, frameworks you authored]
---
## Provenance
- **Generated:** [YYYY-MM-DD] by `/linkedin:setup` Step 3g
- **Sources:** `brain/profile.md` (consolidated) · `brain/operations.md` (anchor) ·
`profile/user-profile.md` (declared) · published corpus
- **Operator edits since generation:** [none / describe]

View file

@ -255,7 +255,7 @@ Before testing, ensure:
3. Agent should ask which post to monitor
4. Provide sample metrics: 500 impressions, 15 reactions, 3 comments, 1 repost
5. Agent should identify the current phase and provide benchmarks
**Expected:** Structured output with metrics snapshot, velocity score, anomaly detection, and recommended actions
**Expected:** Structured output with metrics snapshot, a baseline reading (above/within/below band, or an explicit "no verdict — N post(s), 5 required" refusal), anomaly detection, and recommended actions. No composite score.
**Validates:** Agent file loads correctly, context loading works, output format matches spec
### Test 16: Post-Feedback Monitor — Anomaly Detection

View file

@ -0,0 +1,175 @@
# Brief — RE-R3f: unattended AI discovery (`--headless`, slice e)
> **Slice:** RE-R3f (research-engine rung-2, R3 slice **(e)** in the operator's `(a)→(c)→(b)→(d)→(e)`
> sequence — the doc is numbered r3f by creation order, the concept is slice (e)). Closes the
> production gap named in `docs/research-engine-concepts.local.md` §5: RE-R3c/RE-R3e built a
> **deterministic** headless entry (`schedule`/`run-daily.sh`) that re-renders the brief on a
> schedule with no new discovery — a near-no-op without a prior human-run poll. RE-R3f makes the
> **actual discovery pass**`/linkedin:trends`'s poll → score → capture → brief loop, including
> the `trend-spotter` agent's web research — safe to fire with no operator present.
> **Origin:** Maskinrommet production feedback, register row **MR-F5** — Sunday-morning discovery
> needs to run without the operator present. Elicited via `CLAUDE.local.md`'s feedback-intake
> protocol; validated against live plugin structure before any code/prose was touched.
> **The gate this slice opens:** `docs/research-engine-concepts.local.md` §5 recorded a 2026-06-24
> operator decision that slice (e) sits behind an **explicit re-evaluation gate against product
> maturity (v1.0.0)** — research is "rung-2." The plugin is at v0.8.0 (CLAUDE.md header); the gate
> had not been re-evaluated. Surfaced to the operator 2026-08-10 as a real conflict, not silently
> built around or silently deferred; the operator opened the gate explicitly (AskUserQuestion,
> "Open the gate now") after seeing the trade-off stated plainly.
> **Architectural decision — CONFIRMED (operator, AskUserQuestion 2026-08-10):** Desktop Scheduled
> Tasks (Claude Code's own first-party local scheduler) is the trigger mechanism, **not** a
> hand-rolled cron/launchd wrapper mirroring RE-R3c. A `-p`-mode `claude` invocation cannot be
> replicated by a shell script (it needs a full agent turn, incl. the `trend-spotter` subagent), and
> Claude Code's own headless docs state a hand-rolled `cron` + `claude -p` wrapper loses
> observability/session-resumption/error-recovery relative to the first-party mechanism. The recipe
> for the fallback (hand-rolled `claude -p`) path is documented for portability, but not built as
> repo code.
## 1. The gap — grounded in code and docs
- **`run-daily.sh` (RE-R3c) is deterministic-only by design** (`run-daily.sh:1-8`, `docs/
research-engine/brief-re-r3c.md` C1): it re-renders the brief from the **current** store — no
poll, no capture. Its own comment names the gap: *"The (e) slice will insert a pre-brief AI
capture step here … before the `brief` call below."* Nothing has inserted it.
- **The attended pass blocks on operator input in two places.** `commands/trends.md` Step 1 asks
the user for pillars when `${DATA}/profile/user-profile.md` is missing; Step 5 (Triage) calls
`AskUserQuestion` up to 4× per batch. Fired unattended, either would hang or (in Claude Code's
`dontAsk` permission mode, the correct mode for an unattended run) abort the run — `AskUserQuestion`
is auto-denied unconditionally in that mode regardless of any `--allowedTools` entry (verified
against Claude Code's `permission-modes` docs, 2026-08-10).
- **No documented recipe for firing a plugin command unattended existed in this repo.** RE-R3c's
`schedule` CLI verb only ever targets the deterministic `brief` subcommand
(`run-daily.sh:33`, `cli.ts` `schedule` branch) — it has no path to a full `claude` agent turn.
- **Exit-code trust was unstated.** Claude Code's headless docs are explicit that a `-p` run's
process exit code reflects the CLI process, not the pass's semantic outcome (a tool denial or
auth failure can still print to stdout without a non-zero exit). No existing doc in this repo
told a scheduled caller what to check instead.
## 2. Scope — what is IN (RE-R3f)
### `commands/trends.md` (EDIT) — the `--headless` contract
- New flag `--headless`, documented in the Step 0 flags table + a new **Step 0.5** section stating
the contract explicitly (so it reads as a rule, not something buried three steps down):
- `--demand --headless` together → refused (one line, exit; never silently falls back to
supply-side discovery).
- Step 1 missing profile → print a one-line error and stop; never ask. The source-list fallback
(shipped defaults) is unaffected — it already needs no operator input.
- Step 5 (Triage) → skipped unconditionally. Every kept candidate stays `pending`; the next
attended session (or the next morning brief) surfaces it for a human to resolve.
- Steps 24 and 6 (poll, score, capture, render, `.last-run` marker) run exactly as attended —
`--headless` changes who can be asked, never what gets persisted.
- States plainly: ground success in the same artifacts Step 3 already verifies (`CLI status
--json` capture delta, today's dated brief file) — never in this command's own exit code or
prose.
- No frontmatter change (`allowed-tools` already lists `AskUserQuestion` for the attended path;
leaving it is harmless belt-and-suspenders — `dontAsk` mode denies it unconditionally regardless
of what a command's frontmatter requests).
### `scripts/trends/README.md` (EDIT) — the unattended-discovery section
- A new `## Unattended AI discovery — --headless (RE-R3f, slice e)` section, positioned after the
existing RE-R3c section, explicit that this is a **different mechanism** (a full agent turn, not
a bash/cron wrapper): Desktop Scheduled Tasks as the recommended trigger; the verified `claude -p`
recipe (`--bare --plugin-dir … --permission-mode dontAsk --allowedTools "Read,Bash,Task,WebSearch,
WebFetch" --output-format json`) as the documented fallback for a machine without Desktop
Scheduled Tasks; the `ANTHROPIC_API_KEY` requirement under `--bare`; the background-subagent wait
ceiling (`CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS`, default 10 min) since the research subagent can
run long; the artifact-grounded success check (never the exit code); the `total_cost_usd` cost
visibility note from `--output-format json`.
### `CLAUDE.md` (EDIT) — command table
- `/linkedin:trends` row gains a `--headless` mention (one clause, no restructuring).
### `CHANGELOG.md` (EDIT)
- `[Unreleased]` entry: MR-F5 landed, gate opened, scope summary.
### Docs (this file + `plan-re-r3f.md`, NEW, tracked)
## 3. Non-goals — what is OUT (deferred)
- **A hand-rolled cron/launchd wrapper invoking `claude -p`** — OUT as repo code. The recipe is
documented (README) for portability to a machine without Desktop Scheduled Tasks, but nothing in
this repo shells out to `claude` itself; Desktop Scheduled Tasks is the built mechanism.
- **`schedule.ts` / `run-daily.sh` changes** — OUT. RE-R3c's deterministic brief-only wrapper is
untouched and remains valid for a plain "re-render today's brief" use case distinct from
discovery.
- **A cost cap / per-run candidate ceiling enforced in code** — OUT. `trend-spotter`'s existing
guidance (Anti-Patterns: "max 2-3 trend posts per week") is advisory prose, not a mechanism;
enforcing a hard cap on an unattended AI research run is a real design question, deferred rather
than guessed at under this slice. `total_cost_usd` visibility (README) is the mitigant landed now.
- **`--demand --headless`** — explicitly refused, not built. The demand-sweep's judgment calls are
a later slice's problem, if ever.
- **A new store field / schema bump** — none. `SCHEMA_VERSION` (4) / `BRIEF_SCHEMA_VERSION` (2)
untouched — `--headless` changes command-level control flow only.
- **A new agent, a new CLI verb, a new source/test file** — none. RE-R3f is a prompt-contract EDIT
plus documentation; there is no new TypeScript/bash to unit-test.
## 4. Boundaries / invariants (must hold)
- **`--headless` never changes what gets persisted**, only who can be asked. A headless run and an
attended run that both clear Step 1 must capture/brief identically (Step 5's skip is the only
behavioral divergence, and it is additive-safe: `pending` candidates are exactly what an attended
triage would leave un-triaged if the operator picked "Leave" on all of them).
- **No silent degrade.** `--demand --headless` refuses rather than silently running supply-side
discovery instead; a missing profile refuses rather than silently scoring against no pillars.
- **Success is artifact-grounded, never prose-grounded** — documented explicitly in both the
command file and the README, because Claude Code's own docs establish the exit-code trap is real.
- **Domain-general** — no vendor/sector token in any edit (Section 17 de-niche stays green); the
README's `claude -p` recipe names no user-specific path (the plugin-dir is a placeholder the
operator fills at their own install).
- **Counts unchanged** — no new command (`/linkedin:trends` gains a flag, not a sibling command),
no new agent, no new reference file. Recounted live at land.
## 5. Success criteria (verifiable — no test suite; this is a prompt/doc change)
- **SC1**`commands/trends.md` documents `--headless` in the flags table AND a dedicated Step 0.5
contract section; `grep -c '\-\-headless' commands/trends.md` ≥ 5 (table + Step 0.5 + the three
step-level cross-references).
- **SC2** — Step 1's missing-profile branch explicitly forks on `--headless` (fail vs. ask); Step
2D explicitly refuses `--demand --headless`; Step 5's header states the `--headless` skip
condition inline (not only in Step 0.5) — each verified by direct read of the edited sections.
- **SC3**`scripts/trends/README.md` documents the Desktop Scheduled Tasks recommendation, the
fallback `claude -p` recipe with all four required flags (`--bare`, `--plugin-dir`,
`--permission-mode dontAsk`, `--allowedTools`), the `ANTHROPIC_API_KEY` requirement, and the
artifact-grounded success check — each present as literal text, verified by grep.
- **SC4**`CLAUDE.md`'s `/linkedin:trends` row mentions `--headless`; `CHANGELOG.md` `[Unreleased]`
names MR-F5.
- **SC5** — Section 17 de-niche guard stays green (no vendor/sector token introduced); command/
agent/reference counts unchanged (recounted live).
- **SC6 (explicitly NOT claimed)** — an actual unattended end-to-end run. Per the
`plugin-runtime-is-consumer-side` constraint, a plugin command does not resolve in this dev-repo
session; the real Sunday-discovery proof happens consumer-side, after the operator updates +
reloads + configures a Desktop Scheduled Task. This brief does not claim that proof; it claims the
command-file contract and the documentation are correct and internally consistent.
## 6. Verification
**This session (dev-repo):** direct read-through of every edited section against SC1SC5;
`bash scripts/test-runner.sh` for regression sanity (RE-R3f touches no source/test file the gate
tracks, so `FAIL=0` and every floor unchanged is the expectation, not a new floor); Section 17
de-niche green; counts recounted live.
**Consumer-side (deferred, not gated on this session):** update + reload the plugin, run
`/linkedin:trends --headless` once attended-but-silent (verify it does not prompt), then configure
a Desktop Scheduled Task and confirm a real unattended Sunday pass produces a capture delta + a
dated brief.
## 7. Open questions for the go-gate
Two decisions are **CONFIRMED** (operator, AskUserQuestion 2026-08-10): the gate is opened now
(production need outweighs waiting for v1.0.0); Desktop Scheduled Tasks is the mechanism, not a
hand-rolled wrapper (with the fallback recipe documented, not built). Residual decisions, baked to
the recommended default — confirm or redirect:
- **D1 — `--headless` default mode is long-form (inherits the command's own default)?** YES (rec).
Unattended discovery is for the Sunday chronicle/newsletter pipeline per MR-F5's own framing; an
operator wanting kortform passes `--mode kortform --headless` explicitly. Re-open only if the
unattended default should differ from the attended default.
- **D2 — no hard cost/candidate cap enforced in code this slice?** YES (rec, per §3 non-goal) — cost
visibility (`total_cost_usd`) is landed; a cap is a real question deferred rather than guessed.
Re-open only if a cap should land now.
- **D3 — the fallback `claude -p` recipe is documented but not wrapped in a repo script?** YES
(rec). Building an unused wrapper for a path the operator isn't using (Desktop Scheduled Tasks
covers the real machine) would be speculative code. Re-open only if the fallback path is the one
actually needed.

View file

@ -0,0 +1,113 @@
# Plan — RE-R3f: unattended AI discovery (`--headless`, slice e)
> **Brief:** `docs/research-engine/brief-re-r3f.md`. **Slice:** RE-R3f (research-engine rung-2 — R3
> slice (e), the AI-capture unattended-run slice). No new source/test file: this is a prompt-file
> (`commands/trends.md`) contract change plus documentation. There is nothing here for
> `scripts/test-runner.sh` to gain a new floor for — the plan's "TDD" is a direct-read verification
> against the brief's SC1SC5, not a unit-test suite.
## Files touched (exhaustive)
| File | Change | SC |
|---|---|---|
| `commands/trends.md` | EDIT — `--headless` flag row + new **Step 0.5** contract section; Step 1's missing-profile branch forks on `--headless`; Step 2D refuses `--demand --headless`; Step 5 header states the unconditional skip | SC1, SC2 |
| `scripts/trends/README.md` | EDIT — new `## Unattended AI discovery — --headless (RE-R3f, slice e)` section: Desktop Scheduled Tasks (recommended) + the fallback `claude -p` recipe + `ANTHROPIC_API_KEY` + wait-ceiling + artifact-grounded success check + cost visibility | SC3 |
| `CLAUDE.md` | EDIT — `/linkedin:trends` command-table row gains a `--headless` clause | SC4 |
| `CHANGELOG.md` | EDIT — `[Unreleased]` entry naming MR-F5 | SC4 |
| `docs/research-engine/{brief,plan}-re-r3f.md` | NEW — tracked slice docs (mirrors R3c/R3e/etc.) | — |
| `STATE.md` | EDIT at land — Telling-block reconcile (no new counts; note MR-F5 landed) | — |
**Not touched (scope fence):** `scripts/trends/src/*.ts` (no code — Desktop Scheduled Tasks is the
mechanism, not a repo-owned wrapper); `scripts/trends/run-daily.sh` (RE-R3c stays deterministic-only,
untouched); `agents/trend-spotter.md` (the agent's own contract — poll/score — is unchanged by who
fires it; `--headless` is a command-level concern); `config/*`; `references/*`; command/agent counts
(no new command, no new agent — `--headless` is a flag on an existing command); `scripts/test-runner.sh`
(no new floor — nothing here is `tsx`-tested source).
## Step 1 — `commands/trends.md`: the `--headless` contract
Edited already this session (verify against brief SC1/SC2 by direct read):
1. Step 0 flags table gains the `--headless` row.
2. New **Step 0.5** section states the full contract: `--demand --headless` refusal, Step 1
fail-fast, Step 5 unconditional skip, Steps 24/6 unchanged, artifact-grounded success.
3. Step 1's pillar-loading paragraph forks explicitly: `--headless` → error + stop; else → ask.
4. Step 2D gains the refusal clause for `--demand --headless` before its existing demand-sweep
delegation text.
5. Step 5's header and lead line state the `--headless` skip condition (not buried only in Step 0.5
— a reader working step-by-step must see it at the step itself).
**Verification:** `grep -n -- '--headless' commands/trends.md` shows the table row + Step 0.5 + the
three per-step cross-references (≥5 matches, SC1); re-read Steps 1/2D/5 end-to-end to confirm each
fork is unambiguous (no branch left to model inference under ambiguity, SC2).
## Step 2 — `scripts/trends/README.md`: the unattended-discovery section
Edited already this session. New section placed after the existing RE-R3c ("Autonomous trigger +
headless entry") section, explicit that RE-R3f is a **different mechanism** (full agent turn, not a
bash wrapper): Desktop Scheduled Tasks recommended; the fallback `claude -p …` recipe with all four
required flags; `ANTHROPIC_API_KEY` under `--bare`; `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS`; the
artifact-grounded success check (never trust `$?`/prose); `total_cost_usd` cost-visibility note.
**Verification:** `grep -n -- '--bare\|--permission-mode dontAsk\|--allowedTools\|ANTHROPIC_API_KEY\|Desktop Scheduled Tasks' scripts/trends/README.md` — each present (SC3).
## Step 3 — `CLAUDE.md` + `CHANGELOG.md`
- `CLAUDE.md`: append a `--headless` clause to the existing `/linkedin:trends` row (one line, no
restructuring of the table).
- `CHANGELOG.md`: `[Unreleased]` gains an entry — MR-F5 landed, the gate-opening decision named, the
scope (command contract + docs, no new code), matching this repo's existing changelog voice (see
the 0.8.0 entries for tone/format).
**Verification:** `grep -n -- '--headless' CLAUDE.md`; `grep -n 'MR-F5' CHANGELOG.md`.
## Step 4 — gate + regression sanity
`bash scripts/test-runner.sh``FAIL=0`, every existing floor unchanged (no new source/test file to
raise a floor for); Section 17 de-niche green (no vendor/sector token in any edit — the README's
`claude -p` recipe names a placeholder plugin-dir path, never an operator-specific one);
command/agent/reference counts unchanged, recounted live (`--headless` is a flag, not a new
surface).
## Step 5 — land
Recount counts live; reconcile `STATE.md`'s Telling block (no count change; note MR-F5 landed at
commit); quit the feedback register kvittering per `CLAUDE.local.md`'s protocol ("landed
feedback:MR-F5 @ `<commit>`" — Maskinrommet flips 🔵→🟢 on relay, never written here). Commit order:
**(1)** docs commit — `docs/research-engine/{brief,plan}-re-r3f.md` (tracked, no suffix); **(2)** one
code^H^H^H^Hprompt commit — `commands/trends.md` + `scripts/trends/README.md` + `CLAUDE.md` +
`CHANGELOG.md` (there is no code in this slice — "code commit" here means the prompt-file + docs
edit, landed together since it is one coherent contract change). Push after operator go (public
`open/` remote — confirm before push per the repo's own push-topology memory).
## Verification (testable)
| SC | Check | Command | Expected |
|---|---|---|---|
| SC1 | `--headless` documented | `grep -c -- '--headless' commands/trends.md` | ≥ 5 |
| SC2 | Per-step forks unambiguous | direct read of Steps 1, 2D, 5 | each states the `--headless` branch explicitly, no inference required |
| SC3 | README recipe complete | `grep` for the four flags + `ANTHROPIC_API_KEY` + `Desktop Scheduled Tasks` | all present |
| SC4 | CLAUDE.md + CHANGELOG updated | `grep -- '--headless' CLAUDE.md`; `grep 'MR-F5' CHANGELOG.md` | both present |
| SC5 | Gate green, counts unchanged | `bash scripts/test-runner.sh` | `FAIL=0`; all floors unchanged; Section 17 green |
| SC6 | (explicitly deferred) | — | consumer-side unattended proof is NOT claimed this session (`plugin-runtime-is-consumer-side`) |
## Risks
- **R1 — a scheduled task on the operator's actual machine can't reach Desktop Scheduled Tasks
(older Claude Code version, disabled feature).** *Mitigated:* the fallback `claude -p` recipe is
fully documented in the README, so the operator has a working path either way; not built as repo
code because building an unused wrapper would be speculative (brief §3 D3).
- **R2 — the `--headless` contract is a prompt, so nothing enforces it mechanically; a future edit
to `commands/trends.md` could silently reintroduce a blocking `AskUserQuestion` on the headless
path.** *Accepted, stated honestly:* there is no code-level guard here (unlike RE-R3c's
gate-tested `schedule.ts`). Claude Code's own `dontAsk` permission mode is the real backstop —
`AskUserQuestion` is auto-denied unconditionally regardless of what the command file says — so a
regression fails safe (the run aborts) rather than silently hanging on a phantom prompt.
- **R3 — unattended weekly AI research has a real, currently-uncapped per-run cost.** *Mitigated by
visibility, not a cap* (brief §3 non-goal, D2): `--output-format json`'s `total_cost_usd` is
documented as the thing to log; enforcing a hard ceiling is deferred as a real design question
rather than an arbitrary number picked under this slice.
- **R4 — the gate-reopening decision itself is contestable** (research was explicitly deferred to
v1.0.0 by a prior operator decision). *Mitigated:* the conflict was surfaced explicitly
(AskUserQuestion, 2026-08-10) rather than silently built around or silently deferred; the operator
chose to open it with the trade-off stated plainly. Recorded in the brief's header for future
sessions reading this slice cold.

View file

@ -1,8 +1,8 @@
# Bygge-brief (Voyage-input) — LTL fullspektrum-innholdsmotor → v2.0.0
> **Formål:** Input til `/trekplan` (Voyage-pluginen). Dette er byggingen av LTL-pluginen, drevet som et Voyage-prosjekt: `/trekplan` produserer en kjørbar plan med per-steg Manifests, `/trekexecute --fg` + `/trekcontinue` driver sesjonene, `/trekreview` er release-gate.
> **Detaljert referanse:** [`plan-fullspektrum-innholdsmotor.md`](./plan-fullspektrum-innholdsmotor.md) (samme mappe) — den hardnede planen med §0 orientering, §4 renovering, §5 langform, §6 byggeklosser, §7 render, §10 DoD. **Les den; den er fasit for hva som skal bygges.**
> **Opprinnelig retningsbrief:** [`brief-fullspektrum-innholdsmotor.md`](./brief-fullspektrum-innholdsmotor.md).
> **Detaljert referanse:** [`plan-fullspektrum-innholdsmotor.md`](../plan-fullspektrum-innholdsmotor.md) (i `docs/`) — den hardnede planen med §0 orientering, §4 renovering, §5 langform, §6 byggeklosser, §7 render, §10 DoD. **Les den; den er fasit for hva som skal bygges.**
> **Opprinnelig retningsbrief:** [`brief-fullspektrum-innholdsmotor.md`](../brief-fullspektrum-innholdsmotor.md).
---

View file

@ -34,7 +34,7 @@ writeState(content => updateFollowerCount(content, {
- **Quality Check**: Has content been reviewed against quality scorecard? Hook 110-140 chars, 1,200-1,800 chars total, authentic tone, no external links.
- **5x5x5 Engagement**: Before posting, complete 15-20 min pre-posting engagement — 5 people with overlapping audiences, find their recent posts, write 5 thoughtful comments (15+ words each).
- **First-Hour Plan**: Respond within 5 minutes to first comments. Add value in responses. Target 15+ engagements in first hour.
- **First-Hour Plan**: Respond within 5 minutes to first comments. Add value in responses. Keep the first hour active — early engagement unlocks broader distribution (directional; no fixed threshold).
- **Posting Time**: Post when target audience is most active.
**3. Queue Status Check**

View file

@ -62,6 +62,23 @@ The defensible spine is the **order**, not the multiplier:
> The personal-profile **per-post baseline** (~2.02.6%) is a *different denominator* from
> a format benchmark — never present an account baseline as a carousel rate.
## Post length (text posts)
| Claim | Statement | Source | Confidence |
|-------|-----------|--------|------------|
| Engagement optimum | Median engagement peaks in the **1,3012,500 character** band (2.612.67%, against 2.10% below 400 chars); 2,5013,000 falls back slightly. **Single vendor, personal profiles, one six-month window** — the defensible part is the direction (substance beats soundbite), not the boundaries. | AuthoredUp (372,126 posts, Sep 2025Feb 2026) | medium |
> **On the plugin's own 1,2001,800 standard band:** it overlaps the measured optimum from
> 1,301 up, with its bottom ~100 characters below it, and its ceiling 700 short. Read
> 1,3012,500 as evidence that the shipped ceiling is **conservative, not wrong** — one
> vendor's medians over one window is not enough to move an SSOT that every gate enforces
> (`hooks/prompts/content-quality-gate.md`). Test a longer post against your own baseline
> before concluding anything.
>
> **Two AuthoredUp sample sizes appear in this file** — 621K (NLP-quality-scored engagement
> study) and 372K (this length study). Different studies, different windows; not a
> transcription error.
## External links (in post body)
| Claim | Statement | Source | Confidence |
@ -188,7 +205,7 @@ source.)
---
*Last updated: 2026-07-17 (GR-model correction). Maintained as the single canonical
*Last updated: 2026-07-31 (post-length section added). Maintained as the single canonical
algorithm statement; cite, do not restate.*
*Sources (per-claim quality/confidence noted inline): LinkedIn Engineering — "Engineering
@ -197,6 +214,7 @@ research paper, withdrawn 2025-08-23); LinkedIn Engineering — "Leveraging Dwel
AI (2025-08-11); Laura Lorenzetti, VP & Exec Editor (2026-05-19); Gyanda Sachdeva, VP
Product (2026-02-16); Matt Navarra relaying LinkedIn Sr. Director Product (Aug 2025);
Ordinal link-penalty study (900K, p<0.001); Socialinsider (1.3M); Buffer (2M+); Metricool
(673K); AuthoredUp (621K, NLP-quality-scored); van der Blom Algorithm Insights 2025 (1.8M);
(673K); AuthoredUp (621K, NLP-quality-scored); AuthoredUp post-length study (372,126 posts,
Sep 2025Feb 2026); van der Blom Algorithm Insights 2025 (1.8M);
Originality.ai (8,795 posts); Inc. (buzzword editorial). Full provenance: research brief
`docs/remediation/research/01-linkedin-algorithm-signals.md`.*

View file

@ -224,7 +224,7 @@ After 10 experiments, you know YOUR audience better than any generic advice can
- Month 1: You're learning mechanics (baseline)
- Month 3: You understand YOUR patterns (above average)
- Month 6: You're discovering insights from practice (exceptional)
- Month 12: You're systematically generating unique perspectives (thought leader)
- Month 12: You're systematically generating unique perspectives (recognized expert)
---

View file

@ -208,7 +208,7 @@ After establishing rapport:
**Time investment:** 3-5 hours (including creation and coordination)
**Reach:** Above baseline; deeper integration than a single co-post (no reliable multiplier)
**Benefit:** Creates reusable asset, positions both as thought leaders, deeper integration
**Benefit:** Creates reusable asset, positions both as authorities, deeper integration
**Execution:**
- 1-2 calls to align on framework

View file

@ -139,17 +139,17 @@ This structure creates positive response by opposing forces and compels the "see
**Hook (110-140 chars)**
→ Grab attention, create curiosity
**Context (200-300 chars)**
**Context (250-350 chars)**
→ Set up the situation/problem/observation
→ Why should they care?
→ What's at stake?
**Insight/Argument (400-800 chars)**
**Insight/Argument (550-850 chars)**
→ Your main point
→ Supporting evidence or logic
→ This is the "meat" of the post
**Implication (200-300 chars)**
**Implication (250-350 chars)**
→ What does this mean?
→ Why does it matter?
→ Connect to bigger picture
@ -158,6 +158,10 @@ This structure creates positive response by opposing forces and compels the "see
→ What should the reader do/think?
→ Engagement prompt
*The five components sum to 1,210-1,790 characters — inside the 1,200-1,800 standard
band, so a draft that respects each component band passes the length gate by
construction.*
### The Narrative Arc (For Story-Based Posts)
**Setup (200 chars)**
@ -183,48 +187,54 @@ This structure creates positive response by opposing forces and compels the "see
**CTA (50-100 chars)**
→ Engagement prompt
*Sum: 1,350-1,500 characters — inside the 1,200-1,800 standard band.*
### The Data-Driven Post (For Research/Statistics)
**Stat Hook (100 chars)**
**Stat Hook (110-140 chars)**
→ Lead with the surprising number
**Context (200 chars)**
**Context (250-350 chars)**
→ Where this data comes from
→ Why it matters
**Breakdown (500-700 chars)**
**Breakdown (550-850 chars)**
→ What the data actually means
→ Deeper analysis
→ Connect to reader's reality
**Action (200-300 chars)**
**Action (250-350 chars)**
→ What to do with this information
→ Practical takeaways
**CTA (50-100 chars)**
→ Engagement prompt
*Sum: 1,210-1,790 characters — inside the 1,200-1,800 standard band.*
### The Contrarian Post (For Challenging Norms)
**Bold Claim Hook (110 chars)**
**Bold Claim Hook (110-140 chars)**
→ State the contrarian position clearly
**Common Wisdom (200 chars)**
**Common Wisdom (250-350 chars)**
→ Acknowledge what "everyone" thinks
→ Show you understand the conventional view
**The Challenge (400-600 chars)**
**The Challenge (550-850 chars)**
→ Why the common wisdom fails
→ Evidence or logic for your position
→ Personal experience or data
**Alternative View (300-400 chars)**
**Alternative View (250-350 chars)**
→ What we should do instead
→ The better approach
**CTA (50-100 chars)**
→ Invite discussion/disagreement
*Sum: 1,210-1,790 characters — inside the 1,200-1,800 standard band.*
## Call-to-Action Frameworks
CTAs should encourage engagement while feeling natural, not forced.
@ -346,7 +356,7 @@ Not all engagement is equal. The defensible spine is the **order**, not a fixed
**Key insight:** One save or substantive comment is worth more than many reactions. Focus on content people want to save and share, and cultivate genuine substantive comments. See `references/algorithm-signals-reference.md` (cite, don't restate magnitudes).
### First Hour Critical
- Aim for 15+ engagements in first 60 minutes
- Keep the first 60 minutes active — strong early engagement unlocks broader distribution (directional; no published threshold, so measure against your own first hours rather than a fixed count)
- Respond quickly to early comments (a 30-minute response tends to earn more follow-up comments — figure unverified)
- Seed engagement by notifying key connections

View file

@ -13,11 +13,26 @@ External links in the post body correlate with lower reach (correlational, ~38%
- Lets you add context that didn't fit the post's character limit
- Creates a natural CTA without cluttering the main post
### What this file does *not* claim
The canonical reference rates the first-comment tactic **"neither a magic fix nor a
confirmed penalty — contested"**, at *low* confidence — practitioner heuristic, no large-N
primary source (`references/algorithm-signals-reference.md`). Nothing below upgrades that.
Specifically, this plugin does **not** assert:
- that a link in a comment carries a measured reach cost (no primary source either way)
- that author comments are auto-pinned or auto-promoted (they are not — pinning is manual)
- any percentage figure for what a first comment gains or costs you
Treat every timing and placement rule here as a hypothesis to test against your own
baseline, not a mechanic to optimize against. The one durable claim is the body-link
correlation itself, and even that is a correlation.
## Timing Strategy
### Immediate (within 60 seconds)
**Best for:** Link-sharing, resource lists, CTA
**Why:** Ensures the comment appears at the top before others comment. LinkedIn treats author comments as pinned by default when posted first.
**Why:** Being first gives the comment the best odds of sitting at the top before others arrive. **There is no automatic pinning** — LinkedIn documents pinning as an explicit action the post author takes from the comment's three-dot menu (LinkedIn Help, a524166), and default comment ordering is algorithmic ("Most relevant"), not chronological. If the comment must stay on top, pin it yourself.
### Delayed (15-30 minutes)
**Best for:** Engagement boost, conversation starter, hot take

View file

@ -103,7 +103,7 @@ The defensible **ordering** of engagement signals — **saves > shares > quality
**Used in:** `references/algorithm-signals-reference.md`, `references/engagement-frameworks.md`
### Engagement Velocity
Speed of engagement accumulation in the first hour after posting. 15+ engagements in the first hour unlocks Stage 3 distribution. Monitored at 5/15/30/60/90-minute intervals.
Speed of engagement accumulation in the first hour after posting. Strong early engagement unlocks broader distribution, but the effect is **directional — there is no published engagement count that flips it** (see `references/algorithm-signals-reference.md`, first-hour velocity). Monitored at 5/15/30/60/90-minute intervals.
**Used in:** `references/algorithm-signals-reference.md`, `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/audience-insights/engagement-patterns.md`

View file

@ -131,7 +131,7 @@ Before adding a visual, ask:
| Intermediate | Figma | Custom graphics, consistent templates | Free/$15/mo |
| Advanced | Adobe Illustrator | Complex infographics | $23/mo |
**Recommendation for thought leaders:** Canva or Figma with 2-3 reusable templates. Don't spend time on custom designs for every post.
**Recommendation for creators:** Canva or Figma with 2-3 reusable templates. Don't spend time on custom designs for every post.
## Brand Consistency Checklist

View file

@ -174,7 +174,7 @@ to a finished draft and call it gated).
| Step 4 — Consistency + quality | Primary enforcement: apply rules 16, calibrate per rule 7, report a pass/flag per rule. Rule 8 is verified as historical fact (skeleton existed + was gated). |
| Step 5 — Fact-check sweep | Fixes obey rule 6 (tighten, don't expand). |
| Step 6 — Persona sweep (pre-lock) | Rework obeys rule 6; the leader-takeaway (rule 1) and arc (rule 2) are what the reader jury judges for resonance. A draft that passed rule 8 typically lands here with far fewer spine-level reworks. |
| Step 9 — Hook / conversion gate | Hook revisions obey rule 6 (sharpen the krok by tightening, body stays locked). |
| Step 9 — Hook / conversion gate | Hook revisions obey rule 6 (sharpen the krok by tightening, body stays locked). The hook-forge is not an exception: every variant is cut to the same 110140 band, so choosing between kroks swaps rather than expands. |
## Self-certification boundary

View file

@ -6,18 +6,26 @@ LinkedIn Newsletters are a powerful tool for building an owned audience within L
## When to Launch Your Newsletter
### Minimum Thresholds
### Not a platform gate — a readiness judgment
- **5,000+ followers** (ensures viable initial subscriber base)
**LinkedIn imposes no follower minimum on individual members:** "All LinkedIn members have
access to create a newsletter on LinkedIn" (LinkedIn Help, *Newsletters on LinkedIn FAQ*,
a517914; company pages have separate access criteria). Everything below is an editorial
readiness bar, not a requirement — you *can* launch at any size, the question is whether
you should.
### Readiness signals
- **An audience large enough that a launch issue lands somewhere** — a few thousand
followers is a common working floor, but it is a heuristic, not a threshold
- **3+ months of consistent posting** (proven content discipline)
- **Clear topical authority** (algorithm recognizes your expertise)
- **Reliable content generation system** (can sustain weekly/biweekly publishing)
### Why Wait Until 5,000+
### Why size matters at all
- Newsletter notifications go to all subscribers (empty newsletters damage credibility)
- Every issue notifies every subscriber — thin editions damage credibility at any scale
- Lower subscriber counts reduce perceived authority
- Algorithm favors newsletters from established creators
- Need sufficient content library to repurpose effectively
### Red Flags You're Not Ready
@ -40,7 +48,11 @@ LinkedIn Newsletters are a powerful tool for building an owned audience within L
### LinkedIn Newsletters
- Direct notification to ALL subscribers (inbox + email notification)
- Direct notification to ALL subscribers — but **not necessarily by email**: LinkedIn
de-duplicates across channels, and "if you receive an in-app or push notification, you
should not expect to also receive an email for the same notification" (LinkedIn Help,
a517914). Treat the notification as the reliable channel and email as a bonus; do not
plan an edition around inbox delivery
- Owned audience (subscribers chose to be notified)
- **Ideal for:** Deeper dives, comprehensive frameworks, email list building, monetization
- Higher commitment from subscribers (they opted in)
@ -278,7 +290,7 @@ When someone engages deeply on a topic:
| Mistake | Fix |
|---------|-----|
| Launching too early | Wait until 5,000+ followers and consistent posting habit |
| Launching too early | Nothing on the platform stops you — wait for a consistent posting habit and an audience worth notifying |
| Inconsistent publishing | Choose sustainable frequency and never skip |
| Newsletter as dumping ground for post leftovers | Provide genuine additional value, not reposts |
| Over-promoting newsletter in every post | Subtle mentions, maximum 1 per 5-7 posts |
@ -289,4 +301,4 @@ When someone engages deeply on a topic:
## Bottom Line
Newsletters are powerful for building owned audience and deepening relationships, but only after establishing consistent posting and reaching 5,000+ followers. Quality and consistency matter more than frequency.
Newsletters are powerful for building owned audience and deepening relationships. LinkedIn lets any member start one — the real gate is editorial: a consistent posting habit, a clear topic, and enough content capacity to sustain the cadence. Quality and consistency matter more than frequency.

View file

@ -15,16 +15,46 @@ import { PROVENANCE_VALUES } from "./types.js";
import type { Provenance } from "./types.js";
/**
* Stable slug: lowercase, trim, every non-alphanumeric run a single `-`, with
* leading/trailing dashes stripped. Stable across case + whitespace variation of
* the same label, so it is a safe id key.
* Letters NFD cannot reach. Unicode decomposition turns `å`/`é`/`ñ` into a base
* letter plus a combining mark, but `ø`, `æ`, `ß` and friends are atomic code
* points with no decomposition they need an explicit mapping or they fall into
* the non-alphanumeric run and become a dash. Applied AFTER lowercasing, so only
* the lowercase forms are listed.
*/
const TRANSLITERATIONS: ReadonlyArray<readonly [RegExp, string]> = [
[/æ/g, "ae"],
[/ø/g, "o"],
[/œ/g, "oe"],
[/ß/g, "ss"],
[/[đð]/g, "d"],
[/þ/g, "th"],
[/ł/g, "l"],
];
/**
* Stable slug: lowercase, trim, transliterate, then every non-alphanumeric run
* a single `-`, with leading/trailing dashes stripped. Stable across case +
* whitespace variation of the same label, so it is a safe id key.
*
* Transliteration (N24.6) exists because the plain `[^a-z0-9]` collapse was lossy
* for every non-English label: "Løkkene" slugged to `l-kkene`, and worse `ø`
* and `å` both became `-`, so "møte" and "måte" collapsed onto ONE id. It narrows
* that collision class; it does not close it (`møte` and `mote` still meet, as
* they must for the case/whitespace stability above to hold).
*
* ORDER IS LOAD-BEARING: transliteration runs BEFORE the `[^a-z0-9]+` collapse, so
* the collapse stays the final gate and the output whitelist is unchanged. Anything
* downstream that rests on "a slug cannot contain a %-escape, a path or a URL"
* (the ingestion-guard exposure figures do) rests on that ordering.
*/
export function slugify(label: string): string {
return label
let s = label
.trim()
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
.normalize("NFD")
.replace(/\p{M}+/gu, "");
for (const [pattern, replacement] of TRANSLITERATIONS) s = s.replace(pattern, replacement);
return s.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
/**

View file

@ -29,14 +29,24 @@ function today(): string {
return new Date().toISOString().slice(0, 10);
}
/**
* The bundle-root index. `okf_version` sits in a FRONTMATTER block, not body text
* (OKF-form spec §6, v0.3): upstream carves exactly one exception to "index files
* contain no frontmatter", and it is enumerated to this single key. So nothing else
* rides along here no `type:`/`title:` (those belong to concept files), and
* `okf_layout` stays in body text per §12. Non-root index files carry no block at all.
*
* The value stays 0.1 this migration moves the marker, it does not bump it.
*/
function indexSeed(): string {
return `# Brain — Index (MOC)
return `---
okf_version: 0.1
---
# Brain Index (MOC)
> Map of Content one screen pointing at every tributary, with a freshness flag.
> Generated by \`brain init\`; safe to hand-edit (a re-run never clobbers it).
okf_version: 0.1
| Tributary | What it holds | Freshness |
|-----------|---------------|-----------|
| voice-samples | writing style | |

Binary file not shown.

View file

@ -15,8 +15,9 @@ import { parseProfile } from "../src/profile.js";
* `okr/scripts/okf-check.mjs`, the reference checker:
* - every CONCEPT file (`*.md` except `index.md`) carries a non-empty `type` in a
* leading YAML frontmatter block;
* - the bundle-root `index.md` carries an `okf_version` marker as markdown TEXT
* (index files carry no frontmatter per the OKF spec);
* - the bundle-root `index.md` carries an `okf_version` marker in its FRONTMATTER
* block upstream's single carved exception to "index files carry no
* frontmatter" (spec §6, v0.3; this was body text until N24.6);
* - each directory level has its own `index.md` (progressive disclosure).
*
* EXCLUDED by design (brief §6): the `ingest/` tributary. `ingest/published/*.md`
@ -68,10 +69,39 @@ describe("brain/ bundle is OKF-compatible form (Stage 1)", () => {
rmSync(root, { recursive: true, force: true });
});
test("bundle-root index.md carries an okf_version marker (markdown text, no frontmatter)", () => {
// N24.6 (OKF 0.3 §6): the marker moved from body text INTO the root index.md's
// frontmatter block — upstream's one carved exception to "index files contain no
// frontmatter". The two assertions this replaced were vacuous: `/^okf_version:/m`
// matches a frontmatter line as happily as a body line, and `frontmatterType()`
// reads `type:`, so it returns null with or without a block. Both went green on
// either placement, which is exactly the debt class N24.5 swept.
test("bundle-root index.md carries okf_version INSIDE the leading frontmatter block", () => {
const index = readFileSync(join(root, "brain/index.md"), "utf8");
assert.match(index, /^okf_version:\s*\S+/m, "root index.md declares okf_version");
assert.equal(frontmatterType(index), null, "index.md carries NO frontmatter (OKF reserved file)");
const block = index.match(/^---\n([\s\S]*?)\n---\n/);
assert.ok(block, "root index.md opens with a frontmatter block");
assert.match(block![1], /^okf_version:\s*\S+/m, "the block declares okf_version");
});
test("okf_version does NOT also sit in the body (one marker, one placement)", () => {
const index = readFileSync(join(root, "brain/index.md"), "utf8");
const body = index.replace(/^---\n[\s\S]*?\n---\n/, "");
assert.doesNotMatch(body, /^okf_version:/m, "no leftover body-text marker");
});
test("the root index frontmatter carries okf_version and nothing else", () => {
// Upstream's exception is enumerated to ONE key, so `okf_layout` (this
// convention's own extension marker, spec §12) stays in body text and no
// concept-style `type:`/`title:` may ride along in an index file.
const index = readFileSync(join(root, "brain/index.md"), "utf8");
const block = index.match(/^---\n([\s\S]*?)\n---\n/)![1];
const keys = block.split("\n").filter((l) => l.trim() !== "").map((l) => l.split(":")[0].trim());
assert.deepEqual(keys, ["okf_version"]);
assert.equal(frontmatterType(index), null, "an index file still carries no `type:`");
});
test("a non-root index.md carries no frontmatter at all", () => {
const journal = readFileSync(join(root, "brain/journal/index.md"), "utf8");
assert.doesNotMatch(journal, /^---\n/, "only the BUNDLE-ROOT index may carry a block");
});
test("every concept file under brain/ carries a non-empty frontmatter type", () => {

View file

@ -64,7 +64,7 @@ export function renderKilder(input: RenderKilderInput): string {
out.push(`# Kilder — ${title} (${articleId})`, "");
out.push(
"> Auto-generert fra edition-state `livedSpecifics` + specifics-bank. Per lastbærende påstand: hva backer den.",
"> Levd materiale er KTGs egne ord, aldri AI-oppdiktet (retning §3). Tall er uverifiserte til fakta-sjekket (regel 6/7).",
"> Levd materiale er operatørens egne ord, aldri AI-oppdiktet (retning §3). Tall er uverifiserte til fakta-sjekket (regel 6/7).",
"",
);
out.push("## Lastbærende påstander", "");

View file

@ -62,7 +62,7 @@ describe("renderKilder — the NN-kilder.md artifact", () => {
const expected = `# Kilder — Maskinrommet (05)
> Auto-generert fra edition-state \`livedSpecifics\` + specifics-bank. Per lastbærende påstand: hva backer den.
> Levd materiale er KTGs egne ord, aldri AI-oppdiktet (retning §3). Tall er uverifiserte til fakta-sjekket (regel 6/7).
> Levd materiale er operatørens egne ord, aldri AI-oppdiktet (retning §3). Tall er uverifiserte til fakta-sjekket (regel 6/7).
## Lastbærende påstander

View file

@ -780,7 +780,7 @@ if [ -x "$BR_DIR/node_modules/.bin/tsx" ]; then
BR_OUT=$( set +e; (cd "$BR_DIR" && npm test) 2>&1; echo "BR_EXIT:$?" )
BR_EXIT=$(echo "$BR_OUT" | grep -oE 'BR_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1)
BR_TESTS=$(echo "$BR_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1)
BRAIN_TESTS_FLOOR=127 # SB-S0 34 [id(11)+profile(6)+fold(12)+scaffold(5)] + SB-S1 29 [ingest(14)+publish(9)+cli(6)] + SB-S2 19 [consolidate(12)+consolidate-cli(7)] + SB-S3b 12 [consolidate(10)+consolidate-cli(2)] + SB-S3c 19 [ingest(4)+publish(3)+assemble(8)+cli(4)] + SB-S3d 1 [scaffold dated-anchor seed] + SB-S3e 13 [reconcile: parse(5)+tiers(7)+io(1)]
BRAIN_TESTS_FLOOR=142 # 127 decomposed: SB-S0 34 [id(11)+profile(6)+fold(12)+scaffold(5)] + SB-S1 29 [ingest(14)+publish(9)+cli(6)] + SB-S2 19 [consolidate(12)+consolidate-cli(7)] + SB-S3b 12 [consolidate(10)+consolidate-cli(2)] + SB-S3c 19 [ingest(4)+publish(3)+assemble(8)+cli(4)] + SB-S3d 1 [scaffold dated-anchor seed] + SB-S3e 13 [reconcile: parse(5)+tiers(7)+io(1)]; + 7 added between S3e and N24.6 without a floor bump (measured, not decomposed) + N24.6 8 [id: transliteration(3)+whitelist(1)+ascii-goldens(1), okf-conform: frontmatter placement(3)]
if [ "$BR_EXIT" = "0" ] && [ -n "$BR_TESTS" ] && [ "$BR_TESTS" -ge "$BRAIN_TESTS_FLOOR" ]; then
pass "brain suite green: $BR_TESTS tests pass (floor $BRAIN_TESTS_FLOOR)"
else
@ -2930,6 +2930,379 @@ fi
echo ""
# --- Section 16y: Positioning Brief - generation, consumption, operator gate (N23 / E#4) ---
echo "--- Positioning Brief: distillation + create-surface consumption + regeneration gate (N23) ---"
# A draft is only as anchored as the positioning it is written against. The brief is a
# DISTILLATE of the second brain, not a second copy of the declared profile, and three
# disciplines carry the whole feature - each of which fails silently if eroded:
# (E#4 separation) user-profile.md stays the operator's DECLARED identity; the brief is
# the evidence-weighted distillate. Two files restating the same claim
# is the N22 defect class (conflicting numbers across surfaces), so the
# brief must DEFER to the profile and to the operations anchor.
# (E#4 consumption) a brief nothing reads is a file, not an engine. The create surfaces
# and newsletter Step 1 must read it the way they read voice-samples.
# (E#4 gate) positioning is the operator's own. Regeneration is operator-gated:
# an explicit go, a visible diff, and never a silent overwrite. This is
# the check most likely to be "simplified" away, because a confirmation
# prompt reads as friction rather than the guarantee it is.
# (anti-drift) the brain-consumption rules stay in ONE file (strategy-advisor). The
# hosting command DELEGATES; it does not copy the block. Six copies of
# the standard skeleton (N21) is exactly what that copy would become.
BRIEF_PATH_N23="profile/positioning-brief.md"
SETUP_N23="commands/setup.md"
ADVISOR_N23="agents/strategy-advisor.md"
TMPL_N23="config/positioning-brief.template.md"
DIFFCHK_N23="agents/differentiation-checker.md"
NEWS_N23="commands/newsletter.md"
gate_honest() { # $1 = text; honest iff it REQUIRES a go, SHOWS the change, and FORBIDS a silent overwrite
# -qiF, not -qF: these are concepts in running prose, where sentence position decides the
# casing. Case-pinning would force the prose to fit the lint instead of the other way round.
echo "$1" | grep -qiF "explicit go" \
&& echo "$1" | grep -qiF "never overwrite" \
&& echo "$1" | grep -qiF "diff"
}
GATE_SELFTEST_N23=1
if ! gate_honest "regeneration requires an explicit go: show the diff first and never overwrite the existing brief without it"; then
GATE_SELFTEST_N23=0; echo " non-vacuity FAIL: a fully-gated regeneration probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if gate_honest "$probe"; then
GATE_SELFTEST_N23=0; echo " false-positive FAIL: under-specified gate probe accepted -> $probe"
fi
done <<'NEGATIVE16Y'
regenerate the brief from the brain and overwrite it with the new distillation
ask the operator before regenerating, then write the new brief over the old one
show the diff and regenerate on confirmation, replacing the previous brief
an explicit go is required before regenerating the positioning brief
NEGATIVE16Y
if [ "$GATE_SELFTEST_N23" -eq 1 ]; then
pass "regeneration-gate self-test: predicate needs explicit-go + diff + no-silent-overwrite (1 accepted, 4 under-specified rejected)"
else
fail "regeneration-gate self-test failed - the N23 gate lint is vacuous or over-eager"
fi
# (E#4 generation) the template exists and carries the four positioning questions
if [ -f "$TMPL_N23" ] \
&& grep -qF "## Who I am" "$TMPL_N23" 2>/dev/null \
&& grep -qF "## Who this is for" "$TMPL_N23" 2>/dev/null \
&& grep -qF "## What I hold" "$TMPL_N23" 2>/dev/null \
&& grep -qF "## How I differ" "$TMPL_N23" 2>/dev/null; then
pass "positioning-brief template carries all four positioning sections (E#4)"
else
fail "$TMPL_N23 missing or incomplete - the brief has no declared shape (E#4)"
fi
# (E#4 separation) the template states it is a distillate that defers, not a second profile
if grep -qF "does not restate" "$TMPL_N23" 2>/dev/null \
&& grep -qF "user-profile.md" "$TMPL_N23" 2>/dev/null; then
pass "template records that the brief defers to the declared profile instead of restating it"
else
fail "$TMPL_N23 does not separate distillate from declared profile - two sources of positioning truth (N22 class)"
fi
# (E#4 generation) setup hosts the generator as its own step and names the target path
if grep -qF "## Step 3g: Positioning Brief" "$SETUP_N23" 2>/dev/null \
&& grep -qF "$BRIEF_PATH_N23" "$SETUP_N23" 2>/dev/null; then
pass "setup Step 3g generates the brief to the per-user data dir (E#4)"
else
fail "$SETUP_N23 has no Step 3g writing $BRIEF_PATH_N23 - the generator has no surface (E#4)"
fi
# (E#4 generation) the step is reachable: the Step 2 menu offers it
if grep -qF "Positioning brief" "$SETUP_N23" 2>/dev/null \
&& grep -qF "always show all 8" "$SETUP_N23" 2>/dev/null; then
pass "setup Step 2 menu offers the positioning brief (8 options) - the step is reachable"
else
fail "$SETUP_N23 Step 2 menu does not offer the brief - Step 3g is unreachable (E#4)"
fi
# (anti-drift) setup DELEGATES the distillation instead of copying the brain-consumption block
if grep -qF "linkedin-studio:strategy-advisor" "$SETUP_N23" 2>/dev/null; then
pass "setup delegates the distillation to the namespaced strategy-advisor (no copied brain block)"
else
fail "$SETUP_N23 does not delegate to linkedin-studio:strategy-advisor - the brain rules will be copied (N21 class)"
fi
# (anti-drift) the brain-consumption rules stay in exactly one file
BRAINDOC_N23=$(grep -lF "Consuming the evolving profile" agents/*.md commands/*.md 2>/dev/null | wc -l | tr -d ' ')
if [ "$BRAINDOC_N23" = "1" ]; then
pass "brain-consumption rules live in exactly 1 file - no second copy to drift (SB-S3a)"
else
fail "brain-consumption rules found in $BRAINDOC_N23 files - a copy will drift from the original (N21 class)"
fi
# (E#4 generation) the advisor owns the distillation and writes the four sections
if grep -qF "Distilling the positioning brief" "$ADVISOR_N23" 2>/dev/null \
&& grep -qF "$BRIEF_PATH_N23" "$ADVISOR_N23" 2>/dev/null; then
pass "strategy-advisor owns the positioning distillation (reuses its brain readers)"
else
fail "$ADVISOR_N23 has no positioning-brief distillation - setup would have to reimplement it (E#4)"
fi
# (E#4 separation) the distillation defers to the operator's own anchor over an older inference
if grep -qF "Who I am now" "$ADVISOR_N23" 2>/dev/null \
&& grep -qiF "operator has declared is never overridden by a distilled inference" "$ADVISOR_N23" 2>/dev/null; then
pass "distillation subordinates itself to the operator's dated anchor (frozen-past-self guard extends to positioning)"
else
fail "$ADVISOR_N23 may let a machine distillate outrank the operator's own declaration (E#4)"
fi
# (E#4 gate) regeneration is operator-gated on BOTH surfaces that can trigger it
for f_n23 in "$SETUP_N23" "$ADVISOR_N23"; do
if gate_honest "$(cat "$f_n23" 2>/dev/null)"; then
pass "$(basename "$f_n23"): regeneration is gated (explicit go + diff + no silent overwrite)"
else
fail "$f_n23 may regenerate the brief without an explicit go, a diff, or without preserving the old one (E#4)"
fi
done
# (E#4 consumption) the create surfaces and newsletter Step 1 read the brief, traceably
for f_n23 in commands/post.md commands/quick.md commands/batch.md "$NEWS_N23" "$DIFFCHK_N23"; do
if grep -qF "$BRIEF_PATH_N23" "$f_n23" 2>/dev/null; then
pass "$(basename "$f_n23") reads the positioning brief (E#4 consumption)"
else
fail "$f_n23 does not read $BRIEF_PATH_N23 - the brief steers nothing there (E#4)"
fi
done
# (E#4 consumption) absence is graceful: a fresh install has no brain and no brief
if grep -qF "no brief yet" "$ADVISOR_N23" 2>/dev/null \
|| grep -qF "no brief yet" "$SETUP_N23" 2>/dev/null; then
pass "an absent brief degrades silently (fresh installs have no brain to distil)"
else
fail "absent-brief handling is undocumented - a fresh install would error or nag (E#4)"
fi
echo ""
# --- Section 16z: Promo-Post Engine - hook-forge + shareability gate (N24 / E#7, E#8) ---
echo "--- Promo-Post Engine: hook-forge + shareability gate (N24) ---"
# The edition's promo post is the acknowledgement engine: a peer forwarding it to a peer is the
# signal the edition was written to earn. Two things were missing at Step 9 and both fail QUIETLY:
# (E#7 forge) the shipped krok was the one the draft happened to end on - it never competed
# against an alternative. The forge produces five to eight variants, enforces the
# mobile cutoff PER VARIANT (measured, not estimated), spans at least three hook
# types so the choice is real, and traces every variant to the locked body so a
# variant cannot promise what the edition does not deliver.
# (E#7 honesty) the forge carries ONLY checkable marks - measured length, hook type, body-trace.
# A predicted-engagement score would be an invented composite, which is the exact
# defect class N22 removed (fabricated percentiles / Velocity Score). This check is
# the one most likely to erode, because a ranked list LOOKS more helpful.
# (E#8 deling) a click is one reader; a forward is the acknowledgement. The gate asks who
# forwards this and why, demands a named role and a sender-motive, and CANNOT be
# resolved by re-reading the same copy more generously (never a silent pass).
# (invariant) the forged variant goes THROUGH the N9 randsone gate, never around it - a forge
# that bypassed the fact/language pass would reopen the hole N9 closed.
# Both predicates are compound, so both carry a non-vacuity self-test (mirrors 16p/16y). All
# checks are UNCONDITIONAL .md greps -> they raise the deps-absent floor.
NL_N24="commands/newsletter.md"
PR_N24="agents/persona-reviewer.md"
STEP9_N24=$(awk '/^## Step 9:/{f=1} /^## Step 10:/{f=0} f' "$NL_N24")
forge_honest() { # $1 = text; honest iff it forges a SET, cuts on the band, and refuses an invented score
echo "$1" | grep -qiF "five to eight hook variants" \
&& echo "$1" | grep -qiF "disqualified, not ranked lower" \
&& echo "$1" | grep -qiF "no predicted-engagement score"
}
deling_gated() { # $1 = text; gated iff it asks the named-forwarder question, names the actions, and cannot self-resolve
# -qiF throughout: these are concepts in running prose, where sentence position decides the
# casing (N23 lesson - do not bend the prose to fit the lint).
echo "$1" | grep -qiF "who forwards this to a colleague, and why" \
&& echo "$1" | grep -qiF "RESHARE / SAVE / SEND" \
&& echo "$1" | grep -qiF "never a silent pass"
}
FORGE_SELFTEST_N24=1
if ! forge_honest "forge five to eight hook variants; a variant outside the band is disqualified, not ranked lower, and carries no predicted-engagement score"; then
FORGE_SELFTEST_N24=0; echo " non-vacuity FAIL: a fully-wired hook-forge probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if forge_honest "$probe"; then
FORGE_SELFTEST_N24=0; echo " false-positive FAIL: under-wired forge probe accepted -> $probe"
fi
done <<'NEGATIVE16Z1'
forge five to eight hook variants and rank them by predicted engagement, best first
five to eight hook variants, each disqualified, not ranked lower when out of band, scored 1-10 for reach
generate a few hook options and let the operator pick the strongest one
variants outside the band are disqualified, not ranked lower, and carry no predicted-engagement score
NEGATIVE16Z1
if [ "$FORGE_SELFTEST_N24" -eq 1 ]; then
pass "hook-forge self-test: predicate needs the variant SET + band-cutoff + no invented score (1 accepted, 4 under-wired rejected)"
else
fail "hook-forge self-test failed - the N24 forge lint is vacuous or over-eager"
fi
DELING_SELFTEST_N24=1
if ! deling_gated "the gate asks who forwards this to a colleague, and why, records RESHARE / SAVE / SEND, and allows never a silent pass"; then
DELING_SELFTEST_N24=0; echo " non-vacuity FAIL: a fully-gated shareability probe was not detected"
fi
while IFS= read -r probe; do
[ -z "$probe" ] && continue
if deling_gated "$probe"; then
DELING_SELFTEST_N24=0; echo " false-positive FAIL: under-gated deling probe accepted -> $probe"
fi
done <<'NEGATIVE16Z2'
ask who forwards this to a colleague, and why, then record RESHARE / SAVE / SEND and move on
judge whether the post is shareable and saveable; never a silent pass
who forwards this to a colleague, and why - answer it, and never a silent pass
NEGATIVE16Z2
if [ "$DELING_SELFTEST_N24" -eq 1 ]; then
pass "shareability-gate self-test: predicate needs the named-forwarder question + the three actions + no silent pass (1 accepted, 3 under-gated rejected)"
else
fail "shareability-gate self-test failed - the N24 deling lint is vacuous or over-eager"
fi
# (E#7 forge) Step 9 forges a variant SET and cuts on the band instead of inventing a score
if forge_honest "$STEP9_N24"; then
pass "newsletter.md Step 9 forges five to eight variants, cuts out-of-band, and invents no score (E#7)"
else
fail "newsletter.md Step 9 has no honest hook-forge - the krok still ships unchallenged, or is ranked by an invented number (E#7)"
fi
# (E#7 measurement) the band is MEASURED per variant, with the multibyte-safe counter
if echo "$STEP9_N24" | grep -qF "perl -CSD" \
&& echo "$STEP9_N24" | grep -qiF "enforced per variant, never on the set"; then
pass "Step 9 measures the hook band per variant with the multibyte-safe counter (awk length lies on ae/oe/aa)"
else
fail "Step 9 does not measure the band per variant - byte-counting over-reports Norwegian hooks (E#7)"
fi
# (E#7 real choice) the surviving set must span hook types, or the forge produced rewordings
if echo "$STEP9_N24" | grep -qiF "at least three distinct hook types"; then
pass "Step 9 requires the surviving variants to span at least three hook types (a real choice, not rewordings)"
else
fail "Step 9 does not require type-spanning variants - eight rewordings of one idea is not a choice (E#7)"
fi
# (E#7 no bait) every variant traces to a claim the LOCKED body makes
if echo "$STEP9_N24" | grep -qiF "a claim the locked body actually makes"; then
pass "Step 9 disqualifies a variant that promises what the locked body does not deliver (no bait krok)"
else
fail "Step 9 lets a hook variant promise what the edition never delivers - bait reaches the feed (E#7)"
fi
# (invariant) the chosen variant is gated, not exempted - it re-enters the N9 randsone pass
if echo "$STEP9_N24" | grep -qiF "through the randsone gate, never around it"; then
pass "Step 9 routes the chosen forge variant through the randsone gate (N9 hole stays closed)"
else
fail "Step 9 does not route forged variants through the randsone gate - a forged hook could reach the feed ungated (N9)"
fi
# (E#8 deling) the shareability gate is present at Step 9, with the whole predicate
if deling_gated "$STEP9_N24"; then
pass "newsletter.md Step 9 runs the shareability gate: named forwarder + RESHARE/SAVE/SEND + no silent pass (E#8)"
else
fail "newsletter.md Step 9 missing the shareability gate - nobody asks who would forward the edition (E#8)"
fi
# (E#8 escape hatch) a twice-failed reframe becomes an OPERATOR decision, never a lowered bar
if echo "$STEP9_N24" | grep -qiF "body-level finding" \
&& echo "$STEP9_N24" | grep -qF "/linkedin:pivot"; then
pass "Step 9 routes an unshareable edition to an operator decision (pivot or recorded fold-in), not to a softer read"
else
fail "Step 9 has no honest exit for an unshareable edition - the gate would resolve itself by lowering the bar (E#8)"
fi
# (E#8 persistence) both new results are persisted where the resumption protocol can see them
if echo "$STEP9_N24" | grep -qF "articles.NN.hookForge" \
&& echo "$STEP9_N24" | grep -qF "articles.NN.personaSweep.deling"; then
pass "Step 9 persists the forge and the share verdict to edition-state (resumable, auditable)"
else
fail "Step 9 does not persist hookForge / personaSweep.deling - the gate leaves no trace (E#7/E#8)"
fi
# (E#8 agent contract) konverter-modus carries the SECOND binary, with role + motive + action
if grep -qiF "who forwards this to a colleague, and why" "$PR_N24" 2>/dev/null \
&& grep -qiF "named forwarder role" "$PR_N24" 2>/dev/null \
&& grep -qiF "RESHARE / SAVE / SEND" "$PR_N24" 2>/dev/null; then
pass "persona-reviewer konverter-modus returns the deling binary with a named role and an action (E#8)"
else
fail "$PR_N24 konverter-modus has no share verdict - Step 9's gate has no judge (E#8)"
fi
# (E#8 anti-flattery) a manufactured forwarder is the false green this gate exists to prevent
if grep -qiF "A generic answer is not an answer" "$PR_N24" 2>/dev/null; then
pass "persona-reviewer refuses a generic forwarder ('anyone in tech' names nobody)"
else
fail "$PR_N24 accepts a generic answer to who-forwards-this - the gate would pass on flattery (E#8)"
fi
# (E#7 role separation) the jury judges the chosen krok; the forge stays the editor's pen
if grep -qiF "the forge is the editor's pen" "$PR_N24" 2>/dev/null; then
pass "persona-reviewer does not write or rank the forge variants (the jury judges, the editor writes)"
else
fail "$PR_N24 may rank or rewrite forge variants - that breaks the jury/editor separation (E#7)"
fi
echo ""
# --- Section 16aa: Debt-Sweep Anti-Erosion (N24.5) ---
echo "--- Debt-Sweep Anti-Erosion (N24.5) ---"
# Two defects that a one-time fix does NOT make terminal, because nothing watches them:
# (a) CHANGELOG duplicate version heading. A duplicated "## [x.y.z]" makes the changelog
# ambiguous about which entry a version actually is, and it was rediscovered SIX times
# before this lint existed - each time fixed by hand, each time silently reintroduced by
# the next release edit. Structural, so a grep settles it: no version number may head two
# sections. A deliberately retained historical entry keeps a distinct heading (the
# "-legacy" suffix), which is exactly what makes it visible instead of ambiguous.
# (b) Step 9 persisting state keys the schema template never declares. The template IS the
# schema-of-record an adopter copies; a key that only exists because a command writes it
# is undeclared state, and the drift is invisible until someone reads both files side by
# side. N9 set the precedent (randsoneGate), N24 repeated it twice (hookForge,
# personaSweep.deling) - the pattern recurs precisely because the additive keys are
# backward-compatible and therefore cost nothing to forget.
# The (a) predicate carries a non-vacuity self-test (mirrors 16p/16y/16z): a duplicate-detector
# that never fires would pass a duplicated changelog forever. All checks are UNCONDITIONAL.
dup_version_headings() { # reads stdin; echoes each version number that heads more than one section
grep -oE '^## \[[0-9]+\.[0-9]+\.[0-9]+\]' | sort | uniq -d
}
# Self-test (in-memory, no temp files): the detector must fire on a known duplicate and stay
# silent on distinct headings - including the "-legacy" form used to disambiguate a kept entry.
SELFTEST_DUP=$(printf '## [1.2.0] - 2026-01-01\n## [1.1.0] - 2025-12-01\n## [1.2.0] - 2025-06-01\n' | dup_version_headings)
SELFTEST_CLEAN=$(printf '## [1.2.0] - 2026-01-01\n## [1.2.0-legacy] - 2025-06-01\n## [1.1.0] - 2025-12-01\n' | dup_version_headings)
if [ -n "$SELFTEST_DUP" ] && [ -z "$SELFTEST_CLEAN" ]; then
pass "duplicate-heading self-test: repeated version caught, distinct -legacy heading ignored"
else
fail "duplicate-heading self-test failed - the detector no longer distinguishes a duplicate from a distinct heading"
fi
CHANGELOG_DUPS=$(dup_version_headings < CHANGELOG.md)
if [ -z "$CHANGELOG_DUPS" ]; then
pass "CHANGELOG has no duplicated version heading (each '## [x.y.z]' heads exactly one section)"
else
fail "CHANGELOG version heading(s) duplicated - rename the historical entry (e.g. '-legacy') so the version is unambiguous:"
echo "$CHANGELOG_DUPS"
fi
# (b) every key Step 9 persists must be declared in the schema template - shape AND _doc.
if python3 - <<'PYEOF' 2>/dev/null
import json, sys
d = json.load(open("config/edition-state.template.json"))
art = d["articles"]["01"]
doc = d["_doc"]
missing = [k for k in ("hookForge", "randsoneGate") if k not in art]
missing += ["personaSweep.deling"] if "deling" not in art.get("personaSweep", {}) else []
missing += [f"_doc.{k}" for k in ("hookForge", "randsoneGate", "personaSweep") if k not in doc]
sys.exit(1 if missing else 0)
PYEOF
then
pass "edition-state template declares every Step 9 key (hookForge, randsoneGate, personaSweep.deling) in shape + _doc"
else
fail "config/edition-state.template.json is missing a key Step 9 persists - undeclared state drifts from the schema-of-record"
fi
echo ""
# --- Section 18: Assertion-Count Anti-Erosion (SC6) ---
# The lint self-modifies its own checks, so a green run could mask a silently dropped
# assertion. Pin the total pass()+fail() invocations as a monotonic floor; the count
@ -3010,12 +3383,21 @@ echo ""
# wiring grep + always-present/refusal-included grep + report leads-with-baseline/no-eyeball
# compound grep + normal-variation-not-trend grep + report refusal-honest compound grep +
# WoW-loses-to-baseline grep + analyze verify-before-diagnose grep + analyze never-upgrade-
# refusal grep) = 251.
# refusal grep) = 251; +17 for N23's seventeen UNCONDITIONAL Section-16y checks (regeneration-gate
# self-test + template four-section grep + template distillate/defers grep + setup Step-3g grep +
# setup menu-reachability grep + setup delegation grep + brain-block one-file count + advisor
# distillation grep + advisor declared-over-inferred grep + two per-surface gate greps + five
# consumption greps + absent-brief graceful grep) = 268; +13 for N24's thirteen UNCONDITIONAL
# Section-16z checks (hook-forge self-test + shareability-gate self-test + Step-9 forge_honest
# compound grep + Step-9 per-variant measurement grep + Step-9 type-span grep + Step-9 body-trace
# grep + Step-9 randsone-routing grep + Step-9 deling_gated compound grep + Step-9 operator-exit
# grep + Step-9 persistence grep + persona-reviewer deling-binary compound grep + persona-reviewer
# generic-answer refusal grep + persona-reviewer forge/editor separation grep) = 281.
# NB: the floor tracks the deps-absent MINIMUM (conditional TS suites warn-skip and drop
# the count), so it is bumped only by UNCONDITIONAL new checks — NOT pinned to the
# deps-present TOTAL_CHECKS (that would zero the warn-skip margin and false-fail a fresh
# clone). Runs last so TOTAL_CHECKS sees every prior check.
ASSERT_BASELINE_FLOOR=251
ASSERT_BASELINE_FLOOR=284
TOTAL_CHECKS=$((PASS + FAIL))
if [ "$TOTAL_CHECKS" -ge "$ASSERT_BASELINE_FLOOR" ]; then
pass "assertion-count anti-erosion: $TOTAL_CHECKS checks >= baseline floor $ASSERT_BASELINE_FLOOR"

View file

@ -148,9 +148,56 @@ line `<ISO-ts> exit=<code> <json>` to `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/lin
The nightly run is **deterministic-brief-only (C1)**: it re-renders the brief from the current store
— freshness-aging drops stale trends, `surfacedCount` accumulates day-over-day — but does **not** poll
new sources. A double-fire on the same day is a safe no-op (RE-R3b per-day idempotency: byte-identical
`.md`, `surfacedCount` not double-counted). The autonomous AI capture step (poll → score → capture
before the brief) plugs into the documented seam in `run-daily.sh` as a later slice (e); a
brief-history diff is also a later slice.
`.md`, `surfacedCount` not double-counted). This wrapper is deliberately unaware of the AI capture
step — that is a **different mechanism**, below (RE-R3f).
## Unattended AI discovery — `--headless` (RE-R3f, slice e)
The gap `run-daily.sh` leaves open on purpose: without new captures, the nightly brief is a
near-no-op. Slice (e) closes it by making the **actual discovery pass**`/linkedin:trends`'s
poll → score → capture → brief loop, including the trend-spotter agent's web research — safe to
fire with no operator present, so Sunday discovery can happen unattended.
**This is not a bash/cron wrapper.** `/linkedin:trends --headless` needs a full Claude Code agent
turn (it spawns the `trend-spotter` subagent), which only the `claude` binary itself can run — a
shell script cannot replicate that. Two mechanisms exist for firing it unattended:
- **Claude Code's Desktop Scheduled Tasks (recommended).** A first-party, local, persistent
scheduler purpose-built for exactly this — unlike a hand-rolled cron job, it survives restarts,
keeps session history, and handles auth without a standing `ANTHROPIC_API_KEY`. Set it up to run
`/linkedin:trends --headless [--mode long-form]` on your Sunday cadence.
- **A hand-rolled `claude -p` invocation** (e.g. from cron/launchd directly), for a machine where
Desktop Scheduled Tasks isn't available. The exact recipe, verified against Claude Code's own
headless docs:
```bash
claude -p "/linkedin:trends --headless" \
--bare --plugin-dir "<path to the installed linkedin-studio plugin>" \
--permission-mode dontAsk \
--allowedTools "Read,Bash,Task,WebSearch,WebFetch" \
--output-format json
```
`--bare` skips plugin auto-discovery for a faster/cleaner scripted start, so `--plugin-dir` is
required alongside it. `--bare` also means no OAuth/keychain session — set `ANTHROPIC_API_KEY` in
the environment. `--permission-mode dontAsk` auto-denies anything not in `--allowedTools`
(including `AskUserQuestion`, unconditionally) instead of blocking on a prompt — the command's own
**Step 0.5 contract** (`commands/trends.md`) is what makes that safe rather than a silent abort.
If the research subagent can run long, raise `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` (default
10-minute wait ceiling on a background subagent) or set it to `0`.
**Do not trust the exit code or the model's prose as the success signal** — Claude Code's own docs
are explicit that a `-p` run's process exit reflects the CLI process, not the pass's semantic
outcome. Verify the pass actually did something the same way Step 3 of the command already does:
`CLI status --json`'s capture delta and whether today's `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/
linkedin-studio}/trends/morning-brief/<date>.md` exists. `--output-format json` also reports
`total_cost_usd` per run — log it if you care about the standing cost of a weekly unattended
research pass.
**Scope note:** RE-R3f adds no new source/test files here — the entire mechanism is the
`commands/trends.md` `--headless` contract (a prompt-file change) plus this documentation. No
`schedule.ts`/`run-daily.sh` change; that wrapper stays what RE-R3c built (deterministic brief-only,
above), and remains a separate, valid path for a plain daily re-render with no new discovery.
## Temporal overlay (RE-R3d)

View file

@ -108,9 +108,9 @@ Read `references/engagement-frameworks.md` for hook types, story structures, and
**Standard Post (1,200-1,800 chars):**
1. **Hook (110-140 chars):** Grab attention
2. **Context (200-300 chars):** Set up why this matters
3. **Insight/Argument (400-800 chars):** Main point with evidence
4. **Implication (200-300 chars):** What this means for readers
2. **Context (250-350 chars):** Set up why this matters
3. **Insight/Argument (550-850 chars):** Main point with evidence
4. **Implication (250-350 chars):** What this means for readers
5. **CTA (50-100 chars):** Engagement prompt
**Critical formatting rules:**

View file

@ -6,7 +6,7 @@ description: |
first-hour engagement tactics, collaboration formats, and speaking pipeline management.
This skill should be used when the user wants to build their network, find speaking opportunities, collaborate
with other thought leaders, improve their commenting strategy, or optimize their
with other creators in their niche, improve their commenting strategy, or optimize their
engagement routine.
Triggers on: "speaking opportunities", "conference speaking", "collaboration", "co-author",
@ -67,7 +67,7 @@ Engagement isn't what you do after posting -- it's what enables successful posti
- Continue responding to all comments
- Ask follow-up questions to deepen conversation
**What 15+ engagements in first hour looks like:**
**What an active first hour can look like (illustrative mix, not a threshold):**
- 8-10 thoughtful comments
- 3-5 shares
- 2-3 profile visits with connection requests
@ -182,7 +182,7 @@ Before pursuing speaking, ensure:
4. Generate talk abstracts and bio
5. Build outreach templates
**User: "I want to collaborate with other thought leaders"**
**User: "I want to collaborate with other creators in my niche"**
1. Identify potential partners with scoring
2. Suggest appropriate format for relationship stage
3. Generate outreach messages