docs(linkedin-studio): RE-R3f brief + plan — unattended AI discovery (headless, slice e)

Tracked slice docs for MR-F5: the poll->score->capture->brief loop
made safe to fire with no operator present. Records the 2026-06-24
re-evaluation gate against v1.0.0 maturity and the operator's explicit
decision to open it (2026-08-10), plus the Desktop Scheduled Tasks vs.
hand-rolled cron design decision.

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:20 +02:00
commit 63745ddcab
2 changed files with 288 additions and 0 deletions

View file

@ -0,0 +1,175 @@
# Brief — RE-R3f: unattended AI discovery (`--headless`, slice e)
> **Slice:** RE-R3f (research-engine rung-2, R3 slice **(e)** in the operator's `(a)→(c)→(b)→(d)→(e)`
> sequence — the doc is numbered r3f by creation order, the concept is slice (e)). Closes the
> production gap named in `docs/research-engine-concepts.local.md` §5: RE-R3c/RE-R3e built a
> **deterministic** headless entry (`schedule`/`run-daily.sh`) that re-renders the brief on a
> schedule with no new discovery — a near-no-op without a prior human-run poll. RE-R3f makes 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.
> **Origin:** Maskinrommet production feedback, register row **MR-F5** — Sunday-morning discovery
> needs to run without the operator present. Elicited via `CLAUDE.local.md`'s feedback-intake
> protocol; validated against live plugin structure before any code/prose was touched.
> **The gate this slice opens:** `docs/research-engine-concepts.local.md` §5 recorded a 2026-06-24
> operator decision that slice (e) sits behind an **explicit re-evaluation gate against product
> maturity (v1.0.0)** — research is "rung-2." The plugin is at v0.8.0 (CLAUDE.md header); the gate
> had not been re-evaluated. Surfaced to the operator 2026-08-10 as a real conflict, not silently
> built around or silently deferred; the operator opened the gate explicitly (AskUserQuestion,
> "Open the gate now") after seeing the trade-off stated plainly.
> **Architectural decision — CONFIRMED (operator, AskUserQuestion 2026-08-10):** Desktop Scheduled
> Tasks (Claude Code's own first-party local scheduler) is the trigger mechanism, **not** a
> hand-rolled cron/launchd wrapper mirroring RE-R3c. A `-p`-mode `claude` invocation cannot be
> replicated by a shell script (it needs a full agent turn, incl. the `trend-spotter` subagent), and
> Claude Code's own headless docs state a hand-rolled `cron` + `claude -p` wrapper loses
> observability/session-resumption/error-recovery relative to the first-party mechanism. The recipe
> for the fallback (hand-rolled `claude -p`) path is documented for portability, but not built as
> repo code.
## 1. The gap — grounded in code and docs
- **`run-daily.sh` (RE-R3c) is deterministic-only by design** (`run-daily.sh:1-8`, `docs/
research-engine/brief-re-r3c.md` C1): it re-renders the brief from the **current** store — no
poll, no capture. Its own comment names the gap: *"The (e) slice will insert a pre-brief AI
capture step here … before the `brief` call below."* Nothing has inserted it.
- **The attended pass blocks on operator input in two places.** `commands/trends.md` Step 1 asks
the user for pillars when `${DATA}/profile/user-profile.md` is missing; Step 5 (Triage) calls
`AskUserQuestion` up to 4× per batch. Fired unattended, either would hang or (in Claude Code's
`dontAsk` permission mode, the correct mode for an unattended run) abort the run — `AskUserQuestion`
is auto-denied unconditionally in that mode regardless of any `--allowedTools` entry (verified
against Claude Code's `permission-modes` docs, 2026-08-10).
- **No documented recipe for firing a plugin command unattended existed in this repo.** RE-R3c's
`schedule` CLI verb only ever targets the deterministic `brief` subcommand
(`run-daily.sh:33`, `cli.ts` `schedule` branch) — it has no path to a full `claude` agent turn.
- **Exit-code trust was unstated.** Claude Code's headless docs are explicit that 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 print to stdout without a non-zero exit). No existing doc in this repo
told a scheduled caller what to check instead.
## 2. Scope — what is IN (RE-R3f)
### `commands/trends.md` (EDIT) — the `--headless` contract
- New flag `--headless`, documented in the Step 0 flags table + a new **Step 0.5** section stating
the contract explicitly (so it reads as a rule, not something buried three steps down):
- `--demand --headless` together → refused (one line, exit; never silently falls back to
supply-side discovery).
- Step 1 missing profile → print a one-line error and stop; never ask. The source-list fallback
(shipped defaults) is unaffected — it already needs no operator input.
- Step 5 (Triage) → skipped unconditionally. Every kept candidate stays `pending`; the next
attended session (or the next morning brief) surfaces it for a human to resolve.
- Steps 24 and 6 (poll, score, capture, render, `.last-run` marker) run exactly as attended —
`--headless` changes who can be asked, never what gets persisted.
- States plainly: ground success in the same artifacts Step 3 already verifies (`CLI status
--json` capture delta, today's dated brief file) — never in this command's own exit code or
prose.
- No frontmatter change (`allowed-tools` already lists `AskUserQuestion` for the attended path;
leaving it is harmless belt-and-suspenders — `dontAsk` mode denies it unconditionally regardless
of what a command's frontmatter requests).
### `scripts/trends/README.md` (EDIT) — the unattended-discovery section
- A new `## Unattended AI discovery — --headless (RE-R3f, slice e)` section, positioned after the
existing RE-R3c section, explicit that this is a **different mechanism** (a full agent turn, not
a bash/cron wrapper): Desktop Scheduled Tasks as the recommended trigger; the verified `claude -p`
recipe (`--bare --plugin-dir … --permission-mode dontAsk --allowedTools "Read,Bash,Task,WebSearch,
WebFetch" --output-format json`) as the documented fallback for a machine without Desktop
Scheduled Tasks; the `ANTHROPIC_API_KEY` requirement under `--bare`; the background-subagent wait
ceiling (`CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS`, default 10 min) since the research subagent can
run long; the artifact-grounded success check (never the exit code); the `total_cost_usd` cost
visibility note from `--output-format json`.
### `CLAUDE.md` (EDIT) — command table
- `/linkedin:trends` row gains a `--headless` mention (one clause, no restructuring).
### `CHANGELOG.md` (EDIT)
- `[Unreleased]` entry: MR-F5 landed, gate opened, scope summary.
### Docs (this file + `plan-re-r3f.md`, NEW, tracked)
## 3. Non-goals — what is OUT (deferred)
- **A hand-rolled cron/launchd wrapper invoking `claude -p`** — OUT as repo code. The recipe is
documented (README) for portability to a machine without Desktop Scheduled Tasks, but nothing in
this repo shells out to `claude` itself; Desktop Scheduled Tasks is the built mechanism.
- **`schedule.ts` / `run-daily.sh` changes** — OUT. RE-R3c's deterministic brief-only wrapper is
untouched and remains valid for a plain "re-render today's brief" use case distinct from
discovery.
- **A cost cap / per-run candidate ceiling enforced in code** — OUT. `trend-spotter`'s existing
guidance (Anti-Patterns: "max 2-3 trend posts per week") is advisory prose, not a mechanism;
enforcing a hard cap on an unattended AI research run is a real design question, deferred rather
than guessed at under this slice. `total_cost_usd` visibility (README) is the mitigant landed now.
- **`--demand --headless`** — explicitly refused, not built. The demand-sweep's judgment calls are
a later slice's problem, if ever.
- **A new store field / schema bump** — none. `SCHEMA_VERSION` (4) / `BRIEF_SCHEMA_VERSION` (2)
untouched — `--headless` changes command-level control flow only.
- **A new agent, a new CLI verb, a new source/test file** — none. RE-R3f is a prompt-contract EDIT
plus documentation; there is no new TypeScript/bash to unit-test.
## 4. Boundaries / invariants (must hold)
- **`--headless` never changes what gets persisted**, only who can be asked. A headless run and an
attended run that both clear Step 1 must capture/brief identically (Step 5's skip is the only
behavioral divergence, and it is additive-safe: `pending` candidates are exactly what an attended
triage would leave un-triaged if the operator picked "Leave" on all of them).
- **No silent degrade.** `--demand --headless` refuses rather than silently running supply-side
discovery instead; a missing profile refuses rather than silently scoring against no pillars.
- **Success is artifact-grounded, never prose-grounded** — documented explicitly in both the
command file and the README, because Claude Code's own docs establish the exit-code trap is real.
- **Domain-general** — no vendor/sector token in any edit (Section 17 de-niche stays green); the
README's `claude -p` recipe names no user-specific path (the plugin-dir is a placeholder the
operator fills at their own install).
- **Counts unchanged** — no new command (`/linkedin:trends` gains a flag, not a sibling command),
no new agent, no new reference file. Recounted live at land.
## 5. Success criteria (verifiable — no test suite; this is a prompt/doc change)
- **SC1**`commands/trends.md` documents `--headless` in the flags table AND a dedicated Step 0.5
contract section; `grep -c '\-\-headless' commands/trends.md` ≥ 5 (table + Step 0.5 + the three
step-level cross-references).
- **SC2** — Step 1's missing-profile branch explicitly forks on `--headless` (fail vs. ask); Step
2D explicitly refuses `--demand --headless`; Step 5's header states the `--headless` skip
condition inline (not only in Step 0.5) — each verified by direct read of the edited sections.
- **SC3**`scripts/trends/README.md` documents the Desktop Scheduled Tasks recommendation, the
fallback `claude -p` recipe with all four required flags (`--bare`, `--plugin-dir`,
`--permission-mode dontAsk`, `--allowedTools`), the `ANTHROPIC_API_KEY` requirement, and the
artifact-grounded success check — each present as literal text, verified by grep.
- **SC4**`CLAUDE.md`'s `/linkedin:trends` row mentions `--headless`; `CHANGELOG.md` `[Unreleased]`
names MR-F5.
- **SC5** — Section 17 de-niche guard stays green (no vendor/sector token introduced); command/
agent/reference counts unchanged (recounted live).
- **SC6 (explicitly NOT claimed)** — an actual unattended end-to-end run. Per the
`plugin-runtime-is-consumer-side` constraint, a plugin command does not resolve in this dev-repo
session; the real Sunday-discovery proof happens consumer-side, after the operator updates +
reloads + configures a Desktop Scheduled Task. This brief does not claim that proof; it claims the
command-file contract and the documentation are correct and internally consistent.
## 6. Verification
**This session (dev-repo):** direct read-through of every edited section against SC1SC5;
`bash scripts/test-runner.sh` for regression sanity (RE-R3f touches no source/test file the gate
tracks, so `FAIL=0` and every floor unchanged is the expectation, not a new floor); Section 17
de-niche green; counts recounted live.
**Consumer-side (deferred, not gated on this session):** update + reload the plugin, run
`/linkedin:trends --headless` once attended-but-silent (verify it does not prompt), then configure
a Desktop Scheduled Task and confirm a real unattended Sunday pass produces a capture delta + a
dated brief.
## 7. Open questions for the go-gate
Two decisions are **CONFIRMED** (operator, AskUserQuestion 2026-08-10): the gate is opened now
(production need outweighs waiting for v1.0.0); Desktop Scheduled Tasks is the mechanism, not a
hand-rolled wrapper (with the fallback recipe documented, not built). Residual decisions, baked to
the recommended default — confirm or redirect:
- **D1 — `--headless` default mode is long-form (inherits the command's own default)?** YES (rec).
Unattended discovery is for the Sunday chronicle/newsletter pipeline per MR-F5's own framing; an
operator wanting kortform passes `--mode kortform --headless` explicitly. Re-open only if the
unattended default should differ from the attended default.
- **D2 — no hard cost/candidate cap enforced in code this slice?** YES (rec, per §3 non-goal) — cost
visibility (`total_cost_usd`) is landed; a cap is a real question deferred rather than guessed.
Re-open only if a cap should land now.
- **D3 — the fallback `claude -p` recipe is documented but not wrapped in a repo script?** YES
(rec). Building an unused wrapper for a path the operator isn't using (Desktop Scheduled Tasks
covers the real machine) would be speculative code. Re-open only if the fallback path is the one
actually needed.

View file

@ -0,0 +1,113 @@
# Plan — RE-R3f: unattended AI discovery (`--headless`, slice e)
> **Brief:** `docs/research-engine/brief-re-r3f.md`. **Slice:** RE-R3f (research-engine rung-2 — R3
> slice (e), the AI-capture unattended-run slice). No new source/test file: this is a prompt-file
> (`commands/trends.md`) contract change plus documentation. There is nothing here for
> `scripts/test-runner.sh` to gain a new floor for — the plan's "TDD" is a direct-read verification
> against the brief's SC1SC5, not a unit-test suite.
## Files touched (exhaustive)
| File | Change | SC |
|---|---|---|
| `commands/trends.md` | EDIT — `--headless` flag row + new **Step 0.5** contract section; Step 1's missing-profile branch forks on `--headless`; Step 2D refuses `--demand --headless`; Step 5 header states the unconditional skip | SC1, SC2 |
| `scripts/trends/README.md` | EDIT — new `## Unattended AI discovery — --headless (RE-R3f, slice e)` section: Desktop Scheduled Tasks (recommended) + the fallback `claude -p` recipe + `ANTHROPIC_API_KEY` + wait-ceiling + artifact-grounded success check + cost visibility | SC3 |
| `CLAUDE.md` | EDIT — `/linkedin:trends` command-table row gains a `--headless` clause | SC4 |
| `CHANGELOG.md` | EDIT — `[Unreleased]` entry naming MR-F5 | SC4 |
| `docs/research-engine/{brief,plan}-re-r3f.md` | NEW — tracked slice docs (mirrors R3c/R3e/etc.) | — |
| `STATE.md` | EDIT at land — Telling-block reconcile (no new counts; note MR-F5 landed) | — |
**Not touched (scope fence):** `scripts/trends/src/*.ts` (no code — Desktop Scheduled Tasks is the
mechanism, not a repo-owned wrapper); `scripts/trends/run-daily.sh` (RE-R3c stays deterministic-only,
untouched); `agents/trend-spotter.md` (the agent's own contract — poll/score — is unchanged by who
fires it; `--headless` is a command-level concern); `config/*`; `references/*`; command/agent counts
(no new command, no new agent — `--headless` is a flag on an existing command); `scripts/test-runner.sh`
(no new floor — nothing here is `tsx`-tested source).
## Step 1 — `commands/trends.md`: the `--headless` contract
Edited already this session (verify against brief SC1/SC2 by direct read):
1. Step 0 flags table gains the `--headless` row.
2. New **Step 0.5** section states the full contract: `--demand --headless` refusal, Step 1
fail-fast, Step 5 unconditional skip, Steps 24/6 unchanged, artifact-grounded success.
3. Step 1's pillar-loading paragraph forks explicitly: `--headless` → error + stop; else → ask.
4. Step 2D gains the refusal clause for `--demand --headless` before its existing demand-sweep
delegation text.
5. Step 5's header and lead line state the `--headless` skip condition (not buried only in Step 0.5
— a reader working step-by-step must see it at the step itself).
**Verification:** `grep -n -- '--headless' commands/trends.md` shows the table row + Step 0.5 + the
three per-step cross-references (≥5 matches, SC1); re-read Steps 1/2D/5 end-to-end to confirm each
fork is unambiguous (no branch left to model inference under ambiguity, SC2).
## Step 2 — `scripts/trends/README.md`: the unattended-discovery section
Edited already this session. New section placed after the existing RE-R3c ("Autonomous trigger +
headless entry") section, explicit that RE-R3f is a **different mechanism** (full agent turn, not a
bash wrapper): Desktop Scheduled Tasks recommended; the fallback `claude -p …` recipe with all four
required flags; `ANTHROPIC_API_KEY` under `--bare`; `CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS`; the
artifact-grounded success check (never trust `$?`/prose); `total_cost_usd` cost-visibility note.
**Verification:** `grep -n -- '--bare\|--permission-mode dontAsk\|--allowedTools\|ANTHROPIC_API_KEY\|Desktop Scheduled Tasks' scripts/trends/README.md` — each present (SC3).
## Step 3 — `CLAUDE.md` + `CHANGELOG.md`
- `CLAUDE.md`: append a `--headless` clause to the existing `/linkedin:trends` row (one line, no
restructuring of the table).
- `CHANGELOG.md`: `[Unreleased]` gains an entry — MR-F5 landed, the gate-opening decision named, the
scope (command contract + docs, no new code), matching this repo's existing changelog voice (see
the 0.8.0 entries for tone/format).
**Verification:** `grep -n -- '--headless' CLAUDE.md`; `grep -n 'MR-F5' CHANGELOG.md`.
## Step 4 — gate + regression sanity
`bash scripts/test-runner.sh``FAIL=0`, every existing floor unchanged (no new source/test file to
raise a floor for); Section 17 de-niche green (no vendor/sector token in any edit — the README's
`claude -p` recipe names a placeholder plugin-dir path, never an operator-specific one);
command/agent/reference counts unchanged, recounted live (`--headless` is a flag, not a new
surface).
## Step 5 — land
Recount counts live; reconcile `STATE.md`'s Telling block (no count change; note MR-F5 landed at
commit); quit the feedback register kvittering per `CLAUDE.local.md`'s protocol ("landed
feedback:MR-F5 @ `<commit>`" — Maskinrommet flips 🔵→🟢 on relay, never written here). Commit order:
**(1)** docs commit — `docs/research-engine/{brief,plan}-re-r3f.md` (tracked, no suffix); **(2)** one
code^H^H^H^Hprompt commit — `commands/trends.md` + `scripts/trends/README.md` + `CLAUDE.md` +
`CHANGELOG.md` (there is no code in this slice — "code commit" here means the prompt-file + docs
edit, landed together since it is one coherent contract change). Push after operator go (public
`open/` remote — confirm before push per the repo's own push-topology memory).
## Verification (testable)
| SC | Check | Command | Expected |
|---|---|---|---|
| SC1 | `--headless` documented | `grep -c -- '--headless' commands/trends.md` | ≥ 5 |
| SC2 | Per-step forks unambiguous | direct read of Steps 1, 2D, 5 | each states the `--headless` branch explicitly, no inference required |
| SC3 | README recipe complete | `grep` for the four flags + `ANTHROPIC_API_KEY` + `Desktop Scheduled Tasks` | all present |
| SC4 | CLAUDE.md + CHANGELOG updated | `grep -- '--headless' CLAUDE.md`; `grep 'MR-F5' CHANGELOG.md` | both present |
| SC5 | Gate green, counts unchanged | `bash scripts/test-runner.sh` | `FAIL=0`; all floors unchanged; Section 17 green |
| SC6 | (explicitly deferred) | — | consumer-side unattended proof is NOT claimed this session (`plugin-runtime-is-consumer-side`) |
## Risks
- **R1 — a scheduled task on the operator's actual machine can't reach Desktop Scheduled Tasks
(older Claude Code version, disabled feature).** *Mitigated:* the fallback `claude -p` recipe is
fully documented in the README, so the operator has a working path either way; not built as repo
code because building an unused wrapper would be speculative (brief §3 D3).
- **R2 — the `--headless` contract is a prompt, so nothing enforces it mechanically; a future edit
to `commands/trends.md` could silently reintroduce a blocking `AskUserQuestion` on the headless
path.** *Accepted, stated honestly:* there is no code-level guard here (unlike RE-R3c's
gate-tested `schedule.ts`). Claude Code's own `dontAsk` permission mode is the real backstop —
`AskUserQuestion` is auto-denied unconditionally regardless of what the command file says — so a
regression fails safe (the run aborts) rather than silently hanging on a phantom prompt.
- **R3 — unattended weekly AI research has a real, currently-uncapped per-run cost.** *Mitigated by
visibility, not a cap* (brief §3 non-goal, D2): `--output-format json`'s `total_cost_usd` is
documented as the thing to log; enforcing a hard ceiling is deferred as a real design question
rather than an arbitrary number picked under this slice.
- **R4 — the gate-reopening decision itself is contestable** (research was explicitly deferred to
v1.0.0 by a prior operator decision). *Mitigated:* the conflict was surfaced explicitly
(AskUserQuestion, 2026-08-10) rather than silently built around or silently deferred; the operator
chose to open it with the trade-off stated plainly. Recorded in the brief's header for future
sessions reading this slice cold.