tumwater 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -71
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +34 -0
- package/dist/src/agent/claude/claude-account.js +243 -0
- package/dist/src/agent/claude/claude-args.js +104 -0
- package/dist/src/agent/claude/claude-backend.js +162 -0
- package/dist/src/agent/claude/claude-env.js +84 -0
- package/dist/src/agent/claude/claude-stream.js +384 -0
- package/dist/src/agent/mcp/role-notes-server.js +160 -0
- package/dist/src/agent/role-notes-write.js +65 -0
- package/dist/src/agent/run-agent.js +439 -0
- package/dist/src/agent/session-ledger.js +169 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +107 -18
- package/dist/src/backlog/backlog-md.js +53 -16
- package/dist/src/backlog/backlog-render.js +11 -20
- package/dist/src/backlog/backlog-structure.js +68 -63
- package/dist/src/backlog/backlog-write.js +631 -41
- package/dist/src/backlog/backlog.js +5 -16
- package/dist/src/baseline/main-baseline.js +152 -15
- package/dist/src/baseline/main-red.js +30 -21
- package/dist/src/brief-edit.js +42 -0
- package/dist/src/brief.js +34 -3
- package/dist/src/build/build-check-counts.js +15 -3
- package/dist/src/build/build-check-detect.js +6 -3
- package/dist/src/build/build-check-events.js +51 -6
- package/dist/src/build/build-check-scoped.js +26 -27
- package/dist/src/build/build-check.js +5 -4
- package/dist/src/build/build-stage.js +41 -2
- package/dist/src/build/dep-install.js +5 -1
- package/dist/src/build/host-sleep.js +2 -0
- package/dist/src/change/change-data.js +12 -3
- package/dist/src/cli/brief-command.js +56 -0
- package/dist/src/cli/cli-args.js +30 -2
- package/dist/src/cli/cli-command-args.js +52 -61
- package/dist/src/cli/cli-editor.js +52 -0
- package/dist/src/cli/cli-flag-specs.js +39 -14
- package/dist/src/cli/cli-marker-commands.js +6 -5
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +119 -30
- package/dist/src/cli/cli-repo-gate.js +15 -0
- package/dist/src/cli/cli-run.js +129 -15
- package/dist/src/cli/completion.js +149 -0
- package/dist/src/cli/config-commands.js +72 -38
- package/dist/src/cli/help.js +180 -38
- package/dist/src/cli/log-commands.js +3 -8
- package/dist/src/cli/notify-command.js +68 -0
- package/dist/src/cli/question-commands.js +71 -18
- package/dist/src/cli.js +134 -38
- package/dist/src/collections.js +8 -2
- package/dist/src/concurrency/check-permit.js +2 -2
- package/dist/src/concurrency/semaphore.js +26 -4
- package/dist/src/config/config-editable-keys.js +13 -2
- package/dist/src/config/config-example.js +2 -2
- package/dist/src/config/config-field-checks.js +23 -6
- package/dist/src/config/config-live.js +4 -14
- package/dist/src/config/config-schema.js +18 -7
- package/dist/src/config/config-validation.js +57 -16
- package/dist/src/config/config-views.js +129 -58
- package/dist/src/config/config-write.js +283 -25
- package/dist/src/config/config.js +56 -15
- package/dist/src/config/model-selector.js +32 -1
- package/dist/src/doctor/doctor-backlog.js +97 -31
- package/dist/src/doctor/doctor-checks.js +209 -118
- package/dist/src/doctor/doctor-holds.js +201 -0
- package/dist/src/doctor/doctor-launch-services.js +5 -7
- package/dist/src/doctor/doctor-model-checks.js +228 -86
- package/dist/src/doctor/doctor-notify.js +28 -0
- package/dist/src/doctor/doctor-orphans.js +7 -15
- package/dist/src/doctor/doctor.js +21 -4
- package/dist/src/doctor/outcome.js +16 -0
- package/dist/src/events/event-format.js +72 -7
- package/dist/src/events/event-read.js +4 -2
- package/dist/src/events/event-window.js +82 -3
- package/dist/src/events/events.js +27 -35
- package/dist/src/events/notify.js +89 -30
- package/dist/src/failure/failure-cluster.js +25 -11
- package/dist/src/failure/failure-data.js +122 -16
- package/dist/src/failure/failure-render.js +29 -11
- package/dist/src/failure/failure-state-change.js +30 -3
- package/dist/src/failure/time-spend.js +54 -15
- package/dist/src/files/files.js +107 -1
- package/dist/src/files/json-files.js +5 -4
- package/dist/src/files/json-object.js +34 -17
- package/dist/src/files/stat-cache.js +15 -2
- package/dist/src/files/tail.js +2 -2
- package/dist/src/fleet/error-storm.js +2 -2
- package/dist/src/fleet/fleet-hold.js +21 -3
- package/dist/src/fleet/fleet-polls.js +95 -21
- package/dist/src/fleet/fleet-state.js +55 -7
- package/dist/src/fleet/orchestrator-info.js +20 -0
- package/dist/src/fleet/reclaim.js +28 -28
- package/dist/src/gates/bootstrap-gates.js +5 -3
- package/dist/src/gates/budget-gates.js +34 -21
- package/dist/src/gates/gate-polls.js +18 -11
- package/dist/src/gates/maintenance-quota.js +2 -2
- package/dist/src/gates/role-cap-gates.js +73 -30
- package/dist/src/gates/startup-gate.js +44 -11
- package/dist/src/git/commit-message.js +11 -4
- package/dist/src/git/git-diff.js +41 -44
- package/dist/src/git/git-run.js +2 -0
- package/dist/src/git/git.js +218 -5
- package/dist/src/git/slots-state.js +7 -4
- package/dist/src/git/worktree-pool.js +18 -14
- package/dist/src/git/worktree-use.js +19 -2
- package/dist/src/git/worktree.js +15 -15
- package/dist/src/gui/gui-args.js +49 -0
- package/dist/src/gui/gui-endpoint-commands.js +463 -54
- package/dist/src/gui/gui-endpoints.js +193 -29
- package/dist/src/gui/gui-server.js +19 -2
- package/dist/src/history/history-data.js +145 -20
- package/dist/src/history/history.js +76 -52
- package/dist/src/inbox/inbox-attachments.js +132 -8
- package/dist/src/inbox/inbox-cancel.js +20 -9
- package/dist/src/inbox/inbox-edit.js +100 -20
- package/dist/src/inbox/inbox-submit.js +13 -9
- package/dist/src/inbox/inbox.js +110 -27
- package/dist/src/inbox/pending-prompt.js +71 -19
- package/dist/src/inbox/prompt-commands.js +105 -112
- package/dist/src/init/init.js +18 -16
- package/dist/src/landing/landing-batch.js +23 -7
- package/dist/src/landing/landing-check-failures.js +3 -3
- package/dist/src/landing/landing-core.js +9 -5
- package/dist/src/landing/landing-drain.js +54 -9
- package/dist/src/landing/landing-git.js +1 -2
- package/dist/src/landing/landing-merge.js +96 -43
- package/dist/src/landing/landing-pipeline.js +14 -0
- package/dist/src/landing/landing-queue-render.js +56 -0
- package/dist/src/landing/landing-queue.js +31 -5
- package/dist/src/landing/landing-slot.js +34 -7
- package/dist/src/landing/landing-vetting.js +6 -5
- package/dist/src/loop/leftover.js +8 -7
- package/dist/src/loop/loop-fallback.js +66 -0
- package/dist/src/loop/loop-pi.js +123 -35
- package/dist/src/loop/loop-recovery.js +90 -0
- package/dist/src/loop/loop-state.js +51 -18
- package/dist/src/loop/loop-tick.js +545 -0
- package/dist/src/loop/loop.js +88 -666
- package/dist/src/operator/operator-commands.js +69 -20
- package/dist/src/operator/operator-intent.js +71 -9
- package/dist/src/operator/operator-requests.js +129 -15
- package/dist/src/operator/retire.js +17 -1
- package/dist/src/orchestrator/orchestrator-launch.js +1 -1
- package/dist/src/orchestrator/orchestrator-scheduling.js +98 -10
- package/dist/src/orchestrator/orchestrator.js +50 -28
- package/dist/src/orchestrator/retention.js +10 -1
- package/dist/src/paths.js +36 -2
- package/dist/src/pi/pi-args.js +3 -3
- package/dist/src/pi/pi-backend.js +90 -0
- package/dist/src/pi/pi-bin.js +83 -23
- package/dist/src/pi/pi-event-line.js +14 -12
- package/dist/src/pi/pi-models.js +65 -21
- package/dist/src/pi/pi-run-result.js +7 -1
- package/dist/src/pi/pi-stream.js +170 -44
- package/dist/src/pi/pi-watchdogs.js +70 -50
- package/dist/src/pi/pi.js +9 -304
- package/dist/src/pi-extension/bounded-output.js +1 -1
- package/dist/src/pi-extension/context-shake.js +8 -2
- package/dist/src/pi-extension/role-notes.js +11 -61
- package/dist/src/process/process.js +6 -4
- package/dist/src/process/run-marker.js +22 -9
- package/dist/src/process/supervisor.js +98 -30
- package/dist/src/prompt/dialect.js +35 -0
- package/dist/src/{gates → prompt}/gate-prompts.js +66 -21
- package/dist/src/prompt/prompt-followup.js +37 -9
- package/dist/src/prompt/prompt.js +32 -15
- package/dist/src/redeploy/redeploy.js +1 -1
- package/dist/src/redeploy/redeployer.js +5 -3
- package/dist/src/redeploy/self-reload.js +1 -2
- package/dist/src/report/report-data.js +152 -48
- package/dist/src/report/report-render.js +88 -17
- package/dist/src/report/report.js +22 -9
- package/dist/src/review/protected-edits.js +32 -0
- package/dist/src/review/review-followup.js +12 -7
- package/dist/src/review/review-precheck.js +2 -2
- package/dist/src/review/review-verdict.js +103 -6
- package/dist/src/review/review.js +137 -31
- package/dist/src/review/suite-rerun.js +1 -1
- package/dist/src/roles/loop-ids.js +2 -3
- package/dist/src/roles/role-catalog.js +17 -5
- package/dist/src/roles/role-render.js +40 -13
- package/dist/src/roles/role-view.js +17 -3
- package/dist/src/roles/roles.js +4 -3
- package/dist/src/scheduling/backoff.js +1 -1
- package/dist/src/scheduling/claims.js +61 -7
- package/dist/src/scheduling/scheduling.js +20 -4
- package/dist/src/scheduling/work-landed-cache.js +1 -1
- package/dist/src/status/status-data.js +76 -24
- package/dist/src/status/status-polls.js +25 -1
- package/dist/src/text/datetime.js +9 -0
- package/dist/src/text/markdown.js +2 -2
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/tick/qa-coverage.js +11 -3
- package/dist/src/tick/stage-check.js +30 -4
- package/dist/src/tick/tick-apply.js +21 -9
- package/dist/src/tick/tick-detail-data.js +15 -2
- package/dist/src/tick/tick-detail.js +38 -10
- package/dist/src/tick/tick-finalize.js +20 -4
- package/dist/src/tick/tick-prompt.js +23 -11
- package/dist/src/tick/tick-resume.js +71 -10
- package/dist/src/tick/tick-stage.js +53 -11
- package/dist/src/tick/tick-timing.js +23 -13
- package/dist/src/tick/tick-usage.js +34 -4
- package/dist/src/tick/tick-verdict.js +48 -2
- package/dist/src/ui/badges.js +89 -6
- package/dist/src/ui/gui/gui-client-boot.js +99 -5
- package/dist/src/ui/gui/gui-client-composer.js +13 -8
- package/dist/src/ui/gui/gui-client-drawer.js +141 -20
- package/dist/src/ui/gui/gui-client-fleet.js +271 -18
- package/dist/src/ui/gui/gui-client-history.js +86 -38
- package/dist/src/ui/gui/gui-client-loops.js +87 -14
- package/dist/src/ui/gui/gui-client-model.js +4 -1
- package/dist/src/ui/gui/gui-client-operator.js +150 -10
- package/dist/src/ui/gui/gui-client-pending.js +40 -7
- package/dist/src/ui/gui/gui-client-report.js +203 -53
- package/dist/src/ui/gui/gui-client-settings.js +95 -19
- package/dist/src/ui/gui/gui-client.js +44 -11
- package/dist/src/ui/gui/gui-page.js +8 -1
- package/dist/src/ui/gui/gui-styles.js +1 -0
- package/dist/src/ui/progress-data.js +32 -26
- package/dist/src/ui/status-model.js +44 -44
- package/dist/src/ui/status-payload.js +39 -17
- package/dist/src/ui/status-render.js +15 -6
- package/dist/src/ui/tone.js +3 -1
- package/dist/src/ui/transcript.js +4 -4
- package/dist/src/ui/tui/tui-backlog.js +8 -6
- package/dist/src/ui/tui/tui-frame.js +57 -9
- package/dist/src/ui/tui/tui-history.js +29 -0
- package/dist/src/ui/tui/tui-input.js +95 -6
- package/dist/src/ui/tui/tui-keys.js +1503 -50
- package/dist/src/ui/tui/tui-pane.js +146 -23
- package/dist/src/ui/tui/tui.js +10 -5
- package/dist/src/verdict/fix-claim.js +112 -45
- package/dist/src/verdict/refusal.js +54 -2
- package/dist/src/verdict/reply-contract.js +16 -15
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -21,10 +21,14 @@ npm install -g tumwater
|
|
|
21
21
|
Or run any command without installing: `npx tumwater`. To run from a checkout of this repo
|
|
22
22
|
instead: `npm install && npm run build && npm link`.
|
|
23
23
|
|
|
24
|
+
With `claude` installed and logged in, `npm run smoke:claude` is an opt-in live check of the
|
|
25
|
+
claude backend: it drives the real CLI through a throwaway repo (a few cents of API-equivalent
|
|
26
|
+
spend on a subscription) and is not part of `npm test`, which stays green without claude.
|
|
27
|
+
|
|
24
28
|
## Status
|
|
25
29
|
|
|
26
30
|
<!-- tumwater:status:start -->
|
|
27
|
-
**v0.1.
|
|
31
|
+
**v0.1.3**: working harness. The director and 14 of the 15 roles are enabled by default; `telemetry`,
|
|
28
32
|
which files harness bugs from tumwater's own event log, is opt-in (`roles.telemetry.enabled`).
|
|
29
33
|
|
|
30
34
|
Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
|
|
@@ -34,79 +38,34 @@ Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
|
|
|
34
38
|
## Usage
|
|
35
39
|
|
|
36
40
|
```bash
|
|
37
|
-
cd your-project
|
|
41
|
+
cd your-project # a new or existing directory
|
|
38
42
|
tumwater init "Build a tiny markdown-to-html converter CLI in Python."
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# a fresh/empty project is seeded with
|
|
45
|
-
# "bootstrap": {"untilPlansDone": 5}, holding the maintenance
|
|
46
|
-
# loops until 5 plans are done; remove "bootstrap" from
|
|
47
|
-
# tumwater.json to end that early
|
|
48
|
-
tumwater run # start the loops (Ctrl+C to stop)
|
|
49
|
-
tumwater run --gui # ... and serve the browser dashboard at http://127.0.0.1:7180 from the same process
|
|
50
|
-
tumwater run --for 2h # run for a bounded window (capped at 90d), then drain and exit like Ctrl+C would
|
|
43
|
+
# --template, --file, and --adopt pick a starting point (see docs/cli.md)
|
|
44
|
+
tumwater run # start the loops (Ctrl+C to stop)
|
|
45
|
+
tumwater run --gui # ... and serve the browser dashboard at http://127.0.0.1:7180
|
|
46
|
+
tumwater run --for 2h # run for a bounded window, then drain, summarize, and exit
|
|
47
|
+
tumwater run --once # one round of ticks for cron/CI; exits 3 if a loop's tick failed
|
|
51
48
|
```
|
|
52
49
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
`tumwater
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
intervals, per-role `instances` (`feature` and `bugfix` may each run 1–8 parallel runners, every one
|
|
73
|
-
with its own branch and state; default 1), the daily spend cap
|
|
74
|
-
(`maxDailyCostUsd`, with optional per-role caps `maxDailyCostUsdPerRole` — a loop over its own
|
|
75
|
-
cap starts no new ticks until the next local day or a live edit), a
|
|
76
|
-
`maintenancePerWorkLanding` ratio (default 2; a rolling 24 h allowance of that many
|
|
77
|
-
code-maintenance landings per feature/bugfix/director landing, plus a floor of 12 — when the
|
|
78
|
-
window reaches it the scheduler holds the enabled maintenance loops until it rolls under, and a
|
|
79
|
-
fresh `tumwater wake <role>` or a queued prompt for one admits a single tick anyway), a nightly `quietHours` window
|
|
80
|
-
(e.g. `"23:00-07:00"` local time) during
|
|
81
|
-
which role loops start no new ticks (the director is exempt), with optional per-role windows
|
|
82
|
-
`quietHoursPerRole` — a loop inside its own window starts no new ticks, whether or not the
|
|
83
|
-
fleet-wide window covers now — a `diskHoldGB` free-space floor (default 10; when the volume
|
|
84
|
-
holding the worktrees drops below it, no new work starts until free space recovers 5 GB above it;
|
|
85
|
-
0 disables), a `diskReclaimGB` pressure-reclaim threshold (default 40; below it idle worktrees
|
|
86
|
-
drop their gitignored build outputs before the hold engages; 0 disables), and a
|
|
87
|
-
`worktreeIdleReclaimHours` window (default 24; an hourly pass drops a worktree's gitignored build
|
|
88
|
-
outputs once it has sat unused that long, whatever the free space; 0 disables), a
|
|
89
|
-
`worktreeSlots` count of pooled checkouts shared by role ticks and landing vets (default
|
|
90
|
-
`maxConcurrent` + 1; the director's worktree sits outside the pool) — user-defined
|
|
91
|
-
`customLoops`, and an
|
|
92
|
-
optional `notify` shell command run when the fleet needs a human (a budget pause, a budget warning
|
|
93
|
-
at 80% of the cap while the gate is still open, an error-streak
|
|
94
|
-
breaker trip, a failed landing, a blocked restart — the command gets `TUMWATER_EVENT_TYPE`,
|
|
95
|
-
`TUMWATER_EVENT_LOOP`, and `TUMWATER_EVENT_MESSAGE` in its environment).
|
|
96
|
-
Edits apply live while the fleet runs.
|
|
97
|
-
From the terminal, `tumwater config` prints the effective config as JSON, `tumwater config get
|
|
98
|
-
<key>` reads one resolved value, and `tumwater config set <key> <value>` writes one top-level
|
|
99
|
-
key; dotted keys (`maxDailyCostUsdPerRole.feature 1.5`, `roles.qa.model x`) merge one entry
|
|
100
|
-
into the existing map or role entry, while bare keys replace the whole value.
|
|
101
|
-
|
|
102
|
-
**Backends:** any OpenAI-compatible model pi can reach works; set `model` in `tumwater.json` to
|
|
103
|
-
one `provider/id[:thinking]` selector (plus `fallback` to a free selector), or a map of tiers
|
|
104
|
-
`small`/`default`/`strong`. A role whose primary keeps failing with provider-class errors
|
|
105
|
-
(three consecutive 429s or backend failures) runs its next ticks on its tier's resolved
|
|
106
|
-
`fallback` pair and probes the primary once the 5-minute cooldown elapses, returning to it
|
|
107
|
-
when a probe answers; `tumwater status`, the TUI, the dashboard, and `tumwater role <id>` name
|
|
108
|
-
the off-model episode. See [docs/backends.md](docs/backends.md) for requirements and a
|
|
109
|
-
worked setup.
|
|
50
|
+
A fresh or empty project is seeded with `"bootstrap": {"untilPlansDone": 5}`, which holds the
|
|
51
|
+
maintenance loops until 5 plans are done; remove it from `tumwater.json` to end that early.
|
|
52
|
+
|
|
53
|
+
From another terminal, `tumwater tui` or the browser dashboard watches the fleet, `tumwater status`
|
|
54
|
+
and `tumwater logs -f` check it, `tumwater brief --file <path>` replaces the initial prompt,
|
|
55
|
+
`tumwater prompt "<text>"` queues a request for the director, `tumwater plan` and `tumwater bug`
|
|
56
|
+
file backlog entries, and `tumwater pause`, `resume`, `wake`, and `stop` control the loops.
|
|
57
|
+
`tumwater doctor` audits the fleet and `tumwater report` shows usage and cost. `gui
|
|
58
|
+
--all-interfaces` exposes the dashboard, and with it the director prompt, to your whole network,
|
|
59
|
+
so pair it with `--token <secret>`.
|
|
60
|
+
|
|
61
|
+
Every command, flag, and TUI key is in [docs/cli.md](docs/cli.md); `tumwater help` prints the
|
|
62
|
+
same list, and `tumwater completion bash|zsh` prints shell completion. Settings live in
|
|
63
|
+
`tumwater.json` and edits apply live while the fleet runs; see [docs/config.md](docs/config.md).
|
|
64
|
+
|
|
65
|
+
**Backends:** any OpenAI-compatible model pi can reach works, chosen by `model` in `tumwater.json`
|
|
66
|
+
with a `fallback` that a failing role switches to. `agent: "claude"` runs bare selectors on Claude
|
|
67
|
+
Code instead. Requirements, the fallback and usage-window behavior, and how subscription runs are
|
|
68
|
+
billed (as notional spend, outside the daily cap) are in [docs/backends.md](docs/backends.md).
|
|
110
69
|
|
|
111
70
|
For how the loops, review gate, scheduling, and self-redeploy work, see
|
|
112
71
|
[docs/how-it-works.md](docs/how-it-works.md). For a measured comparison of tumwater's own
|
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The backend seam (plans/agent-backends.md "The seam"): what one non-interactive agent
|
|
2
|
+
* run needs from its backend — how to build the command, how to translate its native stdout
|
|
3
|
+
* into tumwater's canonical event stream, and how to classify its exit. src/agent/run-agent.ts
|
|
4
|
+
* owns everything backend-neutral (spawn, marker, watchdogs, abort, group kill and marker
|
|
5
|
+
* sweep, stderr capture); a backend registered here supplies the rest. src/agent/run-agent.ts
|
|
6
|
+
* registers the pi and claude adapters here.
|
|
7
|
+
*
|
|
8
|
+
* The registry is keyed by id and fed by explicit registration (src/agent/run-agent.ts
|
|
9
|
+
* registers both adapters at load), so a test can register a fake backend without importing a
|
|
10
|
+
* real one. */
|
|
11
|
+
import { hasResumableSession } from "./session-ledger.js";
|
|
12
|
+
/** The backend id used when a config names none: pi, the harness's default backend. */
|
|
13
|
+
export const DEFAULT_AGENT = "pi";
|
|
14
|
+
const registry = new Map();
|
|
15
|
+
/** Register one backend under its id, replacing any previous entry with the same id. */
|
|
16
|
+
export function registerAgentBackend(backend) {
|
|
17
|
+
registry.set(backend.id, backend);
|
|
18
|
+
}
|
|
19
|
+
/** The backend registered under `id`, or undefined when none is — the caller decides whether
|
|
20
|
+
* an unknown id is a hard error. */
|
|
21
|
+
export function agentBackend(id) {
|
|
22
|
+
return registry.get(id);
|
|
23
|
+
}
|
|
24
|
+
/** Whether the backend for `opts.agent` can resume a session in `dir` for a run of
|
|
25
|
+
* `opts.kind`: the one resume guard every resume point uses (plans/agent-backends.md
|
|
26
|
+
* "Sessions"), so a claude author or gate run resumes through its ledger id instead of being
|
|
27
|
+
* read as having no session. Falls back to the backend-neutral session-file check when the
|
|
28
|
+
* backend is unregistered or supplies no `resumable` hook. */
|
|
29
|
+
export function agentSessionResumable(dir, opts) {
|
|
30
|
+
const backend = agentBackend(opts.agent);
|
|
31
|
+
if (backend?.resumable)
|
|
32
|
+
return backend.resumable(dir, opts);
|
|
33
|
+
return hasResumableSession(dir);
|
|
34
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/** The claude account state: which billing pays for claude runs, and the newest subscription
|
|
2
|
+
* usage-window reading each run reported (plans/agent-backends.md §7 "Billing" and §8 "The
|
|
3
|
+
* subscription usage window"). Persisted at `.tumwater/state/claude-account.json`, written only
|
|
4
|
+
* by the orchestrator process, so the sibling parts that throttle or display the subscription
|
|
5
|
+
* window (6b/7–6e/7) read one answer instead of re-deriving it per surface.
|
|
6
|
+
*
|
|
7
|
+
* The file is a cache, never a source of truth: a run that reports billing overwrites it, and a
|
|
8
|
+
* missing or unreadable file reads as "no subscription". `resetsAt` is stored as epoch
|
|
9
|
+
* milliseconds (the run parser reports unix seconds; observeClaudeAccount converts once here),
|
|
10
|
+
* so consumers compare it to `Date.now()` directly. */
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
|
|
13
|
+
import { finiteNumber, isJsonObject, parseJsonObject } from "../../files/json-object.js";
|
|
14
|
+
import { claudeAccountPath } from "../../paths.js";
|
|
15
|
+
import { claudeBackend } from "./claude-backend.js";
|
|
16
|
+
import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
|
|
17
|
+
/** True when a failed run ended on a subscription usage window the account reported as
|
|
18
|
+
* `rejected` without overage — the signal the fleet hold throttles on (plans/agent-backends.md
|
|
19
|
+
* §8). Such a run is a transient of the world, not a failure of the session: its session and
|
|
20
|
+
* worktree edits are kept and resumed after `resetsAt` (part 6e) rather than committed as
|
|
21
|
+
* partial work or reset. API-billed claude and overage are budget/fallback matters, not a
|
|
22
|
+
* window hold, so they read false; a pi 429 reports no window and also reads false. */
|
|
23
|
+
export function subscriptionWindowRejected(run) {
|
|
24
|
+
return (run.transientRateLimit &&
|
|
25
|
+
run.billing === "subscription" &&
|
|
26
|
+
(run.rateWindows ?? []).some((w) => w.status === "rejected" && w.isUsingOverage !== true));
|
|
27
|
+
}
|
|
28
|
+
/** Status severity for ordering: a rejected window limits, an allowed_warning nears its limit,
|
|
29
|
+
* every other reading (allowed/ok) rides at the bottom, and a reset one no longer limits. */
|
|
30
|
+
const WINDOW_RANK = { rejected: 3, allowed_warning: 2, reset: 0 };
|
|
31
|
+
/** The window type as a compact label: `five_hour` → `5h`, `seven_day` → `7d`; an unknown
|
|
32
|
+
* type falls back to its words with underscores as spaces. The one home of the window spelling
|
|
33
|
+
* the status badge and the role view share. */
|
|
34
|
+
export function claudeWindowLabel(type) {
|
|
35
|
+
const m = /^(\w+)_(hour|day|week)$/.exec(type);
|
|
36
|
+
if (!m)
|
|
37
|
+
return type.replace(/_/g, " ");
|
|
38
|
+
const words = { one: "1", two: "2", three: "3", four: "4", five: "5", six: "6", seven: "7", eight: "8", nine: "9", ten: "10" };
|
|
39
|
+
const unit = m[2] === "hour" ? "h" : m[2] === "day" ? "d" : "w";
|
|
40
|
+
const word = m[1] ?? "";
|
|
41
|
+
return `${words[word] ?? word}${unit}`;
|
|
42
|
+
}
|
|
43
|
+
/** The account's windows as a list, most constrained first: by status severity, then the
|
|
44
|
+
* reported utilization (a missing reading sorts below any number), then the earlier reset, then
|
|
45
|
+
* the type name — so a tie always orders the same way. Empty when the account reported none. */
|
|
46
|
+
export function claudeWindowEntries(account) {
|
|
47
|
+
return Object.entries(account.windows)
|
|
48
|
+
.map(([type, w]) => ({ type, ...w }))
|
|
49
|
+
.sort((a, b) => {
|
|
50
|
+
const rank = (WINDOW_RANK[b.status] ?? 1) - (WINDOW_RANK[a.status] ?? 1);
|
|
51
|
+
if (rank !== 0)
|
|
52
|
+
return rank;
|
|
53
|
+
const util = (b.utilization ?? -1) - (a.utilization ?? -1);
|
|
54
|
+
if (util !== 0)
|
|
55
|
+
return util;
|
|
56
|
+
const reset = (a.resetsAt ?? Number.POSITIVE_INFINITY) - (b.resetsAt ?? Number.POSITIVE_INFINITY);
|
|
57
|
+
if (reset !== 0)
|
|
58
|
+
return reset;
|
|
59
|
+
return a.type.localeCompare(b.type);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Read the persisted account state, or null when it is absent or unreadable. A file without a
|
|
63
|
+
* usable top-level `observedAt` reads as absent (the plan's torn-file rule), and a window whose
|
|
64
|
+
* `resetsAt` is at or before `now` reads back with `status: "reset"` — the reader, not every
|
|
65
|
+
* consumer, decides that the window no longer limits. */
|
|
66
|
+
export function readClaudeAccount(root, now = Date.now()) {
|
|
67
|
+
const doc = readJsonFile(claudeAccountPath(root));
|
|
68
|
+
if (doc === null)
|
|
69
|
+
return null;
|
|
70
|
+
const billing = doc.billing === "subscription" || doc.billing === "api" ? doc.billing : undefined;
|
|
71
|
+
const observedAt = finiteNumber(doc.observedAt, Number.NaN);
|
|
72
|
+
if (billing === undefined || !Number.isFinite(observedAt))
|
|
73
|
+
return null;
|
|
74
|
+
const windows = {};
|
|
75
|
+
if (isJsonObject(doc.windows)) {
|
|
76
|
+
for (const [type, value] of Object.entries(doc.windows)) {
|
|
77
|
+
const window = readWindow(value, observedAt, now);
|
|
78
|
+
if (window !== undefined)
|
|
79
|
+
setWindow(windows, type, window);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
billing,
|
|
84
|
+
...(typeof doc.subscriptionType === "string" ? { subscriptionType: doc.subscriptionType } : {}),
|
|
85
|
+
observedAt,
|
|
86
|
+
windows,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** One window entry from the file, healed: a non-object or a status-less entry is dropped, a
|
|
90
|
+
* missing per-window `observedAt` falls back to the file's, and a past `resetsAt` flips the
|
|
91
|
+
* status to `reset`. */
|
|
92
|
+
function readWindow(value, fileObservedAt, now) {
|
|
93
|
+
if (!isJsonObject(value) || typeof value.status !== "string")
|
|
94
|
+
return undefined;
|
|
95
|
+
const observedAt = finiteNumber(value.observedAt, fileObservedAt);
|
|
96
|
+
const resetsAt = finiteNumber(value.resetsAt, Number.NaN);
|
|
97
|
+
const resets = Number.isFinite(resetsAt);
|
|
98
|
+
// utilization heals like the two stamps above: JSON's `1e999` parses to Infinity, and a
|
|
99
|
+
// non-finite fraction would render as `Infinity%` in the badges and poison the
|
|
100
|
+
// claudeWindowEntries comparator (Infinity - Infinity is NaN).
|
|
101
|
+
const utilization = finiteNumber(value.utilization, undefined);
|
|
102
|
+
const window = {
|
|
103
|
+
status: resets && resetsAt <= now ? "reset" : value.status,
|
|
104
|
+
...(resets ? { resetsAt } : {}),
|
|
105
|
+
...(utilization !== undefined ? { utilization } : {}),
|
|
106
|
+
...(typeof value.overageStatus === "string" ? { overageStatus: value.overageStatus } : {}),
|
|
107
|
+
...(value.isUsingOverage === true ? { isUsingOverage: true } : {}),
|
|
108
|
+
observedAt,
|
|
109
|
+
};
|
|
110
|
+
return window;
|
|
111
|
+
}
|
|
112
|
+
/** Store one window under its `rateLimitType`. A plain `windows[type] = window` loses a type of
|
|
113
|
+
* `__proto__`: the assignment hits Object.prototype's setter and rewrites the map's prototype
|
|
114
|
+
* instead of adding an own entry, so the window vanishes from every `Object.entries`/`values`
|
|
115
|
+
* reader (the display, the hold, and accountUsesOverage). Defining the own property keeps even
|
|
116
|
+
* that key — the same inherited-key guard claude-stream's TOOL_NAMES lookup uses. */
|
|
117
|
+
function setWindow(windows, type, window) {
|
|
118
|
+
Object.defineProperty(windows, type, { value: window, enumerable: true, writable: true, configurable: true });
|
|
119
|
+
}
|
|
120
|
+
/** Overwrite the account file with `account`, creating the state directory. The orchestrator's
|
|
121
|
+
* one writer; a failure to write is the caller's to handle (best-effort at the tick boundary).
|
|
122
|
+
* Written through writeJsonAtomic (tmp + rename): a crash or ENOSPC mid-write leaves the
|
|
123
|
+
* previous file whole, not a torn one the reader heals to "no subscription" — which would
|
|
124
|
+
* silently drop the billing answer and the window hold until the next run re-observed them. */
|
|
125
|
+
export function writeClaudeAccount(root, account) {
|
|
126
|
+
writeJsonAtomic(claudeAccountPath(root), account, true);
|
|
127
|
+
}
|
|
128
|
+
/** Fold one run's billing and rate-window readings into the account file: the run's `billing`
|
|
129
|
+
* overwrites the stored answer, and each reported window replaces the stored reading for its
|
|
130
|
+
* `type`, stamped with `observedAt`. A run that reported neither billing nor windows leaves the
|
|
131
|
+
* file untouched (and returns whatever was already there). */
|
|
132
|
+
export function observeClaudeAccount(root, run, now = Date.now()) {
|
|
133
|
+
const existing = readClaudeAccount(root, now);
|
|
134
|
+
const billing = run.billing ?? existing?.billing;
|
|
135
|
+
const reported = run.rateWindows ?? [];
|
|
136
|
+
if (billing === undefined || (run.billing === undefined && reported.length === 0))
|
|
137
|
+
return existing;
|
|
138
|
+
const windows = { ...(existing?.windows ?? {}) };
|
|
139
|
+
for (const w of reported)
|
|
140
|
+
setWindow(windows, w.type, windowFromRun(w, now));
|
|
141
|
+
const account = {
|
|
142
|
+
billing,
|
|
143
|
+
...(existing?.subscriptionType !== undefined ? { subscriptionType: existing.subscriptionType } : {}),
|
|
144
|
+
observedAt: now,
|
|
145
|
+
windows,
|
|
146
|
+
};
|
|
147
|
+
writeClaudeAccount(root, account);
|
|
148
|
+
return account;
|
|
149
|
+
}
|
|
150
|
+
/** One run's rate-window reading in the file's shape: unix-seconds `resetsAt` becomes epoch ms,
|
|
151
|
+
* and absent optional fields stay absent. */
|
|
152
|
+
function windowFromRun(w, now) {
|
|
153
|
+
// The write choke point heals utilization like readWindow does, so a non-finite value
|
|
154
|
+
// handed straight to observeClaudeAccount can never re-enter the file through a save.
|
|
155
|
+
const utilization = finiteNumber(w.utilization, undefined);
|
|
156
|
+
return {
|
|
157
|
+
status: w.status,
|
|
158
|
+
...(typeof w.resetsAt === "number" ? { resetsAt: w.resetsAt * 1000 } : {}),
|
|
159
|
+
...(utilization !== undefined ? { utilization } : {}),
|
|
160
|
+
...(typeof w.overageStatus === "string" ? { overageStatus: w.overageStatus } : {}),
|
|
161
|
+
...(w.isUsingOverage === true ? { isUsingOverage: true } : {}),
|
|
162
|
+
observedAt: now,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Seed the account file before the first run (plans/agent-backends.md §7): `ANTHROPIC_API_KEY`
|
|
166
|
+
* set means claude prefers the key, so billing is API; otherwise `claude auth status` decides —
|
|
167
|
+
* a claude.ai OAuth login is a subscription, any other login is API. An existing file is left
|
|
168
|
+
* alone, and a probe that cannot run or is not logged in leaves the file absent
|
|
169
|
+
* (readClaudeAccount then reads "no subscription"). Returns the account written, or null. */
|
|
170
|
+
export function seedClaudeAccount(root, config, now = Date.now()) {
|
|
171
|
+
const existing = readClaudeAccount(root, now);
|
|
172
|
+
if (existing !== null)
|
|
173
|
+
return existing;
|
|
174
|
+
const billing = seedBilling(config);
|
|
175
|
+
if (billing === null)
|
|
176
|
+
return null;
|
|
177
|
+
const account = {
|
|
178
|
+
billing: billing.billing,
|
|
179
|
+
...(billing.subscriptionType !== undefined ? { subscriptionType: billing.subscriptionType } : {}),
|
|
180
|
+
observedAt: now,
|
|
181
|
+
windows: {},
|
|
182
|
+
};
|
|
183
|
+
writeClaudeAccount(root, account);
|
|
184
|
+
return account;
|
|
185
|
+
}
|
|
186
|
+
/** The billing the environment implies, or null when it cannot be decided: an API key wins, a
|
|
187
|
+
* third-party provider env (`CLAUDE_CODE_USE_BEDROCK`/`_VERTEX`/`_FOUNDRY`) is API billing, and
|
|
188
|
+
* otherwise `claude auth status` names the login. */
|
|
189
|
+
function seedBilling(config) {
|
|
190
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
191
|
+
if (typeof apiKey === "string" && apiKey !== "")
|
|
192
|
+
return { billing: "api" };
|
|
193
|
+
if (claudeThirdPartyProvider(process.env))
|
|
194
|
+
return { billing: "api" };
|
|
195
|
+
const auth = probeAuthStatus(config);
|
|
196
|
+
if (!auth?.loggedIn)
|
|
197
|
+
return null;
|
|
198
|
+
const subscription = claudeAuthUsesSubscription(auth);
|
|
199
|
+
return subscription
|
|
200
|
+
? { billing: "subscription", ...(auth.subscriptionType !== undefined ? { subscriptionType: auth.subscriptionType } : {}) }
|
|
201
|
+
: { billing: "api" };
|
|
202
|
+
}
|
|
203
|
+
/** Parse `claude auth status` stdout, returning null when it is not a JSON object or does not
|
|
204
|
+
* report a boolean `loggedIn` — the one home of the read, shared by this module's
|
|
205
|
+
* probeAuthStatus and doctor's claudeAuthProbe (doctor-model-checks.ts). An optional string
|
|
206
|
+
* field reads as absent when the reply names something else. */
|
|
207
|
+
export function parseClaudeAuthStatus(stdout) {
|
|
208
|
+
const doc = parseJsonObject(stdout);
|
|
209
|
+
if (doc === null || typeof doc.loggedIn !== "boolean")
|
|
210
|
+
return null;
|
|
211
|
+
return {
|
|
212
|
+
loggedIn: doc.loggedIn,
|
|
213
|
+
subscriptionType: typeof doc.subscriptionType === "string" ? doc.subscriptionType : undefined,
|
|
214
|
+
authMethod: typeof doc.authMethod === "string" ? doc.authMethod : undefined,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/** True when an auth-status reply names claude.ai subscription billing — a subscription type
|
|
218
|
+
* (its presence alone signals one) or the explicit claude.ai auth method — rather than API
|
|
219
|
+
* billing. Shared by seedBilling below and doctor's checkClaudeAuth (doctor-model-checks.ts),
|
|
220
|
+
* which must not disagree about which seams are subscription-billed. */
|
|
221
|
+
export function claudeAuthUsesSubscription(auth) {
|
|
222
|
+
return auth.subscriptionType !== undefined || auth.authMethod === "claude.ai";
|
|
223
|
+
}
|
|
224
|
+
/** Run `claude auth status` under the same scrubbed environment a real run gets, bounded by a
|
|
225
|
+
* short deadline. Null when the probe could not run or its reply was unreadable. */
|
|
226
|
+
function probeAuthStatus(config) {
|
|
227
|
+
const resolved = claudeBackend.resolveBin(config);
|
|
228
|
+
const env = claudeEnv(process.env, { quietTimeoutSeconds: 0 });
|
|
229
|
+
try {
|
|
230
|
+
const result = spawnSync(resolved.bin, ["auth", "status"], {
|
|
231
|
+
env,
|
|
232
|
+
encoding: "utf8",
|
|
233
|
+
timeout: 10_000,
|
|
234
|
+
killSignal: "SIGKILL",
|
|
235
|
+
});
|
|
236
|
+
if (result.status !== 0 || typeof result.stdout !== "string")
|
|
237
|
+
return null;
|
|
238
|
+
return parseClaudeAuthStatus(result.stdout);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Building the claude command line for one tick (plans/agent-backends.md "The claude
|
|
2
|
+
* invocation") — pure argv construction plus the prompt-on-stdin pairing, with no subprocess,
|
|
3
|
+
* backend registration, or session ledger. Part 3c supplies the session decision (which
|
|
4
|
+
* `--session-id` or `--resume` to pass) and the notes MCP config path; this module turns those
|
|
5
|
+
* inputs into the invocation's argv.
|
|
6
|
+
*
|
|
7
|
+
* The environment is built separately (claude-env.ts) so each concern has one module.
|
|
8
|
+
*
|
|
9
|
+
* `NODE_OPTIONS` preload decision (2026-10-09): claude 2.1.295 on macOS is a single-file
|
|
10
|
+
* Mach-O native executable (`file` reports `Mach-O 64-bit executable arm64`) and ignores
|
|
11
|
+
* `NODE_OPTIONS` — it exits 0 and prints its version both with tumwater's `--import=data:`
|
|
12
|
+
* preload and with a deliberately broken `--require` path. So the preload stays on claude's
|
|
13
|
+
* environment: claude itself is unaffected, while the Bash tool's `npm` grandchildren inherit
|
|
14
|
+
* it and skip their LaunchServices check-in (src/process/process.ts). The `--settings` Bash-env
|
|
15
|
+
* fallback the design holds in reserve is not needed. */
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
/** Flags the harness passes itself. A passthrough arg equal to one of these is appended AFTER
|
|
19
|
+
* the harness's own copy, and claude's argument parser is last-wins — so it would silently
|
|
20
|
+
* override the isolation flags, the session choice, or the model the harness resolved. Each is
|
|
21
|
+
* rejected by claudeArgs with the offending arg named. This is exactly the design's reserved
|
|
22
|
+
* set (plans/agent-backends.md "Config"); config validation (config-validation.ts) mirrors this
|
|
23
|
+
* set for `agents.claude.args`, so a value that passes validation is never accepted by
|
|
24
|
+
* validation and then thrown here. */
|
|
25
|
+
export const HARNESS_CLAUDE_FLAGS = new Set([
|
|
26
|
+
"-p",
|
|
27
|
+
"--print",
|
|
28
|
+
"--output-format",
|
|
29
|
+
"--verbose",
|
|
30
|
+
"--model",
|
|
31
|
+
"--effort",
|
|
32
|
+
"--session-id",
|
|
33
|
+
"--resume",
|
|
34
|
+
"-r",
|
|
35
|
+
"--continue",
|
|
36
|
+
"-c",
|
|
37
|
+
"--permission-mode",
|
|
38
|
+
"--tools",
|
|
39
|
+
"--setting-sources",
|
|
40
|
+
"--strict-mcp-config",
|
|
41
|
+
"--mcp-config",
|
|
42
|
+
"--disable-slash-commands",
|
|
43
|
+
"--exclude-dynamic-system-prompt-sections",
|
|
44
|
+
"--append-system-prompt-file",
|
|
45
|
+
"--max-budget-usd",
|
|
46
|
+
]);
|
|
47
|
+
/** claude's effort levels are pi's thinking levels with `off`/`minimal` clamped to `low`:
|
|
48
|
+
* claude has no lower setting, and doctor notes the clamp (plans/agent-backends.md "Config").
|
|
49
|
+
* `off` and `minimal` are the only remapped entries; the rest are 1:1. */
|
|
50
|
+
const EFFORT_BY_THINKING = {
|
|
51
|
+
off: "low",
|
|
52
|
+
minimal: "low",
|
|
53
|
+
low: "low",
|
|
54
|
+
medium: "medium",
|
|
55
|
+
high: "high",
|
|
56
|
+
xhigh: "xhigh",
|
|
57
|
+
max: "max",
|
|
58
|
+
};
|
|
59
|
+
/** True when the worktree has an `AGENTS.md` and no `CLAUDE.md`: claude does not read
|
|
60
|
+
* `AGENTS.md` on its own, so the adapter appends it — but only when the repo has no
|
|
61
|
+
* `CLAUDE.md`, which `--setting-sources project` already loads (finding 1). */
|
|
62
|
+
function shouldAppendAgents(cwd) {
|
|
63
|
+
return fs.existsSync(path.join(cwd, "AGENTS.md")) && !fs.existsSync(path.join(cwd, "CLAUDE.md"));
|
|
64
|
+
}
|
|
65
|
+
/** Build one run's claude argv and stdin. The prompt is never an argv element (claude reads
|
|
66
|
+
* `-p` input from stdin). Throws on an unusable invocation — no session choice, both session
|
|
67
|
+
* choices, an unknown thinking level, or a passthrough arg that repeats a harness flag — since
|
|
68
|
+
* each would otherwise silently change what the harness asked for. */
|
|
69
|
+
export function claudeArgs(opts) {
|
|
70
|
+
if ((opts.sessionId === undefined) === (opts.resumeSessionId === undefined)) {
|
|
71
|
+
throw new Error("claudeArgs: pass exactly one of sessionId or resumeSessionId");
|
|
72
|
+
}
|
|
73
|
+
const args = ["-p", "--output-format", "stream-json", "--verbose", "--model", opts.model];
|
|
74
|
+
if (opts.thinking !== undefined) {
|
|
75
|
+
// Own-property test, not a bare index: a level like `constructor` or `toString` would
|
|
76
|
+
// otherwise inherit a function from Object.prototype and bypass the rejection below.
|
|
77
|
+
if (!Object.hasOwn(EFFORT_BY_THINKING, opts.thinking)) {
|
|
78
|
+
throw new Error(`claudeArgs: unknown thinking level ${JSON.stringify(opts.thinking)}`);
|
|
79
|
+
}
|
|
80
|
+
args.push("--effort", EFFORT_BY_THINKING[opts.thinking]);
|
|
81
|
+
}
|
|
82
|
+
if (opts.sessionId !== undefined)
|
|
83
|
+
args.push("--session-id", opts.sessionId);
|
|
84
|
+
else
|
|
85
|
+
args.push("--resume", opts.resumeSessionId);
|
|
86
|
+
args.push("--permission-mode", "bypassPermissions", "--tools", "Bash,Read,Edit,Write,Glob,Grep", "--setting-sources", "project", "--strict-mcp-config");
|
|
87
|
+
if (opts.mcpConfig !== undefined)
|
|
88
|
+
args.push("--mcp-config", opts.mcpConfig);
|
|
89
|
+
args.push("--disable-slash-commands", "--exclude-dynamic-system-prompt-sections");
|
|
90
|
+
if (shouldAppendAgents(opts.cwd))
|
|
91
|
+
args.push("--append-system-prompt-file", "AGENTS.md");
|
|
92
|
+
if (opts.maxBudgetUsd !== undefined && opts.maxBudgetUsd > 0) {
|
|
93
|
+
args.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
94
|
+
}
|
|
95
|
+
for (const arg of opts.args ?? []) {
|
|
96
|
+
// Match the flag itself even when it carries its value as `--flag=value`.
|
|
97
|
+
const flag = arg.split("=", 1)[0];
|
|
98
|
+
if (HARNESS_CLAUDE_FLAGS.has(flag)) {
|
|
99
|
+
throw new Error(`claudeArgs: passthrough arg ${JSON.stringify(arg)} repeats a harness flag`);
|
|
100
|
+
}
|
|
101
|
+
args.push(arg);
|
|
102
|
+
}
|
|
103
|
+
return { args, stdin: opts.prompt };
|
|
104
|
+
}
|