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.
- package/CHANGELOG.md +79 -0
- package/README.md +204 -18
- package/dist/agents/claude-code.js +64 -14
- package/dist/anthropic/usage.js +53 -0
- package/dist/ci/harness-ci.js +38 -0
- package/dist/cli.js +125 -16
- package/dist/daemon/architects.js +6 -2
- package/dist/daemon/design.js +4 -0
- package/dist/daemon/items.js +24 -4
- package/dist/daemon/manager.js +108 -59
- package/dist/daemon/orchestrator.js +579 -91
- package/dist/daemon/requests.js +10 -3
- package/dist/daemon/review.js +334 -0
- package/dist/dashboard/server.js +34 -6
- package/dist/design/owner-time.js +72 -0
- package/dist/design/schemas.js +95 -4
- package/dist/github/app.js +28 -4
- package/dist/github/artifacts.js +35 -0
- package/dist/github/forge.js +95 -0
- package/dist/github/setup.js +60 -10
- package/dist/review/cost-estimate.js +25 -0
- package/dist/review/paid-calls.js +68 -0
- package/dist/roles.js +8 -0
- package/dist/setup/doctor.js +81 -10
- package/dist/setup/init.js +3 -2
- package/dist/setup/setup.js +378 -0
- package/dist/state/events.js +5 -2
- package/dist/state/state.js +158 -29
- package/dist/web/THIRD-PARTY-LICENSES.md +1 -1
- package/dist/web/assets/{index-D27gAaUJ.css → index-CMViLVQH.css} +1 -1
- package/dist/web/assets/index-DLttwGM6.js +96 -0
- package/dist/web/index.html +2 -2
- package/package.json +3 -3
- package/roles/architect.md +24 -5
- package/roles/cost.md +43 -0
- package/roles/manager.md +4 -3
- package/roles/security.md +37 -0
- package/src/ci/harness-ci.ts +42 -1
- package/templates/harness.yml +64 -0
- 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
|
|
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
|
|
92
|
-
|
|
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`
|
|
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
|
-
(
|
|
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
|
|
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
|
|
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` |
|
|
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` |
|
|
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
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/ci/harness-ci.js
CHANGED
|
@@ -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))) {
|