razzou-harness 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/CHANGELOG.md +140 -4
  2. package/README.md +78 -40
  3. package/dist/agents/claude-code.js +9 -2
  4. package/dist/anthropic/usage.js +2 -1
  5. package/dist/checks/runner.js +2 -1
  6. package/dist/ci/harness-ci.js +115 -32
  7. package/dist/cli.js +60 -30
  8. package/dist/daemon/architects.js +2 -0
  9. package/dist/daemon/channel.js +7 -1
  10. package/dist/daemon/design.js +16 -2
  11. package/dist/daemon/items.js +6 -3
  12. package/dist/daemon/main.js +84 -12
  13. package/dist/daemon/manager.js +25 -19
  14. package/dist/daemon/orchestrator.js +366 -157
  15. package/dist/daemon/requests.js +21 -19
  16. package/dist/daemon/review.js +14 -8
  17. package/dist/daemon/web-read.js +143 -33
  18. package/dist/dashboard/server.js +27 -10
  19. package/dist/dashboard/terminals.js +22 -5
  20. package/dist/design/owner-time.js +8 -1
  21. package/dist/design/schemas.js +14 -4
  22. package/dist/git/repo.js +52 -19
  23. package/dist/github/app.js +7 -5
  24. package/dist/github/forge.js +17 -6
  25. package/dist/github/setup.js +10 -3
  26. package/dist/review/paid-calls.js +39 -14
  27. package/dist/roles.js +1 -1
  28. package/dist/sandbox/openshell.js +74 -13
  29. package/dist/sandbox/policy.js +2 -1
  30. package/dist/setup/doctor.js +74 -12
  31. package/dist/setup/init.js +5 -4
  32. package/dist/setup/recover.js +11 -6
  33. package/dist/setup/setup.js +163 -70
  34. package/dist/state/state.js +31 -11
  35. package/dist/util.js +2 -2
  36. package/dist/web/assets/{index-DLttwGM6.js → index-CPJJsPL6.js} +1 -1
  37. package/dist/web/index.html +1 -1
  38. package/images/agent/Dockerfile +11 -2
  39. package/package.json +1 -1
  40. package/roles/architect.md +5 -2
  41. package/roles/manager.md +2 -1
  42. package/shim/harness-mcp.mjs +11 -2
  43. package/src/ci/harness-ci.ts +105 -32
  44. package/templates/harness.yml +5 -2
package/CHANGELOG.md CHANGED
@@ -3,6 +3,142 @@
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.3.0 (2026-11-07)
7
+
8
+ **Upgrading a project from 0.2.x:** install the new version, then run `harness setup`
9
+ in the project, which does both steps, or do them by hand. Run `harness install-ci` and
10
+ commit the CI files it writes before the next `Arch:` pull request: the protected paths
11
+ changed, and the lock the daemon writes lists more, which the old CI script on `main`
12
+ would refuse. Rebuild the agents' image with `harness doctor --build-images`: its tag
13
+ now follows the harness's version. Then open the dashboard from the link the daemon
14
+ prints, once.
15
+
16
+ Fixes from a review of 0.2.0.
17
+
18
+ Protected files:
19
+ - Paths with a non-ASCII character, a quote or a backslash passed every protection rule,
20
+ in CI's `verify` and in the daemon's own checks, because git quoted them: a Developer
21
+ could add `.claude/skills/é/SKILL.md`, and a `CODE` plan could add Terraform in
22
+ `iac/ñ.tf`. Paths are now read NUL-separated.
23
+ - `CLAUDE.md`, `CLAUDE.local.md`, `AGENTS.md` and `.claude/` are protected in every
24
+ folder, as Claude Code reads them there; so are the configs that decide what a check
25
+ runs (`vitest.config.mts` overrode a protected `vitest.config.js`; Vite, Babel, Mocha,
26
+ pytest and tox configs, and package.json's `jest`, `babel` and `mocha` fields, weren't
27
+ protected), and the npm script of `npm run -s lint` or `npm --prefix . test`.
28
+ - A Plan or Fix PR may not add a local link to `package-lock.json`, which could swap a
29
+ check's tool for a stub. An Arch PR is judged by the base branch's rules, so it can't
30
+ protect `src/**` to change `src/` in the same PR.
31
+ - `protect.files` takes globs, `./x` and `dir/`, which matched nothing before, and
32
+ `terraform.dir` may be written `./iac` or `iac/`. `harness lock` leaves out what git
33
+ ignores.
34
+
35
+ Swarm mode:
36
+ - Every Developer under way reaches the Manager: `ask_manager` from a second or third
37
+ attempt at once was refused, and the Manager could answer or stop only the first. A
38
+ plan's Manager acts on its own plan only.
39
+ - Plans in parallel no longer stop on `progress.txt`: both add lines to it, and the
40
+ daemon keeps both sides' lines when it merges `main` into a plan branch. Before it
41
+ merges a pull request, it merges `main` into it when `main` moved since CI ran, so CI
42
+ and the reviewers judge what `main` gets.
43
+ - Modules are built one after another in Swarm too, as the design says: the first plan
44
+ of a second module started alongside the first module's plans, with its own
45
+ Developers, Manager and reviewers. Only the module being built has those now. Any
46
+ module's Architect still works meanwhile, so the next module can be designed while one
47
+ is built; its plans wait their turn. Two modules already under way at the upgrade are
48
+ finished one after the other, the older first: the other's attempts are interrupted,
49
+ and start again in its turn.
50
+ - A plan that a design change drops while it is built no longer holds a place under
51
+ `swarm.maxPlans`, and its Manager is no longer started over and over, each time in a
52
+ new sandbox and session.
53
+ - An `INFRA` or `MIXED` plan's estimate counts only for the head it was made on, and is
54
+ read only once the `cost` job itself succeeded: after a fix round, a head without its
55
+ own estimate merged on the earlier head's.
56
+ - `Arch:` and `Fix:` pull requests get the reviewers' statuses as "not reviewed", so the
57
+ owner merges them without bypassing the rules, which also skipped CI.
58
+ - A blocking finding blocks, whatever the reviewer's verdict says. A usage limit is not
59
+ one of a reviewer's two failures. Two reviews at once each keep their own transcript.
60
+ - Granting the App "Commit statuses: write" takes effect at its next token, within the
61
+ hour, without restarting the daemon; the doctor reads what the installation accepted.
62
+ Swarm's readiness note reads every ruleset on `main`, on every page.
63
+ - The pay-per-use calls the cost reviewer is told about are found in every SDK's
64
+ spelling, without false hits on words in comments.
65
+
66
+ Both modes:
67
+ - A usage limit never shortens a pause already there, such as the owner's or a brake's:
68
+ it replaced it with an hour's pause that lifted itself. An attempt that reports a usage
69
+ limit counts from the third time on the same feature, as other interruptions do.
70
+ - A plan whose step keeps failing, such as a push GitHub refuses, no longer stops every
71
+ other plan: it is tried again for a while, then the pipeline pauses with its error.
72
+ - In Manual, a module whose design lands while another is built waits for it, and the
73
+ other's pull request is still followed: a module whose id sorted first took over.
74
+ - A design change that redefines a feature under way interrupts its attempt in Manual
75
+ too. An accepted feature it redefines is built again while its plan is. A plan past its
76
+ build is frozen: `submit_design` refuses a change to its features. A feature a design
77
+ change drops no longer breaks the Manager's items.
78
+ - Dead ends after a failure: the owner's push to a plan's pull request branch is built
79
+ on, instead of pausing; a CI run the owner starts again on GitHub counts once it passes;
80
+ on resume, a failed deploy is looked at again, and a later successful apply on `main`
81
+ covers an earlier merge, as when GitHub cancels a pending run.
82
+ - An invalid design on `main` pauses the pipeline with the reason, instead of a log line.
83
+ - `harness budget --owner-hours` with the daemon down replaces hours already set, and
84
+ `harness mode` with the daemon down records the switch for it.
85
+
86
+ Setup and the CLI:
87
+ - `harness setup` on a machine without the App creates it and waits for its
88
+ installation before it writes `harness/config.json`: the config had no `project.repo`
89
+ or `project.bot`, and the project stayed local. A later run fills them in.
90
+ - The setup's commit holds the files it wrote, and nothing else you had staged, and it
91
+ pushes only from `main`: run on another branch, it pushed that branch's commits to
92
+ `main`. A setup commit that wasn't pushed is offered again. A ruleset GitHub refuses,
93
+ as on a free plan's private repository, no longer stops the setup.
94
+ - `harness recover` reads GitHub's `main`, not your checkout's, and says what it can't
95
+ rebuild.
96
+ - A daemon is running only if its process started when `daemon.json` says: after a crash
97
+ or a reboot, `harness down` could stop another process that reused the pid. A lock
98
+ keeps two `harness up` apart, and closing the terminal stops the daemon cleanly.
99
+ - The browser opens the dashboard with a code that works once, not the token, which
100
+ showed on xdg-open's command line.
101
+ - Refused owner actions and unknown commands exit with an error.
102
+ - The doctor: the probe's `web_read` check passes only on the harness's own refusal; the
103
+ workflow probe fails when the App's installation grants workflows; the ruleset must
104
+ apply to `main`; the statuses warning reads what the installation accepted; a Claude
105
+ token the usage endpoint refuses is reported; a half-created AWS provider is deleted
106
+ so the next run asks for the key again, and setup asks for the view role's ARN once.
107
+ - AWS providers are named after the repository: two projects in folders of the same name
108
+ shared one.
109
+ - The owner's time: a design without owner actions always fits, and actions done before
110
+ a redesign dropped them still count.
111
+ - Hashes no longer depend on the machine's locale, a failed `terraform show` fails the
112
+ check that needs the plan, and the usage endpoint has a timeout.
113
+
114
+ The sandboxes and the dashboard:
115
+ - `web_read` refuses this machine's own addresses and the networks it is on: over IPv6,
116
+ or with a public IPv4 on an interface, an agent could read the host's services or the
117
+ router. A compressed page past the size limit stops decompressing, instead of growing
118
+ the daemon's memory, and a page full of unclosed comments or tags no longer freezes
119
+ the daemon for minutes. One deadline of 75 seconds covers a read and its redirects,
120
+ within the 90 seconds an agent waits. claude.ai and chatgpt.com are never read. An
121
+ HTML comment ends at `--!>` too, as browsers read it: the text after one was lost.
122
+ - Usage limits are recognised in Claude Code 2.1's words ("You've hit your 5-hour
123
+ limit"): an attempt that hit one counted as failed.
124
+ - An agent can no longer crash the daemon with a line too long for its follower, and a
125
+ terminal whose sandbox ends at once no longer crashes it either. Transcripts and the
126
+ agents' channels pick up again after the OpenShell gateway restarts, a hung `openshell`
127
+ call is stopped, and a tool call too large gets an error, not a wait.
128
+ - A torn line in the Architect's inbox no longer breaks its prompt hook, and an
129
+ `extraHosts` entry for a bundle's host, such as github.com, no longer takes away its
130
+ git access.
131
+ - The dashboard's cookie is named after its port, so two projects' dashboards no longer
132
+ log each other out: after upgrading, open the printed link once. Agent-written
133
+ Markdown loses its styles, which could hide parts of Needs you. The reviewers' "Shell
134
+ in" button works while a review runs.
135
+ - The agents' image pins the AWS CLI, 2.39.6, by checksum.
136
+
137
+ Releases:
138
+ - The release workflow runs the install and the tests in a job without the npm OIDC
139
+ token, and a second job publishes the tarball the first one packed: no dependency's
140
+ code runs where the token can publish.
141
+
6
142
  ## 0.2.0 (2026-11-06)
7
143
 
8
144
  **Upgrading a project from 0.1.x:** install the new version, then run `harness setup`
@@ -14,10 +150,10 @@ new attempt clock of 120 minutes and Manager turn of 90.
14
150
  - **Swarm mode**, designed in `docs/architecture/proposal-v3.md`: a second mode, switched
15
151
  in the dashboard or with `harness mode`, in which the daemon merges a plan's pull
16
152
  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.
153
+ `swarm.merge.code` is `daemon`; `INFRA` and `MIXED` plans need the cost job's
154
+ estimate under `swarm.merge.infraMonthlyUsdCeiling`, or the owner's button (both
155
+ below). A pull request the policy leaves to the owner shows the reason under Needs
156
+ you. `Arch:` and `Fix:` pull requests stay the owner's.
21
157
  - A plan is `CODE`, `INFRA` or `MIXED`, from its features' kinds. Plan pull requests
22
158
  carry the tag in their title, and CI's `verify` job keeps a `CODE` plan's pull
23
159
  request off the Terraform folder.
package/README.md CHANGED
@@ -137,14 +137,17 @@ the token from `claude setup-token` at a hidden prompt, two clicks on GitHub for
137
137
  App, since GitHub creates and installs Apps only in the browser, three questions (your
138
138
  stack's network bundles, AWS or not, the hours of your time), and, with AWS, the role
139
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,
140
+ the repository on GitHub if the project has no remote; creates the App from its
141
+ manifest when this machine has none, and waits for you to install it; writes the
142
+ harness's files; asks the questions and writes `harness/config.json`, with the
143
+ `staging` environment and the variables on GitHub for AWS; commits the files it wrote,
144
+ and only those, with your git once you agree, and pushes them when you are on `main`;
145
+ runs `harness doctor` and fixes what it can (starts the OpenShell gateway, builds the
146
+ agents' image, imports the Claude profile and creates the provider with your token,
146
147
  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
+ and offers to start the daemon. Run it again at any time: what is done is skipped, and a
149
+ setup commit that wasn't pushed is offered again. On a private repository of a free
150
+ plan, where GitHub has no rulesets, it says so and goes on.
148
151
 
149
152
  The same by hand: `harness init` writes the harness's files into the repository, never
150
153
  over existing ones, and prints the setup checklist:
@@ -202,7 +205,10 @@ plans it finds there (see [Configuration](#configuration)).
202
205
  ## Concepts
203
206
 
204
207
  - **Module:** a part of the system you want to build, with one Architect and one
205
- accepted proposal, then its plans. Modules are built one after another. Size a module
208
+ accepted proposal, then its plans. Modules are built one after another, in both modes:
209
+ the one under way is finished before the next starts, and only it has Developers,
210
+ Managers and reviewers. Every module's Architect stays available, so you can design the
211
+ next module while one is built; its plans wait their turn. Size a module
206
212
  so that its design fits in one conversation. With the default model, Opus 5.5, the
207
213
  Architect's context holds up to a million tokens, and a well-sized module never fills
208
214
  it. If a module's design keeps growing, split it into smaller modules: each gets its
@@ -214,7 +220,10 @@ plans it finds there (see [Configuration](#configuration)).
214
220
  tagged in the pull request's title, and a `CODE` plan's pull request may not touch
215
221
  the Terraform folder. Give infrastructure plans of its own.
216
222
  - **Feature:** one change that a Developer can make in one attempt, with at least one
217
- check. Its definition is frozen once its design is merged.
223
+ check. A design change can redefine it while its plan is built: an attempt under way
224
+ stops, and the feature is built again with a fresh budget, even if it was accepted.
225
+ Once its plan is past its build (every feature of it accepted, then its pull request
226
+ and merge), it is frozen: a change goes in a new plan.
218
227
  - **Check:** a shell command, run from the repository root, that exits 0 when the feature
219
228
  works. `pre-merge` checks run on every attempt and in CI; `post-deploy` checks run
220
229
  after an infrastructure deploy.
@@ -226,16 +235,22 @@ plans it finds there (see [Configuration](#configuration)).
226
235
  policy or setting a secret. A **gate** holds back the plans or features it blocks
227
236
  until you mark it done; a **task** holds nothing back.
228
237
  - **Protected files:** the design and its checks, CI, and what agents read as
229
- instructions: `harness/`, `.github/`, `.claude/`, `CLAUDE.md`, `AGENTS.md`,
230
- `ARCHITECTURE.md`, `init.sh`, tool configs and the npm scripts that checks run. A
231
- `Plan:` or `Fix:` pull request may not change them: CI's `verify` job, run with the
232
- base branch's rules, fails it. They change through an `Arch:` pull request, or your own
233
- commits. `harness/lock.json` holds their hashes.
238
+ instructions: `harness/`, `.github/`, `ARCHITECTURE.md`, `init.sh`, and in any folder
239
+ `.claude/`, `CLAUDE.md`, `CLAUDE.local.md` and `AGENTS.md`, which Claude Code reads
240
+ wherever they are; the configs that decide what a check runs (ESLint, Prettier, Biome,
241
+ Vitest, Vite, Jest, Babel, Playwright, Mocha, pytest, tox, and package.json's `jest`,
242
+ `babel` and `mocha` fields); and the npm scripts that checks run. A `Plan:` or `Fix:`
243
+ pull request may not change them, nor add a local link to `package-lock.json`: CI's
244
+ `verify` job, run with the base branch's rules, fails it, and the daemon refuses it
245
+ first. They change through an `Arch:` pull request, also judged by the base branch's
246
+ rules, so what it newly protects counts from the next one, or your own commits.
247
+ `harness/lock.json` holds their hashes.
234
248
  - **`progress.txt`:** one line per accepted feature and satisfied gate, committed with
235
- the work. It only grows.
249
+ the work. It only grows. When plans in parallel both add lines, the daemon keeps both.
236
250
  - **Brakes:** counters and deadlines, not judgment. Three failed attempts in a row, a
237
251
  Manager that leaves an item waiting for 180 minutes, or CI still failing after two fix
238
- rounds pause the pipeline until you resume it.
252
+ rounds pause the pipeline until you resume it. A usage limit pauses it for an hour,
253
+ and never shortens a pause already there.
239
254
 
240
255
  ## The dashboard
241
256
 
@@ -280,10 +295,17 @@ once CI passed and every status in `swarm.requiredStatuses` is green on its head
280
295
  estimate over the ceiling still waiting for you. The button is off by default, and
281
296
  the choice is recorded in the event log.
282
297
 
298
+ Only an estimate of the pull request's current head counts: after a fix round, the new
299
+ head needs its own. Before it merges, the daemon merges `main` into the pull request's
300
+ branch if `main` moved since CI ran, so CI and the reviewers judge what `main` gets; a
301
+ conflict leaves the pull request to you, to resolve on GitHub.
302
+
283
303
  Anything else waits for you, under **Needs you** with the reason. `Arch:` and `Fix:`
284
304
  pull requests are yours in both modes. In Swarm, GitHub's rules need no approval but
285
305
  require the reviewers' statuses, pinned to the App, so even the daemon can't merge
286
- before they pass.
306
+ before they pass. The reviewers read plan pull requests only: the daemon gives an
307
+ `Arch:` or `Fix:` pull request the two statuses as "not reviewed", so you merge it
308
+ without bypassing the rules, and CI still gates it.
287
309
 
288
310
  **Before a launch.** In Swarm the daemon reads your subscription's usage before it
289
311
  starts an attempt, and at or above `swarm.usageCeilingPercent` the pipeline waits for
@@ -302,9 +324,10 @@ with a verdict and findings. The daemon posts the findings on the pull request a
302
324
  review, with comments on the lines of the diff, and sets the reviewer's commit status,
303
325
  `harness/security` or `harness/cost`. A blocking verdict opens a fix round on the
304
326
  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.
327
+ Manager reviews, CI and the reviewers run again on the new head. A blocking finding
328
+ blocks, whatever the verdict says. After `swarm.securityFixRounds` or
329
+ `swarm.costFixRounds`, the pull request is yours. In Manual,
330
+ `swarm.reviewersInManual` runs them as comments and statuses that gate nothing.
308
331
 
309
332
  The App needs **Commit statuses: write** for the verdicts. An App created before this
310
333
  version has read only: give it the permission on GitHub, accept it on the installation,
@@ -318,16 +341,19 @@ Architect is told to declare every dependency. Each attempt starts from the plan
318
341
  branch as it is. When another feature lands first, the daemon merges the new head
319
342
  into the attempt before its checks, so they see the accepted work, and the feature
320
343
  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
344
+ it, and the next one starts from the new head. In both modes, a design change merged
345
+ while a feature is built interrupts its attempt the same way, and the next one builds
346
+ the new definition. Manual mode builds one feature at a time, as before. In both modes, the
324
347
  Architect hears in one line when a plan's pull request opens, waits for you, is
325
348
  blocked by a reviewer, or merges.
326
349
 
327
350
  **Parallel plans.** A plan may declare `dependsOn`, the plans of its module that must
328
351
  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
352
+ Swarm, up to `swarm.maxPlans` plans of the module under way whose dependencies are
353
+ merged build at once, each on its own branch with its own Manager, keyed `module/plan`
354
+ in the Agents view. When one merges, `main` is merged into the others, `progress.txt`
355
+ keeping both sides' lines; a conflict in other files is resolved in that plan's pull
356
+ request. A
331
357
  feature may also depend on a feature of an earlier plan, as `P1/F03`, and then waits
332
358
  for that plan's merge. The Project view draws each module's features as a graph, plan
333
359
  by plan, with an edge per dependency: green accepted, blue under way, red escalated,
@@ -337,7 +363,7 @@ as before.
337
363
  **The cost job.** `harness install-ci` adds a `cost` job to the workflow: on a pull
338
364
  request that has `iac/`, it runs [Infracost](https://www.infracost.io) on the base and
339
365
  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
366
+ CI passed and the `cost` job itself succeeded. It needs the repository secret `INFRACOST_API_KEY`, a free key from
341
367
  `infracost auth login`; without it the job does nothing, no estimate exists, and
342
368
  `INFRA` and `MIXED` plans wait for you with that reason. Infracost sees your Terraform's
343
369
  resource types and sizes, not your code or state. The cost reviewer gets the estimate in
@@ -371,7 +397,7 @@ too.
371
397
  | `harness github-app create --org <org>` | Creates the harness's GitHub App, once per machine |
372
398
  | `harness github-app check --repo <org>/<repo>` | Checks that the App can reach the repository |
373
399
  | `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) |
400
+ | `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, over hours set before (`--agent-hours` sets the threshold) |
375
401
  | `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 |
376
402
  | `harness install-ci [--owner <login>]` | Writes the workflow, its CI script and `CODEOWNERS` again, after an upgrade |
377
403
  | `harness lock` | Writes `harness/lock.json`, the hashes of the protected files |
@@ -385,8 +411,8 @@ too.
385
411
  | `harness message <text>` | Writes to the Manager of the module being built |
386
412
  | `harness retry <module>/<plan>/<feature>` | Gives an escalated feature one more attempt; `--note` adds guidance |
387
413
  | `harness resume` | Lifts a pause |
388
- | `harness mode [manual\|swarm]` | Shows the mode, or switches it |
389
- | `harness recover` | Rebuilds a lost `.harness/` from `main` and GitHub |
414
+ | `harness mode [manual\|swarm]` | Shows the mode, or switches it; with the daemon down, it records the switch for when it starts |
415
+ | `harness recover` | Rebuilds a lost `.harness/` from GitHub's `main` and branches, and lists what it can't: the mode, the budgets, the owner tasks done |
390
416
 
391
417
  ## Configuration
392
418
 
@@ -395,7 +421,7 @@ through an `Arch:` pull request, or a commit of your own on `main`.
395
421
 
396
422
  ```json
397
423
  {
398
- "harnessVersion": "0.1.1",
424
+ "harnessVersion": "0.3.0",
399
425
  "project": { "name": "shop", "repo": "<org>/shop", "bot": "<org>-harness[bot]" },
400
426
  "roles": {
401
427
  "architect": { "model": "claude-opus-5-5", "effort": "max" },
@@ -418,8 +444,8 @@ through an `Arch:` pull request, or a commit of your own on `main`.
418
444
  | `network.bundles` | `["core"]` | The hosts sandboxes reach, by bundle: `core`, `node`, `python`, `aws`, `terraform`, `go`, `rust`, `jvm`, `opentofu`, `browsers`. The Architect gets them all. |
419
445
  | `network.extraHosts` | `[]` | More hosts, read-only |
420
446
  | `checks` | `[]` | Checks that every feature must keep passing, such as `npm run lint` |
421
- | `protect.files`, `protect.jsonFields` | none | More protected files, and protected JSON fields by file, like `{"tsconfig.json": ["/compilerOptions/strict"]}` |
422
- | `sandbox.image` | `razzou-harness/agent:0.1.0` | The agents' image, which `harness doctor --build-images` builds |
447
+ | `protect.files`, `protect.jsonFields` | none | More protected files, as paths or globs (`scripts/*.sh`, `docs/`, `**/Makefile`), and protected JSON fields by file, like `{"tsconfig.json": ["/compilerOptions/strict"]}` |
448
+ | `sandbox.image` | `razzou-harness/agent:<version>` | The agents' image, tagged with the harness's version, which `harness doctor --build-images` builds |
423
449
  | `sandbox.claudeProvider` | `claude-sub` | The OpenShell provider that holds your Claude token |
424
450
  | `sandbox.env` | `{}` | Environment variables for every sandbox |
425
451
  | `github.requiredChecks` | `["verify", "checks"]` | The CI jobs a pull request must pass; `tf-plan` is added with Terraform |
@@ -519,7 +545,7 @@ and Terraform applies only in CI, after you merge.
519
545
  1. In `harness/config.json`, add `aws` and `terraform`:
520
546
 
521
547
  ```json
522
- "aws": { "region": "us-east-1", "provider": "aws-view-shop", "services": ["sts", "s3", "iam", "sqs"] },
548
+ "aws": { "region": "us-east-1", "provider": "aws-view-<org>-shop", "services": ["sts", "s3", "iam", "sqs"] },
523
549
  "terraform": { "dir": "iac", "backendConfig": "backend-configs/staging.hcl", "varFile": "env/staging.tfvars" }
524
550
  ```
525
551
 
@@ -558,8 +584,9 @@ What agents can't do, and what stops them:
558
584
  - **Change AWS.** A read-only role with explicit denies, and applies only in CI after
559
585
  your merge.
560
586
  - **Go anywhere online.** Sandboxes reach listed hosts, read-only. Other pages go
561
- through `web_read`: GET only, public addresses only, checked again after every
562
- redirect.
587
+ through `web_read`: GET only, public addresses only, never this machine's own
588
+ addresses or the networks it is on (IPv6 included), checked again after every
589
+ redirect, within 75 seconds and 2 MB, decompressed.
563
590
  - **Decide alone.** Code runs the loop: deadlines, attempt budgets, checks in fresh
564
591
  sandboxes and the merges, yours or the Swarm policy's, are enforced by the daemon,
565
592
  not left to the agents.
@@ -608,20 +635,31 @@ To report a vulnerability, see `SECURITY.md` in the source repository.
608
635
  | `Claude provider: … is missing` | `claude setup-token`, then `harness doctor` in a terminal: it asks for the token |
609
636
  | `GitHub App: … 404` | Install the App on the repository, from the link `harness github-app create` printed, or the organization's settings |
610
637
  | `ruleset: …` fails | `harness github-setup --repo <org>/<repo>`. A private repository needs a paid GitHub plan for rulesets. |
638
+ | `ruleset: applies to the default branch, master, not to main` | Make `main` the repository's default branch on GitHub, then `harness github-setup --repo <org>/<repo>` |
611
639
  | `.github/… differs from this version of the harness` | `harness install-ci`, then commit the files yourself |
612
640
 
613
641
  While the daemon runs:
614
642
  - **The pipeline is paused.** **Needs you** says why: attempts that failed in a row, a
615
- Manager that didn't act, CI still failing after its fix rounds, a usage limit, or a
616
- broken environment. Fix the cause, then **Resume**.
643
+ Manager that didn't act, CI still failing after its fix rounds, a deploy still failing
644
+ after its Fix PRs, a usage limit, an invalid design on `main`, or a broken environment.
645
+ Fix the cause, then **Resume**. On resume, a failed full check run runs again and a
646
+ failed deploy is looked at again: a later successful apply on `main` counts.
647
+ - **CI fails for a reason outside the code.** Fix it (a secret, the runner), then re-run
648
+ the jobs on GitHub: a passing re-run counts. To fix the code yourself, push to the pull
649
+ request's branch, or resolve a conflict in GitHub's editor: the daemon builds on your
650
+ push, and CI runs again.
617
651
  - **A feature is escalated.** It used its attempts. In **Needs you**, give it one more
618
652
  attempt with your guidance, send it to the Architect (the design may change), or pause.
619
653
  - **The dashboard asks for its token.** Each `harness up` prints a new link: open that
620
- one.
654
+ one. Each project's dashboard keeps its own cookie, named after its port, so several
655
+ can be open at once. The browser it opens by itself logs in with a code that works once, for two
656
+ minutes, so the token never shows on a command line.
621
657
  - **A merge takes minutes to show up.** The daemon polls GitHub: every minute, then every
622
658
  five once nothing has changed for ten minutes.
623
- - **Closing the terminal stopped the daemon.** Run `harness up` again: it picks up from
624
- its event log, and the agents kept working meanwhile.
659
+ - **Closing the terminal stopped the daemon.** It stops cleanly, as with `harness down`.
660
+ Run `harness up` again: it picks up from its event log, and the agents kept working
661
+ meanwhile. A daemon that crashed leaves `.harness/daemon.json` and
662
+ `.harness/daemon.lock` behind: the next `harness up` sees that their process is gone.
625
663
 
626
664
  ## Development
627
665
 
@@ -170,6 +170,13 @@ export function reviewJsonSchema() {
170
170
  const { $schema: _, ...schema } = z.toJSONSchema(ReviewReportSchema, { target: 'draft-7' });
171
171
  return JSON.stringify(schema);
172
172
  }
173
+ /**
174
+ * How a session's error result says the subscription ran out. Claude Code 2.1.x writes
175
+ * "You've hit your 5-hour limit · resets 3pm", "You've reached your …", "You're out of
176
+ * usage credits", "You're out of extra usage" or "Your org is out of usage …"; older
177
+ * versions wrote "Claude AI usage limit reached|<epoch>".
178
+ */
179
+ const USAGE_LIMIT = /usage limit|limit reached|rate.?limit|you['’]ve (?:hit|reached) your|you['’]re out of (?:usage credits|extra usage)|your org is out of usage/i;
173
180
  /** A reviewer's session: its report, or why there is none, read like a Developer's. */
174
181
  export function readReview(stream, exitCode, stderr = '') {
175
182
  const result = lastResult(stream);
@@ -179,7 +186,7 @@ export function readReview(stream, exitCode, stderr = '') {
179
186
  }
180
187
  const usage = toUsage(result.usage, result.total_cost_usd);
181
188
  const text = typeof result.result === 'string' ? result.result : '';
182
- if (result.is_error === true && /usage limit|limit reached|rate.?limit/i.test(text))
189
+ if (result.is_error === true && USAGE_LIMIT.test(text))
183
190
  return { outcome: 'usage_limit', detail: text.slice(0, 500), usage };
184
191
  const report = ReviewReportSchema.safeParse(result.structured_output);
185
192
  if (!report.success)
@@ -198,7 +205,7 @@ export function readResult(stream, exitCode, stderr = '') {
198
205
  }
199
206
  const usage = toUsage(result.usage, result.total_cost_usd);
200
207
  const text = typeof result.result === 'string' ? result.result : '';
201
- if (result.is_error === true && /usage limit|limit reached|rate.?limit/i.test(text)) {
208
+ if (result.is_error === true && USAGE_LIMIT.test(text)) {
202
209
  return { outcome: 'usage_limit', detail: text.slice(0, 500), usage };
203
210
  }
204
211
  const report = ReportSchema.safeParse(result.structured_output);
@@ -2,10 +2,11 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'n
2
2
  import { join } from 'node:path';
3
3
  import { configDir } from '../github/app.js';
4
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. */
5
+ /** Reads the usage with the token the provider was created with. Throws when the endpoint refuses, answers something else, or not within 15 seconds. */
6
6
  export async function readUsage(token, fetchImpl = fetch) {
7
7
  const response = await fetchImpl(USAGE_URL, {
8
8
  headers: { authorization: `Bearer ${token}`, accept: 'application/json', 'anthropic-beta': 'oauth-2025-04-20', 'user-agent': 'razzou-harness' },
9
+ signal: AbortSignal.timeout(15_000),
9
10
  });
10
11
  if (!response.ok)
11
12
  throw new Error(`the usage endpoint answered ${response.status}`);
@@ -50,7 +50,8 @@ async function makePlan(driver, sandbox, workDir, options) {
50
50
  `cd ${sh(`${workDir}/${tf.dir}`)} || exit 1`,
51
51
  `terraform init -input=false -no-color -backend-config=${sh(tf.backendConfig)} > ${sh(`${out}/terraform-init.log`)} 2>&1 || { tail -c 3000 ${sh(`${out}/terraform-init.log`)}; exit 1; }`,
52
52
  `terraform plan -input=false -no-color -lock=false -var-file=${sh(tf.varFile)} -out=${sh(`${out}/plan.bin`)} > ${sh(`${out}/terraform-plan.log`)} 2>&1 || { tail -c 3000 ${sh(`${out}/terraform-plan.log`)}; exit 1; }`,
53
- `terraform show -json ${sh(`${out}/plan.bin`)} > ${sh(`${out}/plan.json`)}`,
53
+ // Without the JSON, the checks would read an empty $HARNESS_PLAN_JSON: the plan fails instead, with terraform's error.
54
+ `terraform show -json ${sh(`${out}/plan.bin`)} > ${sh(`${out}/plan.json`)} || exit 1`,
54
55
  `grep -E '^(Plan:|No changes)' ${sh(`${out}/terraform-plan.log`)} || true`,
55
56
  ].join('\n');
56
57
  const result = await driver.exec(sandbox, script, { timeoutSec: 900 });