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.
- package/LICENSE +21 -0
- package/README.md +131 -0
- package/bin/faberun.mjs +25 -0
- package/integrations/claude-code/statusline-bench.sh +42 -0
- package/integrations/claude-code/statusline.sh +80 -0
- package/package.json +33 -0
- package/skills/faberun/SKILL.md +24 -0
- package/skills/faberun/references/contract.md +380 -0
- package/skills/faberun/references/engineering.md +29 -0
- package/skills/faberun/references/handoffs.md +26 -0
- package/skills/faberun/references/operations.md +184 -0
- package/skills/faberun/references/rules.md +35 -0
- package/skills/faberun/references/workflow.md +23 -0
- package/skills/init-agentkit/SKILL.md +108 -0
- package/skills/init-agentkit/scripts/install-agentkit.sh +127 -0
- package/skills/init-agentkit/templates/.claude/commands/create-adr.md +44 -0
- package/skills/init-agentkit/templates/.github/workflows/quality.yml +43 -0
- package/skills/init-agentkit/templates/.sentrux/baseline.json +9 -0
- package/skills/init-agentkit/templates/.sentrux/rules.toml +21 -0
- package/skills/init-agentkit/templates/AGENTS.md +110 -0
- package/skills/init-agentkit/templates/docs/ABSTRACTIONS.md +30 -0
- package/skills/init-agentkit/templates/docs/ARCHITECTURE.md +31 -0
- package/skills/init-agentkit/templates/docs/GETTING-STARTED.md +44 -0
- package/skills/init-agentkit/templates/docs/VISION.md +33 -0
- package/skills/init-agentkit/templates/docs/adr/0001-record-architecture-decisions.md +36 -0
- package/skills/init-agentkit/templates/docs/adr/0002-root-managed-ai-guidance.md +37 -0
- package/skills/init-agentkit/templates/docs/adr/0003-sentrux-structural-quality-gates.md +49 -0
- package/skills/init-agentkit/templates/docs/adr/README.md +52 -0
- package/skills/init-agentkit/templates/docs/sentrux.md +66 -0
- package/skills/init-agentkit/templates/githooks/commit-msg +22 -0
- package/skills/init-agentkit/templates/githooks/pre-commit +32 -0
- package/src/campaign/brief.mjs +394 -0
- package/src/campaign/chain.mjs +555 -0
- package/src/campaign/handoff.mjs +516 -0
- package/src/campaign/index.mjs +300 -0
- package/src/campaign/journal.mjs +347 -0
- package/src/campaign/layout.mjs +51 -0
- package/src/campaign/metrics-evals.mjs +25 -0
- package/src/campaign/metrics.mjs +517 -0
- package/src/campaign/projection.mjs +250 -0
- package/src/campaign/record.mjs +102 -0
- package/src/campaign/unpark.mjs +56 -0
- package/src/cli/brand.mjs +205 -0
- package/src/cli/campaign.mjs +730 -0
- package/src/cli/contract.mjs +67 -0
- package/src/cli/init.mjs +170 -0
- package/src/cli/launch.mjs +239 -0
- package/src/cli/seat.mjs +139 -0
- package/src/cli/setup.mjs +294 -0
- package/src/cli/skills.mjs +105 -0
- package/src/cli/update.mjs +216 -0
- package/src/cli.mjs +525 -0
- package/src/contract/articles.mjs +12 -0
- package/src/contract/assert.mjs +162 -0
- package/src/contract/definition-of-done.mjs +97 -0
- package/src/contract/final-verification.mjs +96 -0
- package/src/contract/index.mjs +641 -0
- package/src/contract/judge-envelope.mjs +25 -0
- package/src/contract/review-modes.mjs +151 -0
- package/src/contract/runtime.mjs +204 -0
- package/src/contract/schema-version.mjs +25 -0
- package/src/contract/scope-findings.mjs +77 -0
- package/src/contract/snapshot.mjs +639 -0
- package/src/contract/task-packet.mjs +495 -0
- package/src/contract/untrusted.mjs +75 -0
- package/src/contract/verification.mjs +185 -0
- package/src/contract/worker-result.mjs +138 -0
- package/src/engine/assignment.mjs +63 -0
- package/src/engine/backoff.mjs +492 -0
- package/src/engine/bulk-read.mjs +361 -0
- package/src/engine/cancel.mjs +177 -0
- package/src/engine/detach.mjs +101 -0
- package/src/engine/dispatch.mjs +752 -0
- package/src/engine/failover.mjs +192 -0
- package/src/engine/gate.mjs +183 -0
- package/src/engine/judge-gate.mjs +517 -0
- package/src/engine/lifecycle.mjs +772 -0
- package/src/engine/live-preflight.mjs +299 -0
- package/src/engine/mutation.mjs +146 -0
- package/src/engine/notify-queue.mjs +327 -0
- package/src/engine/process-identity.mjs +72 -0
- package/src/engine/process.mjs +774 -0
- package/src/engine/prompts.mjs +289 -0
- package/src/engine/recover.mjs +300 -0
- package/src/engine/result-file.mjs +222 -0
- package/src/engine/resume.mjs +635 -0
- package/src/engine/retry.mjs +334 -0
- package/src/engine/review.mjs +228 -0
- package/src/engine/run-command.mjs +287 -0
- package/src/engine/run-identity.mjs +411 -0
- package/src/engine/runtime-discovery.mjs +235 -0
- package/src/engine/scheduler.mjs +526 -0
- package/src/engine/scope.mjs +378 -0
- package/src/engine/settle.mjs +207 -0
- package/src/engine/state.mjs +148 -0
- package/src/engine/supervise.mjs +713 -0
- package/src/engine/verify.mjs +167 -0
- package/src/harnesses/agy/index.mjs +62 -0
- package/src/harnesses/catalogue.mjs +509 -0
- package/src/harnesses/claude/index.mjs +90 -0
- package/src/harnesses/codex/index.mjs +87 -0
- package/src/harnesses/dsh/closed-packet.patch.yml +42 -0
- package/src/harnesses/dsh/index.mjs +210 -0
- package/src/harnesses/dsh/runner.mjs +259 -0
- package/src/harnesses/exec-jsonl/index.mjs +788 -0
- package/src/harnesses/index.mjs +508 -0
- package/src/harnesses/protocol.mjs +531 -0
- package/src/harnesses/replay/bin.mjs +386 -0
- package/src/harnesses/replay/index.mjs +238 -0
- package/src/harnesses/zcode/index.mjs +276 -0
- package/src/host/config.mjs +87 -0
- package/src/host/home.mjs +149 -0
- package/src/host/package.mjs +23 -0
- package/src/host/preflight.mjs +520 -0
- package/src/host/tool-policy-decisions.mjs +341 -0
- package/src/host/tool-policy-hook.mjs +270 -0
- package/src/notify/index.mjs +359 -0
- package/src/notify/os-macos.mjs +81 -0
- package/src/repo/declared-paths.mjs +220 -0
- package/src/repo/integrate.mjs +546 -0
- package/src/repo/scope-closure.mjs +665 -0
- package/src/repo/signal-block.mjs +16 -0
- package/src/repo/signal.mjs +222 -0
- package/src/repo/source-identity.mjs +295 -0
- package/src/repo/workspace.mjs +557 -0
- package/src/repo/worktree.mjs +352 -0
- package/src/report/final.mjs +200 -0
- package/src/report/metrics-report.mjs +99 -0
- package/src/report/next.mjs +383 -0
- package/src/report/render.mjs +716 -0
- package/src/run/disk-gc.mjs +251 -0
- package/src/run/lock.mjs +329 -0
- package/src/run/node-store.mjs +62 -0
- package/src/run/operations.mjs +286 -0
- package/src/run/store.mjs +187 -0
- package/src/run/usage.mjs +337 -0
- package/src/seat/harnesses.mjs +83 -0
- package/src/seat/index.mjs +239 -0
- package/src/seat/tmux.mjs +208 -0
- package/src/util.mjs +0 -0
- package/src/web/api.mjs +371 -0
- package/src/web/boundary.mjs +88 -0
- package/src/web/index.html +299 -0
- 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.
|