razzou-harness 0.1.3 → 0.2.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 (40) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +204 -18
  3. package/dist/agents/claude-code.js +64 -14
  4. package/dist/anthropic/usage.js +53 -0
  5. package/dist/ci/harness-ci.js +38 -0
  6. package/dist/cli.js +125 -16
  7. package/dist/daemon/architects.js +6 -2
  8. package/dist/daemon/design.js +4 -0
  9. package/dist/daemon/items.js +24 -4
  10. package/dist/daemon/manager.js +108 -59
  11. package/dist/daemon/orchestrator.js +579 -91
  12. package/dist/daemon/requests.js +10 -3
  13. package/dist/daemon/review.js +334 -0
  14. package/dist/dashboard/server.js +34 -6
  15. package/dist/design/owner-time.js +72 -0
  16. package/dist/design/schemas.js +95 -4
  17. package/dist/github/app.js +28 -4
  18. package/dist/github/artifacts.js +35 -0
  19. package/dist/github/forge.js +95 -0
  20. package/dist/github/setup.js +60 -10
  21. package/dist/review/cost-estimate.js +25 -0
  22. package/dist/review/paid-calls.js +68 -0
  23. package/dist/roles.js +8 -0
  24. package/dist/setup/doctor.js +81 -10
  25. package/dist/setup/init.js +3 -2
  26. package/dist/setup/setup.js +378 -0
  27. package/dist/state/events.js +5 -2
  28. package/dist/state/state.js +158 -29
  29. package/dist/web/THIRD-PARTY-LICENSES.md +1 -1
  30. package/dist/web/assets/{index-D27gAaUJ.css → index-CMViLVQH.css} +1 -1
  31. package/dist/web/assets/index-DLttwGM6.js +96 -0
  32. package/dist/web/index.html +2 -2
  33. package/package.json +3 -3
  34. package/roles/architect.md +24 -5
  35. package/roles/cost.md +43 -0
  36. package/roles/manager.md +4 -3
  37. package/roles/security.md +37 -0
  38. package/src/ci/harness-ci.ts +42 -1
  39. package/templates/harness.yml +64 -0
  40. package/dist/web/assets/index-DtxDOPz5.js +0 -96
package/CHANGELOG.md CHANGED
@@ -3,6 +3,85 @@
3
3
  The notable changes in each release of razzou-harness. Before 1.0, a new minor version
4
4
  may change how a project is set up, and its notes say how.
5
5
 
6
+ ## 0.2.0 (2026-11-06)
7
+
8
+ **Upgrading a project from 0.1.x:** install the new version, then run `harness setup`
9
+ in the project: it writes the workflow with the new `cost` job (commit it), waits for
10
+ you to give the App "Commit statuses: write", and stores the token the usage probe
11
+ reads. Without Swarm on, nothing else changes: Manual mode works as before, with the
12
+ new attempt clock of 120 minutes and Manager turn of 90.
13
+
14
+ - **Swarm mode**, designed in `docs/architecture/proposal-v3.md`: a second mode, switched
15
+ in the dashboard or with `harness mode`, in which the daemon merges a plan's pull
16
+ request once CI and the reviewers' statuses pass. `CODE` plans merge when
17
+ `swarm.merge.code` is `daemon`; `INFRA` and `MIXED` plans need the cost estimate
18
+ under `swarm.merge.infraMonthlyUsdCeiling`, which no project produces yet, so they
19
+ wait for the owner. A pull request the policy leaves to the owner shows the reason
20
+ under Needs you. `Arch:` and `Fix:` pull requests stay the owner's.
21
+ - A plan is `CODE`, `INFRA` or `MIXED`, from its features' kinds. Plan pull requests
22
+ carry the tag in their title, and CI's `verify` job keeps a `CODE` plan's pull
23
+ request off the Terraform folder.
24
+ - `harness github-setup` applies the ruleset of the daemon's mode, or of `--mode`: in
25
+ Swarm, no approval, and the reviewers' statuses required and pinned to the App.
26
+ `harness doctor` checks the ruleset against the mode, and that the App may write
27
+ commit statuses.
28
+ - Budgets in the Project view: your hours for the whole project, and a threshold for
29
+ the agent hours, the Developers' attempt clocks summed, red when over.
30
+ - Your time as a design constraint, in both modes, once you set your hours: owner
31
+ actions carry `estimatedMinutes`, a reserve of `ownerTime.reservePercent` stays out
32
+ of designs, and `submit_design` refuses a design whose actions take more than what
33
+ remains. Only owner actions count, not the proposals or pull requests you read. The
34
+ Architect gets `get_owner_time`, and a note when the budget changes.
35
+ - An attempt's deadline is 120 minutes, and the Manager's turn 90, by default.
36
+ - In Swarm, up to `swarm.maxDevelopers` Developers build a plan's independent features
37
+ at once. The plan's head is merged into an attempt before its checks when another
38
+ feature landed first, and the feature lands as one commit on the head; a conflict
39
+ interrupts the attempt, and the next one starts from the new head. A design change
40
+ merged while a feature is built interrupts its attempt the same way. The Architect is
41
+ told to declare every dependency in `dependsOn`, and hears in one line when a plan's
42
+ pull request opens, waits for the owner, is blocked by a reviewer, or merges.
43
+ - Plans in parallel, in Swarm: a plan's `dependsOn` names the plans that must be merged
44
+ first, or the previous plan without it; up to `swarm.maxPlans` plans build at once,
45
+ each with its own Manager, keyed `module/plan`; a feature may depend on an earlier
46
+ plan's feature as `P1/F03`. The Project view draws each module's features as a graph
47
+ with their dependencies, and the Agents view lists the reviewers' sessions.
48
+ - Two reviewers, security and cost, read every plan's pull request once CI passed, in
49
+ Swarm, or in Manual as comments only with `swarm.reviewersInManual`. Each runs in a
50
+ fresh read-only sandbox with a brief: the features, the files changed, the
51
+ pay-per-use AWS calls the change adds or removes, and the Terraform plan's JSON.
52
+ Their findings are posted on the pull request, their verdicts set as the commit
53
+ statuses `harness/security` and `harness/cost`, and a blocking verdict opens a fix
54
+ round, two per reviewer by default, before the pull request is the owner's. The App
55
+ needs "Commit statuses: write"; an older App's tokens fall back to read, and the
56
+ pull request waits for the owner with the reason.
57
+ - `harness setup`, designed in `docs/architecture/proposal-v4.md`: one command that
58
+ sets up this machine and the project, does every step the harness can, and asks only
59
+ for what is the owner's: a `gh` login, the Claude token at a hidden prompt, the
60
+ clicks GitHub needs for the App, three questions, and with AWS the role ARNs and the
61
+ gateway key. It commits and pushes the files it wrote once the owner agrees, runs the
62
+ doctor and fixes what it can, waits for the browser steps, runs the probe, and offers
63
+ to start the daemon. `harness init` and the checklist stay, for the steps by hand.
64
+ - `harness mode` applies the mode's ruleset on `main` in the same command, with your
65
+ own `gh`. In Swarm the daemon reads the rules in effect on `main` through the App,
66
+ and the dashboard's "Before you use Swarm" note lists only what is left: the rules,
67
+ the App's statuses permission, the usage probe's token; it goes once all are done.
68
+ `harness budget --owner-hours <n>` sets your hours, even with the daemon down.
69
+ - A button in the dashboard's header, in Swarm, that lets the daemon merge `INFRA` and
70
+ `MIXED` plans like `CODE` ones, once CI and both reviewers pass; an Infracost estimate
71
+ over the ceiling still waits for the owner. Off by default; the choice is an event.
72
+ - The dependencies with advisories moved past them: zod 4.6.6 or later, and dompurify
73
+ 3.4.17 or later for the dashboard's Markdown.
74
+ - A `cost` job in the CI workflow, written by `harness install-ci`: on a pull request
75
+ with `iac/` and the `INFRACOST_API_KEY` secret, Infracost's estimate of the monthly cost
76
+ the change adds is uploaded as an artifact, which the daemon reads once CI passed.
77
+ `INFRA` and `MIXED` plans then merge themselves in Swarm under
78
+ `swarm.merge.infraMonthlyUsdCeiling`, and the cost reviewer gets the estimate in its
79
+ brief. Without the job's artifact they wait for the owner, with the reason.
80
+ - In Swarm, the daemon reads the subscription's usage before it starts an attempt, and
81
+ at or above `swarm.usageCeilingPercent` the pipeline waits for the window to reset.
82
+ `harness doctor` stores the provider's token in `~/.config/harness/claude.token` for
83
+ it, and asks for it on an older setup once Swarm is on. Spike 5 records the endpoint.
84
+
6
85
  ## 0.1.3 (2026-10-11)
7
86
 
8
87
  - More npm keywords, for a team of agents in sandboxes.
package/README.md CHANGED
@@ -8,6 +8,8 @@ Every agent works in its own [OpenShell](https://github.com/NVIDIA/OpenShell) sa
8
8
  with no credentials, every feature is checked in a fresh one, and nothing reaches `main`
9
9
  until you merge its pull request. A small daemon runs the loop, following Anthropic's
10
10
  [Effective harnesses for long-running agents](https://www.anthropic.com/engineering/effective-harnesses-for-long-running-agents).
11
+ That is Manual mode. For a project too large to read in full, [Swarm mode](#swarm-mode)
12
+ lets the daemon merge the plans that pass CI and two reviewers.
11
13
 
12
14
  - [How it works](#how-it-works)
13
15
  - [Requirements](#requirements)
@@ -15,6 +17,7 @@ until you merge its pull request. A small daemon runs the loop, following Anthro
15
17
  - [Your first module](#your-first-module)
16
18
  - [Concepts](#concepts)
17
19
  - [The dashboard](#the-dashboard)
20
+ - [Swarm mode](#swarm-mode)
18
21
  - [Commands](#commands)
19
22
  - [Configuration](#configuration)
20
23
  - [AWS and Terraform](#aws-and-terraform)
@@ -25,6 +28,49 @@ until you merge its pull request. A small daemon runs the loop, following Anthro
25
28
 
26
29
  ## How it works
27
30
 
31
+ ```mermaid
32
+ flowchart TB
33
+ you(["You"])
34
+
35
+ subgraph machine["Your machine"]
36
+ dash["Dashboard<br/>localhost, behind a token"]
37
+ daemon["Daemon<br/>event log, brakes, .harness/git"]
38
+ end
39
+
40
+ subgraph sandboxes["OpenShell sandboxes, with no credentials"]
41
+ arch["Architect<br/>designs the module with you"]
42
+ mgr["Manager<br/>reviews, read-only"]
43
+ dev["Developers<br/>one feature per attempt"]
44
+ chk["Checks<br/>a fresh sandbox, no agent"]
45
+ end
46
+
47
+ proxy["OpenShell proxy<br/>adds the Claude token,<br/>signs AWS requests"]
48
+ claude["Claude API"]
49
+
50
+ subgraph github["GitHub"]
51
+ pr["Pull requests<br/>Arch:, Plan:, Fix:"]
52
+ ci["CI<br/>with the base branch's rules"]
53
+ mainb["main"]
54
+ end
55
+
56
+ aws["AWS<br/>read-only view role for agents"]
57
+
58
+ you <--> dash
59
+ you -- "talks it through" --> arch
60
+ dash <--> daemon
61
+ daemon <-- "harness tools" --> arch
62
+ daemon <-- "harness tools" --> mgr
63
+ daemon <-- "harness tools" --> dev
64
+ daemon -- "runs" --> chk
65
+ sandboxes --> proxy
66
+ proxy --> claude
67
+ proxy -- "read-only" --> aws
68
+ daemon -- "as a GitHub App:<br/>pushes, opens PRs" --> pr
69
+ pr --> ci
70
+ pr -- "you merge" --> mainb
71
+ mainb -- "CI applies Terraform" --> aws
72
+ ```
73
+
28
74
  You start a module in the dashboard, and talk with its Architect (Claude Code,
29
75
  interactive) in its terminal. The Architect asks you questions until the need is clear,
30
76
  writes proposals until you accept one, then turns it into plans, features, checks and
@@ -41,7 +87,8 @@ daemon builds the module, feature by feature:
41
87
  and the Manager submits it;
42
88
  - the daemon pushes the plan branch and opens one pull request for it, as a GitHub App
43
89
  with no access to workflows. CI checks the PR with the rules of the base branch. If CI
44
- fails, the Manager diagnoses it and a Developer fixes it, twice at most. You merge.
90
+ fails, the Manager diagnoses it and a Developer fixes it, twice at most. You merge,
91
+ or the daemon does in [Swarm mode](#swarm-mode).
45
92
 
46
93
  Infrastructure features take the same path. Agents see AWS through a read-only role
47
94
  whose requests the OpenShell proxy signs, so they plan against the real state but never
@@ -79,17 +126,28 @@ without pull requests.
79
126
 
80
127
  ```sh
81
128
  npm install -g razzou-harness --allow-scripts=node-pty
82
- cd path/to/project && harness init # then follow the checklist it prints
83
- harness doctor --build-images # builds the agents' image
84
- harness doctor --probe # everything an agent must not do, tried
85
- harness up # the daemon, and the dashboard
129
+ cd path/to/project && harness setup # the rest, asking only for what is yours
86
130
  ```
87
131
 
88
132
  The command is also installed as `razzou-harness`, for machines where another `harness`,
89
133
  such as the Harness.io CLI, comes first on the PATH.
90
134
 
91
- `harness init` writes the harness's files into the repository, never over existing
92
- ones, and prints the setup checklist:
135
+ `harness setup` does every step it can and asks only for what is yours: a `gh` login,
136
+ the token from `claude setup-token` at a hidden prompt, two clicks on GitHub for the
137
+ App, since GitHub creates and installs Apps only in the browser, three questions (your
138
+ stack's network bundles, AWS or not, the hours of your time), and, with AWS, the role
139
+ ARNs from your bootstrap and the gateway key. In order, it logs you in to `gh`; creates
140
+ the repository on GitHub if the project has no remote, and writes the harness's files;
141
+ asks the questions and writes `harness/config.json`, with the `staging` environment
142
+ and the variables on GitHub for AWS; commits and pushes the files to `main` with your
143
+ git, once you agree; runs `harness doctor` and fixes what it can (starts the OpenShell
144
+ gateway, builds the agents' image, imports the Claude profile and creates the provider
145
+ with your token, creates the App from its manifest and waits for you to install it,
146
+ applies the ruleset on `main`, creates the AWS provider with your key); runs the probe;
147
+ and offers to start the daemon. Run it again at any time: what is done is skipped.
148
+
149
+ The same by hand: `harness init` writes the harness's files into the repository, never
150
+ over existing ones, and prints the setup checklist:
93
151
  1. Docker, and OpenShell with a running gateway.
94
152
  2. The Claude subscription: `claude setup-token`, then `harness doctor` in a terminal
95
153
  creates the provider, with the token typed at a hidden prompt.
@@ -101,7 +159,9 @@ ones, and prints the setup checklist:
101
159
  may not touch workflows.
102
160
  7. `harness doctor --probe`.
103
161
 
104
- `harness doctor` checks each step, and says what to do about what fails. `--probe` tries,
162
+ `harness doctor --build-images` builds the agents' image, `harness doctor --probe` runs
163
+ the probe, and `harness up` starts the daemon. `harness doctor` checks each step, and
164
+ says what to do about what fails. `--probe` tries,
105
165
  from a sandbox like an agent's and from outside, everything that must be refused: AWS
106
166
  writes and secret reads, other projects' state, `git push`, unlisted hosts and the
107
167
  metadata address, a workflow pushed by the App, a merge without your approval.
@@ -150,14 +210,16 @@ plans it finds there (see [Configuration](#configuration)).
150
210
  - **Proposal:** the Architect's design document, in versions v1, v2 and so on. You
151
211
  accept one.
152
212
  - **Plan:** features that ship together as one pull request. A module's plans are built
153
- in order.
213
+ in order. A plan is `CODE`, `INFRA` or `MIXED`, from its features' kinds: the kind is
214
+ tagged in the pull request's title, and a `CODE` plan's pull request may not touch
215
+ the Terraform folder. Give infrastructure plans of its own.
154
216
  - **Feature:** one change that a Developer can make in one attempt, with at least one
155
217
  check. Its definition is frozen once its design is merged.
156
218
  - **Check:** a shell command, run from the repository root, that exits 0 when the feature
157
219
  works. `pre-merge` checks run on every attempt and in CI; `post-deploy` checks run
158
220
  after an infrastructure deploy.
159
221
  - **Attempt:** one Developer session on one feature, in a fresh sandbox, with a deadline
160
- (90 minutes) and a budget (2 attempts per feature). A failed attempt's successor
222
+ (120 minutes) and a budget (2 attempts per feature). A failed attempt's successor
161
223
  starts with the Manager's guidance. Once the budget is spent, the feature is escalated
162
224
  to you.
163
225
  - **Owner actions:** what only you can do, such as creating an account, approving a
@@ -172,7 +234,7 @@ plans it finds there (see [Configuration](#configuration)).
172
234
  - **`progress.txt`:** one line per accepted feature and satisfied gate, committed with
173
235
  the work. It only grows.
174
236
  - **Brakes:** counters and deadlines, not judgment. Three failed attempts in a row, a
175
- Manager that leaves an item waiting for 40 minutes, or CI still failing after two fix
237
+ Manager that leaves an item waiting for 180 minutes, or CI still failing after two fix
176
238
  rounds pause the pipeline until you resume it.
177
239
 
178
240
  ## The dashboard
@@ -188,7 +250,114 @@ behind a token. Its views:
188
250
  - **Events:** the event log, live, with a filter.
189
251
 
190
252
  The header pauses and resumes the pipeline: a pause lets what runs finish, and starts
191
- nothing new.
253
+ nothing new. It also shows the mode, Manual or Swarm, and switches it.
254
+
255
+ ## Swarm mode
256
+
257
+ Manual mode, the default, is what the rest of this README describes: you review and
258
+ merge every pull request. Past a certain size you become the bottleneck. Swarm mode
259
+ gives the agents more autonomy and you less to read. Its design is
260
+ `docs/architecture/proposal-v3.md` in the source repository; this section says what
261
+ exists today.
262
+
263
+ **The switch.** The dashboard's header switches the mode; so do `harness mode swarm`
264
+ and `harness mode manual`. A switch takes effect at item boundaries: what runs
265
+ finishes under the rules it started with. GitHub's rules on `main` differ per mode, and
266
+ only you can change them: `harness mode` applies the mode's ruleset in the same
267
+ command, with your own `gh`; after a switch from the dashboard, run
268
+ `harness github-setup --repo <org>/<repo>` once. Until the rules match nothing merges.
269
+ The daemon reads the rules in effect on `main` through the App and the dashboard's
270
+ note lists what is still to do, the commands ready to copy: the rules, the App's
271
+ "Commit statuses: write", and the token for the usage probe. The note goes once all
272
+ three are done; `harness doctor` checks the same.
273
+
274
+ **What merges itself.** In Swarm the daemon merges a plan's pull request, as the App,
275
+ once CI passed and every status in `swarm.requiredStatuses` is green on its head:
276
+ - a `CODE` plan, when `swarm.merge.code` is `daemon`;
277
+ - an `INFRA` or `MIXED` plan, when the monthly cost it adds, estimated by CI, is at or
278
+ under `swarm.merge.infraMonthlyUsdCeiling`; or, once the header's button **Let the
279
+ daemon merge infra** is on, like a `CODE` plan, once CI and both reviewers pass, an
280
+ estimate over the ceiling still waiting for you. The button is off by default, and
281
+ the choice is recorded in the event log.
282
+
283
+ Anything else waits for you, under **Needs you** with the reason. `Arch:` and `Fix:`
284
+ pull requests are yours in both modes. In Swarm, GitHub's rules need no approval but
285
+ require the reviewers' statuses, pinned to the App, so even the daemon can't merge
286
+ before they pass.
287
+
288
+ **Before a launch.** In Swarm the daemon reads your subscription's usage before it
289
+ starts an attempt, and at or above `swarm.usageCeilingPercent` the pipeline waits for
290
+ the window to reset. It reads it with the token `harness doctor` stores in
291
+ `~/.config/harness/claude.token` when it creates the Claude provider; on an older
292
+ setup, the doctor asks for the token again once Swarm is on. Without it, the daemon
293
+ learns of a limit only when an attempt hits it, as in Manual.
294
+
295
+ **The reviewers.** Once CI passed on a plan's pull request, two reviewers read it, one
296
+ after the other, each in a fresh read-only sandbox with the Manager's permissions: a
297
+ security reviewer and a cost reviewer, with `roles/security.md` and `roles/cost.md`
298
+ as their prompts and the Manager's model. Each gets a brief: the plan's features, the
299
+ files changed, the pay-per-use AWS calls the change adds or removes, found by pattern
300
+ in the code, and the Terraform plan's JSON when the project has one. Each answers
301
+ with a verdict and findings. The daemon posts the findings on the pull request as a
302
+ review, with comments on the lines of the diff, and sets the reviewer's commit status,
303
+ `harness/security` or `harness/cost`. A blocking verdict opens a fix round on the
304
+ plan branch, like a CI failure: a Developer fixes with the findings as guidance, the
305
+ Manager reviews, CI and the reviewers run again on the new head. After
306
+ `swarm.securityFixRounds` or `swarm.costFixRounds`, the pull request is yours. In
307
+ Manual, `swarm.reviewersInManual` runs them as comments only.
308
+
309
+ The App needs **Commit statuses: write** for the verdicts. An App created before this
310
+ version has read only: give it the permission on GitHub, accept it on the installation,
311
+ and `harness doctor` stops warning. Until then, a plan whose reviewer passed waits for
312
+ you with the reason.
313
+
314
+ **Parallel Developers.** In Swarm, up to `swarm.maxDevelopers` Developers build the
315
+ features of the plan under way at once, each in its own sandbox, as soon as the
316
+ features they depend on are accepted: `dependsOn` in the design decides, so the
317
+ Architect is told to declare every dependency. Each attempt starts from the plan
318
+ branch as it is. When another feature lands first, the daemon merges the new head
319
+ into the attempt before its checks, so they see the accepted work, and the feature
320
+ lands as one commit on the head. A conflict interrupts the attempt, without counting
321
+ it, and the next one starts from the new head. A design change merged while a feature
322
+ is built interrupts its attempt the same way, and the next one builds the new
323
+ definition. Manual mode builds one feature at a time, as before. In both modes, the
324
+ Architect hears in one line when a plan's pull request opens, waits for you, is
325
+ blocked by a reviewer, or merges.
326
+
327
+ **Parallel plans.** A plan may declare `dependsOn`, the plans of its module that must
328
+ be merged before it starts; without it, it waits for the previous plan, as always. In
329
+ Swarm, up to `swarm.maxPlans` plans whose dependencies are merged build at once, each
330
+ on its own branch with its own Manager, keyed `module/plan` in the Agents view; a
331
+ feature may also depend on a feature of an earlier plan, as `P1/F03`, and then waits
332
+ for that plan's merge. The Project view draws each module's features as a graph, plan
333
+ by plan, with an edge per dependency: green accepted, blue under way, red escalated,
334
+ grey not started. Manual mode builds one plan at a time with one Manager per module,
335
+ as before.
336
+
337
+ **The cost job.** `harness install-ci` adds a `cost` job to the workflow: on a pull
338
+ request that has `iac/`, it runs [Infracost](https://www.infracost.io) on the base and
339
+ the head and uploads the estimate as the `cost` artifact, which the daemon reads once
340
+ CI passed. It needs the repository secret `INFRACOST_API_KEY`, a free key from
341
+ `infracost auth login`; without it the job does nothing, no estimate exists, and
342
+ `INFRA` and `MIXED` plans wait for you with that reason. Infracost sees your Terraform's
343
+ resource types and sizes, not your code or state. The cost reviewer gets the estimate in
344
+ its brief. The job is not a required check: verify it on your first `INFRA` pull request
345
+ in a real repository, since the harness's tests only fake its artifact.
346
+
347
+ **Where it stands.** The scanners in CI that the security reviewer would start from,
348
+ gitleaks, semgrep and checkov, are still to come (proposal v3 §5.5): the reviewer reads
349
+ the diff itself.
350
+
351
+ **Budgets.** In both modes, the Project view shows two meters you set there:
352
+ - **Your time.** The hours the whole project may take of you. Once set, every owner
353
+ action in a design, gate or task, carries the Architect's estimate of how long it
354
+ takes you, a reserve of `ownerTime.reservePercent` stays out of what designs may
355
+ plan, and `submit_design` refuses a design whose actions don't fit what remains
356
+ after the other modules. Only owner actions count: the proposals and pull requests
357
+ you read don't. The Architect reads the numbers with `get_owner_time`. The meter
358
+ shows used, planned and the hours, red when over.
359
+ - **Agent hours.** The Developers' attempt clocks summed, against a threshold: red when
360
+ over, and nothing stops.
192
361
 
193
362
  ## Commands
194
363
 
@@ -201,7 +370,9 @@ too.
201
370
  | `harness doctor [--build-images] [--probe]` | Checks the machine and the project, builds the agents' image, and tries what agents must not do |
202
371
  | `harness github-app create --org <org>` | Creates the harness's GitHub App, once per machine |
203
372
  | `harness github-app check --repo <org>/<repo>` | Checks that the App can reach the repository |
204
- | `harness github-setup --repo <org>/<repo>` | Sets merge commits only, and the ruleset on `main`, with your own `gh` |
373
+ | `harness github-setup --repo <org>/<repo> [--mode manual\|swarm]` | Sets merge commits only, and the ruleset on `main` for the daemon's mode, with your own `gh` |
374
+ | `harness budget --owner-hours <n>` | Sets the hours of your time for the whole project; with the daemon down, it records them when it starts (`--agent-hours` sets the threshold) |
375
+ | `harness setup [--repo <owner/name>] [--owner <login>]` | Sets up this machine and the project, asking only for what is yours; run it again any time |
205
376
  | `harness install-ci [--owner <login>]` | Writes the workflow, its CI script and `CODEOWNERS` again, after an upgrade |
206
377
  | `harness lock` | Writes `harness/lock.json`, the hashes of the protected files |
207
378
  | `harness up`, `harness down` | Starts the daemon and the dashboard, or stops the daemon; sandboxes keep running |
@@ -214,6 +385,7 @@ too.
214
385
  | `harness message <text>` | Writes to the Manager of the module being built |
215
386
  | `harness retry <module>/<plan>/<feature>` | Gives an escalated feature one more attempt; `--note` adds guidance |
216
387
  | `harness resume` | Lifts a pause |
388
+ | `harness mode [manual\|swarm]` | Shows the mode, or switches it |
217
389
  | `harness recover` | Rebuilds a lost `.harness/` from `main` and GitHub |
218
390
 
219
391
  ## Configuration
@@ -241,7 +413,7 @@ through an `Arch:` pull request, or a commit of your own on `main`.
241
413
  | `project.repo`, `project.bot` | none | The GitHub repository, and the App's bot login. Without them, the project stays local. |
242
414
  | `roles.<role>.model` | `claude-opus-5-5` | The model of the Architect, the Manager and the Developers |
243
415
  | `roles.<role>.effort` | `max`, `high`, `xhigh` | Claude Code's `--effort`; `null` for a model without effort levels, such as Haiku |
244
- | `roles.developer.attemptTimeoutMinutes` | 90 | An attempt's deadline |
416
+ | `roles.developer.attemptTimeoutMinutes` | 120 | An attempt's deadline |
245
417
  | `roles.developer.maxAttempts` | 2 | Attempts per feature before it is escalated to you |
246
418
  | `network.bundles` | `["core"]` | The hosts sandboxes reach, by bundle: `core`, `node`, `python`, `aws`, `terraform`, `go`, `rust`, `jvm`, `opentofu`, `browsers`. The Architect gets them all. |
247
419
  | `network.extraHosts` | `[]` | More hosts, read-only |
@@ -252,8 +424,16 @@ through an `Arch:` pull request, or a commit of your own on `main`.
252
424
  | `sandbox.env` | `{}` | Environment variables for every sandbox |
253
425
  | `github.requiredChecks` | `["verify", "checks"]` | The CI jobs a pull request must pass; `tf-plan` is added with Terraform |
254
426
  | `brakes.consecutiveFailures` | 3 | Failed attempts in a row that pause the pipeline |
255
- | `brakes.managerTurnMinutes` | 20 | How long an item waits for the Manager before a reminder; as long again, and the pipeline pauses |
427
+ | `brakes.managerTurnMinutes` | 90 | How long an item waits for the Manager before a reminder; as long again, and the pipeline pauses |
256
428
  | `brakes.ciFixRounds`, `brakes.deployFixRounds` | 2, 2 | Fix rounds after CI fails, and `Fix:` pull requests after a deploy fails |
429
+ | `ownerTime.reservePercent` | 20 | The share of your hours designs may not plan for owner actions |
430
+ | `swarm.merge.code` | `daemon` | Who merges a `CODE` plan in Swarm: `daemon`, or `owner` |
431
+ | `swarm.merge.infraMonthlyUsdCeiling` | 50 | The monthly cost an `INFRA` or `MIXED` plan may add and still merge itself in Swarm; `null` leaves them all to you |
432
+ | `swarm.requiredStatuses` | `["harness/security", "harness/cost"]` | The App's statuses a pull request needs before the daemon merges it; `[]` merges on CI alone |
433
+ | `swarm.maxDevelopers`, `swarm.maxPlans` | 3, 2 | Attempts and plans at once in Swarm |
434
+ | `swarm.usageCeilingPercent` | 80 | No launch in Swarm at or above this share of your subscription's limits; `null` turns the probe off |
435
+ | `swarm.securityFixRounds`, `swarm.costFixRounds` | 2, 2 | Fix rounds a reviewer may ask for, before you decide |
436
+ | `swarm.reviewersInManual` | `false` | Run the reviewers in Manual too, as comments only |
257
437
  | `dashboard.port` | 4777 | The dashboard's port, or the next free one |
258
438
  | `aws`, `terraform` | none | See [AWS and Terraform](#aws-and-terraform) |
259
439
 
@@ -369,7 +549,9 @@ What agents can't do, and what stops them:
369
549
  code moves in and out as git bundles. The host never runs code or git hooks that an
370
550
  agent wrote.
371
551
  - **Write to GitHub.** Only the daemon talks to GitHub, as an App with no access to
372
- workflows or settings. Sandboxes may clone and fetch, never push.
552
+ workflows or settings. Sandboxes may clone and fetch, never push. In Swarm mode the
553
+ daemon also merges plans, under the policy above, and GitHub requires the reviewers'
554
+ statuses, pinned to the App, before it can.
373
555
  - **Change the rules.** Checks, CI and the agents' instructions are protected files: a
374
556
  Developer's work can't change them, and CI checks that with the base branch's rules.
375
557
  They change only through an Arch PR you merge, or your own commits.
@@ -379,7 +561,8 @@ What agents can't do, and what stops them:
379
561
  through `web_read`: GET only, public addresses only, checked again after every
380
562
  redirect.
381
563
  - **Decide alone.** Code runs the loop: deadlines, attempt budgets, checks in fresh
382
- sandboxes and your merges are enforced by the daemon, not left to the agents.
564
+ sandboxes and the merges, yours or the Swarm policy's, are enforced by the daemon,
565
+ not left to the agents.
383
566
 
384
567
  What remains possible, by design:
385
568
  - an agent could send code out in the URL of an allowed GET request;
@@ -397,7 +580,10 @@ To report a vulnerability, see `SECURITY.md` in the source repository.
397
580
 
398
581
  - The agents run on your Claude subscription, so their usage counts against your plan's
399
582
  limits. When Claude Code reports that a limit is reached, the attempt under way
400
- doesn't count, and the pipeline pauses for an hour, then goes on.
583
+ doesn't count, and the pipeline pauses for an hour, then goes on. In Swarm mode the
584
+ daemon also reads the usage before each launch, and waits above
585
+ `swarm.usageCeilingPercent` until the window resets; the Project view shows the
586
+ windows.
401
587
  - By default, all three roles run Opus 5.5 at a high effort. To try the harness, or to
402
588
  use less of your plan, set each role's `model` to Haiku 4.5
403
589
  (`claude-haiku-4-5-20251001`) and its `effort` to `null`.
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { ReportSchema } from '../design/schemas.js';
2
+ import { ReportSchema, ReviewReportSchema } from '../design/schemas.js';
3
3
  import { sh, tail } from '../util.js';
4
4
  /** Built-in tools that act only inside the sandbox: no remote triggers, no cloud tasks. Harness tools come through MCP. */
5
5
  const DEVELOPER_TOOLS = 'Bash,Read,Edit,Write,Glob,Grep,WebFetch,WebSearch,TodoWrite';
@@ -31,6 +31,30 @@ export const claudeCode = {
31
31
  ].join('\n');
32
32
  return detach(run.workDir, body);
33
33
  },
34
+ launchReviewer(run) {
35
+ const args = [
36
+ 'claude', '-p',
37
+ '--model', run.model,
38
+ ...(run.effort ? ['--effort', run.effort] : []),
39
+ '--append-system-prompt-file', run.promptFile,
40
+ '--session-id', run.sessionId,
41
+ '--restricted',
42
+ '--tools', MANAGER_TOOLS,
43
+ ...(run.settingsFile ? ['--settings', run.settingsFile] : []),
44
+ '--permission-mode', 'dontAsk',
45
+ '--mcp-config', run.mcpConfigFile,
46
+ '--strict-mcp-config',
47
+ '--output-format', 'stream-json',
48
+ '--verbose',
49
+ ];
50
+ const claude = `${args.map(sh).join(' ')} --json-schema "$(cat ${sh(run.schemaFile)})"`;
51
+ const body = [
52
+ `echo $$ > ${sh(run.pidFile)}`,
53
+ `${claude} < ${sh(run.taskFile)} > ${sh(run.streamFile)} 2> ${sh(run.stderrFile)}`,
54
+ `echo $? > ${sh(run.exitFile)}`,
55
+ ].join('\n');
56
+ return detach(run.workDir, body);
57
+ },
34
58
  launchManager(run) {
35
59
  const args = [
36
60
  'claude', '-p',
@@ -141,24 +165,33 @@ export function reportJsonSchema() {
141
165
  const { $schema: _, ...schema } = z.toJSONSchema(ReportSchema, { target: 'draft-7' });
142
166
  return JSON.stringify(schema);
143
167
  }
168
+ /** The reviewers' report schema (v3 §5.4), the same way. */
169
+ export function reviewJsonSchema() {
170
+ const { $schema: _, ...schema } = z.toJSONSchema(ReviewReportSchema, { target: 'draft-7' });
171
+ return JSON.stringify(schema);
172
+ }
173
+ /** A reviewer's session: its report, or why there is none, read like a Developer's. */
174
+ export function readReview(stream, exitCode, stderr = '') {
175
+ const result = lastResult(stream);
176
+ if (!result) {
177
+ const why = stderr.trim() === '' ? '' : `: ${tail(stderr.trim(), 500)}`;
178
+ return { outcome: exitCode === 0 ? 'no_report' : 'error', detail: `the session ended without a result${why}` };
179
+ }
180
+ const usage = toUsage(result.usage, result.total_cost_usd);
181
+ const text = typeof result.result === 'string' ? result.result : '';
182
+ if (result.is_error === true && /usage limit|limit reached|rate.?limit/i.test(text))
183
+ return { outcome: 'usage_limit', detail: text.slice(0, 500), usage };
184
+ const report = ReviewReportSchema.safeParse(result.structured_output);
185
+ if (!report.success)
186
+ return { outcome: result.is_error === true ? 'error' : 'no_report', detail: text.slice(0, 500), usage };
187
+ return { outcome: 'done', report: report.data, usage };
188
+ }
144
189
  /**
145
190
  * Reads the outcome from the stream's final `result` event and its structured output.
146
191
  * Without one, the end of stderr says why: Claude Code reports a bad flag there.
147
192
  */
148
193
  export function readResult(stream, exitCode, stderr = '') {
149
- let result;
150
- for (const line of stream.split('\n')) {
151
- if (!line.includes('"type":"result"'))
152
- continue;
153
- try {
154
- const event = JSON.parse(line);
155
- if (event.type === 'result')
156
- result = event;
157
- }
158
- catch {
159
- // A partial line from a killed process.
160
- }
161
- }
194
+ const result = lastResult(stream);
162
195
  if (!result) {
163
196
  const why = stderr.trim() === '' ? '' : `: ${tail(stderr.trim(), 500)}`;
164
197
  return { outcome: exitCode === 0 ? 'no_report' : 'error', detail: `the session ended without a result${why}` };
@@ -173,6 +206,23 @@ export function readResult(stream, exitCode, stderr = '') {
173
206
  return { outcome: result.is_error === true ? 'error' : 'no_report', detail: text.slice(0, 500), usage };
174
207
  return { outcome: report.data.outcome, summary: report.data.summary, finding: report.data.finding, notes: report.data.notes, usage };
175
208
  }
209
+ /** The stream's final `result` event, if the session wrote one. */
210
+ function lastResult(stream) {
211
+ let result;
212
+ for (const line of stream.split('\n')) {
213
+ if (!line.includes('"type":"result"'))
214
+ continue;
215
+ try {
216
+ const event = JSON.parse(line);
217
+ if (event.type === 'result')
218
+ result = event;
219
+ }
220
+ catch {
221
+ // A partial line from a killed process.
222
+ }
223
+ }
224
+ return result;
225
+ }
176
226
  function toUsage(usage, cost) {
177
227
  if (!usage || typeof usage !== 'object')
178
228
  return undefined;
@@ -0,0 +1,53 @@
1
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { configDir } from '../github/app.js';
4
+ export const USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
5
+ /** Reads the usage with the token the provider was created with. Throws when the endpoint refuses or answers something else. */
6
+ export async function readUsage(token, fetchImpl = fetch) {
7
+ const response = await fetchImpl(USAGE_URL, {
8
+ headers: { authorization: `Bearer ${token}`, accept: 'application/json', 'anthropic-beta': 'oauth-2025-04-20', 'user-agent': 'razzou-harness' },
9
+ });
10
+ if (!response.ok)
11
+ throw new Error(`the usage endpoint answered ${response.status}`);
12
+ return parseUsage(await response.json());
13
+ }
14
+ /** The windows the body lists, or the two it always carries when the list is missing. */
15
+ export function parseUsage(body) {
16
+ const data = (body ?? {});
17
+ const limits = [];
18
+ const listed = Array.isArray(data.limits) ? data.limits : [];
19
+ for (const limit of listed) {
20
+ if (typeof limit.percent === 'number' && typeof limit.resets_at === 'string') {
21
+ limits.push({ kind: typeof limit.kind === 'string' ? limit.kind : 'limit', percent: limit.percent, resetsAt: limit.resets_at });
22
+ }
23
+ }
24
+ if (limits.length === 0) {
25
+ for (const [kind, key] of [['session', 'five_hour'], ['weekly_all', 'seven_day']]) {
26
+ const window = data[key];
27
+ if (window && typeof window.utilization === 'number' && typeof window.resets_at === 'string')
28
+ limits.push({ kind, percent: window.utilization, resetsAt: window.resets_at });
29
+ }
30
+ }
31
+ if (limits.length === 0)
32
+ throw new Error('the usage endpoint answered without any limit');
33
+ const binding = limits.reduce((top, limit) => (limit.percent > top.percent ? limit : top));
34
+ return { percent: binding.percent, resetsAt: binding.resetsAt, limits };
35
+ }
36
+ /** Where the doctor stores the token, next to the App's key: mode 0600, read by the daemon for this one call. */
37
+ export function claudeTokenFile(dir = configDir()) {
38
+ return join(dir, 'claude.token');
39
+ }
40
+ export function loadClaudeToken(dir = configDir()) {
41
+ const file = claudeTokenFile(dir);
42
+ if (!existsSync(file))
43
+ return undefined;
44
+ const token = readFileSync(file, 'utf8').trim();
45
+ return token === '' ? undefined : token;
46
+ }
47
+ export function saveClaudeToken(token, dir = configDir()) {
48
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
49
+ const file = claudeTokenFile(dir);
50
+ writeFileSync(file, `${token.trim()}\n`, { mode: 0o600 });
51
+ chmodSync(file, 0o600);
52
+ return file;
53
+ }
@@ -181,6 +181,42 @@ export function prKind(author, branch, bot) {
181
181
  return 'Harness';
182
182
  return 'Owner';
183
183
  }
184
+ /**
185
+ * A plan's kind, from its features' (v3 §3.1): CODE with no `infra` feature, INFRA with
186
+ * only `infra` features, MIXED otherwise. A feature without a kind is an `app` one. The
187
+ * same rule as the daemon's `planKind`, kept here so this file stands alone.
188
+ */
189
+ export function planKindOf(features) {
190
+ const infra = features.filter((f) => f.kind === 'infra').length;
191
+ return infra === 0 ? 'CODE' : infra === features.length ? 'INFRA' : 'MIXED';
192
+ }
193
+ /**
194
+ * The plan-kind rules of a Plan PR (v3 §3.1), from the plan file on the base branch: a
195
+ * tag in the title must be the plan's kind, and a CODE plan stays off the Terraform
196
+ * folder, so infrastructure can't reach main as code. The tag grants nothing, and a
197
+ * title without one, from an older daemon, passes.
198
+ */
199
+ async function planKindProblems(input, config) {
200
+ const [, module, plan] = /^plan\/([^/]+)\/([^/]+)$/.exec(input.branch) ?? [];
201
+ if (!module || !plan)
202
+ return [];
203
+ const file = `harness/modules/${module}/plans/${plan}.json`;
204
+ const definition = parseJson(await input.base.read(file));
205
+ if (!definition || !Array.isArray(definition.features))
206
+ return [`${file} is missing or unreadable on the base branch, so the plan's kind is unknown`];
207
+ const kind = planKindOf(definition.features);
208
+ const problems = [];
209
+ const tag = /(?:^|\s)-(CODE|INFRA|MIXED)-(?:\s|$)/.exec(input.title)?.[1];
210
+ if (tag && tag !== kind)
211
+ problems.push(`the title says -${tag}-, but ${module}/${plan} is a ${kind} plan on the base branch: its features' kinds decide`);
212
+ if (kind === 'CODE' && config?.terraform && typeof config.terraform === 'object') {
213
+ const dir = (config.terraform.dir || 'iac').replace(/\/+$/, '');
214
+ const touched = input.changedPaths.filter((p) => p === dir || p.startsWith(`${dir}/`));
215
+ if (touched.length > 0)
216
+ problems.push(`a CODE plan may not change ${dir}/: infrastructure gets plans of its own (v3 §3.1); changed: ${touched.join(', ')}`);
217
+ }
218
+ return problems;
219
+ }
184
220
  /** The `verify` job: the problems that make a PR fail, or none. */
185
221
  export async function verify(input) {
186
222
  const config = parseJson(await input.base.read('harness/config.json'));
@@ -195,6 +231,8 @@ export async function verify(input) {
195
231
  for (const problem of await lockfileProblems(input.head))
196
232
  problems.push(`package-lock.json: ${problem}`);
197
233
  }
234
+ if (kind === 'Plan')
235
+ problems.push(...(await planKindProblems(input, config)));
198
236
  if (kind === 'Arch') {
199
237
  const rules = await protectionRules(input.head);
200
238
  for (const path of input.changedPaths.filter((p) => !archMayChange(p, rules))) {