linkedin-studio/commands/trends.md
Kjell Tore Guttormsen 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

12 KiB
Raw Blame History

name description allowed-tools
linkedin:trends Run a trend discovery pass over the user's own content pillars and source list: delegate the scan to the trend-spotter agent, make sure kept candidates are persisted to the trend store (dedup) and the dated morning brief is written, then return a triage-ranked candidate list the user resolves per id (select/skip, batched). Default scoring mode is long-form (chronicle/newsletter material); `--mode kortform` overrides for feed posts. Use when the user wants a discovery pass, a trend scan, or a morning-brief refresh. Triggers on: "linkedin trends", "trend discovery", "discovery pass", "run a trend scan", "scan my sources", "morning brief", "refresh the brief", "trend sweep".
Read
Bash
Task
AskUserQuestion

LinkedIn Trend Discovery

You are a thin discovery orchestrator. The methodology — source tiers, research routing (MCP-first), relevance scoring, angle selection — lives in the trend-spotter agent and in the scoring SSOT ${CLAUDE_PLUGIN_ROOT}/references/trend-scoring-modes.md. Do not restate any of it here; your job is to invoke the pass correctly, verify its side effects actually happened, and hand the user a triage-ready list.

Data dir shorthand used below: ${DATA} = ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}. Trends CLI shorthand: CLI = cd "${CLAUDE_PLUGIN_ROOT}/scripts/trends" && node --import tsx src/cli.ts.

Step 0: Parse flags

All flags are optional, given after the command name:

Flag Meaning Default
--mode kortform|long-form Scoring mode (see the SSOT for what each rewards) long-form — no arguments means a long-form discovery pass
--fresh-days N Freshness window for the morning brief CLI default (7)
--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.

--demand reroutes the pass (Step 2D below) to the demand side — a different agent (demand-spotter), a different unit (the arc/åre, not the news-item), a different output (the §4 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: 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 — the agent owns Research Routing (its own "Research Routing" section reads the profile's ### Research Tooling block); duplicating it here would drift.

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:

  • The pillars and the resolved source-list path from Step 1 (its Tier 5 demand sources are what this pass polls — the inverse of the Tier 14 supply sources).
  • The theme(s) to sweep (from --topics if given, else the user's pillars).
  • That the three passes are mandatory: demand-sweep (verbatim questions + signal strength + is-it-answered) → pain-point map (fills painPoint) → vocabulary translation (fills readerQuestion).
  • That persistence is mandatory unless --dry-run: the agent runs capture with the reader fields (readerQuestion/painPoint/saturation/demand/verdict/actionability), then renders the §4 arc map with CLI arcs.
  • That an honest null is a finding — a thin/un-swept source is reported, never filled in.

Then present the arc map the agent returns: per vein, the ranked question inventory (reader's words), the honest market verdict (supply-gap = write here · demand-gap = high-value/low-audience · saturated = skip), and the BÆRENDE/STØTTE reading order. On --dry-run, present the same map but state that nothing was persisted. Skip Steps 24. Hand off: a chosen vein feeds /linkedin:newsletter Step 1 (its reader question, pain point, and angle prefill the edition).

Step 2: Run the discovery pass

If --brief-only: skip the agent entirely — go to Step 3 and render the brief from the existing store.

Otherwise delegate to the trend-spotter agent — invoke it via Task with subagent_type: linkedin-studio:trend-spotter (foreground). The prompt MUST state explicitly:

  • The scoring mode from Step 0 (default long-form). Never omit it — the agent falls back to kortform when the caller is silent.
  • The pillars and the resolved source-list path from Step 1.
  • That this is a full digest run and the persistence steps are mandatory, not optional: the agent must run its Step 4.5 (capture — persist kept candidates to the trend store, batch, dedup, scores carried) and Step 4.6 (brief — write the dated morning brief, passing the pillars, and --fresh-days if the user set it).
  • If --dry-run: invert that — the agent must poll and score but skip capture and brief entirely (nothing persisted).
  • That the returned digest must include, per kept candidate: title, 23 sentence summary, source URL(s), composite score + band, recommended angle, and matching pillar/series.

Step 3: Verify the side effects (skip on --dry-run)

Trust but verify — the pass is only done when its artifacts exist:

  1. Store: CLI status --json — confirm the store mutated (captured count reflects the run; on a no-new-trends day {added: 0, merged: N} is a fine outcome, not a failure).
  2. Brief: confirm today's file exists: ${DATA}/trends/morning-brief/<today YYYY-MM-DD>.md. If the agent captured but failed to render the brief, render it directly — the brief is deterministic: CLI brief --pillars "<pillar1,pillar2,…>" (add --fresh-days N if set).
  3. If capture itself did not happen, say so plainly and report what the agent returned — never present an unpersisted digest as if it were in the store.

Step 4: Present the triage-ranked list

Present the candidates ranked highest composite first (the agent's digest already carries the ranking — do not re-rank). Per candidate, the contract is:

N. [Title]  (id: <trend-id>)
   Score: X.X — [Band]  |  Pillar: [pillar/series]
   [23 sentence summary]
   Source: [URL(s)]
   Angle: [recommended angle]

Include each candidate's store id (shown in the brief and via CLI list --json) — the triage 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 or --headless)

On --headless: skip this step unconditionally (Step 0.5AskUserQuestion 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:

  • Velg — you'll write about this: mark selected, moving it onto the brief's "I produksjon" board (valgt) so the queue stops re-surfacing it while it's in progress
  • Skip — not for me: mark skipped (dropped from the queue)
  • Leave — keep it in the queue untouched

Then apply the decisions through the store CLI. Batch by verb — collect all the "Velg" ids and all the "Skip" ids and resolve each set in ONE call (ten candidates ≤ two calls):

CLI select --ids <id1,id2,id3>    # everything chosen this pass
CLI skip   --ids <id4,id5>        # everything rejected this pass

Single-id form (--id <id>) still works for a one-off. "Leave" means no call. A partial batch (some id unknown) still applies the matches, reports the misses, and exits 0. Finish with a one-line summary: N valgt, N skipped, N left in queue. (act — already written — and the auto-act when an edition reaches scheduling arrive with the N7 trend→newsletter bridge.)

Step 6: Write the last-run marker (skip on --dry-run)

On completed runs (including --brief-only), stamp the marker so other surfaces can tell when discovery last ran:

mkdir -p "${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/trends" && \
  date -u +"%Y-%m-%dT%H:%M:%SZ" > "${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/trends/.last-run"

Reference Files

  • ${CLAUDE_PLUGIN_ROOT}/references/trend-scoring-modes.md — scoring SSOT (modes, weights, bands)
  • ${CLAUDE_PLUGIN_ROOT}/config/trends-sources.template.md — shipped source-list defaults (user override: ${DATA}/trends/sources.md)
  • ${CLAUDE_PLUGIN_ROOT}/agents/trend-spotter.md — the discovery methodology this command invokes