faberun 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +131 -0
  3. package/bin/faberun.mjs +25 -0
  4. package/integrations/claude-code/statusline-bench.sh +42 -0
  5. package/integrations/claude-code/statusline.sh +80 -0
  6. package/package.json +33 -0
  7. package/skills/faberun/SKILL.md +24 -0
  8. package/skills/faberun/references/contract.md +380 -0
  9. package/skills/faberun/references/engineering.md +29 -0
  10. package/skills/faberun/references/handoffs.md +26 -0
  11. package/skills/faberun/references/operations.md +184 -0
  12. package/skills/faberun/references/rules.md +35 -0
  13. package/skills/faberun/references/workflow.md +23 -0
  14. package/skills/init-agentkit/SKILL.md +108 -0
  15. package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
  16. package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
  17. package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
  18. package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
  19. package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
  20. package/skills/init-agentkit/templates/AGENTS.md +110 -0
  21. package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
  22. package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
  23. package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
  24. package/skills/init-agentkit/templates/docs/VISION.md +33 -0
  25. package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
  26. package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
  27. package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
  28. package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
  29. package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
  30. package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
  31. package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
  32. package/src/campaign/brief.mjs +394 -0
  33. package/src/campaign/chain.mjs +555 -0
  34. package/src/campaign/handoff.mjs +516 -0
  35. package/src/campaign/index.mjs +300 -0
  36. package/src/campaign/journal.mjs +347 -0
  37. package/src/campaign/layout.mjs +51 -0
  38. package/src/campaign/metrics-evals.mjs +25 -0
  39. package/src/campaign/metrics.mjs +517 -0
  40. package/src/campaign/projection.mjs +250 -0
  41. package/src/campaign/record.mjs +102 -0
  42. package/src/campaign/unpark.mjs +56 -0
  43. package/src/cli/brand.mjs +205 -0
  44. package/src/cli/campaign.mjs +730 -0
  45. package/src/cli/contract.mjs +67 -0
  46. package/src/cli/init.mjs +170 -0
  47. package/src/cli/launch.mjs +239 -0
  48. package/src/cli/seat.mjs +139 -0
  49. package/src/cli/setup.mjs +294 -0
  50. package/src/cli/skills.mjs +105 -0
  51. package/src/cli/update.mjs +216 -0
  52. package/src/cli.mjs +525 -0
  53. package/src/contract/articles.mjs +12 -0
  54. package/src/contract/assert.mjs +162 -0
  55. package/src/contract/definition-of-done.mjs +97 -0
  56. package/src/contract/final-verification.mjs +96 -0
  57. package/src/contract/index.mjs +641 -0
  58. package/src/contract/judge-envelope.mjs +25 -0
  59. package/src/contract/review-modes.mjs +151 -0
  60. package/src/contract/runtime.mjs +204 -0
  61. package/src/contract/schema-version.mjs +25 -0
  62. package/src/contract/scope-findings.mjs +77 -0
  63. package/src/contract/snapshot.mjs +639 -0
  64. package/src/contract/task-packet.mjs +495 -0
  65. package/src/contract/untrusted.mjs +75 -0
  66. package/src/contract/verification.mjs +185 -0
  67. package/src/contract/worker-result.mjs +138 -0
  68. package/src/engine/assignment.mjs +63 -0
  69. package/src/engine/backoff.mjs +492 -0
  70. package/src/engine/bulk-read.mjs +361 -0
  71. package/src/engine/cancel.mjs +177 -0
  72. package/src/engine/detach.mjs +101 -0
  73. package/src/engine/dispatch.mjs +752 -0
  74. package/src/engine/failover.mjs +192 -0
  75. package/src/engine/gate.mjs +183 -0
  76. package/src/engine/judge-gate.mjs +517 -0
  77. package/src/engine/lifecycle.mjs +772 -0
  78. package/src/engine/live-preflight.mjs +299 -0
  79. package/src/engine/mutation.mjs +146 -0
  80. package/src/engine/notify-queue.mjs +327 -0
  81. package/src/engine/process-identity.mjs +72 -0
  82. package/src/engine/process.mjs +774 -0
  83. package/src/engine/prompts.mjs +289 -0
  84. package/src/engine/recover.mjs +300 -0
  85. package/src/engine/result-file.mjs +222 -0
  86. package/src/engine/resume.mjs +635 -0
  87. package/src/engine/retry.mjs +334 -0
  88. package/src/engine/review.mjs +228 -0
  89. package/src/engine/run-command.mjs +287 -0
  90. package/src/engine/run-identity.mjs +411 -0
  91. package/src/engine/runtime-discovery.mjs +235 -0
  92. package/src/engine/scheduler.mjs +526 -0
  93. package/src/engine/scope.mjs +378 -0
  94. package/src/engine/settle.mjs +207 -0
  95. package/src/engine/state.mjs +148 -0
  96. package/src/engine/supervise.mjs +713 -0
  97. package/src/engine/verify.mjs +167 -0
  98. package/src/harnesses/agy/index.mjs +62 -0
  99. package/src/harnesses/catalogue.mjs +509 -0
  100. package/src/harnesses/claude/index.mjs +90 -0
  101. package/src/harnesses/codex/index.mjs +87 -0
  102. package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
  103. package/src/harnesses/dsh/index.mjs +210 -0
  104. package/src/harnesses/dsh/runner.mjs +259 -0
  105. package/src/harnesses/exec-jsonl/index.mjs +788 -0
  106. package/src/harnesses/index.mjs +508 -0
  107. package/src/harnesses/protocol.mjs +531 -0
  108. package/src/harnesses/replay/bin.mjs +386 -0
  109. package/src/harnesses/replay/index.mjs +238 -0
  110. package/src/harnesses/zcode/index.mjs +276 -0
  111. package/src/host/config.mjs +87 -0
  112. package/src/host/home.mjs +149 -0
  113. package/src/host/package.mjs +23 -0
  114. package/src/host/preflight.mjs +520 -0
  115. package/src/host/tool-policy-decisions.mjs +341 -0
  116. package/src/host/tool-policy-hook.mjs +270 -0
  117. package/src/notify/index.mjs +359 -0
  118. package/src/notify/os-macos.mjs +81 -0
  119. package/src/repo/declared-paths.mjs +220 -0
  120. package/src/repo/integrate.mjs +546 -0
  121. package/src/repo/scope-closure.mjs +665 -0
  122. package/src/repo/signal-block.mjs +16 -0
  123. package/src/repo/signal.mjs +222 -0
  124. package/src/repo/source-identity.mjs +295 -0
  125. package/src/repo/workspace.mjs +557 -0
  126. package/src/repo/worktree.mjs +352 -0
  127. package/src/report/final.mjs +200 -0
  128. package/src/report/metrics-report.mjs +99 -0
  129. package/src/report/next.mjs +383 -0
  130. package/src/report/render.mjs +716 -0
  131. package/src/run/disk-gc.mjs +251 -0
  132. package/src/run/lock.mjs +329 -0
  133. package/src/run/node-store.mjs +62 -0
  134. package/src/run/operations.mjs +286 -0
  135. package/src/run/store.mjs +187 -0
  136. package/src/run/usage.mjs +337 -0
  137. package/src/seat/harnesses.mjs +83 -0
  138. package/src/seat/index.mjs +239 -0
  139. package/src/seat/tmux.mjs +208 -0
  140. package/src/util.mjs +0 -0
  141. package/src/web/api.mjs +371 -0
  142. package/src/web/boundary.mjs +88 -0
  143. package/src/web/index.html +299 -0
  144. package/src/web/server.mjs +552 -0
@@ -0,0 +1,380 @@
1
+ # Contract reference
2
+
3
+ Node.js 22+, plain ESM `.mjs`; TypeScript is development-only (`npm run
4
+ typecheck`). Schema version is `3`.
5
+
6
+ ## Shape
7
+
8
+ ```json
9
+ {
10
+ "schemaVersion": 3,
11
+ "contractVersion": "0.3.0",
12
+ "id": "feature-42",
13
+ "campaignId": "feature-42",
14
+ "goal": "Deliver feature 42 with tests",
15
+ "cwd": "../target-repo",
16
+ "maxParallel": 1,
17
+ "stallTimeoutSec": 300,
18
+ "timeoutSec": 2400,
19
+ "runtimeDefaults": { "worker": "flash", "judge": "sol" },
20
+ "runtimes": {
21
+ "flash": { "harness": "dsh", "model": "deepseek-flash", "reasoning": "high",
22
+ "vendor": "deepseek", "sandbox": "danger-full-access",
23
+ "config": { "provider": "deepseek-official", "api_key.env_key": "DEEPSEEK_API_KEY" } },
24
+ "luna": { "harness": "codex", "model": "gpt-5.6-luna", "reasoning": "xhigh" },
25
+ "sol": { "harness": "codex", "model": "gpt-5.6-sol", "reasoning": "xhigh", "vendor": "openai-sol" },
26
+ "opus": { "harness": "claude", "model": "opus", "permissionMode": "acceptEdits" },
27
+ "zcode-flash": { "harness": "zcode", "model": "glm-5.3-flash", "vendor": "zhipu-flash", "permissionMode": "edit" },
28
+ "zcode-pro": { "harness": "zcode", "model": "glm-5.3", "vendor": "zhipu-pro", "permissionMode": "plan" },
29
+ "agy-flash": { "harness": "agy", "model": "gemini-3.8-flash-low" }
30
+ },
31
+ "nodes": [
32
+ {
33
+ "id": "implementation", "type": "backend", "phase": "implementation",
34
+ "taskPacketFile": "packets/implementation.json", "dependsOn": [], "timeoutSec": 2400,
35
+ "definitionOfDone": [
36
+ { "id": "behavior-implemented", "text": "The requested behavior is implemented",
37
+ "proof": { "kind": "command", "ref": "npm test" } },
38
+ { "id": "diff-scoped", "text": "No unrelated files changed",
39
+ "proof": { "kind": "path", "ref": "src/feature-42.ts" } },
40
+ { "id": "design-honored", "text": "The change honors the stated design decisions", "judgment": true }
41
+ ],
42
+ "gate": { "failOn": ["major", "critical"], "maxRevisions": 1 }
43
+ }
44
+ ]
45
+ }
46
+ ```
47
+
48
+ Every `definitionOfDone` item declares `id`, `text`, and how it is proven:
49
+ `proof.kind` `command` (re-runs the command, capped at `min(timeoutSec, 120s)`)
50
+ or `path` (a file must exist), or `judgment: true` for the judge. `proof: {
51
+ kind: "verification", ref: <index> }` reuses a `verification` entry's already
52
+ recorded result by position instead of re-running it — never by comparing argv
53
+ strings, since a joined argv loses shell semantics. A schema-1 string item is
54
+ rejected. There is no spend ceiling in the schema: no `maxInputTokens`,
55
+ `maxCostUsd`, or `usagePolicy`. `timeoutSec` and `stallTimeoutSec` bound an
56
+ attempt; a spent allowance is handled by runtime re-tiering (below). `usage.jsonl` records
57
+ tokens and cost per invocation for **reporting only** — no control path reads
58
+ it.
59
+
60
+ ## Task packets
61
+
62
+ ```json
63
+ {
64
+ "mode": "execution",
65
+ "objective": "One concrete outcome",
66
+ "instructions": ["Exact behavior to implement"],
67
+ "readFiles": ["src/feature.ts"],
68
+ "writeFiles": ["src/feature.ts"],
69
+ "symbols": ["runContract"],
70
+ "decisions": ["Decision already made; do not reopen"],
71
+ "nonGoals": ["Explicitly excluded work"],
72
+ "verification": [{ "argv": ["node", "--test", "test/feature.test.mjs"] }]
73
+ }
74
+ ```
75
+
76
+ `mode` is `execution`, `discovery`, or `autonomous`. `objective`,
77
+ `instructions`, and `verification` are required and non-empty. An execution
78
+ packet requires non-empty `readFiles` and `writeFiles`; read paths are
79
+ relative to `cwd`, cannot escape it, and must exist at validation time —
80
+ except contract loading defers a missing `readFiles` entry a transitive
81
+ dependency declares in its `writeFiles`, or that sits under one of that
82
+ dependency's directory-shaped `writeRoots` entries (a file-shaped entry
83
+ authorizes only that exact path); every other caller still rejects the
84
+ missing read. A
85
+ discovery packet has empty `writeFiles`; with an empty `readFiles` it may
86
+ read the repository read-only to produce an execution packet — the one
87
+ exception to closed scope — otherwise it is closed to the listed files. Each
88
+ `verification` entry is `{argv, cwd?, timeoutSec? (default 120, max 600),
89
+ repeat? (default 1, max 8), env?}` — at most 32 commands, 64 argv items, 32
90
+ KiB argv bytes per command. `env` declares variable *names* only; values
91
+ never travel in the packet. `prompt`/`promptFile` are
92
+ rejected; a node has `taskPacket` or `taskPacketFile`, never both. Measure a
93
+ candidate command's real duration before naming it in `verification` or a
94
+ worker instruction — `preflight <contract.json> --time-verification` runs each
95
+ declared command once and fails the contract when it cannot fit that timeout.
96
+ A suite past 600s never fits: target what the change touches and run the whole
97
+ suite out of band.
98
+
99
+ An `autonomous` packet declares `writeRoots` instead of `writeFiles`:
100
+ whole-repo read, write bounded to the listed files/directories. Scope is
101
+ advisory, not a gate: a completed attempt whose worker result and
102
+ verification both pass keeps unexpected writes as a `scopeFindings` entry and
103
+ still reaches `done`; only a failed verification turns the unexpected paths
104
+ into part of the failure. Redirect a toolchain's cache/build output under
105
+ `.runs/` (git-ignored, outside the snapshot).
106
+ The workspace snapshot skips `.runs`, `.git`, `node_modules`, `.claude`,
107
+ `.codex` at the repository root.
108
+
109
+ The stored `contract.json` inlines every packet (dropping `taskPacketFile`
110
+ and the generated prompt) and carries a `packetHash` the runner validates on
111
+ load, so a run directory is a self-contained resumable record.
112
+
113
+ ## Worker results
114
+
115
+ ```json
116
+ {
117
+ "status": "done",
118
+ "summary": "Implemented the described behavior",
119
+ "changedFiles": ["src/feature.ts"],
120
+ "verification": ["node --test test/feature.test.mjs"],
121
+ "artifacts": [],
122
+ "missingContext": []
123
+ }
124
+ ```
125
+
126
+ `status` is `done` (empty `missingContext`) or `blocked_context` (at least one
127
+ `missingContext` entry) — the only response when the closed context is
128
+ missing something, never repository-wide exploration. Bounded: 32 KiB total,
129
+ 4 KiB summary, 32 entries each in `changedFiles`/`verification`/`artifacts`
130
+ (16 in `missingContext`), 2 KiB per entry (16 KiB per artifact). Unknown
131
+ provider-added fields are dropped; missing/malformed canonical fields are
132
+ rejected (`worker-result.mjs`). A discovery node returns `done` with exactly
133
+ one `artifacts` entry: the execution packet for the next node.
134
+
135
+ ## Runtimes and routing
136
+
137
+ Resolve a worker as `nodes[].runtime`, then `runtimeDefaults.worker`; a judge
138
+ as `nodes[].gate.runtime`, then `runtimeDefaults.judge`. When `runtimes` and
139
+ `runtimeDefaults` are both omitted, the factory composes them from the
140
+ discovery catalogue (`DISCOVERY_RUNTIME_DEFINITIONS`: `dsh-deepseek`,
141
+ `zcode-glm`, `agy-gemini` at tier 1, `codex-gpt` and `claude-sonnet` at tier 2;
142
+ available when the binary answers and every `config["*.env_key"]` it names is
143
+ set): the cheapest available runtime executes, the strongest runtime of a
144
+ *different vendor* judges, persisted in `routing.assignments`; no admissible
145
+ cross-vendor judge fails by name (`runtime_assignment_judge_unavailable`).
146
+
147
+ `harness` names the adapter that runs the turn (`claude`, `codex`, `agy`,
148
+ `dsh`, `zcode`, `exec-jsonl`, `replay`) and `model` what it asks; the two
149
+ vary independently — DeepSeek answers through `dsh`, GLM through `zcode`. Name
150
+ a runtime id `<harness>-<model>` so a recorded run says which harness produced
151
+ it; ids take letters, numbers, dot, underscore, dash only. Vendor is resolved (`resolveVendor` in `harnesses/index.mjs`), not the
152
+ harness name: an explicit `vendor`, else a provider-config override (a codex
153
+ runtime with `config.model_provider: "deepseek"` is vendor `deepseek`), else
154
+ the harness default (`claude`→anthropic, `codex`→openai, `agy`→google,
155
+ `zcode`→zhipu); `dsh`/`replay`/`exec-jsonl` have no default and must declare
156
+ `vendor`. Validation rejects a gate-enabled node whose worker and judge
157
+ resolve to the same vendor, and does the same for every runtime in the
158
+ worker's declared fallback chain (rejecting a cycle in that chain outright)
159
+ — all statically knowable from the contract alone. The symmetric case, a
160
+ judge fallback landing on the vendor of the worker runtime that actually ran,
161
+ cannot be checked statically (it depends on which worker runtime ran this
162
+ attempt) and is instead refused at execution; see Failover below. Two models of one family (a GLM 5.3-flash worker judged by GLM 5.3) pair only by
163
+ declaring distinct `vendor` strings — a claim about review independence.
164
+
165
+ An optional `runtimes[<id>].pricing` object declares `inputPerMTok`,
166
+ `cachedInputPerMTok`, and `outputPerMTok` (each finite and >= 0, at
167
+ least one required, unknown keys rejected) and prices that runtime's canonical
168
+ counters when the harness reports no cost; a missing counter stays `unknown`,
169
+ never zero.
170
+
171
+ Non-empty `taskPacket.verification` is rejected when the resolved worker or a
172
+ worker fallback cannot execute commands. Adapters declare
173
+ `permissionExecution`: `claude` only `bypassPermissions` (default
174
+ `acceptEdits`), `zcode` only `yolo` (also default), `dsh` both its default
175
+ `workspace-write` (measured: executes and writes inside the worktree) and
176
+ `danger-full-access` (only for effects outside it); every `codex` sandbox mode
177
+ executes, and `agy`/`exec-jsonl`/`replay` expose no denying mode. Judge modes
178
+ are excluded because judges review captured results.
179
+
180
+ - `claude`: `permissionMode` (default `acceptEdits`; a node that runs
181
+ commands needs `bypassPermissions`, since headless `acceptEdits` denies
182
+ execution and the worker can only return `blocked_context`). Executable
183
+ override: `executable` or `FABERUN_CLAUDE_BIN`. It disables slash
184
+ commands, MCP, and settings files on every invocation and restricts tools to
185
+ `runtime.tools` (default `Read, Edit, Write, Bash, Glob, Grep`); `--bare` is
186
+ never used because it also disables the tool-policy hook.
187
+ - `codex`: `sandbox` (`read-only`, `workspace-write` default,
188
+ `danger-full-access`); arbitrary `config` entries serialize as `-c
189
+ key=value`; disables browser/computer-use/app/sub-agent tooling and MCP by
190
+ default (`CODEX_PREAMBLE_OVERRIDES`). Executable override: `executable` or
191
+ `FABERUN_CODEX_BIN`. A profile name never selects a custom provider:
192
+ Codex accepts unknown profiles silently.
193
+ - `zcode`: the GLM route — Z.ai's own harness CLI, driven headlessly
194
+ (`zcode --prompt --json`; `executable` / `FABERUN_ZCODE_BIN` override).
195
+ Model and endpoint travel
196
+ as `ZCODE_MODEL` (`config.provider`/model, default `glm`/model; a `[1m]` model
197
+ suffix is stripped — the provider reports the context window itself) and
198
+ `ZCODE_BASE_URL` (default the Z.ai Anthropic-compatible endpoint); the token
199
+ rides the provider-derived `${PROVIDER}_API_KEY` variable built from
200
+ `config["auth_token.env_key"]` (default `ZAI_API_KEY`). `permissionMode`
201
+ maps to `--mode` (`build`/`edit`/`plan`/`yolo`; default `yolo` — a judge
202
+ runtime declares `plan`). No schema flag and no tool policy, and the CLI's
203
+ `--settings`/hooks surface stays unwired:
204
+ `structuredOutput`/`toolPolicy` are `false`, judges arbitrate through the
205
+ prompt-embedded schema, and a `toolPolicy` requirement rejects the runtime.
206
+ Continuation resumes `sess_…` ids. Mid-run metering reads zero; usage
207
+ settles from the terminal result.
208
+ - `agy`: the installed `agy` CLI (or `FABERUN_AGY_BIN`); optional
209
+ `printTimeout`; omit `reasoning` for models without `--effort`.
210
+ - `dsh`: the DeepSeek Harness through the shipped `sdk` JSON-RPC client;
211
+ `headless` drops usage. Normalization assumes streamed `inputTokens` excludes
212
+ `cacheReadTokens`; `usage.jsonl` records it unchanged as uncached input.
213
+ `config.provider` is required (`deepseek-official`); `model` and `reasoning`
214
+ pass through verbatim. Its catalogue is the one `models` prints
215
+ (`deepseek-flash` is the default); unknown ids fail in the harness.
216
+ Authentication stays in `DEEPSEEK_API_KEY`;
217
+ `config["api_key.env_key"]` only names it for `preflight`. `sandbox` maps to
218
+ `DSH_PERMISSION_MODE`, default `workspace-write` (above). Every attempt loads
219
+ `dsh/closed-packet.patch.yml`; `config.patch` stacks one layer. Executable
220
+ override: `executable` or `FABERUN_DSH_BIN`. No default vendor,
221
+ continuation (`session/resume` is ACP-only), or native schema flag; the judge
222
+ schema travels in the prompt.
223
+ - `exec-jsonl`: generic harness for a JSONL-protocol executable — one
224
+ `run.request` on stdin, `run.started`/`message`/`run.completed`/
225
+ `run.failed` on stdout. Set `executable` (or
226
+ `FABERUN_EXEC_JSONL_BIN`), `args`, `versionArgs` when `--version` is
227
+ unsupported.
228
+ - `replay`: stands in for any provider in tests — recorded, already-normalized
229
+ envelopes, zero model calls. `config["replay.recording"]` names a JSONL
230
+ recording consumed strictly in order via a `.cursor` sidecar; each consumed
231
+ line appends one record to `<recording>.invocations.jsonl`. A missing line
232
+ emits `replay_exhausted` (exit 1); a path escape in `files` emits
233
+ `replay_path_escape` (exit 2) and writes nothing.
234
+
235
+ Continuation is capability-gated (`codex`, `claude`, `zcode`, `agy`,
236
+ `exec-jsonl`, `replay` all declare it) and requires an exact fingerprint of
237
+ the runtime definition; a runtime change, a failover hop, or an adapter
238
+ without the capability starts a fresh session carrying prior structured
239
+ summaries forward, never a continuation ID.
240
+
241
+ ### Failover
242
+
243
+ `runtimes[<id>].fallback` names at most one other runtime id — the single hop
244
+ a role takes on provider exhaustion at execution time; a self-loop is
245
+ rejected outright. Runtimes can chain (a fallback whose fallback names a third,
246
+ and so on); validation walks that chain for a gated worker and rejects a cycle,
247
+ but does not walk a chain no gated worker reaches, or a judge's. `tier` groups runtimes
248
+ for composed re-tiering (cheaper tiers first); `costRank` breaks ties. A
249
+ worker fallback is taken unconditionally once reachable and unattempted this
250
+ revision. A judge fallback is admissible only when it differs in vendor from
251
+ the worker runtime that actually ran the attempt; a same-vendor fallback is
252
+ refused and the node parks `attention` with
253
+ `judge_fallback_vendor_conflict`. Either role exhausting its one-hop budget
254
+ without an admissible target ends `exhausted` (worker) or `attention` (judge)
255
+ with `runtime_tier_exhausted`, preserving any announced `exhaustedUntil`.
256
+ Budget, scope, permission, and authority failures never trigger failover.
257
+
258
+ When an exhaustion envelope announces `resetAt` strictly after now and
259
+ strictly before the node's own deadline, the controller waits for it on the
260
+ same runtime instead of taking an edge; a reset outside that window, or none
261
+ announced, takes the declared/synthesized edge. A wait is not a hop and does
262
+ not consume the failover budget.
263
+
264
+ `doctor --discover [--json]` normalizes each harness's exhaustion signal into
265
+ `{available, exhaustedUntil, reason}` (missing CLI → `not_found`; auth
266
+ failure has no reset; a quota response keeps its reset, including Z.ai code
267
+ 1310).
268
+
269
+ ## Graph and states
270
+
271
+ `dependsOn` forms a DAG; a node starts once every dependency is `done`, and a
272
+ failed terminal dependency makes it `blocked`. Terminal states: `done`,
273
+ `no-op`, `blocked`, `failed`, `exhausted`, `stalled`, `canceled` — every node
274
+ ends in exactly one. `stallTimeoutSec` bounds silence on stdout/stderr, but
275
+ only for a harness declaring `streamsOutput` (true for `codex`, `claude`,
276
+ `agy`, `dsh`; false for `zcode`, which dumps its turn at exit);
277
+ others fall back to `timeoutSec` alone.
278
+ `timeoutSec` (default 2400s) caps one invocation and may be overridden per
279
+ node; a node is bounded by `(1 + maxRevisions) × 2 × timeoutSec`. Both clocks
280
+ are monotonic and pause with host suspend. `maxParallel` above 1 dispatches
281
+ every dependency-ready node concurrently, each into its own attempt
282
+ worktree; integration stays serialized. Nodes of one phase need no edge
283
+ between them: a continuation a live invocation already claims is never
284
+ offered to a second node, so one session runs one turn.
285
+
286
+ ## Gates
287
+
288
+ `gate: false` skips review (`none`). A gate object accepts `runtime`
289
+ (judge override), `review` (`none`/`advisory`/`blocking`, default
290
+ `advisory`), `failOn` (default `["critical"]`), `maxRevisions` (default 1).
291
+ `advisory` records the verdict, findings and `maxSeverity` and still settles
292
+ `done` on deterministic verification alone — it never consumes a revision or
293
+ re-dispatches. `blocking` re-dispatches within `maxRevisions` when findings
294
+ reach `failOn`. Validation requires `critical` whenever `major` is in
295
+ `failOn`, and `major` in `failOn` for a `blocking` gate: `["critical"]` alone
296
+ passes every major finding, which is close to no gate.
297
+
298
+ The revision budget counts gate rejections, not worker starts; a resume or a
299
+ crash-restart never consumes one (tracked separately as `attempt` vs.
300
+ `revisions`). Deterministic `verification` commands run once by default
301
+ before any judge and the judge reviews the recorded results, never
302
+ re-running them (`repeat` opts into re-running a flaky check). A judge
303
+ output is `pass` only with empty `findings` and `maxSeverity: none`; for
304
+ Codex judges, normalization selects the last parseable JSON agent message.
305
+ Zero or multiple verdict-shaped messages, a dead judge, or a wall-clock kill
306
+ is a review-protocol defect, not a verdict — one bounded re-ask; if that
307
+ also fails, advisory review completes `done` with `gate.verdict:
308
+ invalid_judge_output`, while blocking review marks the node `blocked` with
309
+ `judge_unavailable`, preserving the worker result and verification for
310
+ `resume` to re-judge. There is no first-class `stopped` state: model a
311
+ falsification gate as a node whose Definition of Done requires a durable
312
+ stop artifact and a fail-closed check, and do not schedule descendants after
313
+ it is accepted.
314
+
315
+ ## Run artifacts
316
+
317
+ Under `<cwd>/.runs/<id>/`:
318
+
319
+ ```text
320
+ contract.json run.json status.json findings.json
321
+ nodes/<id>.json
322
+ logs/<id>.<attempt>.<worker|judge>[.r<n>].jsonl / .err
323
+ operations/<invocationId>.intent.json / .settlement.json
324
+ usage.jsonl integration.jsonl events.jsonl notify.jsonl STATUS.md
325
+ ```
326
+
327
+ `operations/` holds the exact-once intent/settlement record for every
328
+ provider invocation, written before dispatch and merged idempotently after:
329
+ `settled` means a known harness outcome; `unknown_effect` means the request
330
+ may have run without proof and is not permission to retry. Replay of an
331
+ unknown-effect window needs `replayPolicy: "safe"` (default) plus a clean
332
+ persisted scope across the window and passing verification; otherwise it
333
+ settles `reconciled` and blocks the node with `unknown_effect_reconciled` — a
334
+ durable manual-stop attention boundary. All writes happen under the
335
+ controller lock. `usage.jsonl` is one line per invocation: tokens by kind
336
+ (uncached input, cache read, output), `costUsd` with provenance (`priced`, else
337
+ `provider`, else `unknown`), timestamps — reporting only. See
338
+ [operations.md](operations.md) for worktrees, integration, `status.json`,
339
+ notify, the controller lock, and campaigns.
340
+
341
+ ## Resume
342
+
343
+ `resume <run-dir>` continues an interrupted run in place: same run, same
344
+ node, attempt plus one, packet frozen. It adopts completed work first — a
345
+ worker log proving the turn finished recovers an orphaned provider process,
346
+ and a node `blocked` with `judge_unavailable` is re-judged from the
347
+ preserved result, never re-dispatched to a worker. Only then does it
348
+ re-dispatch ordinary failures (`failed`, `stalled`, `canceled`,
349
+ wall-clock-`exhausted`, `blocked`/`dependency_failed`) as attempt plus one,
350
+ with a bounded `## Previous attempt` section (prior error, judge/scope
351
+ findings, failing commands) appended to the regenerated prompt.
352
+ `resume --node <id>` limits the retry to that node and its dependents.
353
+
354
+ `resume --answer <node-id>=<path>` records an operator's answer for a node
355
+ `blocked` with `context_missing`, then re-dispatches it and its dependants,
356
+ narrowed exactly like `--node`. The file is read once, relative to the
357
+ shell's own cwd rather than the contract's, refused above 8 KiB, and
358
+ persisted as an `operator-answer` execution override (`kind`, `at`, `reason`,
359
+ a bounded `text`) — the authored packet and `packetHash` untouched, and a
360
+ repeated answer appends rather than merges. A malformed value, an unknown
361
+ node, an unreadable or oversized file, or a node not blocked on missing
362
+ context each refuse with their own message. The answer is text only, never
363
+ written into the attempt worktree.
364
+
365
+ `unknown_effect_reconciled` is re-dispatched only with an explicit
366
+ `--reconcile <node-id>`. Resume accepts a current `HEAD` that is a
367
+ descendant of the recorded `gitHead` (workers and the orchestrator commit
368
+ between attempts) and records the new head; a non-descendant is refused. A
369
+ `dirtyTreeFingerprint` mismatch is a status warning, not a refusal.
370
+
371
+ ## Environment doctor, cancel, JSON status
372
+
373
+ `doctor [<contract.json>] [--cwd <dir>] [--json]` is mutation-free: checks
374
+ `cwd` is a git work tree, `.runs/` is ignored, `node`/`npm` are on `PATH`,
375
+ and — with a contract — every routed harness exists and probes cleanly.
376
+ `cancel <run-dir>` signals the controller (`SIGTERM` then `SIGKILL` after
377
+ 2s), takes over its now-stale lock, terminates every recorded invocation,
378
+ and marks the run terminal; it cannot act on a lock held by its own process.
379
+ `status --json`/`report --json <run-dir>` emit stable `schemaVersion: 1`
380
+ payloads for streaming monitors instead of `STATUS.md`.
@@ -0,0 +1,29 @@
1
+ # Verification and tools
2
+
3
+ The session itself runs only these; everything else runs detached.
4
+
5
+ | You want | Run |
6
+ | --- | --- |
7
+ | Probe runtimes and host before spending tokens | `preflight <contract.json>`, `doctor [--cwd <dir>]` |
8
+ | Choose runtime and model, and see the effort each accepts | `models [--probe] [--json]` |
9
+ | Prove each verification command fits its own `timeoutSec` | `preflight <contract.json> --time-verification` |
10
+ | Pull unseen campaign events | `campaign sync <id> --cwd <repo> --session-id <s>` |
11
+ | Advance that cursor past an event | `campaign ack <id> --cwd <repo> --session-id <s> --event-id <e>` |
12
+ | Wake only on actionable change, poll every 30s | `campaign watch <id> --cwd <repo> --wake` |
13
+ | Read the campaign indicators | `metrics <campaign-id> [--cwd <dir>] [--json]` |
14
+ | Ask one question about many large files | `bulk-read --question <text> --paths <a,b,c>` -- bullets only, corpora under 1500 lines are refused |
15
+
16
+ **Foreground children.** Worker prompts run builds, watchers, and servers in
17
+ the foreground; only the runner is detached. Keep output bounded
18
+ (`| tail -n 200`). Never instruct a worker to run a command slower than its
19
+ own tool's foreground timeout, including the full test suite — that is what
20
+ `taskPacket.verification` is for, run by the controller after the worker
21
+ declares done. Measure a verification command's real duration before setting
22
+ its `timeoutSec`; a suite can silently outgrow the 600s per-entry cap as it
23
+ grows, and a worker forced to wait past its own timeout backgrounds the
24
+ command and returns prose instead of a result — a protocol failure, not a
25
+ `done`.
26
+
27
+ Keep secrets in env vars; contracts carry variable names only. Claude
28
+ `bypassPermissions` only in a repository-scoped, recoverable environment;
29
+ otherwise `acceptEdits`, letting denials become `blocked`.
@@ -0,0 +1,26 @@
1
+ # Handoffs: capsule, brief, settlement
2
+
3
+ **Campaign first.** Establish or discover the durable campaign before
4
+ launching work; stop instead of guessing when several are active. Attach this
5
+ session, read `HANDOFF.md`, and record every material intent, decision, and
6
+ open question as a campaign event — handoff state, not documentation.
7
+
8
+ **Closed packets.** Each node lists exact `readFiles`, `writeFiles`, and
9
+ `verification`. Workers and judges inspect only those paths and return the
10
+ structured `blocked_context` result instead of exploring.
11
+
12
+ **A packet is an instruction and a detector, not a sandbox.** Only a `claude`
13
+ worker is stopped mechanically, and only on `Write`/`Edit`/`NotebookEdit`: a
14
+ write through `Bash` is never inspected, and no other harness is prevented at
15
+ all. Everything else is caught after the attempt by the scope comparison in
16
+ `engine/scope.mjs`, which is advisory when verification passes. Scope keeps an
17
+ honest worker inside its lane and records what left it. It does not contain an
18
+ adversarial one, so give a worker no credential or write access you would not
19
+ give the packet's whole repository.
20
+
21
+ **Settlement.** Store raw worker output under `.runs/`; bring only status and
22
+ actionable verdicts into the session.
23
+
24
+ The packet shape and the worker-result object live in
25
+ [contract.md](contract.md); the `HANDOFF.md` capsule and the campaign journal
26
+ live in [operations.md](operations.md).
@@ -0,0 +1,184 @@
1
+ # Faberun operations
2
+
3
+ ## Attempt worktrees
4
+
5
+ An execution repository is a git work tree with at least one commit. A run
6
+ creates the integration head `refs/faberun/<run-id>/run` at the recorded
7
+ source `gitHead`. Every worker attempt gets a linked worktree at
8
+ `.runs/worktrees/<run-id>/<node-id>.<attempt>` on branch
9
+ `faberun/<run-id>/<node-id>/<attempt>`, cut from that ref; the node snapshot records
10
+ `worktree.path`, `.branch`, `.baseSha` and the sealed `.commit`. Provider,
11
+ scope, verification and judge processes all use that path; `contract.cwd` stays
12
+ the home of run/control artifacts. An installed root `node_modules` is
13
+ symlinked into every attempt worktree, never copied.
14
+
15
+ A retried attempt never discards the previous one's edits: the controller seals
16
+ the previous worktree first and, when that seal has a diff, cuts the next
17
+ attempt from that sha (`worktree.previousAttempt`); an empty seal falls back to
18
+ the run ref tip.
19
+
20
+ `contract.maxParallel` bounds concurrent nodes; each tick dispatches every
21
+ `pending` node whose dependencies are `done`, up to the free slots, each into
22
+ its own worktree. Integration stays serialized.
23
+
24
+ ## Integration transaction
25
+
26
+ The controller serializes integration. It seals uncommitted attempt edits with a
27
+ commit naming the run/node/attempt (`empty: true` in the journal when there is
28
+ no diff), appends a `prepared` record to `integration.jsonl` (node, attempt,
29
+ attempt sha, previous run-ref tip, candidate sha, verification evidence) before
30
+ creating anything, and builds the candidate — fast-forward or merge — on
31
+ `refs/faberun/<run-id>/candidate` / `.runs/worktrees/<run-id>/.candidate`,
32
+ where node `verification` runs once. A pass advances the run ref with a
33
+ conditional `update-ref` and writes the node `done` with `integratedHead`. A
34
+ failed candidate removes the candidate ref/worktree, leaves the run ref
35
+ untouched, and keeps the attempt worktree. A conflict marks the node `attention`
36
+ with the conflicting paths and cleans the scratch worktree.
37
+
38
+ Resume replays `integration.jsonl`, never ancestry, to identify the one
39
+ unfinished transaction and complete it idempotently. A resume that re-dispatches
40
+ a failed/stalled/exhausted/canceled node cuts the next attempt from the previous
41
+ attempt's sealed sha, the same continuation rule as any other retry.
42
+
43
+ ## Controller lock and takeover
44
+
45
+ One controller drives a run, holding `<run-dir>/controller.lock`: `{pid,
46
+ processStartToken, startedAt, hostname}`. Acquisition is an exclusive create
47
+ with no TTL. A contender treats the lock as stale only once it can prove the
48
+ holder dead — the pid is gone, or its process start token no longer matches
49
+ (pid recycled); anything less is `controller_active` and it exits untouched.
50
+ Takeover renames the lock aside, re-checks the captured record is stale, then
51
+ installs its own; a capture that turns out live is handed back. Worker/judge/
52
+ verification children run detached in their own process group, so before
53
+ dispatching new work `resume`'s recovery pass terminates (`SIGTERM` then
54
+ `SIGKILL`, same as `cancel`) every invocation recorded for a `running` node —
55
+ unless it is still inside its deadline, when it is adopted and its result read.
56
+ `cancel <run-dir>` signals a live controller first, so its own takeover never
57
+ waits on an expiry.
58
+
59
+ `supervise <run-dir> [--detach] [--interval <sec>]` is the watchdog above that.
60
+ It holds no lock and writes no state: every interval (default 30s) it launches
61
+ `resume --detach` when a node is unfinished and no controller is live, exits 0
62
+ once all are terminal, and stops after three failed launches. An empty run
63
+ directory is never resumed — it has not proved it needs to be.
64
+
65
+ ## Runtime discovery
66
+
67
+ `doctor --discover [--json]` performs mutation-free harness discovery,
68
+ reporting `{available, exhaustedUntil, reason}` per runtime (missing CLI →
69
+ `not_found`; auth failure has no reset; quota keeps its reset, including Z.ai
70
+ code 1310). Omitted `runtimes`/`runtimeDefaults` are composed once and persisted
71
+ in `routing.assignments`; exhaustion re-tiers within the current tier only,
72
+ otherwise the node parks `attention` with `runtime_tier_exhausted`. Failover
73
+ rules: [contract.md](contract.md).
74
+
75
+ ## Status
76
+
77
+ `<run-dir>/status.json` (`status --json`'s payload: identifiers, `goal`, `usage`,
78
+ `controller` state, `summary`, and one `nodes[]` entry per node — id, status,
79
+ phase, runtime, attempt, revisions, cost, verdict, note, `errorCode`,
80
+ `blockedBy`) and `.runs/status.json` (a ≤1 KiB pointer: run and campaign id,
81
+ `state`, checkpoints, `activeNode`, `costUsd`, `needsYou`, `attention`,
82
+ `generatedAt`) are written atomically every controller tick and at run terminal.
83
+ `status <run-dir>` renders Needs you, Now, Nodes and Cost;
84
+ `integrations/claude-code/statusline.sh` reads the pointer for an ambient prompt
85
+ segment. `next [--cwd <dir>] [--json]` prints one line per active campaign
86
+ naming the most urgent action and its command; read-only, no lock, writes
87
+ nothing.
88
+
89
+ ## Dashboard
90
+
91
+ `node src/web/server.mjs [--port 4173] [--cwd <repo>]` serves a read-only,
92
+ SSE-refreshed page on `127.0.0.1:4173` over `status.json`, node JSON,
93
+ `events.jsonl`, `usage.jsonl`, `notify.jsonl` and `HANDOFF.md`. Sections:
94
+ campaign picker; **Now**; **Needs you** (each attention item with its resolving
95
+ command); **Runs**; **Run drawer** on row click with per-node log, verification,
96
+ diff, findings and prompt tabs; **Handoff**. The snapshot is bounded to 200 KiB,
97
+ shrinking the drawer's tails, prompt, then handoff.
98
+
99
+ ## Remote API
100
+
101
+ The same server exposes the phone surface under `/api/*`, behind the
102
+ same token and bind. Reads: `GET /api/campaigns`, `/api/campaigns/<id>`,
103
+ `…/brief`, `/api/seats`, and `…/events?after=<cursor>` (one bounded page, ≤32 KiB
104
+ and ≤100 entries, so a client starting at zero never drags the whole journal).
105
+ Writes shell out to the runner CLI and touch no state themselves:
106
+ `POST …/decisions/<id>` → `campaign resolve`; `…/note` → `campaign note`;
107
+ `…/pause` and `…/resume` → `cancel` / `resume --detach`; `/api/seats/<id>/switch`
108
+ → `seat switch`. No replan, contract, routing or gate route exists on purpose:
109
+ the contract is frozen with a digest, and the phone's middle ground is a note.
110
+
111
+ ## Notify
112
+
113
+ On `node.terminal`, `run.terminal` and `attention` the controller renders a
114
+ one-line message from counters and identifiers only (node id, run id, state,
115
+ attempt, error code, done/total — never model text), calls the executable named
116
+ by `FABERUN_NOTIFY_BIN` with that event as JSON on stdin, and appends a
117
+ timestamped receipt (`delivered`, `failed`, `no_transport`) to
118
+ `<run-dir>/notify.jsonl`. Delivery is lossy: **exactly one attempt**, no retry,
119
+ no backoff; `FABERUN_NOTIFY_BACKOFF_MS` appears nowhere in `src`. Unset,
120
+ nothing is spawned and the receipt is `no_transport`.
121
+ `FABERUN_NOTIFY_BIN=os-macos` selects the bundled `osascript` adapter
122
+ (`canWake: false`); any other value is an executable path. A resume never
123
+ re-sends a notification already recorded for the same node, attempt and outcome.
124
+ No transport is a default: `doctor`, `preflight` and the foreground launch warn
125
+ when the variable is empty, and `--wake` reports no adapter can wake a session.
126
+ Campaign-level lines are queued in `.runs/inbox.jsonl`, the managed block's
127
+ append-only record — one object per line `{schemaVersion, eventId, at, type,
128
+ campaignId, runId, nodeId, status, errorCode, dedupeKey, summary}`, deduped on
129
+ `dedupeKey` (first write wins) with one `O_APPEND` write per line.
130
+ `campaign watch --wake --detach` queues there and delivers through
131
+ `<campaign-dir>/notify.jsonl`; a durable `watch.lock` plus the inbox dedupe keep
132
+ two detached watchers from double-sending across a restart.
133
+
134
+ ## Campaigns
135
+
136
+ Every contract requires `campaignId`; campaign state lives at
137
+ `.runs/campaigns/<campaign-id>/` (`campaign.json`, `journal.jsonl`,
138
+ `HANDOFF.md`) and can link multiple runs.
139
+
140
+ ```bash
141
+ node src/cli.mjs campaign <op> <id> [--cwd <dir>] …flags
142
+ init --goal "Goal" | attach --tool codex --session-id <s> --transcript <path> --format jsonl
143
+ note --session-id <s> --kind <intent|decision|supersede|constraint|outcome|next|open-question|retrospective> --text <t>
144
+ resolve --session-id <s> --question-id <q> --text <a> | sync --session-id <s> | ack --session-id <s> --event-id <e>
145
+ watch --wake [--detach] | show | close · list (no id)
146
+ ```
147
+
148
+ `sync` is the user-pull read: campaign header, the newest linked run's
149
+ `status.json` summary, and unseen journal events (≤8000 bytes) after the
150
+ session cursor, without moving it. `ack` is the only cursor writer, keyed by the
151
+ journal's own event id. `watch --wake [--detach]` polls every linked run's
152
+ `status.json` every 30s and prints one line per actionable change (terminal run,
153
+ attention node, stale controller lock, twenty idle minutes), exiting once the
154
+ campaign is closed. `close` refuses until a `retrospective` note exists; a
155
+ closed campaign stays inspectable but rejects further writes. The managed block
156
+ at the bottom of the target repo's `AGENTS.md` mirrors active state and names a
157
+ parked run's nodes, error codes and `resume` command.
158
+
159
+ `HANDOFF.md` is an atomic ≤16 KiB projection of recent intents, decisions,
160
+ constraints, outcomes, next action and open questions, refreshed at
161
+ initialization, registration, transitions and terminal completion;
162
+ `journal.jsonl` is the append-only, fsynced narrative.
163
+
164
+ ## Operator seat
165
+
166
+ The seat is one tmux session, `faberun-seat`, with one window per open
167
+ campaign. It hosts the operator's interactive harness and never drives a run:
168
+ state writes stay with the controller, and a dead pane cannot touch `.runs/`.
169
+ The harness registry (`src/seat/harnesses.mjs`) declares five entries — `claude`,
170
+ `codex`, `zcode`, `dsh`, `agy` — each with interactive argv, an environment
171
+ marker, and `canRenderAmbient` (claude only).
172
+
173
+ ```bash
174
+ node src/cli.mjs seat start <campaign-id> --cwd <dir> [--harness <name>]
175
+ node src/cli.mjs seat attach [<campaign-id>] [--cwd <dir>] [--ssh <host>]
176
+ node src/cli.mjs seat status [--json] [--cwd <dir>]
177
+ node src/cli.mjs seat stop [<campaign-id>] [--cwd <dir>]
178
+ ```
179
+
180
+ `attach` prints the command to paste rather than running `tmux attach`, which
181
+ would nest sessions; `--ssh <host>` prints the remote `ssh -t` line. `status
182
+ --json` lists each window's campaign, harness and ambient capability. tmux is
183
+ optional: every `seat` function returns an explicit unavailable result when the
184
+ binary is absent, and only reattaching is lost.
@@ -0,0 +1,35 @@
1
+ # Load-bearing rules
2
+
3
+ **One contract per approved plan step.** Inspect the repository once, then
4
+ author every node of the step with its `dependsOn` edges in a single turn.
5
+ Serial micro-contracts keep the expensive control session alive for the whole
6
+ physical runtime. Use `mode: "discovery"` only when no packet is possible.
7
+
8
+ **Prove mechanically.** Every Definition of Done item is an object declaring
9
+ its own proof: a verification `command`, a workspace `path`, or `judgment`.
10
+ Proofs gate before any judge runs, so a fully mechanical node costs no judge.
11
+ Contract-level `finalVerification` runs on the phase-terminal node.
12
+
13
+ **Never wait inside a turn.** No `sleep`/`while` loops, no repeated `status`
14
+ calls, no watched background jobs — every tool call re-sends the whole session
15
+ context. Check status once per invocation, report one line, end the turn.
16
+ Interrupt the user only for `blocked`, `failed`, `exhausted`, `stalled`, or
17
+ completion.
18
+
19
+ **No spend ceiling.** `timeoutSec` and `stallTimeoutSec` bound an attempt;
20
+ there is no `maxInputTokens`, `maxCostUsd`, or `usagePolicy`. A spent provider
21
+ allowance is handled by runtime re-tiering and discovery
22
+ ([contract.md](contract.md)), never a ceiling the operator had to guess. Usage
23
+ is recorded per attempt in `usage.jsonl` for reporting only.
24
+
25
+ **Gates.** The judge reviews captured results instead of re-running them.
26
+ Default `failOn` to `critical`, set `maxRevisions` explicitly, keep judge and
27
+ worker runtimes different. After two rejections or an exhaustion, create one
28
+ targeted fix node from the verbatim finding — never copy the graph.
29
+
30
+ Express model choice only in `runtimes`, `runtimeDefaults`, or an explicit
31
+ node override — never as model-specific branches in prose. Resolution order
32
+ and the single-hop fallback edge: [contract.md](contract.md).
33
+
34
+ Stop and ask before destructive production, data, merge, deployment, or
35
+ credential operations, even if a worker proposes them.