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
210 lines
12 KiB
Markdown
210 lines
12 KiB
Markdown
---
|
||
name: linkedin:trends
|
||
description: |
|
||
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".
|
||
allowed-tools:
|
||
- 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 2–4 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 2–4 (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 1–4 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 2–4.** 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, 2–3 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]
|
||
[2–3 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.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:
|
||
|
||
- **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):
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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
|