# Brief — RE-R3c: autonomous trigger — scheduler + headless entry point > **Slice:** RE-R3c (research-engine rung-2, R3 slice 3 — the **autonomy** slice: the trigger that makes the > daily loop *closed* and the headless entry that runs the deterministic morning brief with **no interactive > session**). R3 ("deepen the research engine") is an **arc** of 5 open hulls (substrate §1). R3a took relevance, > R3b took the lifecycle (status + seen-log + re-score). R3c takes hulls **(1) no autonomous trigger** + **(6) no > headless entry point** — the *mechanism* that runs the existing deterministic brief on a schedule, built and > tested deterministically **before** the autonomous AI fan-out (slice e) plugs into it. > **Predecessor:** RE-R3b (`status` exclusion + per-day-idempotent `surfacedCount`/`lastSurfacedAt` — the > dedup-state a nightly loop **depends on** so it never re-surfaces handled work) + RE-R2b (`brief.ts` dated > artifact + the SessionStart surfacing the nightly run feeds) + RE-R3a (composite ranking). > **Substrate:** `docs/research-engine-concepts.local.md` §1 hull (1) (*"ingen autonom trigger … zero cron/launchd/ > scheduler i hele repoet"*) + (6) (*"ingen headless entry point"*) + §B4 (*"behavioral scheduling … a push/delivery > window that gates delivery separately from the sweep"*) + §B3 (the dated digest as a flat plain-text artifact > *"skrevet av Stop-hook eller cron-trigget headless-sesjon"*). R3c builds the cron-triggered headless path B3 > anticipated and the scheduling-window discipline B4 names. > **TDD-order:** RED before code, **two phases** (light-Voyage discipline, inherited): Phase A — assertion-RED via > subprocess against the **existing** CLI (`schedule` is an unknown command today → `usage` exit 2; the wrapper > file is absent → exit 127) — true assertion-RED on the exit-code/stdout assertions, not module-not-found. Phase B > — `schedule.ts` is a NEW module whose exports the tests import; under Node16 ESM a missing named import throws at > module-load, so land **non-throwing stubs** (`launchdPlist → ""`, etc.) first, then record value-assertion RED > against them. See plan Step 1. > **Architectural decisions — CONFIRMED (operator, AskUserQuestion 2026-06-26; baked into the plan):** > - **C1 — deterministic brief-only.** The nightly headless run regenerates the dated brief from the **current > store** (freshness-aging drops stale trends; `surfacedCount` accumulates per distinct day → feeds slice b). > **NO AI capture.** Polling stays operator-driven; the autonomous AI fan-out is **slice (e)**, which plugs into > (c)'s headless seam. Faithful to the operator's `(a)→(c)→(b)→(d)→(e)` sequence: build the trigger mechanism + > headless plumbing (deterministic, testable) **before** the AI sweep it will eventually drive. *Honest framing: > the visible autonomous-research payoff lands with (e); (c) is the mechanism.* > - **C2 — print-first installer.** `schedule` **emits** the launchd plist (macOS) / crontab line (Linux) + the > exact install command; the operator runs it. `--install` writes only the inert launchd plist FILE (never runs > `launchctl`; never touches `crontab`). Matches the global `[voyage]` cron-persistence guard, the push-policy's > operator-authorization, and the "confirm outward-facing/persistent actions" rule. ## 1. Operator decision context (2026-06-26) The research engine is **Tier-1** (operator, 2026-06-23). R1→R3b built the deterministic spine **and** the trend's life after capture: item-schema + triage (R1) → capture bridge (R2a) → dated morning brief + surfacing (R2b) → persisted relevance + composite ranking (R3a) → status lifecycle + seen-log + re-score (R3b). The spine is complete and the lifecycle is correct — **but nothing runs it on its own.** The morning brief exists only when the operator interactively invokes the `brief` CLI (via the `trend-spotter` agent or by hand); the SessionStart hook *surfaces* the latest dated brief (`session-start.mjs:534`) but **never generates one**. There is **zero scheduler** in the repo (verified live: only `scripts/test-runner.sh` exists; no plist, no cron, no launchd in any `.ts`/`.mjs`/`.sh`/config). The loop is open: a brief is only as fresh as the last time the operator remembered to ask for one. R3c closes hulls **(1)** and **(6)** — the **autonomous trigger** and the **headless entry point** — which the operator chose as slice (c) of the full-R3 build-out (2026-06-24, *"ALLE gjenstående R3-slices … i rekkefølge (a) → (c) → (b) → (d) → (e)"*). It is sequenced **after** R3b for a load-bearing reason the operator named: an autonomous loop **must never re-surface handled work**, so it depends on R3b's status-exclusion (acted/skipped dropped from the brief) and its **per-day-idempotent** surfacing (a double-fire doesn't double-count). R3b made the nightly regeneration *safe to automate*; R3c automates it. **What R3c is — and is not (C1).** R3c is the **mechanism**, not the AI sweep. The nightly run is the *existing deterministic* `brief` generation — load store → rank → write the dated `.md` → record surfacing — run with no interaction by a scheduler. It does **not** poll new sources (that is the AI fan-out, slice e). Its honest value without (e): the brief is regenerated every morning from the current store, so SessionStart surfacing is always fresh; freshness-aging drops trends past the window automatically; and `surfacedCount` accumulates day-over-day — the temporal signal slice (b) reads — **without the operator running anything**. (e) later plugs an AI capture step into the documented pre-brief seam to close the full `poll→score→capture→brief` loop. ## 2. The gap — grounded in code - **No autonomous trigger (hull 1).** Repo-wide there is no scheduler: no launchd plist, no crontab artifact, no `launchctl`/`cron` reference in any source or config (verified). Every brief is born of an interactive session. - **No headless entry point (hull 6) — *almost*.** The `brief` subcommand (`cli.ts:297-328`) is **already non-interactive**: it reads flags, writes `/.md`, records surfacing, and exits 0 — no prompts. What is missing is a **robust invocation wrapper** that makes it runnable from a scheduler's *minimal* environment: a launchd/cron job inherits **no shell profile** (no `PATH` from `~/.zshenv`, so a bare `node` is unresolvable), has **no working directory** set to the repo (tsx resolves modules only from `scripts/trends/`), and has **no logging**. Today nothing bridges that gap. - **The brief is operator-pulled, never machine-pushed.** `session-start.mjs:60-77`/`:534` *reads* the latest dated brief (`date`+`summary`, zero-tsx) and surfaces it — it is a pure consumer. Generation lives only in the CLI, invoked by a human. B3's *"cron-trigget headless-sesjon"* writer does not exist. - **The CLI has no scheduling verb.** `cli.ts` exposes `add`/`query`/`list`/`status`/`act`/`skip`/`reset`/ `normalize`/`score`/`capture`/`brief` (`cli.ts:5-14`, `:134-330`) — capture/read/lifecycle, all interactive. There is **no way to emit or install a daily schedule** for the brief. - **The data-dir seam is solved, but only for two runtimes.** `store.ts:252` (`defaultStorePath`) and `hooks/scripts/data-root.mjs:24` (`getDataRoot`) are *twins* of the one seam (`LINKEDIN_STUDIO_DATA ?? ~/.claude/ linkedin-studio`). A scheduler entry running in **shell** needs the same seam for its log path — a **third sanctioned twin**, exactly the inline `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/…` form `references/data-path-convention.md` rule 1 prescribes. It does not exist yet. ## 3. Scope — what is IN (RE-R3c) ### S-schedule — `scripts/trends/src/schedule.ts` (NEW, pure module) Pure string emitters for the schedule artifacts — no clock, no fs, no env, no AI (the CLI injects every resolved value). Mirrors `brief.ts`'s `renderBrief` purity → fully testable, byte-deterministic given inputs. - **`export interface ScheduleSpec`** — `{ platform: "launchd" | "cron"; label: string; nodeBin: string; wrapperPath: string; args: string[]; hour: number; minute: number; logPath: string; workingDir: string; env: Record; }`. All paths are absolute, resolved by the CLI at generation time on the operator's machine. **`env`** is the injected environment map (the CLI builds it — always `NODE_BIN` + a **resolved-absolute** `LINKEDIN_STUDIO_DATA`); the emitter only *renders* it, so it reads no env itself (folded — brief-reviewer #4 / plan-critic #3: the field is canonical, not mid-step). - **`export function launchdPlist(spec: ScheduleSpec): string`** — the plist XML: `Label`, `ProgramArguments` (`["/bin/bash", wrapperPath, ...args]`), `StartCalendarInterval` (`{ Hour: spec.hour, Minute: spec.minute }`), `EnvironmentVariables` (rendered from `spec.env` only — purity), `WorkingDirectory` (`spec.workingDir`), `StandardOutPath`/`StandardErrorPath` (`spec.logPath`), `RunAtLoad` false. A pinned, well-formed template (``); `schedule.test` asserts both key-completeness **and** well-formedness (balanced-tag/parse), `plutil -lint` is the deps-present manual check (folded — brief-reviewer #7). - **`export function crontabLine(spec: ScheduleSpec): string`** — one line: ` * * * /bin/bash >> 2>&1 #