From f3c2d5200b41f53d50a3d172d5ed14d76dfaee46 Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Sun, 7 Jun 2026 15:08:40 -0700 Subject: [PATCH 1/4] =?UTF-8?q?feat(reactor-cli):=20add=20docs-synthesis?= =?UTF-8?q?=20example=20(merged=20PRs=20=E2=86=92=20docs=20PR)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A cost-tiered Reactor example, parallel to agent-observatory, that watches a code repo's merged PRs and opens one PR against the public docs site to keep it current. It is the worked example of the Cost and Context Discipline authoring guidance (the PR→docs shape that guidance was written for): - repo-changes (gateway): folds merged-PR deltas (changed-path list + diffstat, never the full diff). - change-signal (cheap classifier): maps each PR's inline changed paths to per-section doc-impact signals, faceted so an unrelated PR wakes nobody. - five section accumulators (cli/sdk/reactor/reactor-devtools/openprose): each subscribes to its one signal facet and accumulates pending edits (page + why + cited PR); memo-skips at zero cost otherwise. - docs-pr (coalesced actuator): the one effectful node — reads only the pages it edits, drafts the .mdx changes, and opens one PR (git + gh). Dry under REACTOR_OFFLINE or DOCS_SYNTH_DRY_RUN, which is how the replay is produced. Ships static PR fixtures (offline demo) and a git-log connectors.cjs.example (live opt-in). Compile-check passes keyless. The committed replay/ + eval-harness validation is a follow-up keyed run. Co-Authored-By: Claude Opus 4.8 --- .../examples/docs-synthesis/.gitignore | 3 + .../docs-synthesis/PIPELINE-DESIGN.md | 102 +++++++++++++ .../examples/docs-synthesis/README.md | 141 +++++++++++++++++ .../docs-synthesis/change-signal.prose.md | 90 +++++++++++ .../examples/docs-synthesis/cli-docs.prose.md | 52 +++++++ .../docs-synthesis/connectors.cjs.example | 144 ++++++++++++++++++ .../docs-synthesis/devtools-docs.prose.md | 53 +++++++ .../examples/docs-synthesis/docs-pr.prose.md | 75 +++++++++ .../docs-synthesis/openprose-docs.prose.md | 52 +++++++ .../docs-synthesis/reactor-docs.prose.md | 52 +++++++ .../examples/docs-synthesis/reactor.yml | 61 ++++++++ .../docs-synthesis/repo-changes.prose.md | 44 ++++++ .../examples/docs-synthesis/sdk-docs.prose.md | 51 +++++++ 13 files changed, 920 insertions(+) create mode 100644 packages/reactor-cli/examples/docs-synthesis/.gitignore create mode 100644 packages/reactor-cli/examples/docs-synthesis/PIPELINE-DESIGN.md create mode 100644 packages/reactor-cli/examples/docs-synthesis/README.md create mode 100644 packages/reactor-cli/examples/docs-synthesis/change-signal.prose.md create mode 100644 packages/reactor-cli/examples/docs-synthesis/cli-docs.prose.md create mode 100644 packages/reactor-cli/examples/docs-synthesis/connectors.cjs.example create mode 100644 packages/reactor-cli/examples/docs-synthesis/devtools-docs.prose.md create mode 100644 packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md create mode 100644 packages/reactor-cli/examples/docs-synthesis/openprose-docs.prose.md create mode 100644 packages/reactor-cli/examples/docs-synthesis/reactor-docs.prose.md create mode 100644 packages/reactor-cli/examples/docs-synthesis/reactor.yml create mode 100644 packages/reactor-cli/examples/docs-synthesis/repo-changes.prose.md create mode 100644 packages/reactor-cli/examples/docs-synthesis/sdk-docs.prose.md diff --git a/packages/reactor-cli/examples/docs-synthesis/.gitignore b/packages/reactor-cli/examples/docs-synthesis/.gitignore new file mode 100644 index 00000000..279673b8 --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/.gitignore @@ -0,0 +1,3 @@ +/.reactor/ +/connectors.cjs +.serve.log diff --git a/packages/reactor-cli/examples/docs-synthesis/PIPELINE-DESIGN.md b/packages/reactor-cli/examples/docs-synthesis/PIPELINE-DESIGN.md new file mode 100644 index 00000000..fae3b73c --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/PIPELINE-DESIGN.md @@ -0,0 +1,102 @@ +# docs-synthesis — Cost & Context Discipline, applied + +_Why this example is shaped the way it is. It is the worked example of the +**Cost and Context Discipline** authoring guidance +(`skills/open-prose/guidance/authoring.md`): a continuous, fan-out, high-event- +volume Reactor pipeline — the PR→docs shape that guidance was written for._ + +--- + +## The job + +Watch a code repo's merged PRs and keep a separate docs site current. The naive +version is one render per PR: "here is a merged PR, read the repo and the docs, and +update whatever is now stale." That render re-derives every downstream truth on +each event, wanders two repositories to do it, and runs on every PR whether or not +it touches anything documented. Cost scales with the *clock*, not with surprise — +and most PRs (tests, CI, refactors) document nothing. + +This example is the disciplined version. Every rule below maps to a bullet in the +authoring guidance. + +--- + +## Tier the work; let a cheap gatekeeper filter surprise + +`change-signal` is a narrow **classifier**, not a summarizer. It reads only a +merged PR's changed-file-path list (plus title and body) and maps paths to doc +sections, emitting at most a one-line typed signal per affected section — split into +five independently-subscribable facets: + +``` + repo-changes (gateway) ~free: fold the staged PR delta; no model, no git + | #changes + v + change-signal (classifier) CHEAP: inline paths -> per-section signals + | #cli-signal #sdk-signal #reactor-signal #devtools-signal #openprose-signal + v \ \ \ \ \ + cli-docs sdk-docs reactor-docs devtools-docs openprose-docs each: pending edits + \________ \___________ \______________ \_______________/ + | + docs-pr RARE: coalesced; opens one PR; reads only the pages it edits +``` + +A PR touching only `packages/reactor-cli/**` moves `#cli-signal` only. `cli-docs` +renders; the other four sections **memo-skip at zero cost** because their signal +facet never moved. A test-only PR moves nothing downstream at all. This is the +`guard` pattern made structural: expensive synthesis runs only on real change. + +## Bound each narrow render to its inline input + +The classifier and the five accumulators read ONLY the staged PR evidence and their +own prior world-model. Their `### Invariants` forbid running git, opening the diff, +or reading either repository. They emit **pointers** — "section `cli`, page +`telemetry`, stale re: PR #101" — not diffs. `max_turns` caps turns, not context +size; it is the *unscoped task* that explodes cost, so the task is scoped ("classify +into these five shapes," "append one pending edit") and the inputs are bounded. Each +accumulator even carries its section's fixed page list in-contract, so it names a +real target page without reading the docs repo. + +## Concentrate the expensive, effectful work in the rare terminal node + +`docs-pr` is the one render whose *job* is to act — and the guidance is explicit +that the discipline does not override the principle that a render which must explore +(or here, act) should do so. So `docs-pr` MAY read the specific `.mdx` pages its +pending edits name and the cited PRs' actual changes, draft the edits, and open one +PR. It is still bounded (only the named pages and cited PRs, never a repo crawl), +and it fires only when a pending-edit facet actually moved — so its cost is paid +only on genuine doc work, not on every PR. This is "keep the high-volume renders +small enough for a cheap model; reserve the heavier work for the rare node." + +## Validate the cost-shape; do not assume it + +The committed `replay/` plus the eval harness prove the property rather than +asserting it. The deterministic tier (offline, zero spend) checks selective wake: a +CLI-only PR renders `change-signal` + `cli-docs` and **skips** +sdk/reactor/devtools/openprose; a test-only PR renders nothing downstream. The +opt-in judge tier grades the produced pending edits and proposed PR against each +node's `### Maintains` postconditions (every edit cites a PR and names a real page; +the branch name is a deterministic function of the cited PR numbers). + +--- + +## The model-tiering reality (the same honest gap) + +`### Runtime: model` is valid OpenProse, so "a cheap model for the classifier, a +stronger model for the actuator that drafts prose" is *authorable*. But the shipped +CLI wires a single global `render_model` for every node and does not read per-node +`### Runtime` model — tracked as `RB-NodeModel`. So today: one cheap global model +(adequate for the upstream transforms), the actuator runs on it too, and a human +reviews the opened PR. When `RB-NodeModel` lands, the actuator is the obvious node +to point at a stronger model. + +--- + +## What this example is NOT + +It is not a generic "summarize the repo" agent. It does not re-derive the docs from +scratch on each PR, it does not scan two repositories per event, and it does not run +expensive work on PRs that document nothing. The whole point is that cost tracks +surprise: a quiet stream of test/CI/refactor PRs costs ~nothing past the cheap +classify, and a PR that genuinely changes a documented surface produces exactly one +reviewed docs PR. diff --git a/packages/reactor-cli/examples/docs-synthesis/README.md b/packages/reactor-cli/examples/docs-synthesis/README.md new file mode 100644 index 00000000..275d5397 --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/README.md @@ -0,0 +1,141 @@ +# docs-synthesis example + +Merged PRs in the prose monorepo piped into a docs-update pull request. A cheap +classifier turns each merged PR into small per-section *signals*, five first-class +section truths each subscribe to the one signal they care about and accumulate +pending edits, and a terminal actuator composes them into one PR against +`openprose/docs`. See `PIPELINE-DESIGN.md` for the why. + +``` +repo-changes (gateway) ~free: fold merged-PR deltas (paths + diffstat, no diff) + | + change-signal (cheap classifier: reads ONLY the inline changed-path list) + | emits per-section facets: + ├─ #cli-signal ───────► cli-docs + ├─ #sdk-signal ───────► sdk-docs + ├─ #reactor-signal ───► reactor-docs + ├─ #devtools-signal ──► devtools-docs + └─ #openprose-signal ─► openprose-docs + | each: pending edits (page + why + cited PR) + docs-pr (one coalesced PR against openprose/docs) +``` + +The point is cost control by design: each section subscribes to its **own** signal +facet, so a PR that only touched `packages/reactor-cli/**` moves `#cli-signal` +only — cli-docs renders, the other four sections **memo-skip at zero cost**. A PR +that touched no doc-facing source (tests, CI, an internal refactor) emits no signal +at all and costs nothing past the cheap classify. The actuator wakes once per burst +and opens a single PR. + +The five public-docs sections each track one source area of the watched repo: + +| signal facet | docs section (`content/docs/`) | source it tracks | +|---|---|---| +| `#cli-signal` | `cli/` | `packages/reactor-cli/**` | +| `#sdk-signal` | `sdk/` | `packages/reactor/**` public API surface | +| `#reactor-signal` | `reactor/` | `packages/reactor/**` harness behavior + concepts | +| `#devtools-signal` | `reactor-devtools/` | `packages/reactor-devtools/**` | +| `#openprose-signal` | `openprose/` | `skills/open-prose/**`, `references/*.md` | + +Cost levers (all in the contracts): granular per-section facets; stable materiality +(volatile fields like scan time dropped, so a re-seen PR never propagates); `### +Continuity` declares the wake source semantically (no hand-coded cron); and each +upstream render's `### Invariants` lock it to its inline input (never run git, never +read the repo) so it is a small bounded transform, not an open-ended agent loop. The +one render whose job IS to act — `docs-pr` — is allowed to read the specific pages +it edits and the cited PRs, still bounded, never a repo crawl. One cheap global +model is adequate for the upstream transforms; true per-node model tiering (a +stronger model for the actuator) is tracked as `RB-NodeModel`. + +## Inspect (keyless, read-only) + +These read what is already on disk and need no model key: `doctor`, +`compile --check`, `status`, `topology`, `receipts list|cost`, `inspect`, `logs`. + +Actually running the reactor (the compile sessions and the renders) needs a model +key. `REACTOR_OFFLINE=1` forces the provider closed; it is **not** a keyless run at +the CLI — `reactor run --offline` just writes a `failed` receipt, because the +hermetic fake render is a programmatic SDK test seam, not a CLI mode. So compile and +run/serve with a real key; the read-only commands above stay keyless. + +## Run it (needs OPENROUTER_API_KEY) + +Feed the gateway one of two ways: the built-in `static` connector (fixture PR +deltas, no network — a deterministic demo) or `connectors.cjs` (a real scan of +`git log`). `serve` polls the connector and stages arrivals; a one-shot `run` only +drains what is already staged, so use `serve` (or `trigger`) to bring the PRs in. + +```sh +export OPENROUTER_API_KEY=sk-or-... +CLI="node /Users/sl/code/openprose/platform/external/prose/packages/reactor-cli/dist/cli.js" + +$CLI compile --project . # compile the topology + canonicalizers to the IR cache +$CLI topology --project . # repo-changes -> change-signal -> 5 truths -> docs-pr +$CLI serve --project . # polls the static connector, stages the fixtures, + # classifies + renders each touched section; ctrl-c once quiescent + +# inspect what happened: +$CLI status --project . # dispositions + cost +$CLI receipts list --project . # which sections rendered vs memo-skipped +$CLI receipts cost --project . # cost rolled up by surprise cause +``` + +With the four shipped fixtures you should see: PR #101 (CLI-only) renders +`change-signal` + `cli-docs`; PR #102 (CLI+SDK) renders `cli-docs` + `sdk-docs`; PR +#103 (openprose) renders `openprose-docs`; PR #104 (test-only) renders the classifier +and then nothing — every section memo-skips. `docs-pr` coalesces the burst into one +proposed PR. + +### Scan your real merges + +The real scanner ships as `connectors.cjs.example` (opt-in, so the static fixtures +are the default deterministic demo). To enable it: + +```sh +cp connectors.cjs.example connectors.cjs # keyed `github`, matching source_id +export DOCS_SYNTH_REPO=/path/to/the/prose/repo # defaults to this repo's git toplevel +``` + +It runs `git log --first-parent` on the watched repo since a persisted cursor, +extracts the PR number from each merge subject/body, and emits one arrival per +merged PR with the changed-file-path list and diffstat — never the full diff. A +present `connectors.cjs` takes precedence over the static connector. Point `serve` +at it the same way; each merged PR wakes only the sections it touches. + +### The opened PR (the actuator) + +`docs-pr` is the one node with an external effect. Live, it drafts the `.mdx` +changes for the pages its pending edits name, writes them to a branch on a local +clone of `openprose/docs`, and opens one PR (via `git` + `gh`) listing the source +PRs it addresses. The branch name is derived from the cited PR numbers, so a re-run +updates the same branch instead of opening duplicates. It never pushes to the docs +default branch and never touches the watched code repo. Under `REACTOR_OFFLINE` — +or with `DOCS_SYNTH_DRY_RUN=1` — it runs dry: it writes the proposed unified diff +into its world-model instead of touching git/`gh`/network. The committed `replay/` +is produced by a keyed run with `DOCS_SYNTH_DRY_RUN=1`, so the renders are real but +no live PR is opened. + +## Validate without a live run: the eval harness + +The repeatable, **keyless** validation is the reactor eval harness +(`tools/eval-harness/`), not an ad-hoc live run. It turns a committed example run (a +`replay/` state-dir: receipts + world-models + topology) into a normalized +trajectory, applies a deterministic checker (no model — proves the selective-wake / +cost-scales-with-surprise property: a CLI-only PR renders cli-docs and **skips** +sdk/reactor/devtools/openprose; a test-only PR renders nothing downstream), and — +only when an OpenRouter key is resolvable and `REACTOR_OFFLINE` is unset — runs a +judge panel that grades the produced pending edits and proposed PR against this +example's `### Maintains` postconditions (every pending edit cites a PR and names a +real page; the branch name is a deterministic function of the cited PRs). + +```sh +# deterministic, offline, zero spend: +REACTOR_OFFLINE=1 node ../../../../tools/eval-harness/cli.mjs \ + --example docs-synthesis=./replay --scenarios cold_start,no_change_replay + +# add the LLM judge panel (needs key budget; off by default): +node ../../../../tools/eval-harness/cli.mjs --example docs-synthesis=./replay +``` + +The `replay/` is produced once from a `serve`/`run` of this project (needs a key); +thereafter the harness replays + judges it with no re-run. diff --git a/packages/reactor-cli/examples/docs-synthesis/change-signal.prose.md b/packages/reactor-cli/examples/docs-synthesis/change-signal.prose.md new file mode 100644 index 00000000..8cf13cea --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/change-signal.prose.md @@ -0,0 +1,90 @@ +--- +name: change-signal +kind: responsibility +--- + +### Goal + +For each changed PR, classify its inline evidence into small, typed, per-section +doc-impact signals so each downstream documentation section wakes only when its +own source was touched. This is the cheap gatekeeper of the pipeline: it does not +read the diff, draft prose, or open the repo — it maps the changed file paths (and +the title and body) to at most a one-line signal per affected doc section, or +nothing. + +The public docs site has five sections, each tracking one source area of the +watched repo. Route by changed path: + +- `packages/reactor-cli/**` → the `cli` section. +- `packages/reactor/**` that changes the public API surface (the package exports, + the front door, public type signatures) → the `sdk` section. +- `packages/reactor/**` that changes harness behavior or concepts (compile, the + world-model, fingerprints, the reconciler, receipts, continuity, ingestion) → + the `reactor` section. +- `packages/reactor-devtools/**` → the `reactor-devtools` section. +- `skills/open-prose/**` or `references/*.md` → the `openprose` section. + +A single PR may touch several sections (emit a signal for each) or none (emit +nothing). Changes confined to tests, fixtures, CI, build config, or internal +refactors with no doc-facing surface emit no signal at all. + +### Requires + +- the changes facet of repo-changes (each PR's change receipt: number, title, + body, the delimited changed-file-path list, and the diffstat) + +### Maintains + +A set of per-PR doc-impact signals, split into five independently-subscribable +facets so a PR that touches only one source area moves only that one facet. A +facet is absent for a PR when that PR touches no source for that section, so an +unrelated PR never moves an unrelated section. Material per signal: its PR number +and a single short line naming the doc-impact (what changed and which section is +now at risk of being stale). The fetch time and the diffstat byte counts are +immaterial and are excluded from the fingerprint. + +Postconditions: every emitted signal carries a PR number; a signal is emitted only +when the PR's changed paths actually touch that section's source; a PR whose +changes are purely test/CI/build emits no signals at all. + +#### cli-signal +Per PR, a one-line doc-impact for the `cli` section, when the PR touched +`packages/reactor-cli/**`. Absent otherwise. Subscribed to by cli-docs. + +#### sdk-signal +Per PR, a one-line doc-impact for the `sdk` section, when the PR changed the +public API surface of `packages/reactor/**` (exports, front door, public +signatures). Absent otherwise. Subscribed to by sdk-docs. + +#### reactor-signal +Per PR, a one-line doc-impact for the `reactor` section, when the PR changed +harness behavior or concepts in `packages/reactor/**` (compile, world-model, +fingerprints, reconciler, receipts, continuity, ingestion). Absent otherwise. +Subscribed to by reactor-docs. + +#### devtools-signal +Per PR, a one-line doc-impact for the `reactor-devtools` section, when the PR +touched `packages/reactor-devtools/**`. Absent otherwise. Subscribed to by +devtools-docs. + +#### openprose-signal +Per PR, a one-line doc-impact for the `openprose` section, when the PR touched +`skills/open-prose/**` or `references/*.md`. Absent otherwise. Subscribed to by +openprose-docs. + +### Continuity + +- input-driven: re-classify a PR when its fingerprint moves in the gateway set. A + PR whose fingerprint did not move is skipped at zero cost. + +### Invariants + +- This render is a bounded transform over the change receipt already carried in + the wake evidence. Read ONLY that inline evidence (the changed-file-path list, + the title, the body, the diffstat) and this node's prior world-model. Never run + git, open the diff, read the repository or node_modules, scan the filesystem, or + run shell commands. Complete in a few steps. +- Classify into the five fixed section shapes only; emit at most one short line per + facet per PR, and emit nothing for a section the PR's paths do not touch. Do not + draft documentation, do not summarize the diff. +- The only writable surface is this node's published world-model. diff --git a/packages/reactor-cli/examples/docs-synthesis/cli-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/cli-docs.prose.md new file mode 100644 index 00000000..6897ab1c --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/cli-docs.prose.md @@ -0,0 +1,52 @@ +--- +name: cli-docs +kind: responsibility +--- + +### Goal + +Maintain the set of pending documentation edits for the `cli` section of the +public docs site (`content/docs/cli/`), each drawn from a merged PR that touched +the CLI source and each traceable to that PR. This node produces pointers — which +page is now at risk and why — not the edits themselves; drafting the actual `.mdx` +change is the actuator's job. + +The `cli` section has these pages (target a pending edit at one of them): overview, +quickstart, configuration, compile-run-serve, connectors-and-sandbox, +observability, telemetry, command-reference. + +### Requires + +- the cli-signal facet of change-signal (the only wake source: a cli doc-impact + signal moved) + +### Maintains + +The pending-edit set for the `cli` section. Material: the edit set and, per edit, +its stable id, the target page slug (one of the section's pages), a one-line +description of what is now stale and how to update it, and the PR number it was +derived from; plus the open count. The recorded time is immaterial. The set moves +only when an edit's target, description, or status changes, or an edit is added or +cleared; a PR that introduces no new cli doc-impact leaves the set unchanged. + +Postcondition: every pending edit cites the PR number it was derived from and names +a page that exists in the `cli` section. + +#### pending-edits +The pending edits for the `cli` section, each with its target page, description, +and cited PR. Subscribed to by docs-pr. + +### Continuity + +- input-driven: wake only when the cli-signal facet moves. A PR that touched no CLI + source never moves this input, so this section stays silent for it (memo-skip at + zero cost). + +### Invariants + +- This render is a bounded transform over the cli-signal it was woken with and this + node's prior pending-edit set. Add, update, or clear the one affected edit; pick + the target page from the section's known page list above. Never run git, read the + docs repository, scan the filesystem, or run shell commands. Complete in a few + steps. +- The only writable surface is this node's published world-model. diff --git a/packages/reactor-cli/examples/docs-synthesis/connectors.cjs.example b/packages/reactor-cli/examples/docs-synthesis/connectors.cjs.example new file mode 100644 index 00000000..9a0710da --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/connectors.cjs.example @@ -0,0 +1,144 @@ +// connectors.cjs — REAL merged-PR scanner over `git log` (opt-in live path). +// +// Keyed `github` to match `source_id: github` in reactor.yml. When this file is +// present alongside reactor.yml, the plugin connector for source_id `github` wins +// over the built-in `static` connector at run/serve time (except under the offline +// gate, which injects a fake fetch and ignores the plugin). +// +// fetch(req): run `git log` on the watched repo for first-parent merges to the +// default branch since a persisted cursor (last-seen commit sha), and return ONE +// arrival per merged PR with the inline evidence change-signal needs: number, +// title, body, the semicolon-delimited changed-file-path list, and a diffstat. +// It never returns the full diff. +// extract(payload): the arrivals are already { id, item }; pass them through. +// +// Plain Node, no dependencies. Point it at the repo whose merges drive the public +// docs: by default the prose monorepo this example lives in. Override with +// DOCS_SYNTH_REPO=/path/to/repo. + +const fs = require("fs"); +const path = require("path"); +const { execFileSync } = require("child_process"); + +const CURSOR_PATH = path.join(__dirname, ".reactor", "git-scan-cursor.json"); +const BODY_MAX = 600; +const MAX_PRS_PER_POLL = 25; // cap a cold scan so one poll stays bounded + +function repoRoot() { + if (process.env.DOCS_SYNTH_REPO) return process.env.DOCS_SYNTH_REPO; + // default: the git toplevel containing this example + try { + return git(["rev-parse", "--show-toplevel"], __dirname).trim(); + } catch { + return __dirname; + } +} + +function git(args, cwd) { + return execFileSync("git", args, { cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 }); +} + +function loadCursor() { + try { + return JSON.parse(fs.readFileSync(CURSOR_PATH, "utf8")); + } catch { + return {}; + } +} + +function saveCursor(map) { + try { + fs.mkdirSync(path.dirname(CURSOR_PATH), { recursive: true }); + fs.writeFileSync(CURSOR_PATH, JSON.stringify(map, null, 2)); + } catch { + // best-effort cursor; a failed write just means a re-scan next poll + } +} + +// Pull a PR number out of a merge commit subject/body: +// "Merge pull request #123 from ..." or "Squash title (#123)" +function prNumber(subject, body) { + const m = (subject + "\n" + body).match(/(?:pull request #|\(#)(\d+)\)?/); + return m ? Number(m[1]) : null; +} + +function scan() { + const root = repoRoot(); + const prev = loadCursor(); + const sinceSha = prev[root]; + + // First-parent log: each entry is a merge (or squash) onto the default branch. + // Record separator \x1e, field separator \x1f. + const range = sinceSha ? `${sinceSha}..HEAD` : "-30"; // cold start: last 30 + const fmt = "%H%x1f%s%x1f%b%x1e"; + let out = ""; + try { + const args = ["log", "--first-parent", `--pretty=format:${fmt}`]; + if (sinceSha) args.push(range); + else args.push("-n", "30"); + out = git(args, root); + } catch { + return { arrivals: [], next: prev }; // not a git repo / git unavailable + } + + const records = out.split("\x1e").map((r) => r.trim()).filter(Boolean); + const arrivals = []; + let newestSha = sinceSha; + + for (const rec of records.slice(0, MAX_PRS_PER_POLL)) { + const [sha, subject = "", body = ""] = rec.split("\x1f"); + if (!newestSha) newestSha = sha; // records are newest-first + const number = prNumber(subject, body); + if (number === null) continue; // not a PR merge — skip + + let files = ""; + let diffstat = ""; + try { + files = git(["show", "--name-only", "--pretty=format:", sha], root) + .split("\n") + .map((l) => l.trim()) + .filter(Boolean) + .join(";"); + const stat = git(["show", "--stat", "--pretty=format:", sha], root) + .trim() + .split("\n") + .filter(Boolean); + diffstat = stat.length ? stat[stat.length - 1].trim() : ""; + } catch { + // a malformed/empty commit — emit identity only + } + + arrivals.push({ + id: `pr-${number}`, + item: { + number, + title: subject, + body: body.slice(0, BODY_MAX), + files, + diffstat, + merge_sha: sha.slice(0, 7), + fingerprint: `pr-${number}:${sha.slice(0, 7)}`, + }, + }); + } + + const next = { ...prev }; + if (newestSha) next[root] = newestSha; + return { arrivals, next }; +} + +module.exports = { + connectors: { + github: { + // fetch returns the raw merged-PR arrivals; the cursor is persisted here so + // a re-poll dedups even before the host cursor sees it. + fetch: async () => { + const { arrivals, next } = scan(); + saveCursor(next); + return arrivals; // already in { id, item } shape + }, + // arrivals are already { id, item }; pass them through unchanged. + extract: (payload) => (Array.isArray(payload) ? payload : []), + }, + }, +}; diff --git a/packages/reactor-cli/examples/docs-synthesis/devtools-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/devtools-docs.prose.md new file mode 100644 index 00000000..abe31680 --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/devtools-docs.prose.md @@ -0,0 +1,53 @@ +--- +name: devtools-docs +kind: responsibility +--- + +### Goal + +Maintain the set of pending documentation edits for the `reactor-devtools` section +of the public docs site (`content/docs/reactor-devtools/`), each drawn from a +merged PR that touched the devtools source and each traceable to that PR. This +node produces pointers — which page is now at risk and why — not the edits +themselves; drafting the actual `.mdx` change is the actuator's job. + +The `reactor-devtools` section has these pages (target a pending edit at one of +them): index, quickstart, state-dirs-and-replay, the-viewer, describe, recording, +reference. + +### Requires + +- the devtools-signal facet of change-signal (the only wake source: a devtools + doc-impact signal moved) + +### Maintains + +The pending-edit set for the `reactor-devtools` section. Material: the edit set +and, per edit, its stable id, the target page slug (one of the section's pages), a +one-line description of what is now stale and how to update it, and the PR number +it was derived from; plus the open count. The recorded time is immaterial. The set +moves only when an edit's target, description, or status changes, or an edit is +added or cleared; a PR that introduces no new devtools doc-impact leaves the set +unchanged. + +Postcondition: every pending edit cites the PR number it was derived from and names +a page that exists in the `reactor-devtools` section. + +#### pending-edits +The pending edits for the `reactor-devtools` section, each with its target page, +description, and cited PR. Subscribed to by docs-pr. + +### Continuity + +- input-driven: wake only when the devtools-signal facet moves. A PR that touched + no devtools source never moves this input, so this section stays silent for it + (memo-skip at zero cost). + +### Invariants + +- This render is a bounded transform over the devtools-signal it was woken with and + this node's prior pending-edit set. Add, update, or clear the one affected edit; + pick the target page from the section's known page list above. Never run git, + read the docs repository, scan the filesystem, or run shell commands. Complete in + a few steps. +- The only writable surface is this node's published world-model. diff --git a/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md b/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md new file mode 100644 index 00000000..3317b4d8 --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md @@ -0,0 +1,75 @@ +--- +name: docs-pr +kind: responsibility +--- + +### Goal + +Turn the pending documentation edits from the five sections into one pull request +against the public docs repository (`openprose/docs`), coalesced once per burst. +This is the terminal actuator: unlike the upstream transforms, its job IS to act — +to read the specific pages its proposals name, draft the concrete `.mdx` changes, +and open a single PR. It runs only when there is real doc work, so its cost is paid +only on genuine surprise. + +For each pending edit, draft the change to its target page under +`content/docs/
/.mdx`, preserving the Fumadocs frontmatter +convention (a `title` and a `description` only) and the section's existing voice. +Write all drafted changes onto one branch of a local clone of the docs repo, +commit, and open one PR that lists the source PRs it was derived from. When live, +use git and the GitHub CLI (`gh`) for the branch, commit, and PR. Derive the branch +name deterministically from the set of cited source-PR numbers, so a re-run with +the same pending edits updates the same branch instead of opening a duplicate PR. + +### Requires + +- the pending-edits facet of cli-docs +- the pending-edits facet of sdk-docs +- the pending-edits facet of reactor-docs +- the pending-edits facet of devtools-docs +- the pending-edits facet of openprose-docs + +### Maintains + +A docs-update pull request. Material: the content hash of the proposed unified diff +across the touched `.mdx` pages, the deterministic branch name (derived from the +set of cited source-PR numbers), and the fingerprint tuple of the pending-edit +facets consumed in this render. Once a PR has been opened for a branch, its number +is material too. The generated time and any local clone path are immaterial. The PR +is a derived projection: it is fingerprinted over the structured pending-edit +snapshot plus the diff hash, never over free-form prose, so a re-render of +identical pending edits produces an identical fingerprint and opens no second PR. + +Postcondition: the branch name is a deterministic function of the cited source-PR +numbers; every touched page exists under its section; the diff hash is computed +from the bytes actually drafted. + +#### opened-pr +The opened pull request: its branch name, the set of source-PR numbers it +addresses, and (once live and opened) its PR number and url. + +### Continuity + +- input-driven, coalesced: wake when any of the five pending-edits facets moves. + When several sections' pending edits move in the same burst, render once after + the upstream receipts settle, not once per section, so one burst yields one PR. +- memo-skip: if all subscribed input fingerprints are unchanged, open nothing and + publish nothing. + +### Invariants + +- This render's job is to act, so — unlike the upstream transforms — it MAY read + the specific `.mdx` pages its pending edits name and the cited source PRs' actual + changes. It is still bounded: read ONLY those named pages and cited PRs, never a + full docs-repo crawl, a full code-repo crawl, or node_modules. Compose the diff + FROM the pending-edit pointers it was woken with; do not re-derive the section + truths. +- The only writable external surface is a branch on a local clone of the docs + repository and the pull request opened from it. Never push to or modify the + watched code repository, and never push to the docs repository's default branch. +- Dry-run guard: when `REACTOR_OFFLINE` is set or `DOCS_SYNTH_DRY_RUN` is truthy, + do not run git, `gh`, or any network; instead write the proposed unified diff and + the deterministic branch name into this node's world-model as a dry-run plan. The + reactive core (classify, accumulate, coalesce) is identical in both modes; only + this terminal effect is gated. The committed `replay/` is produced by a keyed run + with `DOCS_SYNTH_DRY_RUN=1`, so the renders are real but no live PR is opened. diff --git a/packages/reactor-cli/examples/docs-synthesis/openprose-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/openprose-docs.prose.md new file mode 100644 index 00000000..3b4d053b --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/openprose-docs.prose.md @@ -0,0 +1,52 @@ +--- +name: openprose-docs +kind: responsibility +--- + +### Goal + +Maintain the set of pending documentation edits for the `openprose` section of the +public docs site (`content/docs/openprose/`), each drawn from a merged PR that +touched the language skill or references and each traceable to that PR. This node +produces pointers — which page is now at risk and why — not the edits themselves; +drafting the actual `.mdx` change is the actuator's job. + +The `openprose` section has these pages (target a pending edit at one of them): +index, declare-outcomes, contracts, prosescript, typed-image, harness-agnostic, +setup. + +### Requires + +- the openprose-signal facet of change-signal (the only wake source: an openprose + doc-impact signal moved) + +### Maintains + +The pending-edit set for the `openprose` section. Material: the edit set and, per +edit, its stable id, the target page slug (one of the section's pages), a one-line +description of what is now stale and how to update it, and the PR number it was +derived from; plus the open count. The recorded time is immaterial. The set moves +only when an edit's target, description, or status changes, or an edit is added or +cleared; a PR that introduces no new openprose doc-impact leaves the set unchanged. + +Postcondition: every pending edit cites the PR number it was derived from and names +a page that exists in the `openprose` section. + +#### pending-edits +The pending edits for the `openprose` section, each with its target page, +description, and cited PR. Subscribed to by docs-pr. + +### Continuity + +- input-driven: wake only when the openprose-signal facet moves. A PR that touched + no language skill or references never moves this input, so this section stays + silent for it (memo-skip at zero cost). + +### Invariants + +- This render is a bounded transform over the openprose-signal it was woken with + and this node's prior pending-edit set. Add, update, or clear the one affected + edit; pick the target page from the section's known page list above. Never run + git, read the docs repository, scan the filesystem, or run shell commands. + Complete in a few steps. +- The only writable surface is this node's published world-model. diff --git a/packages/reactor-cli/examples/docs-synthesis/reactor-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/reactor-docs.prose.md new file mode 100644 index 00000000..1f6b65bd --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/reactor-docs.prose.md @@ -0,0 +1,52 @@ +--- +name: reactor-docs +kind: responsibility +--- + +### Goal + +Maintain the set of pending documentation edits for the `reactor` section of the +public docs site (`content/docs/reactor/`), each drawn from a merged PR that +changed harness behavior or concepts and each traceable to that PR. This node +produces pointers — which page is now at risk and why — not the edits themselves; +drafting the actual `.mdx` change is the actuator's job. + +The `reactor` section has these pages (target a pending edit at one of them): +index, the-dag-and-compile, world-model-and-fingerprints, reconciler-and-receipts, +continuity-and-ingestion. + +### Requires + +- the reactor-signal facet of change-signal (the only wake source: a reactor + doc-impact signal moved) + +### Maintains + +The pending-edit set for the `reactor` section. Material: the edit set and, per +edit, its stable id, the target page slug (one of the section's pages), a one-line +description of what is now stale and how to update it, and the PR number it was +derived from; plus the open count. The recorded time is immaterial. The set moves +only when an edit's target, description, or status changes, or an edit is added or +cleared; a PR that introduces no new reactor doc-impact leaves the set unchanged. + +Postcondition: every pending edit cites the PR number it was derived from and names +a page that exists in the `reactor` section. + +#### pending-edits +The pending edits for the `reactor` section, each with its target page, +description, and cited PR. Subscribed to by docs-pr. + +### Continuity + +- input-driven: wake only when the reactor-signal facet moves. A PR that changed no + harness behavior never moves this input, so this section stays silent for it + (memo-skip at zero cost). + +### Invariants + +- This render is a bounded transform over the reactor-signal it was woken with and + this node's prior pending-edit set. Add, update, or clear the one affected edit; + pick the target page from the section's known page list above. Never run git, + read the docs repository, scan the filesystem, or run shell commands. Complete in + a few steps. +- The only writable surface is this node's published world-model. diff --git a/packages/reactor-cli/examples/docs-synthesis/reactor.yml b/packages/reactor-cli/examples/docs-synthesis/reactor.yml new file mode 100644 index 00000000..d71eb882 --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/reactor.yml @@ -0,0 +1,61 @@ +# docs-synthesis — merged PRs in the prose monorepo piped into docs-update PRs. +# +# A `repo-changes` GATEWAY accepts merged-PR deltas (number, title, body, the +# changed-file-path list, and a diffstat — never the full diff). A `change-signal` +# responsibility classifies each PR's changed paths into per-section doc-impact +# signals, then five section truths (cli-docs, sdk-docs, reactor-docs, +# devtools-docs, openprose-docs) each subscribe to the one signal facet they care +# about and accumulate pending edits, and a `docs-pr` actuator composes the pending +# edits across sections into ONE pull request against openprose/docs. Unrelated PRs +# memo-skip the sections they do not touch. +# +# Offline: the built-in `static` connector below ships fixture PR deltas, so +# `reactor serve` (or `reactor trigger`) ingests them with no key and no network. +# Live: drop in connectors.cjs (keyed `github`) to read merged PRs from `git log`. +state: + dir: ./.reactor + +model: + # ONE cheap global model for the whole pipeline. The tiered design makes every + # upstream render a small typed transform (classify changed paths, append one + # pending edit, coalesce facets), so a fast, cheap model is adequate there. The + # terminal `docs-pr` actuator is the one render that drafts real .mdx prose and + # would most benefit from a stronger model — but per-node model selection (a + # node's `### Runtime` model) is NOT yet honored by the CLI (tracked as + # RB-NodeModel), so today the actuator runs on this same global model and a human + # reviews the opened PR. The llm-as-judge tier uses an expensive model instead + # (configured in the eval harness, not here). + provider: openai + # Cheap model for the high-volume RENDERS (classify, accumulate, coalesce). + # Compile is the rare phase (once per contract change) and needs stricter + # structured-output fidelity, so it uses the medium model — amortized across the + # whole life of a stable contract. + render_model: gpt-5.4-mini + compile_model: gpt-5.4 + temperature: 0 + # Bounded renders: each upstream render is a short transform, not an open-ended + # agent loop. The cap is the backstop; the contracts' ### Invariants do the real + # bounding (read only the inline input; never run git or scan the repo). The + # actuator is allowed to read the specific pages it edits, also bounded. + max_turns: 24 + +sandbox: + mode: none + shell_timeout_ms: 300000 + +gateways: + - node: repo-changes + source_id: github + poll: 60s + connector: + type: static + id_field: id + # NOTE: the CLI's reactor.yml parser accepts inline FLOW maps/lists for + # connector items (one line), with scalar fields only — block-style list + # items and nested maps/arrays do not parse here. So `files` is a + # semicolon-delimited path list (a scalar), which change-signal splits. + # Four fixtures exercise selective wake: a CLI-only PR (moves cli-docs only), + # a CLI+SDK PR (moves cli-docs + sdk-docs), an openprose PR (moves + # openprose-docs only), and a test-only no-op PR (moves nothing downstream). + # Add more {...} entries (same line) for a fuller demo. + items: [{ id: pr-101, number: 101, title: "feat(cli): add anonymous opt-out telemetry", body: "Adds a REACTOR_TELEMETRY=0 opt-out and an anonymous usage ping. New env var and a note in the run path.", files: "packages/reactor-cli/src/telemetry.ts;packages/reactor-cli/src/commands/run.ts", diffstat: "2 files changed, 64 insertions", merge_sha: "a1b2c3d", fingerprint: "pr-101:a1b2c3d" }, { id: pr-102, number: 102, title: "feat: bring-your-own LLM provider configuration", body: "Lets users configure a custom provider/base_url/api_key_env. Adds a public adapter export and a CLI config block.", files: "packages/reactor/src/adapters/provider.ts;packages/reactor/src/index.ts;packages/reactor-cli/src/config.ts", diffstat: "3 files changed, 121 insertions, 8 deletions", merge_sha: "b2c3d4e", fingerprint: "pr-102:b2c3d4e" }, { id: pr-103, number: 103, title: "feat(openprose): add the Typed Image authoring surface", body: "Introduces the Typed Image authoring surface to the language skill and the spec reference.", files: "skills/open-prose/SKILL.md;references/01-Language.md", diffstat: "2 files changed, 210 insertions", merge_sha: "c3d4e5f", fingerprint: "pr-103:c3d4e5f" }, { id: pr-104, number: 104, title: "test: stabilize reconciler flake", body: "Flaky same-epoch test; adds a deterministic clock to the test harness. No behavior or API change.", files: "packages/reactor/src/__tests__/reconciler.test.ts;.github/workflows/ci.yml", diffstat: "2 files changed, 30 insertions, 12 deletions", merge_sha: "d4e5f6a", fingerprint: "pr-104:d4e5f6a" }] diff --git a/packages/reactor-cli/examples/docs-synthesis/repo-changes.prose.md b/packages/reactor-cli/examples/docs-synthesis/repo-changes.prose.md new file mode 100644 index 00000000..7415e9a8 --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/repo-changes.prose.md @@ -0,0 +1,44 @@ +--- +name: repo-changes +kind: gateway +--- + +### Goal + +Accept merged-PR deltas arriving from the edge and expose them as a materialized +set of changed PRs that responsibilities can subscribe to. A delta carries only a +PR's identity and a change receipt: its number, title, body, the set of changed +file paths (as a delimited list), and a one-line diffstat. It never carries the +full diff. + +### Maintains + +The set of changed PRs, folded from the external arrivals staged at the edge. +Material: the changed-PR set (unordered, keyed by PR number) and, for each PR, its +number and its change fingerprint. The scan time is immaterial and is excluded +from the fingerprint, so a re-poll that finds the same merged PR produces an +identical world-model fingerprint and the reconciler skips before any downstream +render runs. + +#### changes +The changed-PR set. Each PR is individually addressable by its number, so a +downstream node that subscribes to this set wakes when any PR's fingerprint moves. +Material per PR: the PR number, the change fingerprint, the title, the body, the +delimited changed-file-path list, and the diffstat — the inline evidence the +classifier needs and nothing more. + +### Continuity + +- external-driven: wake when a merged PR arrives at the gateway. A re-poll that + finds no moved fingerprint stages nothing and wakes nothing. + +### Invariants + +- This render is a bounded fold. Take exactly the arrivals already staged at the + edge and add or update their entries in the changed-PR set. Complete in a few + steps. +- The only readable input is the staged arrivals and this node's prior + world-model. Never run git, scan the filesystem, open the repository or + node_modules, or run shell commands to look for changes: the staged arrivals are + the only input. +- The only writable surface is this gateway's published world-model. diff --git a/packages/reactor-cli/examples/docs-synthesis/sdk-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/sdk-docs.prose.md new file mode 100644 index 00000000..7d18e9b6 --- /dev/null +++ b/packages/reactor-cli/examples/docs-synthesis/sdk-docs.prose.md @@ -0,0 +1,51 @@ +--- +name: sdk-docs +kind: responsibility +--- + +### Goal + +Maintain the set of pending documentation edits for the `sdk` section of the +public docs site (`content/docs/sdk/`), each drawn from a merged PR that changed +the SDK's public API surface and each traceable to that PR. This node produces +pointers — which page is now at risk and why — not the edits themselves; drafting +the actual `.mdx` change is the actuator's job. + +The `sdk` section has these pages (target a pending edit at one of them): index, +front-door, agents, adapters, run, internals. + +### Requires + +- the sdk-signal facet of change-signal (the only wake source: an sdk doc-impact + signal moved) + +### Maintains + +The pending-edit set for the `sdk` section. Material: the edit set and, per edit, +its stable id, the target page slug (one of the section's pages), a one-line +description of what is now stale and how to update it, and the PR number it was +derived from; plus the open count. The recorded time is immaterial. The set moves +only when an edit's target, description, or status changes, or an edit is added or +cleared; a PR that introduces no new sdk doc-impact leaves the set unchanged. + +Postcondition: every pending edit cites the PR number it was derived from and names +a page that exists in the `sdk` section. + +#### pending-edits +The pending edits for the `sdk` section, each with its target page, description, +and cited PR. Subscribed to by docs-pr. + +### Continuity + +- input-driven: wake only when the sdk-signal facet moves. A PR that changed no + public SDK surface never moves this input, so this section stays silent for it + (memo-skip at zero cost). + +### Invariants + +- This render is a bounded transform over the sdk-signal it was woken with and this + node's prior pending-edit set. Add, update, or clear the one affected edit; pick + the target page from the section's known page list above. Never run git, read the + docs repository, scan the filesystem, or run shell commands. Complete in a few + steps. +- The only writable surface is this node's published world-model. From 11e87f6ad9ace3d85e4f109362d6be031cfdf7cc Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:47:08 -0700 Subject: [PATCH 2/4] feat(reactor-cli): make docs-synthesis actuator target concrete docs-pr now operates on the docs clone at $DOCS_SYNTH_DOCS_REPO with explicit git/gh steps (branch docs-synthesis/prs-, edit named pages, push, gh pr create). Generic and runnable; the dry-run guard is unchanged. Co-Authored-By: Claude Opus 4.8 --- .../examples/docs-synthesis/README.md | 5 ++-- .../examples/docs-synthesis/docs-pr.prose.md | 25 +++++++++++++------ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/reactor-cli/examples/docs-synthesis/README.md b/packages/reactor-cli/examples/docs-synthesis/README.md index 275d5397..2b650f69 100644 --- a/packages/reactor-cli/examples/docs-synthesis/README.md +++ b/packages/reactor-cli/examples/docs-synthesis/README.md @@ -106,8 +106,9 @@ at it the same way; each merged PR wakes only the sections it touches. `docs-pr` is the one node with an external effect. Live, it drafts the `.mdx` changes for the pages its pending edits name, writes them to a branch on a local -clone of `openprose/docs`, and opens one PR (via `git` + `gh`) listing the source -PRs it addresses. The branch name is derived from the cited PR numbers, so a re-run +clone of `openprose/docs` (the path in `DOCS_SYNTH_DOCS_REPO`), and opens one PR +(via `git` + `gh`) listing the source PRs it addresses. The branch name is derived +from the cited PR numbers, so a re-run updates the same branch instead of opening duplicates. It never pushes to the docs default branch and never touches the watched code repo. Under `REACTOR_OFFLINE` — or with `DOCS_SYNTH_DRY_RUN=1` — it runs dry: it writes the proposed unified diff diff --git a/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md b/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md index 3317b4d8..b609d022 100644 --- a/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md +++ b/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md @@ -12,14 +12,23 @@ to read the specific pages its proposals name, draft the concrete `.mdx` changes and open a single PR. It runs only when there is real doc work, so its cost is paid only on genuine surprise. -For each pending edit, draft the change to its target page under -`content/docs/
/.mdx`, preserving the Fumadocs frontmatter -convention (a `title` and a `description` only) and the section's existing voice. -Write all drafted changes onto one branch of a local clone of the docs repo, -commit, and open one PR that lists the source PRs it was derived from. When live, -use git and the GitHub CLI (`gh`) for the branch, commit, and PR. Derive the branch -name deterministically from the set of cited source-PR numbers, so a re-run with -the same pending edits updates the same branch instead of opening a duplicate PR. +Operate on a local clone of the docs repository at the path in the +`DOCS_SYNTH_DOCS_REPO` environment variable. For each pending edit, draft the +change to its target page under `content/docs/
/.mdx`, preserving the +Fumadocs frontmatter convention (a `title` and a `description` only) and the +section's existing voice. Then, when live (see the dry-run guard below): + +1. `cd "$DOCS_SYNTH_DOCS_REPO"` and ensure a clean checkout of the default branch. +2. Create one branch named `docs-synthesis/prs-` (e.g. + `docs-synthesis/prs-101-102-103`) — a deterministic function of the cited + source-PR numbers, so a re-run with the same pending edits reuses the branch + instead of opening a duplicate PR. +3. Write the drafted `.mdx` changes to the named pages, `git add` exactly those + pages, and commit with a message listing the source PRs. +4. `git push` the branch and open one PR with `gh pr create` (base = the docs + default branch), whose body lists each source PR and the page it touched. + +Capture the resulting branch name and PR url/number for the world-model. ### Requires From f0c71e4d41df6d656fb0c3063120b46b2be8d84a Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Sun, 7 Jun 2026 20:58:41 -0700 Subject: [PATCH 3/4] fix(reactor-cli): give docs-synthesis accumulators unique facet names All five section accumulators exposed a facet literally named `pending-edits`, so docs-pr's five requires were ambiguous and the *-pending -> docs-pr edges failed to wire. Rename to cli-pending / sdk-pending / reactor-pending / devtools-pending / openprose-pending (the same distinct-naming discipline agent-observatory uses for unambiguous wiring) and point docs-pr at each. Co-Authored-By: Claude Opus 4.8 --- .../examples/docs-synthesis/cli-docs.prose.md | 2 +- .../examples/docs-synthesis/devtools-docs.prose.md | 2 +- .../examples/docs-synthesis/docs-pr.prose.md | 10 +++++----- .../examples/docs-synthesis/openprose-docs.prose.md | 2 +- .../examples/docs-synthesis/reactor-docs.prose.md | 2 +- .../examples/docs-synthesis/sdk-docs.prose.md | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/reactor-cli/examples/docs-synthesis/cli-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/cli-docs.prose.md index 6897ab1c..b3e65cd6 100644 --- a/packages/reactor-cli/examples/docs-synthesis/cli-docs.prose.md +++ b/packages/reactor-cli/examples/docs-synthesis/cli-docs.prose.md @@ -32,7 +32,7 @@ cleared; a PR that introduces no new cli doc-impact leaves the set unchanged. Postcondition: every pending edit cites the PR number it was derived from and names a page that exists in the `cli` section. -#### pending-edits +#### cli-pending The pending edits for the `cli` section, each with its target page, description, and cited PR. Subscribed to by docs-pr. diff --git a/packages/reactor-cli/examples/docs-synthesis/devtools-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/devtools-docs.prose.md index abe31680..4187cb1a 100644 --- a/packages/reactor-cli/examples/docs-synthesis/devtools-docs.prose.md +++ b/packages/reactor-cli/examples/docs-synthesis/devtools-docs.prose.md @@ -33,7 +33,7 @@ unchanged. Postcondition: every pending edit cites the PR number it was derived from and names a page that exists in the `reactor-devtools` section. -#### pending-edits +#### devtools-pending The pending edits for the `reactor-devtools` section, each with its target page, description, and cited PR. Subscribed to by docs-pr. diff --git a/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md b/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md index b609d022..66c6177e 100644 --- a/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md +++ b/packages/reactor-cli/examples/docs-synthesis/docs-pr.prose.md @@ -32,11 +32,11 @@ Capture the resulting branch name and PR url/number for the world-model. ### Requires -- the pending-edits facet of cli-docs -- the pending-edits facet of sdk-docs -- the pending-edits facet of reactor-docs -- the pending-edits facet of devtools-docs -- the pending-edits facet of openprose-docs +- the cli-pending facet of cli-docs +- the sdk-pending facet of sdk-docs +- the reactor-pending facet of reactor-docs +- the devtools-pending facet of devtools-docs +- the openprose-pending facet of openprose-docs ### Maintains diff --git a/packages/reactor-cli/examples/docs-synthesis/openprose-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/openprose-docs.prose.md index 3b4d053b..ced3356a 100644 --- a/packages/reactor-cli/examples/docs-synthesis/openprose-docs.prose.md +++ b/packages/reactor-cli/examples/docs-synthesis/openprose-docs.prose.md @@ -32,7 +32,7 @@ cleared; a PR that introduces no new openprose doc-impact leaves the set unchang Postcondition: every pending edit cites the PR number it was derived from and names a page that exists in the `openprose` section. -#### pending-edits +#### openprose-pending The pending edits for the `openprose` section, each with its target page, description, and cited PR. Subscribed to by docs-pr. diff --git a/packages/reactor-cli/examples/docs-synthesis/reactor-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/reactor-docs.prose.md index 1f6b65bd..35417e65 100644 --- a/packages/reactor-cli/examples/docs-synthesis/reactor-docs.prose.md +++ b/packages/reactor-cli/examples/docs-synthesis/reactor-docs.prose.md @@ -32,7 +32,7 @@ cleared; a PR that introduces no new reactor doc-impact leaves the set unchanged Postcondition: every pending edit cites the PR number it was derived from and names a page that exists in the `reactor` section. -#### pending-edits +#### reactor-pending The pending edits for the `reactor` section, each with its target page, description, and cited PR. Subscribed to by docs-pr. diff --git a/packages/reactor-cli/examples/docs-synthesis/sdk-docs.prose.md b/packages/reactor-cli/examples/docs-synthesis/sdk-docs.prose.md index 7d18e9b6..12f6051c 100644 --- a/packages/reactor-cli/examples/docs-synthesis/sdk-docs.prose.md +++ b/packages/reactor-cli/examples/docs-synthesis/sdk-docs.prose.md @@ -31,7 +31,7 @@ cleared; a PR that introduces no new sdk doc-impact leaves the set unchanged. Postcondition: every pending edit cites the PR number it was derived from and names a page that exists in the `sdk` section. -#### pending-edits +#### sdk-pending The pending edits for the `sdk` section, each with its target page, description, and cited PR. Subscribed to by docs-pr. From d3d28297db181e92b5cc72aa86a286c2bdb78f9b Mon Sep 17 00:00:00 2001 From: irl-dan <97565471+irl-dan@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:28:30 -0700 Subject: [PATCH 4/4] docs(reactor-cli): make docs-synthesis gateway/classifier inputs explicit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Point repo-changes at its upstream `inbox.json` (the phantom ingress producer) and forbid git/filesystem inspection; point change-signal at the upstream `changes` records and their `files` field. Clarifies the intended data path — the connector stages PRs into the gateway's ingress, the gateway folds them, the classifier routes by changed path. NOTE (live-run finding): in the current CLI/SDK build the staged ingress inbox does NOT surface as a readable upstream to the gateway render (a render's readable upstream = its mounted inbound edges; the ingress is an unmounted producer), so an LLM gateway cannot actually fold connector arrivals. The canonical agent-observatory example exhibits the same empty result. Filed as feedback; this wording is the correct intent for when that path is wired. Co-Authored-By: Claude Opus 4.8 --- .../docs-synthesis/change-signal.prose.md | 7 ++++- .../docs-synthesis/repo-changes.prose.md | 31 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/reactor-cli/examples/docs-synthesis/change-signal.prose.md b/packages/reactor-cli/examples/docs-synthesis/change-signal.prose.md index 8cf13cea..e411d008 100644 --- a/packages/reactor-cli/examples/docs-synthesis/change-signal.prose.md +++ b/packages/reactor-cli/examples/docs-synthesis/change-signal.prose.md @@ -5,13 +5,18 @@ kind: responsibility ### Goal -For each changed PR, classify its inline evidence into small, typed, per-section +For each changed PR, classify its evidence into small, typed, per-section doc-impact signals so each downstream documentation section wakes only when its own source was touched. This is the cheap gatekeeper of the pipeline: it does not read the diff, draft prose, or open the repo — it maps the changed file paths (and the title and body) to at most a one-line signal per affected doc section, or nothing. +Read the `changes` set from your upstream (repo-changes) by reference. For each PR +record, split its `files` field on `;` to get the changed file paths, and route by +those paths (the title and body are only tie-breakers). Your input is these +upstream records — never run `git`, open a diff, or scan the filesystem. + The public docs site has five sections, each tracking one source area of the watched repo. Route by changed path: diff --git a/packages/reactor-cli/examples/docs-synthesis/repo-changes.prose.md b/packages/reactor-cli/examples/docs-synthesis/repo-changes.prose.md index 7415e9a8..dddbbe60 100644 --- a/packages/reactor-cli/examples/docs-synthesis/repo-changes.prose.md +++ b/packages/reactor-cli/examples/docs-synthesis/repo-changes.prose.md @@ -5,11 +5,19 @@ kind: gateway ### Goal -Accept merged-PR deltas arriving from the edge and expose them as a materialized -set of changed PRs that responsibilities can subscribe to. A delta carries only a -PR's identity and a change receipt: its number, title, body, the set of changed -file paths (as a delimited list), and a one-line diffstat. It never carries the -full diff. +Fold the merged-PR records staged in this node's upstream inbox into a +materialized set of changed PRs that responsibilities can subscribe to. + +Your ONLY input is your upstream ingress, which publishes a single file +`inbox.json`: a JSON array of merged-PR records. Each record has `number`, +`title`, `body`, `files` (a semicolon-delimited list of changed file paths), +`diffstat`, and `merge_sha`. Read `inbox.json` from your upstream by reference +(use your upstream-list / upstream-read tools — the ingress is your upstream +producer even though it carries no `### Requires` line), and add or update each +record in the `changes` set, keyed by PR number, carrying its number, title, body, +files, and diffstat forward as the change receipt. There is NO git repository, no +source tree, and no working-directory file to inspect — `inbox.json` is the only +source of truth. ### Maintains @@ -34,11 +42,10 @@ classifier needs and nothing more. ### Invariants -- This render is a bounded fold. Take exactly the arrivals already staged at the - edge and add or update their entries in the changed-PR set. Complete in a few - steps. -- The only readable input is the staged arrivals and this node's prior - world-model. Never run git, scan the filesystem, open the repository or - node_modules, or run shell commands to look for changes: the staged arrivals are - the only input. +- This render is a bounded fold over `inbox.json` from your upstream and this + node's prior world-model. Read `inbox.json`, fold each PR record into `changes`, + and finish in a few steps. +- Do NOT run `git`, `ls`, `find`, `grep`, or any shell command to discover + changes, and do NOT scan the filesystem or the working directory: there is no + repository here. The staged `inbox.json` from your upstream is the only input. - The only writable surface is this gateway's published world-model.