razzou-harness 0.1.2 → 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 +85 -0
- package/README.md +215 -25
- 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 +11 -4
- 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,91 @@
|
|
|
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
|
+
|
|
85
|
+
## 0.1.3 (2026-10-11)
|
|
86
|
+
|
|
87
|
+
- More npm keywords, for a team of agents in sandboxes.
|
|
88
|
+
- The README says what a module is, a part of the system you want to build, and how to
|
|
89
|
+
size one: its design fits in the Architect's context.
|
|
90
|
+
|
|
6
91
|
## 0.1.2 (2026-10-11)
|
|
7
92
|
|
|
8
93
|
- The source is public, on GitHub, and the package links to it.
|
package/README.md
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
**Your AI dev team, with you in charge.**
|
|
4
4
|
|
|
5
|
-
You describe a module
|
|
6
|
-
at a time, and a Manager reviews each one.
|
|
7
|
-
[OpenShell](https://github.com/NVIDIA/OpenShell) sandbox
|
|
8
|
-
feature is checked in a fresh one, and nothing reaches `main`
|
|
9
|
-
request. A small daemon runs the loop, following Anthropic's
|
|
5
|
+
You describe a module, a part of the system you want to build. An Architect designs it
|
|
6
|
+
with you, Developers build it one feature at a time, and a Manager reviews each one.
|
|
7
|
+
Every agent works in its own [OpenShell](https://github.com/NVIDIA/OpenShell) sandbox
|
|
8
|
+
with no credentials, every feature is checked in a fresh one, and nothing reaches `main`
|
|
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 @@ request. A small daemon runs the loop, following Anthropic's
|
|
|
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 @@ request. A small daemon runs the loop, following Anthropic's
|
|
|
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.
|
|
@@ -141,19 +201,25 @@ plans it finds there (see [Configuration](#configuration)).
|
|
|
141
201
|
|
|
142
202
|
## Concepts
|
|
143
203
|
|
|
144
|
-
- **Module:** a
|
|
145
|
-
plans. Modules are built one after another.
|
|
204
|
+
- **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
|
|
206
|
+
so that its design fits in one conversation. With the default model, Opus 5.5, the
|
|
207
|
+
Architect's context holds up to a million tokens, and a well-sized module never fills
|
|
208
|
+
it. If a module's design keeps growing, split it into smaller modules: each gets its
|
|
209
|
+
own Architect, with a fresh context.
|
|
146
210
|
- **Proposal:** the Architect's design document, in versions v1, v2 and so on. You
|
|
147
211
|
accept one.
|
|
148
212
|
- **Plan:** features that ship together as one pull request. A module's plans are built
|
|
149
|
-
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.
|
|
150
216
|
- **Feature:** one change that a Developer can make in one attempt, with at least one
|
|
151
217
|
check. Its definition is frozen once its design is merged.
|
|
152
218
|
- **Check:** a shell command, run from the repository root, that exits 0 when the feature
|
|
153
219
|
works. `pre-merge` checks run on every attempt and in CI; `post-deploy` checks run
|
|
154
220
|
after an infrastructure deploy.
|
|
155
221
|
- **Attempt:** one Developer session on one feature, in a fresh sandbox, with a deadline
|
|
156
|
-
(
|
|
222
|
+
(120 minutes) and a budget (2 attempts per feature). A failed attempt's successor
|
|
157
223
|
starts with the Manager's guidance. Once the budget is spent, the feature is escalated
|
|
158
224
|
to you.
|
|
159
225
|
- **Owner actions:** what only you can do, such as creating an account, approving a
|
|
@@ -168,7 +234,7 @@ plans it finds there (see [Configuration](#configuration)).
|
|
|
168
234
|
- **`progress.txt`:** one line per accepted feature and satisfied gate, committed with
|
|
169
235
|
the work. It only grows.
|
|
170
236
|
- **Brakes:** counters and deadlines, not judgment. Three failed attempts in a row, a
|
|
171
|
-
Manager that leaves an item waiting for
|
|
237
|
+
Manager that leaves an item waiting for 180 minutes, or CI still failing after two fix
|
|
172
238
|
rounds pause the pipeline until you resume it.
|
|
173
239
|
|
|
174
240
|
## The dashboard
|
|
@@ -184,7 +250,114 @@ behind a token. Its views:
|
|
|
184
250
|
- **Events:** the event log, live, with a filter.
|
|
185
251
|
|
|
186
252
|
The header pauses and resumes the pipeline: a pause lets what runs finish, and starts
|
|
187
|
-
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.
|
|
188
361
|
|
|
189
362
|
## Commands
|
|
190
363
|
|
|
@@ -197,7 +370,9 @@ too.
|
|
|
197
370
|
| `harness doctor [--build-images] [--probe]` | Checks the machine and the project, builds the agents' image, and tries what agents must not do |
|
|
198
371
|
| `harness github-app create --org <org>` | Creates the harness's GitHub App, once per machine |
|
|
199
372
|
| `harness github-app check --repo <org>/<repo>` | Checks that the App can reach the repository |
|
|
200
|
-
| `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 |
|
|
201
376
|
| `harness install-ci [--owner <login>]` | Writes the workflow, its CI script and `CODEOWNERS` again, after an upgrade |
|
|
202
377
|
| `harness lock` | Writes `harness/lock.json`, the hashes of the protected files |
|
|
203
378
|
| `harness up`, `harness down` | Starts the daemon and the dashboard, or stops the daemon; sandboxes keep running |
|
|
@@ -210,6 +385,7 @@ too.
|
|
|
210
385
|
| `harness message <text>` | Writes to the Manager of the module being built |
|
|
211
386
|
| `harness retry <module>/<plan>/<feature>` | Gives an escalated feature one more attempt; `--note` adds guidance |
|
|
212
387
|
| `harness resume` | Lifts a pause |
|
|
388
|
+
| `harness mode [manual\|swarm]` | Shows the mode, or switches it |
|
|
213
389
|
| `harness recover` | Rebuilds a lost `.harness/` from `main` and GitHub |
|
|
214
390
|
|
|
215
391
|
## Configuration
|
|
@@ -237,7 +413,7 @@ through an `Arch:` pull request, or a commit of your own on `main`.
|
|
|
237
413
|
| `project.repo`, `project.bot` | none | The GitHub repository, and the App's bot login. Without them, the project stays local. |
|
|
238
414
|
| `roles.<role>.model` | `claude-opus-5-5` | The model of the Architect, the Manager and the Developers |
|
|
239
415
|
| `roles.<role>.effort` | `max`, `high`, `xhigh` | Claude Code's `--effort`; `null` for a model without effort levels, such as Haiku |
|
|
240
|
-
| `roles.developer.attemptTimeoutMinutes` |
|
|
416
|
+
| `roles.developer.attemptTimeoutMinutes` | 120 | An attempt's deadline |
|
|
241
417
|
| `roles.developer.maxAttempts` | 2 | Attempts per feature before it is escalated to you |
|
|
242
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. |
|
|
243
419
|
| `network.extraHosts` | `[]` | More hosts, read-only |
|
|
@@ -248,8 +424,16 @@ through an `Arch:` pull request, or a commit of your own on `main`.
|
|
|
248
424
|
| `sandbox.env` | `{}` | Environment variables for every sandbox |
|
|
249
425
|
| `github.requiredChecks` | `["verify", "checks"]` | The CI jobs a pull request must pass; `tf-plan` is added with Terraform |
|
|
250
426
|
| `brakes.consecutiveFailures` | 3 | Failed attempts in a row that pause the pipeline |
|
|
251
|
-
| `brakes.managerTurnMinutes` |
|
|
427
|
+
| `brakes.managerTurnMinutes` | 90 | How long an item waits for the Manager before a reminder; as long again, and the pipeline pauses |
|
|
252
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 |
|
|
253
437
|
| `dashboard.port` | 4777 | The dashboard's port, or the next free one |
|
|
254
438
|
| `aws`, `terraform` | none | See [AWS and Terraform](#aws-and-terraform) |
|
|
255
439
|
|
|
@@ -365,7 +549,9 @@ What agents can't do, and what stops them:
|
|
|
365
549
|
code moves in and out as git bundles. The host never runs code or git hooks that an
|
|
366
550
|
agent wrote.
|
|
367
551
|
- **Write to GitHub.** Only the daemon talks to GitHub, as an App with no access to
|
|
368
|
-
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.
|
|
369
555
|
- **Change the rules.** Checks, CI and the agents' instructions are protected files: a
|
|
370
556
|
Developer's work can't change them, and CI checks that with the base branch's rules.
|
|
371
557
|
They change only through an Arch PR you merge, or your own commits.
|
|
@@ -375,7 +561,8 @@ What agents can't do, and what stops them:
|
|
|
375
561
|
through `web_read`: GET only, public addresses only, checked again after every
|
|
376
562
|
redirect.
|
|
377
563
|
- **Decide alone.** Code runs the loop: deadlines, attempt budgets, checks in fresh
|
|
378
|
-
sandboxes and
|
|
564
|
+
sandboxes and the merges, yours or the Swarm policy's, are enforced by the daemon,
|
|
565
|
+
not left to the agents.
|
|
379
566
|
|
|
380
567
|
What remains possible, by design:
|
|
381
568
|
- an agent could send code out in the URL of an allowed GET request;
|
|
@@ -393,7 +580,10 @@ To report a vulnerability, see `SECURITY.md` in the source repository.
|
|
|
393
580
|
|
|
394
581
|
- The agents run on your Claude subscription, so their usage counts against your plan's
|
|
395
582
|
limits. When Claude Code reports that a limit is reached, the attempt under way
|
|
396
|
-
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.
|
|
397
587
|
- By default, all three roles run Opus 5.5 at a high effort. To try the harness, or to
|
|
398
588
|
use less of your plan, set each role's `model` to Haiku 4.5
|
|
399
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
|
+
}
|