linkedin-studio/CLAUDE.md
Kjell Tore Guttormsen 63506f7d5c feat(linkedin-studio): N16 — out-of-network-andel + patterns-oppdatering + boundary-map [skip-docs]
Reach-splitten (in/out-of-network) er native i LinkedIns post-analytics siden juni
2026, men vises som PROSENT og finnes ikke i CSV-eksporten. Planen antok to
manuelle antall; verifiseringen viste prosent, så modellen er ett felt —
outOfNetworkPct — og in-network er komplementet.

- parseOptionalPercent: egen parser, ikke parseOptionalCount. Komma er desimal
  (36,5 -> 36.5, aldri 365), og verdi >100 avvises: i én kolonne kan ikke et
  absolutt antall skilles fra en andel, så svaret er unknown, ikke en gjetning.
  Blank/ikke-numerisk/negativ -> unknown; ekte 0 beholdes.
- Ett lagret halvpart, kryssjekket: In-network godtas og lagres som komplement;
  et transkribert par som ikke summerer til ~100 (±1 avrunding) forkastes som
  unknown i stedet for å bli halvveis trodd.
- weightedOutOfNetworkPct: impressions-vektet roll-up (avgOutOfNetworkPct, uke +
  måned). Flatt snitt lar en 50-visnings-post slå en på 10 000; poster uten
  avlesning ekskluderes, og null vekt gir undefined — aldri 0, aldri NaN.
- Reach inngår ALDRI i engagementRate (distribusjon != engasjement). Rapporten
  leser den som akvisisjon (ut) vs resonans (inn), og sier «ikke ført for denne
  perioden» framfor å estimere. En reach-innsikt går inn i N15s do-next-kanal.
- Step 7c (A2-F11): rapporten tilbyr diff mot brukerens engagement-patterns.md
  med eksplisitt go — aldri stille skriving, aldri inn i den shippede malen.
- Boundary-map (E#9): dwell eksplisitt umålbar, saves partner-gated, reach
  native men CSV-eksport uverifisert.
- Reach-frie importer er byte-identiske med før, på skjerm og på disk.

TDD: rødt bevist først (10 feilende), analytics 119 -> 144 tester, tsc ren.
test-runner 232 -> 247 (Section 16w, gulv 213 -> 228). Alle suiter grønne.
CHANGELOG: N15-oppføringen manglet og er backfilt sammen med N16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QxvWAjte7vPcF79QeSRvRJ
2026-07-25 15:56:04 +02:00

17 KiB
Raw Blame History

LinkedIn Studio Plugin (v0.7.0)

Full-spectrum LinkedIn content engine — short-form feed posts, carousels, video scripts, and long-form newsletter editions — with the 2026 relevance-ranking model baked in. Maturity v0.7.0: M0 (per-user data dir ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/, idempotent session-start migration — see references/data-path-convention.md) complete; the 29 pre-0.7.0 command surfaces all through the interactive quality-gate (docs/hardening/log.md) AND through independent cold-review (29/29, R2aR5 — docs/hardening/review*.md); /linkedin:trends (shipped in 0.7.0) is not yet through the hardening gate. Remaining for v1.0.0: a GUI. Version history → CHANGELOG.md.

Architecture

  • State file: ~/.claude/linkedin-studio.local.md (YAML frontmatter, auto-initialized from config/state-file.template.md)
  • State updater: hooks/scripts/state-updater.mjs — deterministic state mutations (post tracking, streak, content history pruning). Pure functions, tested, no AI involvement
  • Clipboard helper: hooks/scripts/clipboard-helper.mjs — cross-platform clipboard (macOS pbcopy, Linux xclip/xsel, WSL clip.exe). All content commands auto-copy
  • iCal generator: hooks/scripts/ical-generator.mjs — RFC 5545 calendar files for batch scheduling. Standalone CLI + importable module
  • Figure renderer: render/build-figur.mjs — coded data figures (SVG/HTML → PNG via headless Chrome); three targets (article/carousel/single); brand tokens from the user data dir's profile/brand-tokens.json, neutral defaults otherwise (see references/figure-design-guidelines.md). Standalone CLI + importable module
  • 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-usageusedIn), so cross-edition re-use of the same material is visible rather than silently encouraged by the dedupe
  • 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
  • Contract-gate: scripts/contract-gate/ (TypeScript, needs tsx + npm install) — deterministic §B/§C1 rule-gate on the full draft (/linkedin:newsletter Step 4.5, before the AI sweeps); ratifies rules.ts against the edition's §E-manifest, then gates with BLOCK/WARN
  • Fold-in loop: capture → classify → promote → enforce, documented domain-generally in references/fold-in-loop.md. The four /linkedin:newsletter steps where operator judgment enters the text (2.5, 3a, 5.5, 6.5) append corrections to articles.NN.foldIns[] (decision: "pending"); Step 11 (retro) empties the queue with an explicit operator JA/NEI — mechanical → an atomic contract-gate promotion (only counts when --ratify is green), judgment → the operator's opt-in language-rules file (${DATA}/language-rules/<lang>.md, template config/language-rules.template.md), which Step 4 reads while drafting (prevent) and language-reviewer reads at Step 6.5 (catch). Rejected rows are kept, never deleted. Step 11 also reports effort from the measured phaseLog and asks one friction question whose answer goes back to the operator — the plugin never writes their register
  • Do-next contract: recordDoNext() in hooks/scripts/state-updater.mjs — the one channel from measurement to creation. /linkedin:report, /linkedin:analyze, an /linkedin:ab-test Adopt verdict and the post-feedback-monitor's 48h learning each persist 23 directives (imperative + evidence pointer, tagged with their source) to ## Do-Next Directives in the state file; the create surfaces (post/quick/batch/create Step 0, newsletter Step 1) read them before drafting, so a recommendation changes the next piece instead of dying in chat. Lifetime is replace-by-source + a 60-day age floor: each writer supersedes only its own rows, so the section is a live steering signal, never a backlog. newsletter Step 1 additionally calibrates on the previous edition's measured numbers via the queue-id match (drafts/queue.jsonanalytics/posts/*.json by publish date), with an explicit honest-miss branch — an unmeasured edition is stated as such, never estimated
  • Reach split (N16): the in-network/out-of-network share is native in post analytics (Discovery, under impressions; global rollout from June 2026) but shown as percentages and absent from the CSV (export status unverified) — so it follows the saves pattern as an optional manual column. parseOptionalPercent() is a separate parser, not parseOptionalCount: a comma is a decimal mark (36,5 → 36.5, never 365) and a value >100 is refused (a count and a share are indistinguishable in one column). Only the out-of-network half is stored (outOfNetworkPct) — in-network is its complement, and a transcribed pair that does not sum to ~100 (±1 rounding) is discarded as unknown rather than half-trusted. Roll-ups (avgOutOfNetworkPct, weekly + monthly) are impressions-weighted via weightedOutOfNetworkPct() and stay undefined on zero weight — never 0, never NaN. Reach is not in engagementRate (distribution ≠ engagement); reports read it as acquisition (out) vs resonance (in), and an unentered split is stated as such, never estimated. /linkedin:report Step 7c additionally offers an operator-gated diff against the per-user audience-insights/engagement-patterns.md baseline (A2-F11 — explicit go, never a silent write, never into the shipped template)
  • Analytics metrics (S16): parsed CSV columns + an optional, manually-entered saves count (count-only in native LinkedIn analytics since ~Sept 2025, no CSV export; the Marketing API exposes POST_SAVE on /memberCreatorPostAnalytics from v202604, but access is partner-gated — so manual entry remains the right UX). parseOptionalCount(): blank / non-numeric / negative → undefined (unknown, never 0), a genuine 0 is kept; saves surfaced per-post + as totalSaves, but not folded into engagementRate. dwell stays explicitly unmeasurable (internal to LinkedIn, no export/API). All analytics I/O routes through the getAnalyticsRoot() seam (M0 per-user data-dir).

Hooks

9 hooks across 7 events. All Node.js (.mjs). PreToolUse/PostToolUse hooks use parameterized content-gatekeeper.mjs with isLinkedInContent() check.

Event Purpose
SessionStart Load state, REMEMBER.md, milestone tracker, editions in flight + publishing-slot vacancy
PreToolUse (Write|Edit) Content quality gate, voice guardian, topic rotation gate
Stop State update, pre-publish reminders, content history
UserPromptSubmit LinkedIn context enrichment (three-tier matching)
PostToolUse (Write) Post-creation automation (5x5x5, posting time)
PreCompact Preserve LinkedIn context during compaction
Notification Posting reminders (rate-limited 30min)

Session markers: /tmp/linkedin-hooks/session-active (Stop hook gating, 12h staleness). Shared modules: linkedin-content-filter.mjs, queue-manager.mjs, personalization-score.mjs, state-updater.mjs, clipboard-helper.mjs, slots.mjs.

State updates: post tracking, streak, and content history are handled deterministically by state-updater.mjs (Stop hook + commands); entries older than 90 days are auto-pruned at session start.

Hook editing: edit hooks/hooks.template.json + hooks/prompts/*.md, then run python3 hooks/scripts/compile-hooks.py. Do not edit hooks.json directly. Prompts load at runtime via gatekeeper scripts; the compile step is only needed when adding type: prompt hooks.

Commands (30)

All content commands auto-copy output to clipboard via clipboard-helper.mjs; interactive steps are minimized (angle/format/post-type inferred from context, max 2 questions per post). Organized into five journeys (Start · Create · Engage · Measure · Grow): create/measure are guided creation/analytics front-doors, onboarding/strategy the Start/Grow front-doors, and the 28 atomic commands are the execution tier.

Command Purpose
/linkedin Router — status line + five-journey command menu
/linkedin:create Create front-door — routes to the command owning the format (post/quick/react/carousel/video/multiplatform/batch/newsletter); delegates only
/linkedin:measure Measure front-door — routes to the analytics command (import/report/analyze/audit/ab-test); delegates only
/linkedin:onboarding Multi-step onboarding wizard (profile → setup → first-post); Start-journey front-door
/linkedin:first-post First-post accelerator (10 min)
/linkedin:setup Guided personalization setup
/linkedin:react URL-to-post pipeline
/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: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)
/linkedin:batch Create a full week of content
/linkedin:calendar View/manage post scheduling queue + publish action
/linkedin:firsthour Post-publish first-hour / reply-loop sprint — delegates to engagement-coach, persists the plan (recordFirstHourPlan), hands off to post-feedback-monitor
/linkedin:carousel Structured multi-slide carousel generator
/linkedin:video Video script generator (30s-2min)
/linkedin:multiplatform Adapt content for other platforms (long-form → /linkedin:newsletter)
/linkedin:analyze Content/performance analysis
/linkedin:audit Periodic content strategy audit
/linkedin:import Import CSV export → structured JSON (delegates analysis to /linkedin:report)
/linkedin:report Generate weekly performance report
/linkedin:ab-test Design and manage A/B content tests
/linkedin:strategy Growth strategy + authority building; Grow-journey front-door
/linkedin:competitive Competitive analysis of niche
/linkedin:monetize Monetization strategy and funnels (unlocks at ~1K followers)
/linkedin:outreach Outreach orchestrator — collaborations + speaking (unlocks at ~1K followers)
/linkedin:profile Profile/topic-relevance optimization

Agents (20)

Agent Model Color Responsibility
content-optimizer Sonnet Blue Optimize existing posts
strategy-advisor Sonnet Green Growth recommendations
analytics-interpreter Sonnet Yellow Audience pattern analysis + weekly/monthly performance reports (interpret/report modes)
engagement-coach Sonnet Magenta 5x5x5 + first-hour tactics + CEA commenting + target selection
content-planner Sonnet Cyan Content audit + weekly/monthly plans
network-builder Sonnet Teal Strategic networking + outreach
content-repurposer Sonnet Purple Format conversion + evergreen refresh
trend-spotter (inherits session) White Trending topics + opportunity scores
demand-spotter (inherits session) Silver Demand-sweep «innenfra og ut» (N7.5): reader's verbatim questions (Tier-5 sources) → pain-point map → vocabulary translation → §4 arc map with honest market verdict (supply-gap/demand-gap/saturated)
voice-trainer Sonnet Pink Voice profile building + drift detection
differentiation-checker Sonnet Gray Originality scoring + commodity detection
post-feedback-monitor Opus Lime Post-publish 48h monitoring
video-scripter Sonnet Violet Video script creation with pacing
fact-checker Opus Brown Factual-claim verification against primary sources + post-cutoff web-search mandate (longform)
editorial-reviewer Opus Orange Editor's craft gate (Step 5.5, before persona sweep): prosa-håndverk + narrativ-arkitektur, ≤10 flags BLOCK/REWORK/NICE, operator-gated via SendUserFile (longform)
persona-reviewer Opus Olive Reader-persona gate — skeleton (before prose) + resonance (before lock) + hook-conversion (after lock), blocking hard-fail list (longform)
voice-scrubber Opus Red De-AI scrub + Norwegian-chronicle voice-drift correction (longform)
content-reviewer Opus Maroon Cold/headless argument-integrity review on a frozen draft (Step 6.5): logical holes, unsupported assumptions, contradiction, missing concretization, unanswered objection; ≤8 flags (longform)
language-reviewer Opus Navy Cold/headless Norwegian-language review on a frozen draft (Step 6.5): repetition, anglicisms, stiff register, self-contradiction, rhythm; ≤10 flags (longform)
fact-reviewer Opus Gold Cold/headless re-verification on the frozen/pivoted version (Step 6.5, web search): verifiable claims, quote precision, number attribution, source quality + pivot-risk (longform)

Rule: Always read assets/voice-samples/ before generating content.

Invocation form: commands invoke plugin agents by their namespaced type — subagent_type: linkedin-studio:<name>, never the bare <name> (a bare type does not resolve and the Task call fails).

Reload requirement: a NEW agent file under agents/ registers only after a Claude Code session reload (the plugin agent set is built at session start). Reload before invoking a freshly-added agent.

Content Quality Rules

  1. Hook: 110-140 characters (mobile cutoff)
  2. Post length: 1,200-1,800 chars (standard), 150-500 chars (quick)
  3. No external links in post body (correlate with lower reach; see references/algorithm-signals-reference.md)
  4. No corporate buzzwords: leverage, synergy, paradigm shift, thought leader, disruptive, value proposition, ecosystem, holistic approach
  5. Topic must align with user's 5 core expertise areas (topic-relevance signal)
  6. Topic rotation: no back-to-back same pillar, no pillar >50% in 14 days (warn-only)
  7. Progressive onboarding: personalization score hidden until 3+ posts; voice guardian suppressed until 5+ voice samples