# 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 `<outDir>/<day>.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<string, string>; }`. 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
  (`<?xml … !DOCTYPE plist …>`); `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:
  `<minute> <hour> * * * <env-prefix> /bin/bash <wrapperPath> <args…> >> <logPath> 2>&1  # <label>` where
  `<env-prefix>` is `spec.env` rendered as cron's inline `K=V K=V` form. **The function returns the line as a
  STRING; it never executes `crontab`** (the execution guard + C2; the literal `crontabLine` does not match the
  guard's `\bcrontab\b` word-boundary pattern — §5).
- **`export function installInstructions(spec: ScheduleSpec, plistTargetPath?: string): string`** — the exact
  operator commands. launchd: *"written to `<plistTargetPath>` — activate with `launchctl bootstrap gui/$(id -u)
  <plistTargetPath>`"*. cron: *"add the line above with `(crontab -l 2>/dev/null; echo '<line>') | crontab -`"*.
  Print-first surfaces these so the operator runs them.
- **`export function uninstallInstructions(spec, plistTargetPath?): string`** — symmetric removal (launchd:
  `launchctl bootout …` + `rm <plist>`; cron: the line-removal `grep -v` recipe).
- **`export function defaultLabel(): string`** — `"com.linkedin-studio.trends.daily"` (the **plugin namespace**,
  domain-general — not the user's domain; no vendor/sector token).

### S-wrapper — `scripts/trends/run-daily.sh` (NEW, headless invocation wrapper)

The single tested headless entry, invoked identically by **both** the launchd plist and the crontab line (one
entry → one test). Bash 3.2-compatible (operator's macOS: no `declare -A`, no `mapfile`, all expansions quoted,
ASCII-only).

- Resolves **its own directory** (`DIR="$(cd "$(dirname "$0")" && pwd)"`) so it is relocatable — no hard-coded
  repo path — and **`cd "$DIR"`** so `--import tsx` resolves `node_modules` from the package even under cron's
  `$HOME` CWD (folded — brief-reviewer #1: the plist sets `WorkingDirectory`, but cron does not — `cd` makes the
  one wrapper scheduler-agnostic).
- Resolves **node** from a minimal scheduler env: `NODE_BIN="${NODE_BIN:-$(command -v node 2>/dev/null)}"`;
  if still empty, fall back to common locations; exit 127 with a logged error if none. (The scheduler bakes
  `NODE_BIN=<process.execPath>` so resolution always succeeds; the fallback is for a manual invocation.)
- Resolves the **log path** from the canonical inline seam — `LOG="${LINKEDIN_STUDIO_DATA:-$HOME/.claude/
  linkedin-studio}/trends/cron.log"` — the **fourth sanctioned data-path twin** (shell), the exact form
  `references/data-path-convention.md` rule 1 prescribes (documented as such, like `data-root.mjs`'s twin comment).
  The scheduler **always bakes a resolved-absolute `LINKEDIN_STUDIO_DATA`** into the artifact env, so a scheduled
  run never evaluates `$HOME` (sidesteps the `set -u` `HOME`-unset edge — folded — plan-critic #9 / brief-reviewer
  #9); the `$HOME` fallback is only for a manual invocation, where `HOME` is set. `mkdir -p` its dir.
- Runs the **deterministic** brief: `OUT="$("$NODE_BIN" --import tsx "$DIR/src/cli.ts" brief "$@" --json 2>&1)";
  CODE=$?` then **compacts** `OUT` to one line (`tr '\n' ' ' | tr -s ' '`) — `brief --json` is **pretty-printed**
  (`cli.ts:323` `JSON.stringify(…, null, 2)`), so the structured log line must collapse the newlines (folded —
  plan-critic #1). The scheduler bakes `--pillars … --fresh-days N` into `"$@"`; the wrapper hard-codes the
  `brief` subcommand and adds `--json`. Appends **one** line `<ISO-ts> exit=<CODE> <compact-OUT>` to `$LOG`;
  `exit $CODE`. **No AI** — `brief` is the deterministic store→artifact path (C1).
- **The (e) seam (documented, not built):** a one-line comment marks where slice (e) will insert a pre-brief
  capture step (`claude -p … trend-spotter | cli.ts capture`) before the `brief` call. R3c builds **only** the
  deterministic path.

### S-cli — `scripts/trends/src/cli.ts` (EDIT) — the `schedule` subcommand

- **`schedule --pillars <a,b> [--at HH:MM] [--fresh-days N] [--platform auto|launchd|cron] [--install]
  [--uninstall] [--store <path>]`**:
  - Resolves **platform**: `auto` (default) → `process.platform === "darwin" ? "launchd" : "cron"`.
  - Resolves **time** from `--at` (default `07:00`); validates `HH ∈ 0–23`, `MM ∈ 0–59` → `usage` exit 2 on bad
    input. `--pillars` is **required** (a schedule with no pillars is meaningless) → `usage` exit 2 if absent.
  - Resolves the absolute paths **from the runtime**, never hard-coded: `nodeBin = process.execPath` (absolute);
    `wrapperPath = join(dirname(fileURLToPath(import.meta.url)), "..", "run-daily.sh")` (`cli.ts` is at
    `scripts/trends/src/`, so `..` → `scripts/trends/`); `workingDir = join(dirname(fileURLToPath(import.meta.url)),
    "..")`; **`logPath = join(dirname(defaultStorePath()), "cron.log")`** — derived from `defaultStorePath()`
    (`<root>/trends/trends.json` → `<root>/trends/cron.log`), **NOT** from the `--store` override, so it matches
    the wrapper's data-root-anchored log exactly (folded — all three reviewers: a `--store` outside the data dir
    must not split the plist `StandardOutPath` from the wrapper's own log file).
  - Builds **`env`** (always): `{ NODE_BIN: process.execPath, LINKEDIN_STUDIO_DATA: <resolved-absolute root> }`
    where the root = `process.env.LINKEDIN_STUDIO_DATA ?? join(homedir(), ".claude", "linkedin-studio")` — baked
    so the scheduled run is pinned to the install-time root **and** never evaluates `$HOME` (the wrapper's
    `set -u` `HOME`-unset edge).
  - Builds **`args`** = `["--pillars", <p>, "--fresh-days", String(N)]` (+ `["--store", storePath]` when an explicit
    non-default `--store` was given, so the scheduled run targets the same store). **No leading `"brief"`** — the
    wrapper hard-codes the `brief` subcommand (folded — plan-critic #8 / brief-reviewer #5: avoids
    `cli.ts brief brief …`).
  - Builds the `ScheduleSpec` and dispatches:
    - **default / `--print`** → print the artifact (`launchdPlist` or `crontabLine`) **+** `installInstructions`
      to stdout. **No fs.** Exit 0.
    - **`--install`** → launchd: `mkdirSync` + `writeFileSync` the plist to `~/Library/LaunchAgents/<label>.plist`
      (an **inert** file; reversible) and print the single `launchctl bootstrap` command — **the tool never runs
      `launchctl`**. cron: print the line + the `crontab -` install command — **the tool never runs `crontab`**
      (the global guard + C2). Exit 0.
    - **`--uninstall`** → launchd: print the `launchctl bootout` command + (if the plist file exists) `rm` it;
      cron: print the line-removal recipe. Exit 0.
  - **Exit-code contract unchanged** (0 success / 2 usage). `schedule` introduces **no new exit code**: an autonomy
    install never *runs* the system mutation, so there is no install-failure path to encode — the operator runs the
    one printed command. (Update the header doc-comment `cli.ts:36-37` to note `schedule` is print-first and never
    shells `launchctl`/`crontab`.)
- **Imports** `launchdPlist`, `crontabLine`, `installInstructions`, `uninstallInstructions`, `defaultLabel` from
  `./schedule.js`; **adds `dirname` to the `node:path` import** (`cli.ts:41` imports only `join` today — folded —
  plan-critic #5), `homedir` from `node:os`, `fileURLToPath` from `node:url` (`defaultStorePath` is already imported,
  `cli.ts:45`). The DAG stays acyclic: `schedule.ts` is a **leaf** (imports nothing from the package); `cli.ts`
  is the existing root.
- **Usage + header synopsis** (`cli.ts:5-14`, `:86-100`): add the `schedule …` line + a one-line header note that
  `schedule` emits/installs a daily headless brief (print-first; deterministic — no AI capture; that is slice e).

### Wiring (D-default — WIRE, mirrors R3a/R3b)

- `agents/trend-spotter.md` (EDIT, **prose-only, minimal**): one line — the morning brief can now be **scheduled**
  to regenerate autonomously (deterministic, from the store) via `schedule`; the agent's polling remains the
  capture path (autonomous AI polling is a later slice). No batch-shape change. Domain-general (no vendor/sector
  token).
- `scripts/trends/README.md` (EDIT): document the headless wrapper + the `schedule` subcommand (print-first,
  launchd/cron, `--install`/`--uninstall`), the **deterministic-brief-only boundary (C1)** and the (e) AI-capture
  seam, the `cron.log`, and the R3b per-day idempotency that makes a double-fire safe.
- `scripts/test-runner.sh` (EDIT): bump `TRENDS_TESTS_FLOOR` (`:709`, currently 171) to the `tests N` line reported
  after Steps 1–5, **append** `+ RE-R3c: scheduler +N` to the inline breakdown comment. Add **Section 16l**
  ("Trends Scheduler / Headless Wiring", RE-R3c) **after Section 16k's closing block (`~:1305`), before Section 18
  (`:1307`)** (16k is the last 16x before the anti-erosion Section 18; preserve that order). Mirror 16k's shape:
  **unconditional**, deps-absent-safe (`grep -qF` + a non-vacuity self-test emitting **one** pass/fail).
  Recommended **6 emitters** (all on tracked source — no `tsx`): (1) self-test; (2) `export function launchdPlist`
  in `schedule.ts`; (3) `export function crontabLine` in `schedule.ts`; (4) `command === "schedule"` in `cli.ts`
  (the verb); (5) `cli.ts" brief` in `run-daily.sh` (the wrapper invokes the deterministic brief — the sentinel
  matches the literal `…cli.ts" brief`, folded — plan-critic #8); (6) the data-path twin in `run-daily.sh`
  (`LINKEDIN_STUDIO_DATA:-`). **6 unconditional emitters → bump `ASSERT_BASELINE_FLOOR` 105 → exactly 111**
  (`:1329`; "live recount" is the safety net; the expected value is the pinned 105 + 6). Insert the 16l clause into
  the **header-enumeration prose chain at `:57`** (before "…the assertion-count anti-erosion floor (SC6) in Section
  18"), and **append the R3b (→105) + R3c-16l (→111) narration** to the Section-18 floor-history comment
  (`~:1310-1324`, which still stops at "= 99" — folded — scope-guardian #7).

## 4. Non-goals — what is OUT (deferred)

- **AI capture in the nightly run** (`poll→score→capture` via a headless `claude -p` trend-spotter) — **slice (e)**,
  behind the post-(d) re-evaluation gate. R3c builds the deterministic headless path + the documented (e) seam; it
  adds **no** AI invocation, no `claude -p`, no API dependency in the scheduler context.
- **Running `launchctl` / `crontab` autonomously** — OUT (C2 print-first). `schedule` prints the activation
  command; `--install` writes only the inert launchd plist FILE. The operator runs the one system-mutating command.
- **A `/linkedin:schedule` command wrapper** (plugin surface) — OUT for R3c (would change the command count). R3c
  ships the CLI subcommand + README; a command front-door is a later ergonomic slice. Counts stay **29/19/27**.
- **Windows Task Scheduler** — OUT. launchd (macOS) + cron (Linux) cover the plugin's runtimes; a Windows emitter
  is a later portability add.
- **A lock / mutex / run-marker** — unneeded. R3b's per-day-idempotent surfacing + the per-day brief filename make
  a double-fire a safe no-op; B4's separate *delivery* window is not needed for a once-daily calendar job.
- **A config-file pillar source** — OUT. Pillars are `--pillars`, **baked into the schedule artifact** at
  generation (the operator supplies them once at install). A config/profile-resolved pillar source is a later
  nicety.
- **Brief history / day-over-day diff** ("what's new since yesterday" — hull 7) — **slice (d)**.
- **Re-scoring / time-decay recompute on a schedule** — OUT. Re-score is on **re-capture** (R3b); R3c does no
  capture, so the nightly run re-ranks the *unchanged* scores against the *current* freshness window only.
- **Schema bumps** — none. R3c touches **no** store field and **no** brief frontmatter field
  (`SCHEMA_VERSION` stays 4; `BRIEF_SCHEMA_VERSION` stays 1). It adds a new *module* + a *wrapper* + a *CLI verb* —
  no data shape changes.
- **New agent / new command / new reference doc** — none. R3c adds **two source files** (`schedule.ts`,
  `run-daily.sh`) + their tests, and EDITs `cli.ts` + one agent (prose) + README + gate. `store.ts`/`brief.ts`/
  `item.ts`/`score.ts`/`types.ts` are **untouched** (the nightly run reuses the existing deterministic `brief`
  path). Counts stay 29/19/27.

## 5. Boundaries / invariants (must hold)

- **TDD iron law (two-phase RED):** failing tests land **BEFORE** implementation. Phase A — subprocess
  assertion-RED against the existing CLI (`schedule` unknown → exit 2; `run-daily.sh` absent → exit 127) on the
  exit-0/stdout assertions. Phase B — `schedule.ts` exports are imported by the test; land non-throwing stubs
  first (Node16 ESM throws a missing named import at module-load), then record value-assertion RED against them.
  The plan does **not** claim a single "everything fails before any code" run.
- **`schedule.ts` is pure** (no clock, no fs, no env, no AI): every value the emitters use is injected via
  `ScheduleSpec`. The CLI is the only edge that reads `process.execPath`/`import.meta.url`/`defaultStorePath`.
  Mirrors `renderBrief`'s purity.
- **Determinism of the nightly run:** the wrapper invokes the **deterministic** `brief` (whose byte-determinism
  R2b/R3a/R3b proved); given `(store, pillars, day, freshDays)` the written `.md` is byte-identical. The wrapper
  adds only a timestamped log line + an exit code.
- **No autonomous system mutation (C2):** `schedule` (default) writes **nothing**; `--install` writes only an
  inert launchd plist file (reversible `rm`); the tool **never** runs `launchctl` or `crontab`. The global guard is
  an **execution** guard (`voyage` `pre-bash-executor.mjs`, pattern `\bcrontab\b|>\s*/etc/cron` — verified live), so
  it inspects **bash commands**, not file content: the new files' printed strings (`crontab -`, `launchctl
  bootstrap`) are written by `Write`/emitted by the CLI and are **fine**, and the 16l grep uses `crontabLine`
  (no `\bcrontab\b` word-boundary match). **No code path — source or test — ever *executes* a command containing the
  bare word `crontab` or `launchctl …`**; the install commands are printed STRINGS the operator runs, and tests
  assert those strings on **stdout/the written file** without executing them (a test that *ran* `crontab` would trip
  the guard and mutate the real system — explicitly forbidden).
- **One data-dir seam, four sanctioned runtime twins:** `store.ts:253` (TS store), `data-root.mjs:25` (hooks
  `.mjs`), `analytics/src/utils/storage.ts:54` (TS analytics — the existing third, named in `data-root.mjs:44`),
  `run-daily.sh` (shell — NEW fourth). The shell form is the canonical inline `${LINKEDIN_STUDIO_DATA:-$HOME/
  .claude/linkedin-studio}` expansion (`references/data-path-convention.md` rule 1), **not** a new seam; documented
  as a twin (like `data-root.mjs`'s comment) and asserted behaviorally (SC8 newly binds `store.ts`'s
  `defaultStorePath` into the consistency check — `dirname(defaultStorePath()) == getDataRoot('trends') == the
  wrapper's `${…}/trends`).
- **Domain-general:** no hard-coded user/repo path in the committed **source** (`schedule.ts`/`run-daily.sh`/the
  `cli.ts` edit) — every concrete path is resolved at generation/run on the operator's machine and lives only in
  the **generated** artifact (outside the repo, in `~/Library/LaunchAgents` / the crontab). The launchd Label is
  the plugin namespace; pillars are args. Section 17 de-niche stays green.
- **Bash 3.2-compatible wrapper** (operator's macOS): no `declare -A`/`mapfile`/`|&`; all expansions quoted;
  ASCII-only (a multibyte char crashes under `set -u` on bash 3.2).
- **Minimal-env robustness:** the wrapper must run from launchd/cron's profile-less env — node resolved via baked
  `NODE_BIN` (absolute `process.execPath`) with a `command -v` fallback; `WorkingDirectory`/`cd` set so tsx
  resolves; log dir `mkdir -p`'d.
- **Hook unaffected:** the SessionStart surfacing reads `date`+`summary` only and never shells to tsx; R3c touches
  neither the hook nor the frontmatter schema, so surfacing is unchanged. The hook suite must still pass untouched
  (regression sanity; R3c adds no hook test).
- **No schema/SSOT change:** `references/trend-scoring-modes.md`, `types.ts`, `store.ts`, `brief.ts` untouched
  (R3c changes no data shape and no scoring/render math).
- **Pathguard:** the two NEW files are under `scripts/trends/` — **write-allowed** (the global pre-write-pathguard
  allowlists `~/repos/*`; `cli.ts`/`test-runner.sh` were added there with no friction — folded — scope-guardian #4:
  the earlier ".mjs-under-hooks/scripts-only" phrasing was a fabricated mechanism; the *conclusion* holds). EDITs
  are to existing files. *(Implementation risk, not a docs-step blocker: if any Write is nonetheless blocked, the
  operator authorizes via the R2b `!cp` fallback — see plan Risk R4.)*
- **Counts** (refs/agents/commands 27/19/29) unchanged. **Recounted live at land**, never pinned/guessed.

## 6. Success criteria (testable)

- **SC1 (launchd plist emit)** — `schedule --pillars ai,gov --platform launchd --at 07:30 --print` → stdout is a
  **key-complete + well-formed** plist (balanced-tag/parse asserted, not just substring greps — folded —
  brief-reviewer #7) containing `Label` = `com.linkedin-studio.trends.daily`, `ProgramArguments` invoking
  `run-daily.sh` with `--pillars ai,gov` (the wrapper supplies `brief`), `StartCalendarInterval` `Hour 7`/`Minute
  30`, `StandardOutPath`/`StandardErrorPath` = the resolved `cron.log` path, `NODE_BIN` +
  `LINKEDIN_STUDIO_DATA` in `EnvironmentVariables`; exit 0. Two `--print` runs (same args) → byte-identical (the
  emitter is pure). `plutil -lint` is the deps-present manual check (Step 7).
- **SC2 (crontab line emit — string only)** — `schedule --pillars ai,gov --platform cron --at 07:30 --print` →
  stdout contains `30 7 * * * NODE_BIN=… /bin/bash …/run-daily.sh brief --pillars ai,gov >> <log> 2>&1  #
  com.linkedin-studio.trends.daily` **+** the `(crontab -l 2>/dev/null; echo '<line>') | crontab -` install
  instruction; exit 0. The test asserts the **emitted string** and **never executes `crontab`**.
- **SC3 (platform auto)** — `schedule --pillars ai --print` (no `--platform`) → launchd on darwin, cron elsewhere;
  asserted against `process.platform` (the subprocess inherits the host platform; the assertion branches on it).
- **SC4 (print-first writes nothing)** — `schedule --pillars ai --platform launchd --print` with `HOME=<tmp>` →
  exit 0, stdout has the plist, **and `<tmp>/Library/LaunchAgents` is absent/empty** (no fs write); crontab never
  invoked.
- **SC5 (`--install` launchd: inert plist file, no launchctl)** — `schedule --pillars ai --platform launchd
  --install` with `HOME=<tmp>` → `<tmp>/Library/LaunchAgents/com.linkedin-studio.trends.daily.plist` **exists**
  with the SC1 plist content; stdout prints the `launchctl bootstrap` command; **`launchctl` is never run** (the
  test asserts only the file + stdout; no system job is created); exit 0.
- **SC6 (`--install` cron: never self-installs)** — `schedule --pillars ai --platform cron --install` → stdout has
  the line + the `crontab -` instruction; exit 0; **`crontab` is never invoked** (no system mutation; asserted by
  stdout only).
- **SC7 (headless wrapper runs the deterministic brief + logs)** — `run-daily.sh --pillars ai --store <tmp>/
  s.json --out <tmp>/mb` (the wrapper supplies `brief`) with `LINKEDIN_STUDIO_DATA=<tmp>` on a seeded fresh store →
  writes `<tmp>/mb/<today>.md` (the dated brief), appends **exactly one** `<ISO-ts> exit=0 {…compact-json…}` line
  to `<tmp>/trends/cron.log` (the multi-line `brief --json` collapsed — folded — plan-critic #1), exit 0. A
  **second** run the same day → the brief `.md` is byte-identical (idempotent re-render), the seen-log is **not**
  double-counted (R3b per-day idempotency), the log gains a second line. **CWD-independence:** the same invocation
  with `cwd=<tmp-unrelated>` (not the package dir) still resolves `tsx` and succeeds (the wrapper's `cd "$DIR"` —
  folded — brief-reviewer #1). The test invokes via `bash run-daily.sh …` so the absent-file RED is exit 127
  (folded — plan-critic #7). **No AI** is invoked (C1).
- **SC8 (data-path twin consistency)** — the wrapper's `${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/
  trends` resolves to the **same** directory as `dirname(defaultStorePath())` (TS — the base the CLI's `logPath`
  uses, **not** the `--store` override) and `getDataRoot('trends')` (hooks `.mjs`), for both the default root and
  an overridden `LINKEDIN_STUDIO_DATA`. Asserted behaviorally (resolve all three for a temp override; assert
  equal). Because `logPath` derives from `defaultStorePath()`, a custom `--store` never splits the plist
  `StandardOutPath` from the wrapper's log (folded — all three reviewers).
- **SC9 (usage / validation)** — `schedule` with **no** `--pillars` → `usage` exit 2; `--at 25:00` / `--at 7:99` /
  `--at noon` → `usage` exit 2; `--platform bogus` → `usage` exit 2. Each leaves the fs untouched.
- **SC10 (gate + wiring + de-niche)** — `bash scripts/test-runner.sh` → `FAIL=0`: trends suite green at the bumped
  `TRENDS_TESTS_FLOOR`; new **Section 16l** green (`launchdPlist`/`crontabLine` in `schedule.ts`, `command ===
  "schedule"` in `cli.ts`, the `cli.ts brief` + `LINKEDIN_STUDIO_DATA:-` sentinels in `run-daily.sh`, non-vacuity
  self-test); `ASSERT_BASELINE_FLOOR` = **111** (105 + 6); Section 17 de-niche green; counts 29/19/27; the hook
  suite still green untouched (`node --test hooks/scripts/__tests__/*.test.mjs`).

## 7. Verification

**Deterministic:** `bash scripts/test-runner.sh` → `FAIL=0`; trends suite ≥ new floor; Section 16l self-test +
greps pass; `ASSERT_BASELINE_FLOOR` = 111; Section 17 de-niche green; ref/agent/command counts unchanged.
**Regression sanity:** `node --test hooks/scripts/__tests__/*.test.mjs` → still green untouched (R3c touches no
hook; adds no hook test).
**Behavioural (manual):**
1. Seed a store: `echo '[{"source":"tavily","title":"A","url":"https://e/a","topics":["ai","gov"],
   "publishedAt":"<~2d ago>"}]' | node --import tsx src/cli.ts capture --store /tmp/r3c.json`.
2. `node --import tsx src/cli.ts schedule --pillars ai,gov --platform launchd --at 07:00 --print` → inspect the
   plist; `node --import tsx src/cli.ts schedule … --platform cron --print` → inspect the crontab line + install
   instruction.
3. **Lint the plist (macOS):** pipe the `--print` plist to `plutil -lint -` → "OK" (a malformed plist won't load).
4. `LINKEDIN_STUDIO_DATA=/tmp/r3c-data ./run-daily.sh brief --pillars ai,gov --store /tmp/r3c.json --out
   /tmp/r3c-data/trends/morning-brief` → confirm `/tmp/r3c-data/trends/morning-brief/<today>.md` written + a line
   appended to `/tmp/r3c-data/trends/cron.log`; exit 0.
5. Re-run step 4 same day → `.md` byte-identical; `cron.log` gains a second line; `list --json` shows
   `surfacedCount:1` (not 2 — per-day idempotent).
6. `schedule --pillars ai --platform launchd --install` with a throwaway `HOME` → confirm the plist file is
   written under `<HOME>/Library/LaunchAgents/` and the `launchctl bootstrap` command is printed (do **not** run it
   against the real system unless intentionally activating).

## 8. Open questions for the go-gate

Two architectural decisions are **CONFIRMED** (operator, 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). Residual decisions, all baked to the recommended
default — confirm or redirect with "Go":
- **D1 — schedule time default `07:00`, `--at HH:MM` overrides?** YES (rec). A morning brief wants a pre-workday
  fire; the operator tunes it. Re-open only for a different default hour.
- **D2 — launchd Label = `com.linkedin-studio.trends.daily` (plugin namespace)?** YES (rec). Reverse-DNS, the
  plugin's own namespace (domain-general; no user-domain token). Drop only for a different naming scheme.
- **D3 — `--install` writes the launchd plist FILE but never runs `launchctl`/`crontab`?** YES (rec). The strictest
  honest print-first: the tool prepares the inert artifact, the operator activates it. Re-open only to make
  `--install` a pure no-op (print-only, no file write).
- **D4 — `--platform auto` defaults via `process.platform`?** YES (rec). darwin→launchd, else→cron. Explicit
  `--platform` overrides (e.g. to emit a crontab line on a Mac for a Linux box). Drop only to require `--platform`.
- **D5 — pillars baked into the artifact at generation (no config-file source)?** YES (rec). The operator supplies
  `--pillars` once at install; the schedule carries them. A profile-resolved pillar source is a later nicety.
- **D6 — `cron.log` under `<data>/trends/cron.log`?** YES (rec). Colocated with the store + morning-brief under the
  data-dir seam, survives reinstalls. Drop only for a different log location.
- **D7 — a single `.sh` wrapper invoked by BOTH launchd + cron (vs a CLI `run` subcommand)?** YES (rec). A shell
  wrapper handles the launchd/cron minimal-env robustness (node resolution, `cd`, logging) the CLI cannot; the CLI
  `brief` stays the deterministic core. One wrapper → one tested entry. Re-open only to push the robustness into a
  CLI `run` verb (more TS, but then the plist must still bake node).
- **D8 — no `/linkedin:schedule` command (CLI + README only)?** YES (rec). Keeps the command count; a command
  front-door is a later ergonomic slice. Re-open only if the scheduler should be operator-facing via a slash
  command now.
- **D9 — commit split?** Docs commit first, then **one** code commit (rec) — the scheduler (module + wrapper + CLI
  verb + wiring) is one coherent feature. Re-open only for a module-then-wiring split.

## 9. Light-Voyage review — folded

Three Opus reviewers ran on the drafts, each verifying claims against live code. **scope-guardian: ALIGNED**
(0 creep / 0 gaps; every SC1–SC10 traces to a step; no AI/capture, no schema bump, counts 29/19/27 + untouched-
files claim verified live; 1 MAJOR line-cite + 6 MINOR accuracy/precision). **brief-reviewer: PROCEED_WITH_RISKS**
(RED premises TRUE, the data-path trio genuinely consistent, C2 no-execution path confirmed; 2 MAJOR + 5 MEDIUM/LOW).
**plan-critic: REVISE → 73/C** (the two-phase RED, de-niche safety, bash-3.2 wrapper, path resolution, and the
+6→111 gate arithmetic all verified correct against live code; 4 MAJOR + 5 MINOR; the C grade is largely the
legacy-manifest-format penalty — these are hand-authored slice docs, not trekexecute manifests). **All findings
folded** (per-finding resolution in `plan-re-r3c.md §Plan-critic — folded`). Headlines:

- **[MAJOR, folded — plan-critic #1] `brief --json` is pretty-printed** (`cli.ts:323` `JSON.stringify(…, null, 2)`),
  so the wrapper's "one log line" / SC7 contract was false. → the wrapper **compacts** `OUT` (`tr '\n' ' ' | tr -s
  ' '`) after capturing `CODE`; SC7 asserts exactly one line.
- **[MAJOR, folded — brief-reviewer #1] the wrapper never `cd`s to its package dir**, so the cron path (CWD `$HOME`)
  would fail to resolve `tsx`. → the wrapper adds **`cd "$DIR"`**; SC7 gains a CWD-independence assertion.
- **[MAJOR/MINOR, folded — all three] the `logPath` expression was self-contradicting and used the `--store`
  override base.** → pinned to **`join(dirname(defaultStorePath()), "cron.log")`** (the data-root anchor, matching
  the wrapper); the `".."` and `storePath` variants removed.
- **[MAJOR, folded — brief-reviewer #4 / plan-critic #3] `ScheduleSpec.env` was stated three ways.** → `env:
  Record<string,string>` is now **canonical** in §3, always carrying `NODE_BIN` + a resolved-absolute
  `LINKEDIN_STUDIO_DATA`; the emitters render it only (purity holds), and the always-baked root also fixes the
  `HOME`-unset `set -u` edge.
- **[MAJOR, folded — all three] `ASSERT_BASELINE_FLOOR` line-cite `:1259` → `:1329`** (verified live; value 105→111
  correct). `TRENDS_TESTS_FLOOR` `:705` → `:709`; header-enum insertion `:49-53` → `:57`; the Section-18
  floor-history comment (still "= 99") gets the R3b(→105)+R3c(→111) narration appended.
- **[MINOR, folded — plan-critic #5] `dirname` is not imported in `cli.ts`** (`:41` is `join` only). → the plan adds
  `dirname` to the `node:path` import (+ `homedir`/`fileURLToPath`).
- **[MINOR, folded — plan-critic #8 / brief-reviewer #5] stray double `brief`** (`cli.ts brief brief …`). → the
  baked `args` drops the leading `"brief"`; the wrapper owns the subcommand; the 16l sentinel matches `cli.ts" brief`.
- **[MINOR, folded — scope-guardian #4] the pathguard justification was a fabricated mechanism.** → restated:
  `~/repos/*` is allowlisted; the conclusion (new files write-allowed) holds.
- **[LOW, folded — brief-reviewer #7] SC1 "lint-valid" was only grep-checked.** → SC1 asserts **well-formed**
  (balanced-tag/parse) + key-complete; `plutil -lint` stays the deps-present manual check.
- **[LOW, folded — brief-reviewer #8] the twin census undercounted (3 → 4).** → `analytics/storage.ts` named as the
  existing third; `run-daily.sh` is the fourth.
