Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/reactor-cli/examples/docs-synthesis/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/.reactor/
/connectors.cjs
.serve.log
102 changes: 102 additions & 0 deletions packages/reactor-cli/examples/docs-synthesis/PIPELINE-DESIGN.md
Original file line number Diff line number Diff line change
@@ -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.
142 changes: 142 additions & 0 deletions packages/reactor-cli/examples/docs-synthesis/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# 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` (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
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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
name: change-signal
kind: responsibility
---

### Goal

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:

- `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.
52 changes: 52 additions & 0 deletions packages/reactor-cli/examples/docs-synthesis/cli-docs.prose.md
Original file line number Diff line number Diff line change
@@ -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.

#### cli-pending
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.
Loading
Loading