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

@ -7,6 +7,21 @@ 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** — 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.

View file

@ -58,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

@ -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

@ -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)