faberun 0.3.0 → 0.6.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/README.md +152 -100
- package/package.json +8 -2
- package/skills/faberun/SKILL.md +6 -5
- package/skills/faberun/references/contract.md +23 -11
- package/skills/faberun/references/engineering.md +3 -1
- package/skills/faberun/references/operations.md +19 -12
- package/skills/faberun/references/rules.md +3 -1
- package/src/campaign/chain.mjs +6 -2
- package/src/campaign/index.mjs +17 -1
- package/src/campaign/metrics.mjs +3 -3
- package/src/cli/brand.mjs +2 -1
- package/src/cli/setup.mjs +109 -30
- package/src/cli/skills.mjs +308 -8
- package/src/cli.mjs +2 -1
- package/src/contract/final-verification.mjs +31 -2
- package/src/contract/index.mjs +27 -24
- package/src/contract/runtime.mjs +5 -1
- package/src/contract/task-packet.mjs +20 -9
- package/src/contract/verification.mjs +1 -1
- package/src/engine/backoff.mjs +1 -1
- package/src/engine/dispatch.mjs +5 -3
- package/src/engine/gate.mjs +12 -0
- package/src/engine/process-identity.mjs +39 -0
- package/src/engine/prompts.mjs +18 -0
- package/src/engine/resume.mjs +2 -2
- package/src/engine/review.mjs +9 -1
- package/src/engine/run-command.mjs +23 -2
- package/src/engine/run-identity.mjs +14 -0
- package/src/engine/scheduler.mjs +45 -12
- package/src/engine/settle.mjs +29 -0
- package/src/engine/supervise.mjs +32 -6
- package/src/engine/verify.mjs +98 -9
- package/src/harnesses/agy/index.mjs +3 -0
- package/src/harnesses/claude/index.mjs +5 -0
- package/src/harnesses/codex/index.mjs +3 -0
- package/src/harnesses/dsh/index.mjs +26 -0
- package/src/harnesses/exec-jsonl/index.mjs +2 -0
- package/src/harnesses/index.mjs +10 -3
- package/src/harnesses/replay/index.mjs +2 -0
- package/src/harnesses/zcode/index.mjs +3 -0
- package/src/host/preflight.mjs +5 -1
- package/src/notify/index.mjs +45 -2
- package/src/repo/source-identity.mjs +4 -3
- package/src/report/render.mjs +128 -49
- package/src/web/index.html +1 -1
package/README.md
CHANGED
|
@@ -1,131 +1,183 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
with a single command.
|
|
3
|
+
<h1 align="center">faberun</h1>
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
`
|
|
21
|
-
|
|
22
|
-
`~/.
|
|
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
|
-
|
|
26
|
-
ln -s "$(pwd)/skills/init-agentkit" ~/.claude/skills/init-agentkit
|
|
49
|
+
npm install -g faberun
|
|
27
50
|
```
|
|
28
51
|
|
|
29
|
-
|
|
52
|
+
Or run the package without installing it:
|
|
30
53
|
|
|
31
|
-
|
|
54
|
+
```bash
|
|
55
|
+
npx faberun --help
|
|
56
|
+
```
|
|
32
57
|
|
|
33
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
66
|
+
An installed copy updates itself from the newest GitHub release:
|
|
54
67
|
|
|
55
68
|
```bash
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
faberun update
|
|
70
|
+
```
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
##
|
|
172
|
+
## History
|
|
122
173
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
##
|
|
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
|
+
"version": "0.6.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":
|
|
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
|
},
|
package/skills/faberun/SKILL.md
CHANGED
|
@@ -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
|
|
14
|
+
| Launch and resume | [workflow](references/workflow.md), [operations](references/operations.md) |
|
|
15
15
|
| Dispatch a node | [handoffs](references/handoffs.md) |
|
|
16
|
-
| Supervise, answer
|
|
17
|
-
| Verify, judge
|
|
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
|
-
|
|
22
|
-
|
|
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`
|
|
81
|
-
dependency declares in its `writeFiles`, or that sits under
|
|
82
|
-
|
|
83
|
-
|
|
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.
|
|
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` (
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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.
|
|
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
|
|
69
|
-
`
|
|
70
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
package/src/campaign/chain.mjs
CHANGED
|
@@ -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;
|
package/src/campaign/index.mjs
CHANGED
|
@@ -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:
|
|
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 }),
|
package/src/campaign/metrics.mjs
CHANGED
|
@@ -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 `
|
|
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 `
|
|
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
|
-
* `
|
|
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
|
}
|