# Plan — RE-R3c: autonomous trigger — scheduler + headless entry point > **Brief:** `docs/research-engine/brief-re-r3c.md`. **Slice:** RE-R3c (research-engine rung-2 — R3 slice 3, the > **autonomy** slice: the scheduler trigger + the headless entry that runs the deterministic morning brief with no > interactive session). Closes hulls (1) no autonomous trigger + (6) no headless entry point. > **TDD-order (two-phase RED — light-Voyage discipline, inherited):** Step 1 records RED in two phases — **(A)** > subprocess assertion-RED against the **existing** CLI (`schedule` is an unknown command → `usage` exit 2; the > `run-daily.sh` wrapper file is absent → exit 127) on the exit-0/stdout assertions; **(B)** for the NEW > `schedule.ts` exports the tests import, land **non-throwing stubs** first (Node16 ESM throws a missing named > import at module-load, not on assertion), then record value-assertion RED against them. Then GREEN: `schedule.ts` > stubs→real → `run-daily.sh` wrapper → `cli.ts` `schedule` verb → wire `trend-spotter.md` (prose) + README → > gate floors + Section 16l → behavioural → land. > **Counts recounted live at land, never pinned/guessed.** > **Architectural decisions (CONFIRMED, AskUserQuestion 2026-06-26):** C1 deterministic brief-only (no AI capture — > that is slice e) · C2 print-first installer (emit + the operator runs the system mutation; `--install` writes only > the inert launchd plist file; the tool never runs `launchctl`/`crontab`). Go-gate D1–D9 baked to recommended > defaults (brief §8). > **Light-Voyage:** scope-guardian / brief-reviewer / plan-critic to run on these drafts; findings folded in > §Plan-critic — folded before the code commit. ## Goal Make the daily research loop **closed** and **headless**. (1) **Autonomous trigger** — a `schedule` CLI verb that emits (print-first) a launchd plist (macOS) / crontab line (Linux) firing a daily brief, and `--install` that writes only the inert launchd plist file (the operator runs the one activation command). (2) **Headless entry point** — `run-daily.sh`, a bash-3.2 wrapper that runs the **deterministic** `brief` from a scheduler's profile-less env (resolves node + the data-dir log seam + working dir), invoked identically by launchd and cron. **No AI capture** (C1 — that is slice e, which plugs into the documented pre-brief seam); **no schema change** (`SCHEMA_VERSION` 4 / `BRIEF_SCHEMA_VERSION` 1 untouched); **no new agent/command/reference**. Two new source files under `scripts/trends/` (`schedule.ts` pure module + `run-daily.sh` wrapper) + their tests; EDIT `cli.ts` + `trend-spotter.md` (prose) + README + the gate. ## Files touched (exhaustive — for scope-guardian) | File | Change | SC | |---|---|---| | `scripts/trends/src/schedule.ts` | **NEW (pure module)** — `ScheduleSpec` (incl. `env: Record`); `launchdPlist`/`crontabLine`/`installInstructions`/`uninstallInstructions`/`defaultLabel`. No clock/fs/env/AI (renders `spec.env`; reads no env). | SC1, SC2 | | `scripts/trends/run-daily.sh` | **NEW (wrapper)** — bash 3.2; resolves own dir + **`cd "$DIR"`** + node (`NODE_BIN`/`command -v`) + log seam (`${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/trends/cron.log`); runs `cli.ts" brief "$@" --json`, **compacts** the pretty-printed json to one line; logs ` exit= `; (e)-seam comment. | SC7, SC8 | | `scripts/trends/src/cli.ts` | **EDIT** — `schedule --pillars [--at HH:MM] [--fresh-days N] [--platform auto\|launchd\|cron] [--install] [--uninstall] [--store

]`; adds `dirname` (node:path) + `homedir` (node:os) + `fileURLToPath` (node:url) imports; resolves paths from runtime (`process.execPath`/`import.meta.url`/`defaultStorePath`); `logPath = join(dirname(defaultStorePath()), "cron.log")`; `env` always `{NODE_BIN, LINKEDIN_STUDIO_DATA(resolved)}`; `args` without leading `"brief"`; print-first dispatch; `--install` writes the inert plist only (never `launchctl`/`crontab`); usage + header doc; **no new exit code** | SC1–SC6, SC9 | | `scripts/trends/tests/schedule.test.ts` | **NEW** — pure-emitter assertions: plist keys (Label/ProgramArguments/StartCalendarInterval/Std*Path/EnvironmentVariables), crontab line shape, install/uninstall instruction strings, `defaultLabel`, determinism | SC1, SC2 | | `scripts/trends/tests/cli.test.ts` | **EDIT** — subprocess: `schedule --print` (launchd + cron) emits the artifact + instruction, exit 0; `--platform auto` branches on `process.platform`; `--print` writes nothing (temp HOME LaunchAgents empty); `--install` launchd writes the plist file + prints `launchctl` (never runs it); `--install` cron prints only; no `--pillars`/bad `--at`/bad `--platform` → exit 2 | SC3–SC6, SC9 | | `scripts/trends/tests/run-daily.test.ts` | **NEW** — subprocess: wrapper on a seeded store writes the dated brief + appends to `cron.log` + exit 0; second same-day run → byte-identical `.md`, `surfacedCount` not double-counted, second log line; data-path twin consistency (wrapper log dir == `defaultStorePath` dir == `getDataRoot('trends')`) | SC7, SC8 | | `scripts/trends/src/brief.ts` | **UNTOUCHED** — the nightly run reuses the existing deterministic `brief`/`rankForBrief`/`renderBrief`. Listed to assert it is *not* in scope. | — | | `scripts/trends/src/store.ts` · `types.ts` · `item.ts` · `score.ts` | **UNTOUCHED** — no data-shape / scoring / render change. Listed to assert they are *not* in scope. | — | | `agents/trend-spotter.md` | **EDIT (prose-only, minimal)** — one line: the brief can now be scheduled to regenerate autonomously (deterministic, from the store) via `schedule`; polling stays the capture path (autonomous AI polling is a later slice). Domain-general. | — | | `scripts/trends/README.md` | **EDIT** — the headless wrapper + `schedule` (print-first, launchd/cron, `--install`/`--uninstall`), the deterministic-brief-only boundary (C1) + the (e) seam, `cron.log`, per-day idempotency | — | | `scripts/test-runner.sh` | **EDIT** — `TRENDS_TESTS_FLOOR` 171→recount + breakdown comment (`:709`); NEW unconditional **Section 16l** (after 16k's block `~:1305`, before Section 18 header `:1307`); `ASSERT_BASELINE_FLOOR` 105→**111** (`:1329`); header-enumeration chain (`:57`) + Section-18 floor-history narration (`~:1310-1324`) | SC10 | | `docs/research-engine/{brief,plan}-re-r3c.md` | **NEW** — slice docs (TRACKED, like `docs/second-brain/*`) | — | | `STATE.md` | **EDIT at land** — Telling-block reconcile (trends floor, ASSERT floor 111, gate total; schema unchanged v4). *Land bookkeeping, LOCAL-ONLY.* | — | **Not touched (scope fence):** `brief.ts`/`store.ts`/`types.ts`/`item.ts`/`score.ts` (the nightly run reuses the existing brief path; no data/scoring/render change) · `references/trend-scoring-modes.md` + `algorithm-signals- reference.md` (no scoring change) · the SessionStart hook + its tests (R3c generates the brief the hook already surfaces; no hook change, no frontmatter-schema change, no new hook test) · `config/*` · `commands/*` (29 — no new command) · `agents/*` count (19 — `trend-spotter.md` is a prose EDIT) · `references/*` (27) · `.gitignore` (`scripts/trends/{node_modules,build}` already covered; `cron.log` lives under the external data dir, never in the repo) · `BRIEF_SCHEMA_VERSION` (1) · `SCHEMA_VERSION` (4). ## Step 1 — (RED, two phases) failing tests across schedule/cli/wrapper **Phase A — subprocess assertion-RED against the pre-edit code** (no new import needed; the CLI is invoked as a subprocess and the wrapper file is simply absent): - `cli.test.ts` — **`schedule`**: `schedule --pillars ai,gov --platform launchd --print` today → `usage` (unknown command) **exit 2**, no plist on stdout → RED against the assertion (expects exit 0 + a plist with `Label com.linkedin-studio.trends.daily`); `--platform cron --print` → RED (expects the crontab line + the `crontab -` instruction string); `--print` with a temp `HOME` → RED (expects `/Library/LaunchAgents` empty *and* exit 0); no `--pillars` / `--at 25:00` / `--platform bogus` → these already exit 2 today (unknown command), so assert the **post-implementation** behaviour (still exit 2, but for the validation reason) — recorded as RED only where the message/route differs (kept minimal; the load-bearing RED is the `--print` emit). *(Fail today: `schedule` is an unknown command.)* - `run-daily.test.ts` — **wrapper**: invoke **via `bash scripts/trends/run-daily.sh --pillars ai --store --out /mb`** (through `bash`, NOT a direct executable spawn — folded — plan-critic #7: a direct exec of a missing file throws ENOENT/`status:null`, a module-not-found-class failure; `bash ` exits **127**, a clean assertion-RED) with `LINKEDIN_STUDIO_DATA=` → the file is **absent** → exit **127** → RED against the assertion (expects the dated `.md` written + exactly one `cron.log` line + exit 0). *(Fail today: the wrapper does not exist.)* **Phase B — stub-first, then value-assertion RED** (`schedule.test` imports the new `schedule.ts` exports): - Land **non-throwing stubs** so the imports resolve: in `schedule.ts` — `launchdPlist → ""`, `crontabLine → ""`, `installInstructions → ""`, `uninstallInstructions → ""`, `defaultLabel → ""` (+ the `ScheduleSpec` interface, erased by tsx). (Wrong-value stubs Step 2 replaces.) - `schedule.test.ts`: `launchdPlist(spec)` contains `Label` + `spec.label` + `StartCalendarInterval` + `spec.hour`/`spec.minute` + the `cron.log` path (stub `""` → RED); `crontabLine(spec)` matches the ` * * * … run-daily.sh … >> 2>&1 #