faberun 0.3.0 → 0.7.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 (51) hide show
  1. package/README.md +152 -100
  2. package/package.json +10 -2
  3. package/skills/faberun/SKILL.md +6 -5
  4. package/skills/faberun/references/contract.md +23 -11
  5. package/skills/faberun/references/engineering.md +3 -1
  6. package/skills/faberun/references/operations.md +19 -12
  7. package/skills/faberun/references/rules.md +3 -1
  8. package/src/campaign/chain.mjs +6 -2
  9. package/src/campaign/index.mjs +17 -1
  10. package/src/campaign/metrics.mjs +3 -3
  11. package/src/cli/brand.mjs +2 -1
  12. package/src/cli/campaign.mjs +2 -0
  13. package/src/cli/contract.mjs +2 -0
  14. package/src/cli/manual.mjs +341 -0
  15. package/src/cli/seat.mjs +2 -0
  16. package/src/cli/setup.mjs +109 -30
  17. package/src/cli/skills.mjs +310 -8
  18. package/src/cli.mjs +3 -2
  19. package/src/contract/final-verification.mjs +31 -2
  20. package/src/contract/index.mjs +28 -25
  21. package/src/contract/runtime.mjs +5 -1
  22. package/src/contract/snapshot.mjs +7 -1
  23. package/src/contract/task-packet.mjs +20 -9
  24. package/src/contract/verification.mjs +1 -1
  25. package/src/engine/backoff.mjs +1 -1
  26. package/src/engine/dispatch.mjs +31 -4
  27. package/src/engine/gate.mjs +12 -0
  28. package/src/engine/process-identity.mjs +39 -0
  29. package/src/engine/prompts.mjs +18 -0
  30. package/src/engine/resume.mjs +2 -2
  31. package/src/engine/review.mjs +9 -1
  32. package/src/engine/run-command.mjs +23 -2
  33. package/src/engine/run-identity.mjs +14 -0
  34. package/src/engine/scheduler.mjs +45 -12
  35. package/src/engine/settle.mjs +29 -0
  36. package/src/engine/supervise.mjs +32 -6
  37. package/src/engine/verify.mjs +98 -9
  38. package/src/harnesses/agy/index.mjs +3 -0
  39. package/src/harnesses/claude/index.mjs +5 -0
  40. package/src/harnesses/codex/index.mjs +3 -0
  41. package/src/harnesses/dsh/index.mjs +26 -0
  42. package/src/harnesses/exec-jsonl/index.mjs +2 -0
  43. package/src/harnesses/index.mjs +10 -3
  44. package/src/harnesses/replay/index.mjs +2 -0
  45. package/src/harnesses/zcode/index.mjs +3 -0
  46. package/src/host/preflight.mjs +5 -1
  47. package/src/notify/index.mjs +45 -2
  48. package/src/repo/source-identity.mjs +4 -3
  49. package/src/report/final.mjs +3 -2
  50. package/src/report/render.mjs +134 -51
  51. package/src/web/index.html +1 -1
package/README.md CHANGED
@@ -1,131 +1,183 @@
1
- # skills
1
+ <p align="center"><img src="assets/faberun-icon.png" width="160" alt="The hornero on its clay nest, the Faberun mark"></p>
2
2
 
3
- Personal catalog of reusable agent skills, installable into any repository
4
- with a single command.
3
+ <h1 align="center">faberun</h1>
5
4
 
6
- All skills live under [`skills/`](skills/). Each skill is one folder
7
- with a `SKILL.md`; scripts, references, and templates live inside the skill so
8
- it stays a single copyable unit.
5
+ <p align="center">From intent to running software.</p>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/feliperun/faberun/actions/workflows/ci.yml"><img src="https://github.com/feliperun/faberun/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
9
+ <a href="https://github.com/feliperun/faberun/releases/latest"><img src="https://img.shields.io/github/v/release/feliperun/faberun" alt="Latest release"></a>
10
+ <a href="https://www.npmjs.com/package/faberun"><img src="https://img.shields.io/npm/v/faberun" alt="npm version"></a>
11
+ </p>
12
+
13
+ Faberun is a development orchestration system that turns intent into verified
14
+ software. It manages the process around software creation: plans, tasks,
15
+ dependencies, execution, validation, evidence, retries and progress toward a
16
+ defined outcome. It is not another coding agent, and it does not generate code
17
+ without a definition of done.
18
+
19
+ Faberun is model- and harness-agnostic. Claude Code, Codex, OpenCode and
20
+ whatever comes next are workers; Claude, GPT, Gemini, DeepSeek, GLM and other
21
+ models are engines; Faberun sits above them. It keeps the intent, coordinates
22
+ the work, tracks what was actually completed, validates the result and decides
23
+ what should happen next.
24
+
25
+ Software should be built, not merely generated. A craftsman does not depend on
26
+ one hammer, so Faberun does not depend on one model or one agent: tools,
27
+ models and harnesses can change, and the work remains.
9
28
 
10
29
  ## Install
11
30
 
31
+ The installer resolves the newest release, checks the requirements and runs
32
+ `faberun setup`:
33
+
12
34
  ```bash
13
- npx github:feliperun/faberun # every `mine` skill → .claude/skills/ of the current repo
14
- npx github:feliperun/faberun faberun # one named skill
15
- npx github:feliperun/faberun list # show the catalog
16
- npx github:feliperun/faberun --global # install into ~/.claude/skills/ instead
17
- npx github:feliperun/faberun --force # replace skills that already exist
35
+ curl -fsSL https://raw.githubusercontent.com/feliperun/faberun/main/install.sh | sh
18
36
  ```
19
37
 
20
- `npx` needs the repository to be reachable (public, or private with git
21
- credentials). Without npx, copy or symlink a skill folder into
22
- `~/.claude/skills/` or `.claude/skills/`:
38
+ `faberun setup` runs at the end of the installer and registers the `faberun`
39
+ skill for the harnesses it finds (Claude Code, Codex and the shared
40
+ `~/.agents/skills`, plus any measured convention); `faberun skills register`
41
+ redoes it.
42
+
43
+ It needs Node 22 or newer, git, and one harness CLI on `PATH`. The npm and
44
+ source installs need Node and git alone.
45
+
46
+ From the npm registry:
23
47
 
24
48
  ```bash
25
- cp -r skills/faberun ~/.claude/skills/faberun
26
- ln -s "$(pwd)/skills/init-agentkit" ~/.claude/skills/init-agentkit
49
+ npm install -g faberun
27
50
  ```
28
51
 
29
- ## Skills
52
+ Or run the package without installing it:
30
53
 
31
- ### faberun
54
+ ```bash
55
+ npx faberun --help
56
+ ```
32
57
 
33
- Executes large implementation plans as observable multi-model DAGs outside the
34
- orchestrator's context: declarative routing (Claude or Codex workers, including
35
- DeepSeek through Codex custom providers), closed task packets, structured
36
- cross-model quality gates, bounded revisions, campaign journaling with
37
- `HANDOFF.md`, and a built-in `supervise` watchdog that keeps resuming a dead
38
- controller until the run is terminal — from any host scheduler (launchd, cron,
39
- CI, or another agent), with no dependency on the orchestrator's runtime.
58
+ From a checkout, as a contributor:
40
59
 
41
- Release 1 makes a long campaign cheap to watch and cheap to finish. Liveness is
42
- rendered ambiently from a bounded heartbeat at zero token cost, and the control
43
- session pulls campaign events on its own cursor instead of being woken by
44
- progress. Every Definition of Done item now declares how it is proven, so a
45
- mechanically provable node spends no judge at all, while contract-level
46
- `finalVerification` keeps a phase from closing on partial proof. An interrupted
47
- run is continued in place by `resume`, never re-authored, and the factory rides
48
- out provider exhaustion, quota resets, dead leases, and flaky networks
49
- deterministically. `metrics` reports effectiveness and efficiency of a
50
- campaign together, from what the runs recorded. Detail:
51
- [references/release-1.md](skills/faberun/references/release-1.md).
60
+ ```bash
61
+ git clone https://github.com/feliperun/faberun.git
62
+ cd faberun
63
+ node src/cli.mjs --help
64
+ ```
52
65
 
53
- Quickstart, in the repository that will receive the implementation:
66
+ An installed copy updates itself from the newest GitHub release:
54
67
 
55
68
  ```bash
56
- FABERUN=/path/to/faberun/src/cli.mjs
57
- TARGET=/path/to/target-repository
69
+ faberun update
70
+ ```
58
71
 
59
- rg -qxF '.runs/' "$TARGET/.gitignore" || printf '\n.runs/\n' >> "$TARGET/.gitignore"
60
- node "$FABERUN" campaign init feature-42 --cwd "$TARGET" --goal "Deliver feature 42"
61
- node "$FABERUN" campaign attach feature-42 --cwd "$TARGET" --tool codex --session-id <session-id> --no-transcript
72
+ ## Quickstart
73
+
74
+ The full walkthrough, from a fresh machine to a first verified node, is in
75
+ [Getting started](docs/GETTING-STARTED.md).
76
+
77
+ | Step | Command | What it does |
78
+ | --- | --- | --- |
79
+ | 1 | `faberun setup` | Onboards the machine: checks node and git, discovers the harnesses, and writes the default worker and judge. |
80
+ | 2 | `faberun init` | Prepares a repository: confirms a git work tree, ignores `.runs/`, and installs the `faberun` skill. |
81
+ | 3 | `faberun campaign init <id> --goal "..."` | Opens the durable campaign that carries the intent across runs. |
82
+ | 4 | write a contract | Fixes `contract.json`: the node DAG, each packet's read and write scope, and each definition of done. |
83
+ | 5 | `faberun validate contract.json` | Parses the contract and prints the report the authoring turn reads. |
84
+ | 6 | `faberun preflight contract.json` | Checks the host, the runtime binaries and their credentials without dispatching a worker. |
85
+ | 7 | `faberun run --detach contract.json` | Starts the run in its own process and returns with the run directory. |
86
+ | 8 | `faberun status <run-dir>` / `faberun next` | Renders one run, or names the most urgent action across the active campaigns. |
87
+ | 9 | `faberun supervise --detach <run-dir>` | Watches the run and resumes it when the controller dies. |
88
+
89
+ ## How it works
90
+
91
+ The operator writes the intent into a campaign and an authored contract, a
92
+ schema-versioned DAG whose nodes each carry a closed task packet. The controller
93
+ schedules every dependency-ready node and dispatches its packet to a worker
94
+ inside an attempt worktree, where the worker sees only the files the packet
95
+ names. The controller then runs the deterministic verification once, and a
96
+ judge from a different vendor reviews the recorded result without re-running it.
97
+ A passing attempt is sealed and integrated onto the run ref, a campaign promotes
98
+ each run onto its landing branch, and the orchestrator lands that branch.
99
+ Campaigns, handoffs and the `supervise` watchdog carry the work across sessions,
100
+ so an interrupted run is continued in place instead of being re-authored.
101
+
102
+ ```text
103
+ intent
104
+ └─ contract: validate · preflight
105
+ └─ controller
106
+ ├─ worker attempt in an attempt worktree
107
+ │ └─ deterministic verification · cross-vendor judge
108
+ └─ integration ref · promotion · landing branch
62
109
  ```
63
110
 
64
- Inspect the target once, write the contract and its task packets, then:
111
+ The vocabulary is in [Concepts](docs/CONCEPTS.md), and the layers, process
112
+ model and gates are in [Architecture](docs/ARCHITECTURE.md).
113
+
114
+ ## Harnesses
115
+
116
+ A runtime is one harness running one model. Any runtime can be a worker, and a
117
+ judge of another vendor reviews what it produced.
118
+
119
+ | Harness | Default vendor | Example model |
120
+ | --- | --- | --- |
121
+ | `claude` | Anthropic | `claude-sonnet` |
122
+ | `codex` | OpenAI | `codex-gpt` |
123
+ | `agy` | Google | `agy-gemini` |
124
+ | `dsh` | declared per runtime; DeepSeek in the discovery entry | `dsh-deepseek` |
125
+ | `zcode` | Zhipu | `zcode-glm` |
126
+ | `exec-jsonl` | declared per runtime | the model its command names |
127
+ | `replay` | declared per runtime | the recorded model |
128
+
129
+ ## Documentation
130
+
131
+ - [Documentation map](docs/README.md): every document and the question it answers.
132
+ - [Vision](docs/VISION.md): why Faberun exists and what it refuses to become.
133
+ - [Concepts](docs/CONCEPTS.md): each term, where it lives, and its invariant.
134
+ - [Getting started](docs/GETTING-STARTED.md): install to a first verified node.
135
+ - [Commands](docs/COMMANDS.md): every verb's synopsis, flags, exit codes and one example.
136
+ - [Architecture](docs/ARCHITECTURE.md): the layers, the process model and the quality gates.
137
+ - [Decisions](docs/adr/README.md): the active ADRs and the format they use.
138
+ - [Design system](DESIGN.md): the identity, the palette and the documentation grammar.
139
+ - [History](docs/history/README.md): the dated records and the path mapping from before the move.
140
+ - [Agent playbook](AGENTS.md): the contributor and agent rules for this repository.
141
+
142
+ ## Development
143
+
144
+ - `npm run check` runs `node --check` over every `.mjs` under `bin/`, `src/`,
145
+ `test/`, `evals/` and `.claude/hooks/`.
146
+ - `npm run typecheck` runs `tsc` over the whole tree in `checkJs` mode and must
147
+ be clean.
148
+ - `npm test` runs the test suite.
149
+ - `node evals/run.mjs --class deterministic --assert-no-model` runs every
150
+ deterministic case with zero model calls.
151
+ - `node evals/run.mjs --class deterministic --verify-discriminating` requires
152
+ each case's declared mutation to make the case fail.
153
+
154
+ [AGENTS.md](AGENTS.md) is the playbook. The skills shipped in `skills/` are
155
+ [faberun](skills/faberun/SKILL.md), for orchestrating agents, and
156
+ [init-agentkit](skills/init-agentkit/SKILL.md), an optional kit that bootstraps
157
+ `AGENTS.md`, `docs/`, ADRs and githooks into another repository. Install either
158
+ into a repository with:
65
159
 
66
160
  ```bash
67
- node "$FABERUN" validate contract.json
68
- node "$FABERUN" preflight contract.json
69
- node "$FABERUN" run --detach contract.json
70
- node "$FABERUN" supervise --detach "$TARGET/.runs/<run-id>" # unattended resume
161
+ faberun skills install faberun
162
+ faberun skills install init-agentkit
71
163
  ```
72
164
 
73
- | Goal | Command |
74
- | --- | --- |
75
- | Find the active campaign | `campaign list --cwd <repo>` |
76
- | Pull unseen campaign events | `campaign sync <id> --cwd <repo> --session-id <s>` |
77
- | Advance the session cursor | `campaign ack <id> --cwd <repo> --session-id <s> --event-id <e>` |
78
- | Validate a contract | `validate <contract.json>` |
79
- | Check credentials, models, binaries | `preflight <contract.json>` / `doctor [--cwd <dir>]` |
80
- | Start without blocking the session | `run --detach <contract.json>` |
81
- | Read current state | `status <run-dir>` / `status --json <run-dir>` |
82
- | Name the next action per campaign | `next [--cwd <dir>] [--json]` |
83
- | View attempts and tokens | `report <run-dir>` |
84
- | Read what a stopped node needs: gate findings and blocking questions | `findings <run-dir>` |
85
- | Read the campaign indicators | `metrics <campaign-id> --cwd <repo>` |
86
- | Stop a run and terminate its providers | `cancel <run-dir>` |
87
- | Resume an interrupted run | `resume --detach <run-dir>` |
88
- | Keep finishing a run whose controller died | `supervise --detach <run-dir> [--interval 30]` |
89
-
90
- Operational detail: [SKILL.md](skills/faberun/SKILL.md) and the
91
- [contract reference](skills/faberun/references/contract.md).
92
-
93
- ### init-agentkit
94
-
95
- Bootstraps the agent kit into a repository: canonical `AGENTS.md` with
96
- `CLAUDE.md`/`GEMINI.md`/`CURSOR.md`/`AGENT.md` symlinks, base docs (VISION,
97
- ARCHITECTURE, ABSTRACTIONS, GETTING-STARTED), ADRs with template and index, the
98
- Sentrux structural quality gate, a `create-adr` slash command, and githooks.
99
- Always ask which compatibility rule applies before running it — see
100
- [SKILL.md](skills/init-agentkit/SKILL.md).
101
-
102
- ### Session continuity
103
-
104
- Long sessions stay cheap across usage-limit resets: a curated handoff is saved
105
- to `.claude/session-handoff.md` while the session is warm, and this
106
- repository's [SessionStart hook](.claude/hooks/session-start.mjs) injects a
107
- fresh handoff into every new session automatically. The save/resume protocol
108
- lives inside faberun as
109
- [references/session-memory.md](skills/faberun/references/session-memory.md).
110
-
111
- ## Development
165
+ `faberun skills install` copies a skill into a repository; `faberun skills
166
+ register` links the `faberun` skill into every installed harness's own skills
167
+ directory instead, so the harness has it in every repository.
112
168
 
113
- - Node.js 22 or newer; the runtime is plain ESM `.mjs` with no runtime
114
- dependencies. TypeScript is a development-only check (`checkJs`/`noEmit`).
115
- - `npm run check` — syntax; `npm run typecheck` — static types; `npm test` — the suite.
116
- - The skills of this repository stay active inside it through symlinks in
117
- `.claude/skills/`.
118
- - [AGENTS.md](AGENTS.md) is the canonical guidance; the other agent files are
119
- symlinks to it — never edit them.
169
+ Never run `npm install` inside an attempt worktree: husky's `prepare` script
170
+ dirties the ignore snapshot the controller compares against.
120
171
 
121
- ## Inspiration
172
+ ## History
122
173
 
123
- The memory-layer split (session handoff / campaign handoff / standing memory)
124
- draws on [ai-memory](https://github.com/akitaonrails/ai-memory) by Akita on
125
- Rails. Most other patterns here compile-not-retrieve summaries,
126
- start-of-session handoff injection, cross-harness workstreams — converged
127
- independently.
174
+ Faberun began as an internal orchestration tool under an earlier name, and this
175
+ repository began as a personal library of agent skills. Neither record is
176
+ rewritten: the dated history is under `docs/history/`, and the campaign specs
177
+ and journals are under `docs/campaigns/`. The
178
+ [documentation map](docs/README.md) and the
179
+ [history index](docs/history/README.md) point to both.
128
180
 
129
- ## License
181
+ ## Licence
130
182
 
131
183
  [MIT](LICENSE).
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "faberun",
3
- "version": "0.3.0",
3
+ "version": "0.7.0",
4
4
  "description": "Faberun is a development orchestration system that turns intent into verified software: harness- and model-agnostic, it keeps the intent, coordinates the work, verifies the result and decides what happens next.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "faberun": "bin/faberun.mjs"
8
8
  },
9
- "repository": "github:feliperun/faberun",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/feliperun/faberun.git"
12
+ },
10
13
  "homepage": "https://github.com/feliperun/faberun",
11
14
  "files": [
12
15
  "bin",
@@ -14,6 +17,9 @@
14
17
  "skills",
15
18
  "integrations"
16
19
  ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
17
23
  "engines": {
18
24
  "node": ">=22"
19
25
  },
@@ -21,6 +27,8 @@
21
27
  "check": "for f in bin/*.mjs .claude/hooks/*.mjs src/*.mjs src/*/*.mjs src/*/*/*.mjs evals/*.mjs test/*.mjs test/*/*.mjs; do node --check \"$f\" || exit 1; done",
22
28
  "typecheck": "tsc",
23
29
  "test": "node --test test/*.test.mjs test/*/*.test.mjs",
30
+ "docs": "node src/cli/manual.mjs --write",
31
+ "docs:check": "node src/cli/manual.mjs --check",
24
32
  "prepare": "husky"
25
33
  },
26
34
  "devDependencies": {
@@ -11,14 +11,15 @@ Read [rules](references/rules.md) first.
11
11
  | Action | Read |
12
12
  | --- | --- |
13
13
  | Author a contract (fallback) | [contract](references/contract.md), [engineering](references/engineering.md) |
14
- | Launch, resume, integrate | [workflow](references/workflow.md), [operations](references/operations.md) |
14
+ | Launch and resume | [workflow](references/workflow.md), [operations](references/operations.md) |
15
15
  | Dispatch a node | [handoffs](references/handoffs.md) |
16
- | Supervise, answer attention | [operations](references/operations.md), [handoffs](references/handoffs.md) |
17
- | Verify, judge, settle | [engineering](references/engineering.md), [handoffs](references/handoffs.md) |
16
+ | Supervise, answer | [operations](references/operations.md), [handoffs](references/handoffs.md) |
17
+ | Verify, judge | [engineering](references/engineering.md) |
18
+ | Install, set up, update | [operations](references/operations.md) |
18
19
 
19
20
  Watchdog re-invocations:
20
21
 
21
- node src/cli.mjs supervise <run-dir>
22
- node src/cli.mjs supervise campaign <id> [--allow-main]
22
+ faberun supervise <run-dir>
23
+ faberun supervise campaign <id> [--allow-main]
23
24
 
24
25
  launchd: `StartInterval 300`; `launchctl load -w ~/Library/LaunchAgents/faberun.plist`.
@@ -57,6 +57,15 @@ attempt; a spent allowance is handled by runtime re-tiering (below). `usage.json
57
57
  tokens and cost per invocation for **reporting only** — no control path reads
58
58
  it.
59
59
 
60
+ At the contract level, `sharedVerification` is the same command schema as
61
+ `finalVerification`, appended to every node's attempt and integration-candidate
62
+ verification after the packet's own commands and before `finalVerification`,
63
+ which only the phase-terminal node carries. Declare it for the fast repository
64
+ ratchets (source shape, field ownership, brand, docs diet): a node whose write
65
+ set breaks a rule then fails on its own attempt instead of on the
66
+ phase-terminal node's full suite. Both sets count in the node budget and in
67
+ `preflight --time-verification`.
68
+
60
69
  ## Task packets
61
70
 
62
71
  ```json
@@ -77,10 +86,10 @@ it.
77
86
  `instructions`, and `verification` are required and non-empty. An execution
78
87
  packet requires non-empty `readFiles` and `writeFiles`; read paths are
79
88
  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
89
+ except contract loading defers a missing `readFiles` or `scopeAcknowledged`
90
+ entry a transitive dependency declares in its `writeFiles`, or that sits under
91
+ its directory-shaped `writeRoots` entries (a file-shaped entry authorizes only
92
+ that exact path); every other caller still rejects the
84
93
  missing read. A
85
94
  discovery packet has empty `writeFiles`; with an empty `readFiles` it may
86
95
  read the repository read-only to produce an execution packet — the one
@@ -93,8 +102,6 @@ rejected; a node has `taskPacket` or `taskPacketFile`, never both. Measure a
93
102
  candidate command's real duration before naming it in `verification` or a
94
103
  worker instruction — `preflight <contract.json> --time-verification` runs each
95
104
  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
105
 
99
106
  An `autonomous` packet declares `writeRoots` instead of `writeFiles`:
100
107
  whole-repo read, write bounded to the listed files/directories. Scope is
@@ -174,12 +181,17 @@ worker fallback cannot execute commands. Adapters declare
174
181
  `acceptEdits`), `zcode` only `yolo` (also default), `dsh` both its default
175
182
  `workspace-write` (measured: executes and writes inside the worktree) and
176
183
  `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
184
+ executes, and `agy`/`exec-jsonl`/`replay` expose no denying mode. Each adapter
185
+ also declares `signalsProcesses` (`true`, `false`, or `null` when unmeasured):
186
+ a worker whose adapter declares `false` gets a `## Sandbox` prompt warning not
187
+ to run tests that start and terminate child processes, and
188
+ `requiredCapabilities.signalsProcesses: true` admits only an adapter declaring
189
+ `true`. Judge modes
178
190
  are excluded because judges review captured results.
179
191
 
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
192
+ - `claude`: `permissionMode` (a node that runs commands needs
193
+ `bypassPermissions`, or the worker can only return `blocked_context`).
194
+ Executable
183
195
  override: `executable` or `FABERUN_CLAUDE_BIN`. It disables slash
184
196
  commands, MCP, and settings files on every invocation and restricts tools to
185
197
  `runtime.tools` (default `Read, Edit, Write, Bash, Glob, Grep`); `--bare` is
@@ -334,7 +346,7 @@ settles `reconciled` and blocks the node with `unknown_effect_reconciled` — a
334
346
  durable manual-stop attention boundary. All writes happen under the
335
347
  controller lock. `usage.jsonl` is one line per invocation: tokens by kind
336
348
  (uncached input, cache read, output), `costUsd` with provenance (`priced`, else
337
- `provider`, else `unknown`), timestamps — reporting only. See
349
+ `provider`, else `unknown`), timestamps. See
338
350
  [operations.md](operations.md) for worktrees, integration, `status.json`,
339
351
  notify, the controller lock, and campaigns.
340
352
 
@@ -22,7 +22,9 @@ declares done. Measure a verification command's real duration before setting
22
22
  its `timeoutSec`; a suite can silently outgrow the 600s per-entry cap as it
23
23
  grows, and a worker forced to wait past its own timeout backgrounds the
24
24
  command and returns prose instead of a result — a protocol failure, not a
25
- `done`.
25
+ `done`. A harness whose adapter declares `signalsProcesses: false` never
26
+ runs a test that terminates processes; the controller's verification is the
27
+ proof.
26
28
 
27
29
  Keep secrets in env vars; contracts carry variable names only. Claude
28
30
  `bypassPermissions` only in a repository-scoped, recoverable environment;
@@ -19,7 +19,7 @@ the run ref tip.
19
19
 
20
20
  `contract.maxParallel` bounds concurrent nodes; each tick dispatches every
21
21
  `pending` node whose dependencies are `done`, up to the free slots, each into
22
- its own worktree. Integration stays serialized.
22
+ its own worktree.
23
23
 
24
24
  ## Integration transaction
25
25
 
@@ -65,10 +65,9 @@ directory is never resumed — it has not proved it needs to be.
65
65
  ## Runtime discovery
66
66
 
67
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,
68
+ reporting `{available, exhaustedUntil, reason}` per runtime. Omitted
69
+ `runtimes`/`runtimeDefaults` are composed once and persisted in
70
+ `routing.assignments`; exhaustion re-tiers within the current tier only,
72
71
  otherwise the node parks `attention` with `runtime_tier_exhausted`. Failover
73
72
  rules: [contract.md](contract.md).
74
73
 
@@ -116,8 +115,7 @@ attempt, error code, done/total — never model text), calls the executable name
116
115
  by `FABERUN_NOTIFY_BIN` with that event as JSON on stdin, and appends a
117
116
  timestamped receipt (`delivered`, `failed`, `no_transport`) to
118
117
  `<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`.
118
+ no backoff. Unset, nothing is spawned and the receipt is `no_transport`.
121
119
  `FABERUN_NOTIFY_BIN=os-macos` selects the bundled `osascript` adapter
122
120
  (`canWake: false`); any other value is an executable path. A resume never
123
121
  re-sends a notification already recorded for the same node, attempt and outcome.
@@ -138,7 +136,7 @@ Every contract requires `campaignId`; campaign state lives at
138
136
  `HANDOFF.md`) and can link multiple runs.
139
137
 
140
138
  ```bash
141
- node src/cli.mjs campaign <op> <id> [--cwd <dir>] …flags
139
+ faberun campaign <op> <id> [--cwd <dir>] …flags
142
140
  init --goal "Goal" | attach --tool codex --session-id <s> --transcript <path> --format jsonl
143
141
  note --session-id <s> --kind <intent|decision|supersede|constraint|outcome|next|open-question|retrospective> --text <t>
144
142
  resolve --session-id <s> --question-id <q> --text <a> | sync --session-id <s> | ack --session-id <s> --event-id <e>
@@ -171,10 +169,10 @@ The harness registry (`src/seat/harnesses.mjs`) declares five entries — `claud
171
169
  marker, and `canRenderAmbient` (claude only).
172
170
 
173
171
  ```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>]
172
+ faberun seat start <campaign-id> --cwd <dir> [--harness <name>]
173
+ faberun seat attach [<campaign-id>] [--cwd <dir>] [--ssh <host>]
174
+ faberun seat status [--json] [--cwd <dir>]
175
+ faberun seat stop [<campaign-id>] [--cwd <dir>]
178
176
  ```
179
177
 
180
178
  `attach` prints the command to paste rather than running `tmux attach`, which
@@ -182,3 +180,12 @@ would nest sessions; `--ssh <host>` prints the remote `ssh -t` line. `status
182
180
  --json` lists each window's campaign, harness and ambient capability. tmux is
183
181
  optional: every `seat` function returns an explicit unavailable result when the
184
182
  binary is absent, and only reattaching is lost.
183
+
184
+ ## Install, set up, update
185
+
186
+ `faberun setup` writes the user runtime config that composition honours when a contract omits runtimes.
187
+ `faberun init` prepares a target repository (`.runs` ignored, skill installed, agent kit optional and always asked).
188
+ `faberun update --check` reads the release channel and writes the cache the banner shows; `faberun update` switches versions only after the new one proves itself.
189
+ `faberun skills install [name]` installs the shipped skills.
190
+ `faberun skills register [--harness a,b] [--copy] [--force]` links the faberun skill into each installed harness's skills directory.
191
+ `faberun campaign unpark <id> [--force]` clears a parked campaign once its run is no longer parked so `supervise campaign` can continue.
@@ -8,7 +8,9 @@ physical runtime. Use `mode: "discovery"` only when no packet is possible.
8
8
  **Prove mechanically.** Every Definition of Done item is an object declaring
9
9
  its own proof: a verification `command`, a workspace `path`, or `judgment`.
10
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.
11
+ Contract-level `finalVerification` runs on the phase-terminal node;
12
+ `sharedVerification` runs on every node, so a ratchet-breaking write set
13
+ fails on its own attempt.
12
14
 
13
15
  **Never wait inside a turn.** No `sleep`/`while` loops, no repeated `status`
14
16
  calls, no watched background jobs — every tool call re-sends the whole session
@@ -36,6 +36,7 @@ import { assertContractManifestIntact, parkCampaign, promoteRunInCampaign } from
36
36
  import { readCampaign } from "./record.mjs";
37
37
  import { validateContract } from "../contract/index.mjs";
38
38
  import { defaultControllerIdentity, storedContractDigest, verifyControllerIdentity } from "../engine/run-identity.mjs";
39
+ import { refuseSelfSignal } from "../engine/process-identity.mjs";
39
40
  import { HEARTBEAT_INTERVAL_MS, createHeartbeat, groupAlive, heartbeatBreach, readHeartbeat, runProgress, waitForGroupGone } from "../engine/supervise.mjs";
40
41
  import { pidAlive, processStartToken } from "../run/lock.mjs";
41
42
  import { delay, errorCode, errorMessage } from "../util.mjs";
@@ -147,10 +148,12 @@ function groupKill(pid, signal) {
147
148
  /**
148
149
  * Terminate the coordinator's process group, bounded: `SIGTERM`, then `SIGKILL`
149
150
  * after the named grace. The takeover writes its own lock only once the group
150
- * is gone, so two coordinators never overlap.
151
+ * is gone, so two coordinators never overlap. A lock naming this process or its
152
+ * parent is refused before any signal: that group is the caller's own, so the
153
+ * refusal is recorded and the takeover leaves it alone.
151
154
  *
152
155
  * @param {string} campaignPath
153
- * @param {{lock?: Record<string, unknown>|null, kill?: (pid: number, signal: string) => void, alive?: (pid: number) => boolean, sleep?: (ms: number) => Promise<void>, now?: () => number, graceMs?: number, killGraceMs?: number}} [options]
156
+ * @param {{lock?: Record<string, unknown>|null, kill?: (pid: number, signal: string) => void, alive?: (pid: number) => boolean, sleep?: (ms: number) => Promise<void>, now?: () => number, graceMs?: number, killGraceMs?: number, append?: (event: Record<string, unknown>) => void}} [options]
154
157
  * @returns {Promise<boolean>}
155
158
  */
156
159
  export async function terminateCoordinatorGroup(campaignPath, options = {}) {
@@ -158,6 +161,7 @@ export async function terminateCoordinatorGroup(campaignPath, options = {}) {
158
161
  if (!lock || /** @type {{invalid?: true}} */ (lock).invalid) return false;
159
162
  const record = /** @type {Record<string, unknown>} */ (lock);
160
163
  const pid = /** @type {number} */ (record.pid);
164
+ if (refuseSelfSignal(pid, options.append)) return false;
161
165
  const kill = options.kill ?? groupKill;
162
166
  const alive = options.alive ?? ((target) => pidAlive(target) || groupAlive(target));
163
167
  const sleep = options.sleep ?? delay;
@@ -110,7 +110,7 @@ export function resolveCampaign(runsDir, campaignId) {
110
110
  if (campaigns.length) {
111
111
  throw new Error(`no active campaign under ${campaignsDir(runsDir)}; all campaigns are closed`);
112
112
  }
113
- throw new Error(`no campaign found under ${campaignsDir(runsDir)}; initialize one with: runner.mjs campaign init`);
113
+ throw new Error(`no campaign found under ${campaignsDir(runsDir)}; initialize one with: faberun campaign init`);
114
114
  }
115
115
  if (active.length > 1) {
116
116
  const ids = active.map((entry) => entry.campaign.id).join(", ");
@@ -237,6 +237,21 @@ export function parkCampaign(campaignPath, attention) {
237
237
  return parked;
238
238
  }
239
239
 
240
+ /**
241
+ * A promotion record is only worth keeping when it actually moved the land
242
+ * branch. `promoteRun`'s `already_promoted` result reports the branch's
243
+ * *current* head, which may have advanced past this run since it last landed
244
+ * (another run promoted in between, or a coordinator restart is replaying the
245
+ * same call); recording it would add a second entry for a run that never
246
+ * moved anything.
247
+ *
248
+ * @param {import("../repo/integrate.mjs").PromoteRecord} record
249
+ * @returns {boolean}
250
+ */
251
+ export function promotionMovedBranch(record) {
252
+ return record.status === "promoted";
253
+ }
254
+
240
255
  /**
241
256
  * Promote a run onto the campaign's landing branch and record it. The branch
242
257
  * name comes from the campaign record, never from the caller, so a campaign
@@ -257,6 +272,7 @@ export function promoteRunInCampaign({ campaignPath, repo, runId, runHead, baseS
257
272
  finalVerificationPassed,
258
273
  allowMain,
259
274
  onPromoted: (record) => {
275
+ if (!promotionMovedBranch(record)) return;
260
276
  recordPromotion(campaignPath, {
261
277
  runId: record.runId,
262
278
  ...(contractPath === undefined ? {} : { contractPath }),
@@ -23,7 +23,7 @@
23
23
  * 1 only when a whole run had to be re-authored after a failure that
24
24
  * `resume` could not repair.
25
25
  *
26
- * The second half of this module is the `runner.mjs metrics` command: reading
26
+ * The second half of this module is the `faberun metrics` command: reading
27
27
  * a campaign's linked runs and parsing the command's flags live here, while
28
28
  * `metrics-report.mjs` decides how the projection is printed.
29
29
  */
@@ -397,7 +397,7 @@ function orderOf(entry) {
397
397
  return Number.isFinite(entry.atMs) ? entry.atMs : 0;
398
398
  }
399
399
 
400
- /** Flags of `runner.mjs metrics`, declared here so the router only names them. */
400
+ /** Flags of `faberun metrics`, declared here so the router only names them. */
401
401
  /** @type {import("node:util").ParseArgsOptionsConfig} */
402
402
  export const METRICS_OPTIONS = { cwd: { type: "string" }, json: { type: "boolean" } };
403
403
 
@@ -479,7 +479,7 @@ function readRunNodes(runDir) {
479
479
  }
480
480
 
481
481
  /**
482
- * `runner.mjs metrics <campaign-id> [--cwd <dir>] [--json]`: project the
482
+ * `faberun metrics <campaign-id> [--cwd <dir>] [--json]`: project the
483
483
  * campaign's recorded artefacts and return what the command prints. Reading
484
484
  * only, and never a write: a report of a closed campaign must not touch it.
485
485
  *
package/src/cli/brand.mjs CHANGED
@@ -188,7 +188,7 @@ export function renderUsage() {
188
188
  "<status|report> <run-dir> [--json]",
189
189
  "findings <run-dir>",
190
190
  "doctor [<contract.json>] [--cwd <dir>] [--discover] [--json]",
191
- "setup [--yes] [--harnesses <a,b>] [--worker <id>] [--judge <id>] [--json]",
191
+ "setup [--yes] [--no-skill] [--harnesses <a,b>] [--worker <id>] [--judge <id>] [--json]",
192
192
  "init [--cwd <dir>] [--yes] [--no-skill] [--agentkit] [--greenfield|--stable] [--json]",
193
193
  "update [--check] [--json]",
194
194
  "models [--probe] [--json]",
@@ -200,6 +200,7 @@ export function renderUsage() {
200
200
  "seat <start|attach|status|stop> [<campaign-id>] [--cwd <dir>] ...",
201
201
  "skills list",
202
202
  "skills install [<name>...] [--target <dir>] [--global] [--force]",
203
+ "skills register [--harness <a,b>] [--copy] [--force] [--json]",
203
204
  ];
204
205
  return `${groups.map((group) => `usage: faberun ${group}`).join("\n")}\n`;
205
206
  }
@@ -728,3 +728,5 @@ function usage() {
728
728
  );
729
729
  process.exitCode = 2;
730
730
  }
731
+
732
+ export default OPERATION_OPTIONS;
@@ -65,3 +65,5 @@ function usage() {
65
65
  process.stderr.write("usage: faberun contract validate <contract.json>\n");
66
66
  process.exitCode = 2;
67
67
  }
68
+
69
+ export default OPERATION_OPTIONS;