tumwater 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/README.md +63 -28
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-eligibility.js +332 -43
- package/dist/src/backlog/backlog-md.js +79 -17
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +178 -441
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +32 -30
- package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
- package/dist/src/{brief.js → brief/brief.js} +33 -8
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +13 -11
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +9 -7
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +43 -10
- package/dist/src/cli/cli-editor.js +13 -1
- package/dist/src/cli/cli-flag-specs.js +31 -6
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-query-commands.js +439 -56
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +124 -68
- package/dist/src/cli/completion.js +70 -12
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +264 -124
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +13 -224
- package/dist/src/cli.js +78 -36
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +107 -325
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +494 -68
- package/dist/src/doctor/doctor-checks.js +509 -58
- package/dist/src/doctor/doctor-holds.js +139 -78
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +2 -3
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +76 -25
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +219 -106
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +80 -30
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +6 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +79 -59
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -716
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +105 -54
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +25 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +179 -104
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +29 -70
- package/dist/src/inbox/inbox-attachments.js +66 -10
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +114 -73
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +104 -50
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +130 -13
- package/dist/src/loop/loop-tick.js +190 -88
- package/dist/src/loop/loop.js +68 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +113 -45
- package/dist/src/operator/operator-intent.js +74 -53
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +47 -28
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
- package/dist/src/orchestrator/orchestrator.js +149 -96
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +124 -38
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +4 -3
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +36 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +29 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +259 -254
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +235 -44
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +4 -3
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +55 -34
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +109 -131
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +16 -14
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +15 -8
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +28 -25
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +72 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +50 -39
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +142 -25
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +23 -22
- package/dist/src/tick/tick-detail.js +37 -24
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +67 -53
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +75 -10
- package/dist/src/ui/badges.js +113 -69
- package/dist/src/ui/gui/gui-client-boot.js +202 -29
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +94 -37
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +90 -47
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +60 -43
- package/dist/src/ui/gui/gui-client-operator.js +73 -43
- package/dist/src/ui/gui/gui-client-pending.js +34 -31
- package/dist/src/ui/gui/gui-client-report.js +330 -47
- package/dist/src/ui/gui/gui-client-settings.js +126 -18
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +18 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +176 -102
- package/dist/src/ui/status-payload.js +114 -36
- package/dist/src/ui/status-render.js +121 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +9 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +61 -57
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +44 -50
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +87 -1821
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +52 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +44 -26
- package/dist/src/verdict/fix-claim.js +56 -33
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +43 -11
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -56
- package/dist/src/landing/landing-slot.js +0 -256
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
package/README.md
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
# tumwater
|
|
2
2
|
|
|
3
|
-
tumwater is an opinionated autonomous development
|
|
4
|
-
[pi](https://github.com/badlogic/pi-mono). You write a short project brief, and a
|
|
5
|
-
|
|
3
|
+
tumwater is an opinionated autonomous development team, built on
|
|
4
|
+
[pi](https://github.com/badlogic/pi-mono). You write a short project brief, and a team of
|
|
5
|
+
roles (feature, bugfix, planning, tests, cleanup, docs, and more) builds the project
|
|
6
6
|
one small, reviewed commit at a time. It came from wanting to write only the brief and let a team
|
|
7
|
-
of always-on specialists do the rest: each
|
|
7
|
+
of always-on specialists do the rest: each role owns one concern, merges one change per run,
|
|
8
8
|
sleeps when it has nothing to do, and wakes when main moves. All project state lives in the
|
|
9
|
-
local git repo, and no remote is ever touched.
|
|
9
|
+
local git repo, and no remote is ever touched. The words used here (team, role, run, merge) are
|
|
10
|
+
defined in [docs/glossary.md](docs/glossary.md).
|
|
10
11
|
|
|
11
|
-

|
|
12
13
|
|
|
13
14
|
## Install
|
|
14
15
|
|
|
@@ -23,13 +24,16 @@ instead: `npm install && npm run build && npm link`.
|
|
|
23
24
|
|
|
24
25
|
With `claude` installed and logged in, `npm run smoke:claude` is an opt-in live check of the
|
|
25
26
|
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
|
+
spend on a subscription) and is not part of `npm test`, which stays green without claude. `npm run
|
|
28
|
+
smoke:opencode` is the same live check for the opencode backend, driving the real `opencode` CLI
|
|
29
|
+
against its free DeepSeek tier; it needs an OpenCode V2 install (`brew install opencode`) and is
|
|
30
|
+
also outside `npm test`.
|
|
27
31
|
|
|
28
32
|
## Status
|
|
29
33
|
|
|
30
34
|
<!-- tumwater:status:start -->
|
|
31
|
-
**v0.1.
|
|
32
|
-
which files
|
|
35
|
+
**v0.1.4**: working tumwater. The director and 14 of the 15 roles are enabled by default; `selfcheck`,
|
|
36
|
+
which files tumwater bugs from tumwater's own event log, is opt-in (`roles.selfcheck.enabled`).
|
|
33
37
|
|
|
34
38
|
Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
|
|
35
39
|
[QUESTIONS.md](QUESTIONS.md) (open questions).
|
|
@@ -41,40 +45,71 @@ Open work: [PLANS.md](PLANS.md) (planned), [BUGS.md](BUGS.md) (open bugs),
|
|
|
41
45
|
cd your-project # a new or existing directory
|
|
42
46
|
tumwater init "Build a tiny markdown-to-html converter CLI in Python."
|
|
43
47
|
# --template, --file, and --adopt pick a starting point (see docs/cli.md)
|
|
44
|
-
tumwater
|
|
45
|
-
tumwater
|
|
46
|
-
tumwater
|
|
47
|
-
tumwater
|
|
48
|
+
tumwater start # start the team (Ctrl+C to stop)
|
|
49
|
+
tumwater start --gui # ... and serve the browser dashboard at http://127.0.0.1:7180
|
|
50
|
+
tumwater start --tui # ... and attach the terminal TUI in the same window
|
|
51
|
+
tumwater start --for 2h # run for a bounded window, then finish current work, summarize, and exit
|
|
52
|
+
tumwater start --once # one round of runs for cron/CI; exits 3 if a run failed
|
|
48
53
|
```
|
|
49
54
|
|
|
50
55
|
A fresh or empty project is seeded with `"bootstrap": {"untilPlansDone": 5}`, which holds the
|
|
51
|
-
maintenance
|
|
56
|
+
maintenance roles until 5 plans are done; remove it from `tumwater.json` to end that early.
|
|
52
57
|
|
|
53
|
-
From another terminal, `tumwater tui` or the browser dashboard watches the
|
|
54
|
-
and `tumwater logs -f` check it, `tumwater brief --file <path>` replaces the
|
|
55
|
-
`tumwater prompt "<text>"`
|
|
56
|
-
file backlog entries,
|
|
57
|
-
`tumwater doctor` audits the
|
|
58
|
+
From another terminal, `tumwater tui` or the browser dashboard watches the team, `tumwater status`
|
|
59
|
+
and `tumwater logs -f` check it, `tumwater brief --file <path>` replaces the brief,
|
|
60
|
+
`tumwater prompt "<text>"` sends a message to the director's inbox, `tumwater plan` and
|
|
61
|
+
`tumwater bug` file backlog entries, `tumwater revert <sha>` undoes one merged change on main,
|
|
62
|
+
and `tumwater pause`, `resume`, `wake`, and `stop` control the roles. `tumwater doctor` audits the team and `tumwater report` shows usage and cost. `gui
|
|
58
63
|
--all-interfaces` exposes the dashboard, and with it the director prompt, to your whole network,
|
|
59
64
|
so pair it with `--token <secret>`.
|
|
60
65
|
|
|
66
|
+
`tumwater prompt --attach <image>` gives a role an image to read at its next run. When a role's
|
|
67
|
+
model cannot read images, the command refuses the attachment up front (naming the fix) unless
|
|
68
|
+
the role reroutes a blind read to a vision model, in which case it says where the message will
|
|
69
|
+
run; `tumwater doctor` probes each claimed image-input model once (cached 7 days, `--reprobe` to
|
|
70
|
+
force) and the dashboards and `tumwater role <id>` say which roles read images and where a blind
|
|
71
|
+
run reroutes.
|
|
72
|
+
|
|
61
73
|
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
|
|
74
|
+
same list, and `tumwater completion bash|zsh|fish` prints shell completion. Settings live in
|
|
75
|
+
`tumwater.json` and edits apply live while the team runs; see [docs/config.md](docs/config.md).
|
|
64
76
|
|
|
65
77
|
**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. `
|
|
67
|
-
Code instead
|
|
68
|
-
|
|
78
|
+
with a `fallback` that a failing role switches to. `backend: "claude"` runs bare model ids on Claude
|
|
79
|
+
Code instead, and a model id that starts with `opencode/` runs on the OpenCode CLI — its free
|
|
80
|
+
DeepSeek tier needs no login or API key. Requirements, the fallback, free-ness, and usage-window
|
|
81
|
+
behavior, and how subscription runs are billed (as estimated spend, outside the daily cap) are in
|
|
82
|
+
[docs/backends.md](docs/backends.md).
|
|
83
|
+
|
|
84
|
+
**Mixing providers:** a bare model id that starts with `claude-` runs on the claude CLI even
|
|
85
|
+
under the default `backend: "pi"`, so one config can mix a Claude strong tier with pi providers:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"model": {
|
|
90
|
+
"default": "huggingface/deepseek-ai/DeepSeek-V4.1-Flash:deepinfra",
|
|
91
|
+
"strong": "claude-opus-5-5"
|
|
92
|
+
},
|
|
93
|
+
"fallback": "omlx/Qwen3.8-27B-MLX-oQ4e-mtp"
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The default tier runs on pi through Hugging Face, the strong model jobs (the director, the plan
|
|
98
|
+
role, the reviewer, and the conflict resolver) run on the claude CLI's included usage, and the fallback runs on pi through
|
|
99
|
+
oMLX — all concurrently, under each provider's cap (set it with `maxConcurrentPerProvider`;
|
|
100
|
+
`tumwater status` shows how many runs each key has going out of its cap). A model id that starts
|
|
101
|
+
with `opencode/` routes to the OpenCode CLI under any backend, so its free tier can sit beside pi
|
|
102
|
+
and claude in one config. Prefix `anthropic/` (`anthropic/claude-…`)
|
|
103
|
+
to keep a Claude model on pi instead (billed per token). See [plans/agent-backends.md §3](plans/agent-backends.md).
|
|
69
104
|
|
|
70
|
-
For how the
|
|
105
|
+
For how the roles, review gate, scheduling, and updates work, see
|
|
71
106
|
[docs/how-it-works.md](docs/how-it-works.md). For a measured comparison of tumwater's own
|
|
72
107
|
code against human-written open source, see [docs/code-metrics.md](docs/code-metrics.md).
|
|
73
108
|
|
|
74
|
-
## Appendix:
|
|
109
|
+
## Appendix: the brief
|
|
75
110
|
|
|
76
|
-
The brief this repository was started from, kept for history.
|
|
77
|
-
between the markers below on every
|
|
111
|
+
The brief this repository was started from, kept for history. tumwater still reads the brief from
|
|
112
|
+
between the markers below on every run.
|
|
78
113
|
|
|
79
114
|
<!-- tumwater:prompt:start -->
|
|
80
115
|
Idea: agentic harness
|
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** Appending a backend's configured passthrough args to an already-built argv. Shared by the
|
|
3
|
+
* claude and opencode argv builders (claude-args.ts and opencode-args.ts), whose passthrough
|
|
4
|
+
* handling is otherwise identical, so the two backends reject a reserved flag by the same
|
|
5
|
+
* rule and the same message shape. A passthrough arg equal to a flag the harness itself sets
|
|
6
|
+
* is appended AFTER the harness's own copy, and both backends' parsers are last-wins, so it
|
|
7
|
+
* would silently override the model, the session choice, or the isolation flags — each such
|
|
8
|
+
* arg is rejected here with the offending arg named. `moduleName` names the blocking backend
|
|
9
|
+
* in the error (e.g. `claudeArgs` or `opencodeArgs`); `reserved` is that backend's
|
|
10
|
+
* HARNESS_*_FLAGS set. */
|
|
11
|
+
export function appendPassthroughArgs(args, passthrough, reserved, moduleName) {
|
|
12
|
+
for (const arg of passthrough ?? []) {
|
|
13
|
+
// Match the flag itself even when it carries its value as `--flag=value`.
|
|
14
|
+
const flag = arg.split("=", 1)[0];
|
|
15
|
+
if (reserved.has(flag)) {
|
|
16
|
+
// Name the reserved flag (the arg may carry it as `--flag=value`, so `flag` here is the
|
|
17
|
+
// normalized token) and the fix, so the operator knows exactly which passthrough to drop
|
|
18
|
+
// rather than which of several harness-set flags the arg collided with.
|
|
19
|
+
throw new Error(`${moduleName}: passthrough arg ${JSON.stringify(arg)} repeats a flag tumwater sets (` +
|
|
20
|
+
`${flag} is reserved) — drop it from your passthrough args`);
|
|
21
|
+
}
|
|
22
|
+
args.push(arg);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -26,9 +26,9 @@ export function agentBackend(id) {
|
|
|
26
26
|
* "Sessions"), so a claude author or gate run resumes through its ledger id instead of being
|
|
27
27
|
* read as having no session. Falls back to the backend-neutral session-file check when the
|
|
28
28
|
* backend is unregistered or supplies no `resumable` hook. */
|
|
29
|
-
export function agentSessionResumable(dir, opts) {
|
|
29
|
+
export function agentSessionResumable(dir, opts, configDir) {
|
|
30
30
|
const backend = agentBackend(opts.agent);
|
|
31
31
|
if (backend?.resumable)
|
|
32
|
-
return backend.resumable(dir, opts);
|
|
32
|
+
return backend.resumable(dir, opts, configDir);
|
|
33
33
|
return hasResumableSession(dir);
|
|
34
34
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/**
|
|
3
|
+
* The per-backend capabilities that the config, prompt, watchdog, progress and doctor layers
|
|
4
|
+
* read (plans/opencode-backend.md "Groundwork: the claude-only branches become backend
|
|
5
|
+
* traits"). Pure — imports nothing from the runtime registry (agent-backend.ts) — so those
|
|
6
|
+
* layers can read a backend's traits without pulling the registry in. That is the same reason
|
|
7
|
+
* `AGENT_DEFAULT_MODELS` lives in src/config/model-selector.ts.
|
|
8
|
+
*
|
|
9
|
+
* An id with no entry reads as pi, because src/agent/run-agent.ts routes an unregistered id to
|
|
10
|
+
* the pi backend.
|
|
11
|
+
*/
|
|
12
|
+
import { HARNESS_CLAUDE_FLAGS } from "./claude/claude-args.js";
|
|
13
|
+
import { HARNESS_OPENCODE_FLAGS } from "./opencode/opencode-args.js";
|
|
14
|
+
import { HARNESS_PI_FLAGS } from "../config/config-schema.js";
|
|
15
|
+
/** Whether the backend streams tool output. pi streams content-bearing tool updates, so its
|
|
16
|
+
* calls are classified per command; claude and opencode send none, so silence carries no hang
|
|
17
|
+
* signal and every call is judged by the buffered threshold (plans/agent-backends.md
|
|
18
|
+
* "Watchdogs and progress"). Only claude and opencode are false; an unknown id reads as pi. */
|
|
19
|
+
export function streamsToolOutput(agent) {
|
|
20
|
+
// Only claude and opencode stream no tool output; every other id (including an unregistered
|
|
21
|
+
// one) reads as pi, which does stream.
|
|
22
|
+
return agent !== "claude" && agent !== "opencode";
|
|
23
|
+
}
|
|
24
|
+
/** Whether the backend's model jobs resolve through pi's models.json. True for pi only (and an
|
|
25
|
+
* unknown id, which reads as pi); a claude or opencode job reads no pi pricing and needs no
|
|
26
|
+
* definitions file. */
|
|
27
|
+
export function usesPiModels(agent) {
|
|
28
|
+
// Only claude and opencode read no pi models.json; every other id (including an unregistered
|
|
29
|
+
// one) reads as pi, which does.
|
|
30
|
+
return agent !== "claude" && agent !== "opencode";
|
|
31
|
+
}
|
|
32
|
+
/** The reason string config validation prints for a reserved `backends.<id>.args` flag, or
|
|
33
|
+
* undefined when the flag is not reserved for that backend. Wraps claude's HARNESS_CLAUDE_FLAGS
|
|
34
|
+
* and pi's HARNESS_PI_FLAGS, keeping each backend's reason wording. The flag is the bare name —
|
|
35
|
+
* checkReservedArgs splits `--mode=x` on `=` before calling. */
|
|
36
|
+
export function reservedPassthroughFlag(agent, flag) {
|
|
37
|
+
switch (agent) {
|
|
38
|
+
case "pi":
|
|
39
|
+
return HARNESS_PI_FLAGS.get(flag);
|
|
40
|
+
case "claude":
|
|
41
|
+
return HARNESS_CLAUDE_FLAGS.has(flag) ? "it is managed by tumwater" : undefined;
|
|
42
|
+
case "opencode":
|
|
43
|
+
return HARNESS_OPENCODE_FLAGS.has(flag) ? "it is managed by tumwater" : undefined;
|
|
44
|
+
default:
|
|
45
|
+
// An unknown backend has no known harness argv to clash with, so nothing is reserved.
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** A seam's model as one readable token: `<provider>/<model>` for a pi seam (and any unknown
|
|
50
|
+
* id, which reads as pi), the bare model for claude — or "claude backend" when there is none —
|
|
51
|
+
* and `opencode/<model>` for opencode, whose ids all read under the opencode namespace. The
|
|
52
|
+
* one home of the label, shared by the doctor report and the image-input surface strings so
|
|
53
|
+
* they cannot drift. */
|
|
54
|
+
export function modelLabel(agent, provider, model) {
|
|
55
|
+
if (agent === "claude")
|
|
56
|
+
return model ?? "claude backend";
|
|
57
|
+
if (agent === "opencode")
|
|
58
|
+
return `opencode/${model ?? "?"}`;
|
|
59
|
+
return `${provider ?? "?"}/${model ?? "?"}`;
|
|
60
|
+
}
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
* milliseconds (the run parser reports unix seconds; observeClaudeAccount converts once here),
|
|
10
10
|
* so consumers compare it to `Date.now()` directly. */
|
|
11
11
|
import { spawnSync } from "node:child_process";
|
|
12
|
+
import os from "node:os";
|
|
13
|
+
import { resolvedAgentConfig } from "../../config/config-views.js";
|
|
12
14
|
import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
|
|
13
15
|
import { finiteNumber, isJsonObject, parseJsonObject } from "../../files/json-object.js";
|
|
14
16
|
import { claudeAccountPath } from "../../paths.js";
|
|
15
|
-
import { claudeBackend } from "./claude-backend.js";
|
|
17
|
+
import { claudeBackend, claudeConfigDir } from "./claude-backend.js";
|
|
16
18
|
import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
|
|
17
19
|
/** True when a failed run ended on a subscription usage window the account reported as
|
|
18
|
-
* `rejected` without overage — the signal the
|
|
20
|
+
* `rejected` without overage — the signal the team hold throttles on (plans/agent-backends.md
|
|
19
21
|
* §8). Such a run is a transient of the world, not a failure of the session: its session and
|
|
20
22
|
* worktree edits are kept and resumed after `resetsAt` (part 6e) rather than committed as
|
|
21
23
|
* partial work or reset. API-billed claude and overage are budget/fallback matters, not a
|
|
@@ -25,6 +27,39 @@ export function subscriptionWindowRejected(run) {
|
|
|
25
27
|
run.billing === "subscription" &&
|
|
26
28
|
(run.rateWindows ?? []).some((w) => w.status === "rejected" && w.isUsingOverage !== true));
|
|
27
29
|
}
|
|
30
|
+
/** The subscription account's used-up usage window, or null: a window reading `rejected` with no
|
|
31
|
+
* overage, or (with `includeOverage`, the default) a window whose current reading shows
|
|
32
|
+
* `isUsingOverage`, and a `resetsAt` still ahead of `now`; among several, the one that resets
|
|
33
|
+
* last. With a `reserve` (config `claudeWindowReserve`), a window whose utilization has reached
|
|
34
|
+
* its type's fraction counts too, and comes back with `reserve: true`; a rejected or overage
|
|
35
|
+
* window outranks a reserve one, so the flag is set only when no window is used up for real.
|
|
36
|
+
* The budget gate's window move (pollBudgetGate) reads the default; the team hold
|
|
37
|
+
* (claudeWindowObservation) passes `includeOverage: false`, because overage never holds a seam.
|
|
38
|
+
* Only subscription billing produces one; a missing account reads null. */
|
|
39
|
+
export function claudeWindowUsedUp(account, now, includeOverage = true, reserve) {
|
|
40
|
+
if (account?.billing !== "subscription")
|
|
41
|
+
return null;
|
|
42
|
+
let furthest = null;
|
|
43
|
+
let reserved = null;
|
|
44
|
+
for (const [type, w] of Object.entries(account.windows)) {
|
|
45
|
+
if (typeof w.resetsAt !== "number" || w.resetsAt <= now)
|
|
46
|
+
continue;
|
|
47
|
+
const used = w.isUsingOverage === true ? includeOverage : w.status === "rejected";
|
|
48
|
+
if (used) {
|
|
49
|
+
if (furthest === null || w.resetsAt > furthest.resetsAt)
|
|
50
|
+
furthest = { type, resetsAt: w.resetsAt };
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (windowReserved(w, reserve?.[type]) && (reserved === null || w.resetsAt > reserved.resetsAt))
|
|
54
|
+
reserved = { type, resetsAt: w.resetsAt, reserve: true };
|
|
55
|
+
}
|
|
56
|
+
return furthest ?? reserved;
|
|
57
|
+
}
|
|
58
|
+
/** Has this window's utilization reached its reserve fraction? False without a fraction or a
|
|
59
|
+
* reading. */
|
|
60
|
+
export function windowReserved(w, fraction) {
|
|
61
|
+
return fraction !== undefined && typeof w.utilization === "number" && w.utilization >= fraction;
|
|
62
|
+
}
|
|
28
63
|
/** Status severity for ordering: a rejected window limits, an allowed_warning nears its limit,
|
|
29
64
|
* every other reading (allowed/ok) rides at the bottom, and a reset one no longer limits. */
|
|
30
65
|
const WINDOW_RANK = { rejected: 3, allowed_warning: 2, reset: 0 };
|
|
@@ -40,6 +75,14 @@ export function claudeWindowLabel(type) {
|
|
|
40
75
|
const word = m[1] ?? "";
|
|
41
76
|
return `${words[word] ?? word}${unit}`;
|
|
42
77
|
}
|
|
78
|
+
/** The fraction of a subscription usage window consumed (0–1) as a whole-number percent: a
|
|
79
|
+
* `0.64` utilization reads `64`. The one home of the fraction→percent conversion, shared by
|
|
80
|
+
* the status badge and the doctor's hold reason so the two render a window's fill
|
|
81
|
+
* identically (the browser dashboard's own window line is embedded client-side and keeps its
|
|
82
|
+
* inline copy). */
|
|
83
|
+
export function claudeUtilizationPct(utilization) {
|
|
84
|
+
return Math.round(utilization * 100);
|
|
85
|
+
}
|
|
43
86
|
/** The account's windows as a list, most constrained first: by status severity, then the
|
|
44
87
|
* reported utilization (a missing reading sorts below any number), then the earlier reset, then
|
|
45
88
|
* the type name — so a tie always orders the same way. Empty when the account reported none. */
|
|
@@ -67,7 +110,7 @@ export function readClaudeAccount(root, now = Date.now()) {
|
|
|
67
110
|
const doc = readJsonFile(claudeAccountPath(root));
|
|
68
111
|
if (doc === null)
|
|
69
112
|
return null;
|
|
70
|
-
const billing = doc.billing === "subscription" || doc.billing === "api" ? doc.billing : undefined;
|
|
113
|
+
const billing = doc.billing === "subscription" || doc.billing === "api" ? (doc.billing) : undefined;
|
|
71
114
|
const observedAt = finiteNumber(doc.observedAt, Number.NaN);
|
|
72
115
|
if (billing === undefined || !Number.isFinite(observedAt))
|
|
73
116
|
return null;
|
|
@@ -88,7 +131,7 @@ export function readClaudeAccount(root, now = Date.now()) {
|
|
|
88
131
|
}
|
|
89
132
|
/** One window entry from the file, healed: a non-object or a status-less entry is dropped, a
|
|
90
133
|
* missing per-window `observedAt` falls back to the file's, and a past `resetsAt` flips the
|
|
91
|
-
* status to `reset
|
|
134
|
+
* status to `reset` and drops the overage flags. */
|
|
92
135
|
function readWindow(value, fileObservedAt, now) {
|
|
93
136
|
if (!isJsonObject(value) || typeof value.status !== "string")
|
|
94
137
|
return undefined;
|
|
@@ -99,12 +142,14 @@ function readWindow(value, fileObservedAt, now) {
|
|
|
99
142
|
// non-finite fraction would render as `Infinity%` in the badges and poison the
|
|
100
143
|
// claudeWindowEntries comparator (Infinity - Infinity is NaN).
|
|
101
144
|
const utilization = finiteNumber(value.utilization, undefined);
|
|
145
|
+
const reset = resets && resetsAt <= now;
|
|
102
146
|
const window = {
|
|
103
|
-
status:
|
|
147
|
+
status: reset ? "reset" : value.status,
|
|
104
148
|
...(resets ? { resetsAt } : {}),
|
|
105
149
|
...(utilization !== undefined ? { utilization } : {}),
|
|
106
|
-
|
|
107
|
-
...(value.
|
|
150
|
+
// a reset window is no longer in overage: dropping both keeps every reader in agreement
|
|
151
|
+
...(!reset && typeof value.overageStatus === "string" ? { overageStatus: value.overageStatus } : {}),
|
|
152
|
+
...(!reset && value.isUsingOverage === true ? { isUsingOverage: true } : {}),
|
|
108
153
|
observedAt,
|
|
109
154
|
};
|
|
110
155
|
return window;
|
|
@@ -225,7 +270,15 @@ export function claudeAuthUsesSubscription(auth) {
|
|
|
225
270
|
* short deadline. Null when the probe could not run or its reply was unreadable. */
|
|
226
271
|
function probeAuthStatus(config) {
|
|
227
272
|
const resolved = claudeBackend.resolveBin(config);
|
|
228
|
-
const
|
|
273
|
+
const configDir = resolvedAgentConfig(config, "claude").configDir;
|
|
274
|
+
const env = claudeEnv(process.env, {
|
|
275
|
+
quietTimeoutSeconds: 0,
|
|
276
|
+
// The seed probes auth under the same env a real run gets, so a team config dir's login
|
|
277
|
+
// is what it reports (plans/notes-command.md "Keeping funes out of team runs").
|
|
278
|
+
...(configDir !== undefined
|
|
279
|
+
? { configDir: claudeConfigDir(process.env, os.homedir(), configDir) }
|
|
280
|
+
: {}),
|
|
281
|
+
});
|
|
229
282
|
try {
|
|
230
283
|
const result = spawnSync(resolved.bin, ["auth", "status"], {
|
|
231
284
|
env,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
/** Building the claude command line for one tick (plans/agent-backends.md "The claude
|
|
2
3
|
* invocation") — pure argv construction plus the prompt-on-stdin pairing, with no subprocess,
|
|
3
4
|
* backend registration, or session ledger. Part 3c supplies the session decision (which
|
|
4
|
-
* `--session-id` or `--resume` to pass)
|
|
5
|
-
*
|
|
5
|
+
* `--session-id` or `--resume` to pass); this module turns those inputs into the invocation's
|
|
6
|
+
* argv.
|
|
6
7
|
*
|
|
7
8
|
* The environment is built separately (claude-env.ts) so each concern has one module.
|
|
8
9
|
*
|
|
@@ -15,12 +16,13 @@
|
|
|
15
16
|
* fallback the design holds in reserve is not needed. */
|
|
16
17
|
import fs from "node:fs";
|
|
17
18
|
import path from "node:path";
|
|
19
|
+
import { appendPassthroughArgs } from "../agent-args.js";
|
|
18
20
|
/** Flags the harness passes itself. A passthrough arg equal to one of these is appended AFTER
|
|
19
21
|
* the harness's own copy, and claude's argument parser is last-wins — so it would silently
|
|
20
22
|
* override the isolation flags, the session choice, or the model the harness resolved. Each is
|
|
21
23
|
* rejected by claudeArgs with the offending arg named. This is exactly the design's reserved
|
|
22
24
|
* set (plans/agent-backends.md "Config"); config validation (config-validation.ts) mirrors this
|
|
23
|
-
* set for `
|
|
25
|
+
* set for `backends.claude.args`, so a value that passes validation is never accepted by
|
|
24
26
|
* validation and then thrown here. */
|
|
25
27
|
export const HARNESS_CLAUDE_FLAGS = new Set([
|
|
26
28
|
"-p",
|
|
@@ -68,14 +70,18 @@ function shouldAppendAgents(cwd) {
|
|
|
68
70
|
* each would otherwise silently change what the harness asked for. */
|
|
69
71
|
export function claudeArgs(opts) {
|
|
70
72
|
if ((opts.sessionId === undefined) === (opts.resumeSessionId === undefined)) {
|
|
71
|
-
|
|
73
|
+
// Name which side fired so the invariant that would otherwise silently change the run's
|
|
74
|
+
// session is diagnosable at a glance: a conflicting pair (both set) differs from a missing
|
|
75
|
+
// choice (neither) and the two bugs have different fixes.
|
|
76
|
+
const state = opts.sessionId === undefined ? "neither is set" : "both are set";
|
|
77
|
+
throw new Error(`claudeArgs: pass exactly one of sessionId or resumeSessionId (${state})`);
|
|
72
78
|
}
|
|
73
79
|
const args = ["-p", "--output-format", "stream-json", "--verbose", "--model", opts.model];
|
|
74
80
|
if (opts.thinking !== undefined) {
|
|
75
81
|
// Own-property test, not a bare index: a level like `constructor` or `toString` would
|
|
76
82
|
// otherwise inherit a function from Object.prototype and bypass the rejection below.
|
|
77
83
|
if (!Object.hasOwn(EFFORT_BY_THINKING, opts.thinking)) {
|
|
78
|
-
throw new Error(`claudeArgs: unknown thinking level ${JSON.stringify(opts.thinking)}`);
|
|
84
|
+
throw new Error(`claudeArgs: unknown thinking level ${JSON.stringify(opts.thinking)} (known: ${Object.keys(EFFORT_BY_THINKING).join(", ")})`);
|
|
79
85
|
}
|
|
80
86
|
args.push("--effort", EFFORT_BY_THINKING[opts.thinking]);
|
|
81
87
|
}
|
|
@@ -84,21 +90,12 @@ export function claudeArgs(opts) {
|
|
|
84
90
|
else
|
|
85
91
|
args.push("--resume", opts.resumeSessionId);
|
|
86
92
|
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
93
|
args.push("--disable-slash-commands", "--exclude-dynamic-system-prompt-sections");
|
|
90
94
|
if (shouldAppendAgents(opts.cwd))
|
|
91
95
|
args.push("--append-system-prompt-file", "AGENTS.md");
|
|
92
96
|
if (opts.maxBudgetUsd !== undefined && opts.maxBudgetUsd > 0) {
|
|
93
97
|
args.push("--max-budget-usd", String(opts.maxBudgetUsd));
|
|
94
98
|
}
|
|
95
|
-
|
|
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
|
-
}
|
|
99
|
+
appendPassthroughArgs(args, opts.args, HARNESS_CLAUDE_FLAGS, "claudeArgs");
|
|
103
100
|
return { args, stdin: opts.prompt };
|
|
104
101
|
}
|
|
@@ -16,32 +16,47 @@
|
|
|
16
16
|
import fs from "node:fs";
|
|
17
17
|
import os from "node:os";
|
|
18
18
|
import path from "node:path";
|
|
19
|
-
import { fileURLToPath } from "node:url";
|
|
20
19
|
import { randomUUID } from "node:crypto";
|
|
21
20
|
import { resolvedAgentConfig } from "../../config/config-views.js";
|
|
22
21
|
import { MODEL_TIERS } from "../../config/config-schema.js";
|
|
23
22
|
import { routeModel } from "../../config/model-selector.js";
|
|
24
|
-
import { agentBaseEnv } from "../../process/run-marker.js";
|
|
23
|
+
import { agentBaseEnv, withNotesPath } from "../../process/run-marker.js";
|
|
25
24
|
import { resolveBackendBin } from "../../pi/pi-bin.js";
|
|
25
|
+
import { expandHomeTilde } from "../../files/files.js";
|
|
26
26
|
import { claudeArgs } from "./claude-args.js";
|
|
27
27
|
import { claudeEnv, claudeThirdPartyProvider } from "./claude-env.js";
|
|
28
28
|
import { claudeTranslator } from "./claude-stream.js";
|
|
29
29
|
import { resumableSession } from "../session-ledger.js";
|
|
30
|
-
/** claude's per-cwd project directory name under
|
|
31
|
-
* is not a letter or digit becomes `-`. Exported with
|
|
32
|
-
* and the tests create and look for exactly the file
|
|
30
|
+
/** claude's per-cwd project directory name under the claude config dir's `projects/`:
|
|
31
|
+
* every character that is not a letter or digit becomes `-`. Exported with
|
|
32
|
+
* `claudeTranscriptPath` so the PATH fake and the tests create and look for exactly the file
|
|
33
|
+
* the resume check reads. */
|
|
33
34
|
export function claudeProjectSlug(cwd) {
|
|
34
35
|
return cwd.replace(/[^A-Za-z0-9]/g, "-");
|
|
35
36
|
}
|
|
37
|
+
/** claude's config directory: `$CLAUDE_CONFIG_DIR` when non-empty (a leading `~` expands
|
|
38
|
+
* against the home dir), else `~/.claude`. A configured team `configDir` (the expanded
|
|
39
|
+
* `backends.claude.configDir`) wins over any inherited env var — the team's own directory is
|
|
40
|
+
* never overridden by the operator's shell. This mirrors claude's own lookup and pi's
|
|
41
|
+
* `piAgentDir`; exported so doctor reads the same dir the adapter writes transcripts into. */
|
|
42
|
+
export function claudeConfigDir(env = process.env, homeDir = os.homedir(), configured) {
|
|
43
|
+
if (configured)
|
|
44
|
+
return expandHomeTilde(configured, homeDir);
|
|
45
|
+
const dir = env.CLAUDE_CONFIG_DIR;
|
|
46
|
+
if (!dir)
|
|
47
|
+
return path.join(homeDir, ".claude");
|
|
48
|
+
return expandHomeTilde(dir, homeDir);
|
|
49
|
+
}
|
|
36
50
|
/** The transcript file claude writes for a session started in `cwd`: its resume check must find
|
|
37
51
|
* a real file before passing `--resume`. claude's own layout is
|
|
38
|
-
*
|
|
39
|
-
* cannot drift.
|
|
40
|
-
|
|
41
|
-
|
|
52
|
+
* `<config>/projects/<slug>/<sessionId>.jsonl`; exported so the fake and the resume logic
|
|
53
|
+
* cannot drift. `configDir` is the configured team dir, threaded through so the resume check
|
|
54
|
+
* looks where the run's own env will write. */
|
|
55
|
+
export function claudeTranscriptPath(cwd, id, env = process.env, homeDir = os.homedir(), configDir) {
|
|
56
|
+
return path.join(claudeConfigDir(env, homeDir, configDir), "projects", claudeProjectSlug(cwd), `${id}.jsonl`);
|
|
42
57
|
}
|
|
43
58
|
/** The CLI version every gated claude model needs (plans/agent-backends.md finding 9): on
|
|
44
|
-
* 2.1.216 Opus 5.5 is refused outright and Haiku 5.5 is mispriced about 250×, and
|
|
59
|
+
* 2.1.216 Opus 5.5 is refused outright and Haiku 5.5 is mispriced about 250×, and team runs
|
|
45
60
|
* set DISABLE_AUTOUPDATER so they never update themselves. The gated models are the three
|
|
46
61
|
* tier defaults, derived from AGENT_DEFAULT_MODELS through the same routing the config uses
|
|
47
62
|
* rather than listed again, so this table cannot drift from the models a seam resolves. */
|
|
@@ -65,37 +80,27 @@ function classifyClaudeExit(info) {
|
|
|
65
80
|
}
|
|
66
81
|
return {};
|
|
67
82
|
}
|
|
68
|
-
/** The inline `--mcp-config` JSON that runs the bundled role-notes server (part 5/7): the
|
|
69
|
-
* same dist module pi's extension resolves to, spawned by the same node running this harness
|
|
70
|
-
* so it needs no PATH lookup. */
|
|
71
|
-
function roleNotesMcpConfig() {
|
|
72
|
-
const server = fileURLToPath(new URL("../mcp/role-notes-server.js", import.meta.url));
|
|
73
|
-
return JSON.stringify({ mcpServers: { tumwater: { command: process.execPath, args: [server] } } });
|
|
74
|
-
}
|
|
75
83
|
/** The ledger record this run may resume, or null: the newest record of `opts.kind` for
|
|
76
84
|
* claude, at `opts.cwd` when one is given, whose transcript file exists. Shared by
|
|
77
85
|
* `resolveClaudeSession` and the backend's `resumable` hook, so the resume guard and the
|
|
78
86
|
* invocation can never drift on what counts as resumable. */
|
|
79
|
-
function claudeResumableRecord(dir, opts) {
|
|
87
|
+
function claudeResumableRecord(dir, opts, configDir) {
|
|
80
88
|
const record = resumableSession(dir, opts);
|
|
81
89
|
if (record === null)
|
|
82
90
|
return null;
|
|
83
91
|
if (opts.cwd !== undefined && record.cwd !== opts.cwd)
|
|
84
92
|
return null;
|
|
85
|
-
if (!fs.existsSync(claudeTranscriptPath(record.cwd, record.id)))
|
|
93
|
+
if (!fs.existsSync(claudeTranscriptPath(record.cwd, record.id, process.env, os.homedir(), configDir)))
|
|
86
94
|
return null;
|
|
87
95
|
return record;
|
|
88
96
|
}
|
|
89
97
|
/** The session id this run uses: for a continue, the ledger's newest record of this kind when
|
|
90
98
|
* it is claude's, this cwd's, and its transcript file exists; otherwise a fresh UUID. Returns
|
|
91
|
-
* the id and the `--resume` id when resuming.
|
|
92
|
-
|
|
99
|
+
* the id and the `--resume` id when resuming. `configDir` is the configured team dir, so the
|
|
100
|
+
* resume check looks where the run's own env will write transcripts. */
|
|
101
|
+
function resolveClaudeSession(run, configDir) {
|
|
93
102
|
if (run.continueSession) {
|
|
94
|
-
const record = claudeResumableRecord(run.sessionDir, {
|
|
95
|
-
kind: run.kind,
|
|
96
|
-
agent: "claude",
|
|
97
|
-
cwd: run.cwd,
|
|
98
|
-
});
|
|
103
|
+
const record = claudeResumableRecord(run.sessionDir, { kind: run.kind, agent: "claude", cwd: run.cwd }, configDir);
|
|
99
104
|
if (record !== null)
|
|
100
105
|
return { sessionId: record.id, resumeSessionId: record.id };
|
|
101
106
|
}
|
|
@@ -117,35 +122,35 @@ export const claudeBackend = {
|
|
|
117
122
|
if (run.config.model === undefined) {
|
|
118
123
|
throw new Error("claude backend: no model resolved for this run");
|
|
119
124
|
}
|
|
120
|
-
|
|
125
|
+
// The configured team config dir (plans/notes-command.md "Keeping funes out of team runs"):
|
|
126
|
+
// the run's env, its resume lookup, and the account seed all follow it; unset, nothing changes.
|
|
127
|
+
const claudeCfg = resolvedAgentConfig(run.config, "claude");
|
|
128
|
+
const configDir = claudeCfg.configDir;
|
|
129
|
+
const expandedConfigDir = configDir !== undefined ? claudeConfigDir(process.env, os.homedir(), configDir) : undefined;
|
|
130
|
+
const { sessionId, resumeSessionId } = resolveClaudeSession(run, configDir);
|
|
121
131
|
const { args, stdin } = claudeArgs({
|
|
122
132
|
cwd: run.cwd,
|
|
123
133
|
model: run.config.model,
|
|
124
134
|
thinking: run.config.thinking,
|
|
125
135
|
...(resumeSessionId !== undefined ? { resumeSessionId } : { sessionId }),
|
|
126
|
-
// The operator's `
|
|
136
|
+
// The operator's `backends.claude.args` passthrough, appended after the harness flags (the
|
|
127
137
|
// builder rejects any entry that repeats a harness-managed flag). resolvedAgentConfig
|
|
128
138
|
// owns the view so a caller cannot mutate the loaded config through the returned array.
|
|
129
|
-
args:
|
|
139
|
+
args: claudeCfg.args,
|
|
130
140
|
prompt: run.prompt,
|
|
131
141
|
// The caller computes the remaining daily budget for an API-billed claude run and the
|
|
132
142
|
// adapter forwards it; a subscription run or the director passes none (part 7a/7).
|
|
133
143
|
...(run.maxBudgetUsd !== undefined ? { maxBudgetUsd: run.maxBudgetUsd } : {}),
|
|
134
|
-
// claude does not load the bundled pi extensions, so a run carrying a notebook gets the
|
|
135
|
-
// role_notes MCP server instead. Passed on EVERY claude invocation that carries a
|
|
136
|
-
// notebook — fresh or resumed, a transient retry, a SUMMARY follow-up — because claude
|
|
137
|
-
// keeps no flags across --resume, so a resume without this would lose the tool.
|
|
138
|
-
...(run.notesPath !== undefined ? { mcpConfig: roleNotesMcpConfig() } : {}),
|
|
139
144
|
});
|
|
140
145
|
// agentBaseEnv applies the run marker and the macOS preload (shared with the pi adapter);
|
|
141
146
|
// claudeEnv layers the backend's own scrub-and-set on top. The notebook path rides beside
|
|
142
|
-
// the marker the same way pi's adapter carries it (
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
: { ...agentBaseEnv(run.runMarker), TUMWATER_NOTES_PATH: run.notesPath };
|
|
147
|
+
// the marker the same way pi's adapter carries it (withNotesPath); from part 2/4 the run
|
|
148
|
+
// saves its note through the `tumwater-notes` command on PATH, so no MCP server reaches the
|
|
149
|
+
// run and `--strict-mcp-config` (always in claude-args.ts) keeps it that way.
|
|
150
|
+
const base = withNotesPath(agentBaseEnv(run.runMarker, undefined, run.binDir), run.notesPath);
|
|
147
151
|
const env = claudeEnv(base, {
|
|
148
152
|
quietTimeoutSeconds: run.config.quietTimeoutSeconds,
|
|
153
|
+
...(expandedConfigDir !== undefined ? { configDir: expandedConfigDir } : {}),
|
|
149
154
|
});
|
|
150
155
|
return { args, stdin, env, sessionId };
|
|
151
156
|
},
|
|
@@ -155,8 +160,13 @@ export const claudeBackend = {
|
|
|
155
160
|
// seed the account file, so both must classify it the same way.
|
|
156
161
|
return claudeTranslator({ thirdPartyProvider: claudeThirdPartyProvider(process.env) });
|
|
157
162
|
},
|
|
158
|
-
resumable(dir, opts) {
|
|
159
|
-
return claudeResumableRecord(dir, opts) !== null;
|
|
163
|
+
resumable(dir, opts, configDir) {
|
|
164
|
+
return claudeResumableRecord(dir, opts, configDir) !== null;
|
|
165
|
+
},
|
|
166
|
+
// Every Claude model reads images (Claude Code's Read tool returns them as image blocks), so
|
|
167
|
+
// a claude seam is always capable — the doctor probes nothing and never works around it.
|
|
168
|
+
imageInput() {
|
|
169
|
+
return true;
|
|
160
170
|
},
|
|
161
171
|
classifyExit: classifyClaudeExit,
|
|
162
172
|
};
|