From b43757462b77e07c6a952963d9ffd56c9692cbc0 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 26 Jun 2026 10:13:39 +0200 Subject: [PATCH] =?UTF-8?q?docs(linkedin-studio):=20RE-R3c=20brief=20+=20p?= =?UTF-8?q?lan=20=E2=80=94=20autonomous=20trigger=20(scheduler=20+=20headl?= =?UTF-8?q?ess=20entry),=20light-Voyage=20hardened?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice (c) of the R3 build-out: a `schedule` CLI verb (print-first launchd plist / cron-table line) + `run-daily.sh`, a bash-3.2 headless wrapper that runs the DETERMINISTIC morning brief from a profile-less scheduler env. Closes hulls #1 (no autonomous trigger) + #6 (no headless entry point). Operator-confirmed (AskUserQuestion 2026-06-26): C1 deterministic brief-only (no AI capture — that is slice e, which plugs into the documented pre-brief seam); C2 print-first installer (the tool emits the artifact + the install command; `--install` writes only the inert launchd plist file; never runs the scheduler activation itself). Light-Voyage hardened — three Opus reviewers, each verifying against live code: scope-guardian ALIGNED (0 creep/0 gaps), brief-reviewer PROCEED_WITH_RISKS, plan-critic REVISE. All findings folded, incl. the pretty-printed `brief --json` log-line compaction, the `cd "$DIR"` cron fix, the logPath base pinned to `dirname(defaultStorePath())`, the canonical `ScheduleSpec.env`, and the `ASSERT_BASELINE_FLOOR` :1259->:1329 cite. No schema/count change (29/19/27, store v4). Tracked feature-design. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_011vmzxpsFpc8q19LaogAWLD --- docs/research-engine/brief-re-r3c.md | 424 +++++++++++++++++++++++++++ docs/research-engine/plan-re-r3c.md | 376 ++++++++++++++++++++++++ 2 files changed, 800 insertions(+) create mode 100644 docs/research-engine/brief-re-r3c.md create mode 100644 docs/research-engine/plan-re-r3c.md diff --git a/docs/research-engine/brief-re-r3c.md b/docs/research-engine/brief-re-r3c.md new file mode 100644 index 0000000..0f78ae5 --- /dev/null +++ b/docs/research-engine/brief-re-r3c.md @@ -0,0 +1,424 @@ +# 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 #