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
This commit is contained in:
Kjell Tore Guttormsen 2026-08-10 20:57:34 +02:00
commit 3290e8fb7d
4 changed files with 111 additions and 8 deletions

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: