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:
parent
63745ddcab
commit
3290e8fb7d
4 changed files with 111 additions and 8 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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 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, 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 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:
|
||||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue