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
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/** Building the environment for one claude run (plans/agent-backends.md "The claude
|
|
2
2
|
* invocation", "Findings" 2 and 4) — pure scrub-and-set over a base environment, with no
|
|
3
3
|
* subprocess. The caller layers the run-marker, the macOS LaunchServices preload, and the
|
|
4
|
-
* notebook on first (src/
|
|
5
|
-
* same), then hands the result here.
|
|
4
|
+
* notebook on first (src/process/run-marker.ts's `withNotesPath`), then hands the result here.
|
|
6
5
|
*
|
|
7
6
|
* Inherited Claude Code session variables break auth: spawned from inside a Claude Code
|
|
8
7
|
* session, a child that keeps `CLAUDECODE`, `CLAUDE_CODE_*`, `CLAUDE_AGENT_SDK_*` or the host's
|
|
@@ -13,7 +12,7 @@
|
|
|
13
12
|
*
|
|
14
13
|
* The set half disables auto-memory, backgrounded Bash tasks (finding 4: claude refuses a long
|
|
15
14
|
* foreground command, backgrounds it, and abandons it when `-p` ends, which would report work
|
|
16
|
-
* never verified), the auto-updater (a mid-
|
|
15
|
+
* never verified), the auto-updater (a mid-team self-update must not swap the binary under a
|
|
17
16
|
* run), telemetry, and error reporting; and raises the Bash timeouts so a slow suite runs in
|
|
18
17
|
* the foreground. `BASH_MAX_TIMEOUT_MS` is derived from `quietTimeoutSeconds` so claude's own
|
|
19
18
|
* Bash timeout fires before tumwater's quiet kill and the model sees a tool error rather than a
|
|
@@ -80,5 +79,10 @@ export function claudeEnv(base, opts) {
|
|
|
80
79
|
env.DISABLE_AUTOUPDATER = "1";
|
|
81
80
|
env.DISABLE_TELEMETRY = "1";
|
|
82
81
|
env.DISABLE_ERROR_REPORTING = "1";
|
|
82
|
+
// A team config dir (plans/notes-command.md "Keeping funes out of team runs"): force claude's
|
|
83
|
+
// own config and transcripts under it, so a funes hook on ~/.claude/projects never indexes
|
|
84
|
+
// team runs. Unset leaves any inherited CLAUDE_CONFIG_DIR (or claude's default) untouched.
|
|
85
|
+
if (opts.configDir)
|
|
86
|
+
env.CLAUDE_CONFIG_DIR = opts.configDir;
|
|
83
87
|
return env;
|
|
84
88
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* on the seam's optional `warnings()` — src/agent/run-agent.ts forwards each as an
|
|
15
15
|
* `agent_unknown_event` warning after the run. `cost_unknown` is recorded the same way when a
|
|
16
16
|
* run produced no authoritative `run_cost`. */
|
|
17
|
-
import { isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
|
|
17
|
+
import { canonicalUsageBlock, isJsonObject, objectOrEmpty, nonNegativeNumber, parseJsonObject } from "../../files/json-object.js";
|
|
18
18
|
/** claude's tool names → the canonical pi names the readers already know. Glob/Grep are kept
|
|
19
19
|
* lower-cased; MultiEdit collapses onto edit (pi has one edit tool). Anything else is
|
|
20
20
|
* lower-cased so a future tool still reads as a name rather than as `undefined`. */
|
|
@@ -37,20 +37,10 @@ function str(value) {
|
|
|
37
37
|
}
|
|
38
38
|
/** claude's usage block → pi's canonical usage block, with `cost.total` 0 because the run's
|
|
39
39
|
* authoritative cost rides the final `run_cost` event (a split-block message repeats its usage
|
|
40
|
-
* on every event, so summing per event would double-count — the fold takes it once).
|
|
40
|
+
* on every event, so summing per event would double-count — the fold takes it once). The
|
|
41
|
+
* wire-field reads stay here; the block's assembly is the shared canonicalUsageBlock. */
|
|
41
42
|
function canonicalUsage(usage) {
|
|
42
|
-
|
|
43
|
-
const cacheRead = nonNegativeNumber(usage?.cache_read_input_tokens, 0);
|
|
44
|
-
const cacheWrite = nonNegativeNumber(usage?.cache_creation_input_tokens, 0);
|
|
45
|
-
const output = nonNegativeNumber(usage?.output_tokens, 0);
|
|
46
|
-
return {
|
|
47
|
-
input,
|
|
48
|
-
cacheRead,
|
|
49
|
-
cacheWrite,
|
|
50
|
-
output,
|
|
51
|
-
totalTokens: input + cacheRead + cacheWrite + output,
|
|
52
|
-
cost: { total: 0 },
|
|
53
|
-
};
|
|
43
|
+
return canonicalUsageBlock(nonNegativeNumber(usage?.input_tokens, 0), nonNegativeNumber(usage?.cache_read_input_tokens, 0), nonNegativeNumber(usage?.cache_creation_input_tokens, 0), nonNegativeNumber(usage?.output_tokens, 0), 0);
|
|
54
44
|
}
|
|
55
45
|
/** One claude content block → one pi content block, or undefined for a block pi has no
|
|
56
46
|
* vocabulary for (dropped, like any other unmapped native shape). `tool_use` becomes pi's
|
|
@@ -76,7 +66,7 @@ function canonicalBlock(block) {
|
|
|
76
66
|
return {
|
|
77
67
|
type: "toolCall",
|
|
78
68
|
id: str(block.id) ?? "",
|
|
79
|
-
name: Object.hasOwn(TOOL_NAMES, name) ? TOOL_NAMES[name] : name.toLowerCase(),
|
|
69
|
+
name: Object.hasOwn(TOOL_NAMES, name) ? (TOOL_NAMES[name]) : name.toLowerCase(),
|
|
80
70
|
arguments: args,
|
|
81
71
|
};
|
|
82
72
|
}
|
|
@@ -347,7 +337,8 @@ export function claudeTranslator(opts = {}) {
|
|
|
347
337
|
return out;
|
|
348
338
|
}
|
|
349
339
|
case "tool_progress":
|
|
350
|
-
// A 30 s heartbeat of a running tool call: known, and the open call already tracks
|
|
340
|
+
// A 30 s heartbeat of a running tool call: known, and the open call already tracks
|
|
341
|
+
// liveness.
|
|
351
342
|
return [];
|
|
352
343
|
default:
|
|
353
344
|
record(type ?? "unknown");
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** The seam's answer to "who in this team can read images" (plans/image-input.md
|
|
2
|
+
* "Capability"). One resolver, `seamImageInput`, is the only place that rule lives; the doctor
|
|
3
|
+
* (src/doctor/doctor-model-checks.ts) and the later reroute parts both read it, so the answer
|
|
4
|
+
* is never reached two ways. It lives at runtime level because it needs the backend registry:
|
|
5
|
+
* a claude seam is always capable through the AgentBackend.imageInput hook, a pi seam reads the
|
|
6
|
+
* model's `input` in models.json, and any other backend answers null unless it declares a hook. */
|
|
7
|
+
import { configForRole, roleSeamTier, withModelOverrides } from "../config/config-views.js";
|
|
8
|
+
import { BORROW_ORDER, resolveTierFallbacks } from "../config/config-fallbacks.js";
|
|
9
|
+
import { seamAgent } from "../config/model-selector.js";
|
|
10
|
+
import { modelLabel } from "./backend-traits.js";
|
|
11
|
+
import { piModelImageInput, piPairFree, piModelsPath, readPiProviders } from "../pi/pi-models.js";
|
|
12
|
+
import { agentBackend } from "./agent-backend.js";
|
|
13
|
+
import { imageProbeEntryFresh, imageProbeKey } from "../doctor/image-probe.js";
|
|
14
|
+
/** One seam's image-input capability (plans/image-input.md "Capability"): `true` when the
|
|
15
|
+
* seam's backend declares image support, `false` when it is text-only, `null` when it cannot
|
|
16
|
+
* be answered from what the team knows (an unknown pair, a backend with no hook and no
|
|
17
|
+
* catalog entry — the doctor's `tier models` check already fails such pairs).
|
|
18
|
+
*
|
|
19
|
+
* The backend bookends: a backend with an `imageInput` hook (claude → true) owns the answer;
|
|
20
|
+
* the default pi backend leaves it to models.json's `input` field, where absent means text-only
|
|
21
|
+
* (pi's own default for a custom model). `probes` is the doctor's live-probe cache (part 2/5): a
|
|
22
|
+
* claimed pi seam whose probe FAILED reads false — a claim is only as good as the verified
|
|
23
|
+
* answer — while a cached pass (or an unprobed claim) stands on the catalog. claude seams are
|
|
24
|
+
* never probed, so the hook answer is never overridden. */
|
|
25
|
+
export function seamImageInput(seam, providers, probes) {
|
|
26
|
+
const agent = seamAgent(seam);
|
|
27
|
+
const backend = agentBackend(agent);
|
|
28
|
+
// A backend with an imageInput hook owns the answer outright (claude: true for every model).
|
|
29
|
+
if (backend?.imageInput !== undefined)
|
|
30
|
+
return backend.imageInput(seam.model);
|
|
31
|
+
// An unknown backend without a hook has no catalog to consult — unknown, never assume.
|
|
32
|
+
if (agent !== "pi")
|
|
33
|
+
return null;
|
|
34
|
+
const base = piModelImageInput(providers, seam.provider, seam.model);
|
|
35
|
+
if (base !== true)
|
|
36
|
+
return base;
|
|
37
|
+
// A claimed pi seam whose live probe failed (fresh, claim unchanged) reads false. Only pi is
|
|
38
|
+
// probed, so the probe key carries the pi agent.
|
|
39
|
+
const entry = probes?.[imageProbeKey(agent, seam.provider, seam.model)];
|
|
40
|
+
if (entry?.verdict === "fail" && imageProbeEntryFresh(entry))
|
|
41
|
+
return false;
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
/** The image-capable reroute target for `role`, or null when no peer tier can read images
|
|
45
|
+
* (plans/image-input.md "Reroute target"). Walks the role's seam tier through BORROW_ORDER,
|
|
46
|
+
* skipping the tier itself — small → default → strong, default → strong → small, strong →
|
|
47
|
+
* default (a strong-tier seam never drops to small, the same rule as the budget fallback) —
|
|
48
|
+
* and takes the first tier whose model reads images. `regime` picks the pair set (see
|
|
49
|
+
* RerouteRegime); `modelsPath` and `account` are injectable so tests need no real pi install
|
|
50
|
+
* or account. */
|
|
51
|
+
export function imageReroute(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, probes, providers = null) {
|
|
52
|
+
const tier = roleSeamTier(config, role);
|
|
53
|
+
const candidates = BORROW_ORDER[tier].filter((t) => t !== tier);
|
|
54
|
+
if (candidates.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const providerMap = providers ?? readPiProviders(modelsPath);
|
|
57
|
+
const capable = (rc) => seamImageInput(rc, providerMap, probes) === true;
|
|
58
|
+
if (regime === "primary") {
|
|
59
|
+
for (const t of candidates) {
|
|
60
|
+
const rc = withModelOverrides(config, t, {});
|
|
61
|
+
if (capable(rc))
|
|
62
|
+
return { tier: t, config: rc };
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
// Fallback regime: only the tiers' free fallback pairs qualify (resolveTierFallbacks over the
|
|
67
|
+
// price check), never a priced primary model — a reroute must not spend past the cap.
|
|
68
|
+
const resolved = resolveTierFallbacks(config, (p) => piPairFree(providerMap, p.provider, p.model, account));
|
|
69
|
+
for (const t of candidates) {
|
|
70
|
+
const pair = resolved[t].pair;
|
|
71
|
+
if (!pair)
|
|
72
|
+
continue;
|
|
73
|
+
const rc = withModelOverrides(config, t, {
|
|
74
|
+
provider: pair.provider,
|
|
75
|
+
model: pair.model,
|
|
76
|
+
thinking: pair.thinking,
|
|
77
|
+
});
|
|
78
|
+
if (capable(rc))
|
|
79
|
+
return { tier: t, config: rc };
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/** The config of imageReroute, for callers that want only the target model. */
|
|
84
|
+
export function imageRerouteTarget(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, probes, providers = null) {
|
|
85
|
+
return imageReroute(config, role, regime, modelsPath, account, probes, providers)?.config ?? null;
|
|
86
|
+
}
|
|
87
|
+
/** A seam's model as one readable token, the same label the doctor's `image input` check uses
|
|
88
|
+
* (`claude/<id>` for a claude seam, `provider/model` for pi). The one home of the label, so
|
|
89
|
+
* the surface strings (the images alert, the role page, the status rows) and the doctor report
|
|
90
|
+
* cannot drift, and delegates to backend-traits' modelLabel. */
|
|
91
|
+
export function seamImageLabel(agent, provider, model) {
|
|
92
|
+
return modelLabel(agent, provider, model);
|
|
93
|
+
}
|
|
94
|
+
/** Compute RoleImageInfo for one loop. `modelsPath` and `probes` are injectable; the providers
|
|
95
|
+
* catalog is read (stat-cached) here and also handed to imageReroute so a poll that asks many
|
|
96
|
+
* loops still parses the catalog once. All reads mirror the doctor's `image input` check, so
|
|
97
|
+
* the surface and the check agree about capability. */
|
|
98
|
+
export function roleImageInfo(config, role, modelsPath = piModelsPath(), probes) {
|
|
99
|
+
const providers = readPiProviders(modelsPath);
|
|
100
|
+
const rc = configForRole(config, role);
|
|
101
|
+
const imageInput = seamImageInput(rc, providers, probes);
|
|
102
|
+
const rr = imageReroute(config, role, "primary", modelsPath, null, probes, providers);
|
|
103
|
+
const imageRerouteStr = rr
|
|
104
|
+
? `${rr.tier} (${seamImageLabel(rr.config.agent, rr.config.provider, rr.config.model)})`
|
|
105
|
+
: null;
|
|
106
|
+
return { imageInput, imageReroute: imageRerouteStr };
|
|
107
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** Installing the per-team `tumwater-notes` shim every backend's run finds on its PATH
|
|
3
|
+
* (plans/notes-command.md "Reaching it from a run"): run-agent calls `ensureNotesShim(root)`
|
|
4
|
+
* before building each invocation, and the backend prepends the returned dir to the run's
|
|
5
|
+
* PATH (agentBaseEnv's `binDir`, src/process/run-marker.ts). The shim is a two-line `sh`
|
|
6
|
+
* script that execs the compiled notes CLI, so a model's shell — pi's, Claude Code's or
|
|
7
|
+
* OpenCode's — runs the same command on every backend.
|
|
8
|
+
*
|
|
9
|
+
* The `<dist>` in the exec path is the shared distDir (src/build/build-info.ts), derived from
|
|
10
|
+
* this module's own location so a staged build (.tumwater/build/<sha>) points its shim at its
|
|
11
|
+
* own copy rather than the installed package's. The script is written
|
|
12
|
+
* atomically and made executable, only when its content differs, so a redeploy re-points it
|
|
13
|
+
* without churning the file. */
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { distDir } from "../build/build-info.js";
|
|
17
|
+
import { writeTextAtomic } from "../files/files.js";
|
|
18
|
+
import { notesShimDir } from "../paths.js";
|
|
19
|
+
/** The shim's file name — the command name every run sees on its PATH. */
|
|
20
|
+
export const NOTES_SHIM_NAME = "tumwater-notes";
|
|
21
|
+
/** Install the `tumwater-notes` shim under `<root>/.tumwater/bin` and return that directory.
|
|
22
|
+
* The shim is written atomically with mode 0755, only when its content differs from what is
|
|
23
|
+
* already there (a redeploy that changes `<dist>` rewrites it). Never throws: a failure to
|
|
24
|
+
* write is the caller's to absorb — run-agent logs one warning and runs without the shim. */
|
|
25
|
+
export function ensureNotesShim(root) {
|
|
26
|
+
const dir = notesShimDir(root);
|
|
27
|
+
const target = path.join(dir, NOTES_SHIM_NAME);
|
|
28
|
+
const content = `#!/bin/sh\nexec "${process.execPath}" "${path.join(distDir(), "src", "notes-cli.js")}" "$@"\n`;
|
|
29
|
+
let needsWrite = true;
|
|
30
|
+
try {
|
|
31
|
+
needsWrite = fs.readFileSync(target, "utf8") !== content;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Absent or unreadable — write it.
|
|
35
|
+
}
|
|
36
|
+
if (needsWrite) {
|
|
37
|
+
writeTextAtomic(target, content);
|
|
38
|
+
}
|
|
39
|
+
fs.chmodSync(target, 0o755);
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** Building one OpenCode run's command line (plans/opencode-backend.md "The invocation") —
|
|
3
|
+
* pure argv construction plus the prompt-on-stdin pairing, with no subprocess, backend
|
|
4
|
+
* registration, or session database. Part 5/7 wires this into the adapter; this module turns
|
|
5
|
+
* its inputs (the resolved model, the catalog's variant, the session choice, the session
|
|
6
|
+
* name, and the configured passthrough args) into the invocation's argv and stdin.
|
|
7
|
+
*
|
|
8
|
+
* The environment is built separately (opencode-env.ts) so each concern has one module.
|
|
9
|
+
*
|
|
10
|
+
* `.args` passthrough: a passthrough arg equal to a flag the harness itself sets would be
|
|
11
|
+
* appended AFTER the harness's own copy and OpenCode's parser is last-wins, so it would
|
|
12
|
+
* silently override the resolved model, the session choice, or the isolation flags. Each is
|
|
13
|
+
* rejected by opencodeArgs with the offending arg named, matching the same rejection in
|
|
14
|
+
* claudeArgs and the reserved set from plans/opencode-backend.md "Config and routing". */
|
|
15
|
+
/** Flags the harness invocation sets. A passthrough arg equal to one of these is rejected by
|
|
16
|
+
* opencodeArgs with the offending arg named, so an operator's `backends.opencode.args` can
|
|
17
|
+
* never silently override the resolved model, the session choice, or the isolation flags. This
|
|
18
|
+
* is the reserved set from plans/opencode-backend.md "Config and routing"; part 3 adds
|
|
19
|
+
* `--title`, which tumwater now sets (finding 6). */
|
|
20
|
+
import { appendPassthroughArgs } from "../agent-args.js";
|
|
21
|
+
export const HARNESS_OPENCODE_FLAGS = new Set([
|
|
22
|
+
"run",
|
|
23
|
+
"--format",
|
|
24
|
+
"--model",
|
|
25
|
+
"-m",
|
|
26
|
+
"--session",
|
|
27
|
+
"-s",
|
|
28
|
+
"--continue",
|
|
29
|
+
"-c",
|
|
30
|
+
"--fork",
|
|
31
|
+
"--standalone",
|
|
32
|
+
"--server",
|
|
33
|
+
"--attach",
|
|
34
|
+
"--auto",
|
|
35
|
+
"--yolo",
|
|
36
|
+
"--dangerously-skip-permissions",
|
|
37
|
+
"--agent",
|
|
38
|
+
"--thinking",
|
|
39
|
+
"--dir",
|
|
40
|
+
"--title",
|
|
41
|
+
]);
|
|
42
|
+
/** Build one run's opencode argv and stdin. The prompt is never an argv element (OpenCode
|
|
43
|
+
* reads a non-TTY stdin to EOF, and the runner closes it). A model with no `/` names a model
|
|
44
|
+
* on OpenCode's own gateway and gets the `opencode/` provider; anything with a slash is the
|
|
45
|
+
* provider-qualified id and passes as is. Throws on a passthrough arg that repeats a harness
|
|
46
|
+
* flag, since OpenCode's last-wins parser would otherwise silently override what the harness
|
|
47
|
+
* asked for. */
|
|
48
|
+
/** The opencode-namespaced model id: a bare id (no `/`) names a model on OpenCode's own
|
|
49
|
+
* gateway and gets the `opencode/` provider; an id bearing a slash is provider-qualified and
|
|
50
|
+
* passes as-is. The single home of this routing rule — shared by the argv builder
|
|
51
|
+
* (opencodeArgs, here) and the catalog's cache key (opencode-catalog.ts's opencodeModelKey) —
|
|
52
|
+
* so the id a run is invoked with always matches the id a run is priced by. */
|
|
53
|
+
export function opencodeQualifiedModel(model) {
|
|
54
|
+
return model.includes("/") ? model : `opencode/${model}`;
|
|
55
|
+
}
|
|
56
|
+
export function opencodeArgs(opts) {
|
|
57
|
+
const model = opencodeQualifiedModel(opts.model) + (opts.variant !== undefined ? `#${opts.variant}` : "");
|
|
58
|
+
const args = ["run", "--standalone", "--format", "json", "--auto", "--model", model, "--session", opts.sessionId, "--title", opts.title];
|
|
59
|
+
appendPassthroughArgs(args, opts.args, HARNESS_OPENCODE_FLAGS, "opencodeArgs");
|
|
60
|
+
return { args, stdin: opts.prompt };
|
|
61
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** The opencode adapter for the agent seam (plans/opencode-backend.md "The invocation",
|
|
3
|
+
* "Sessions", §6). It shapes one run's argv/stdin/env, translates opencode's `run --format
|
|
4
|
+
* json` into the canonical stream, runs the authoritative session export after the process
|
|
5
|
+
* exits, and classifies the exit's stderr — everything backend-specific. src/agent/run-agent.ts
|
|
6
|
+
* owns the spawn and records this run in the session ledger; opencode mints its own session id
|
|
7
|
+
* (a `ses_…` id, or the resumed record's id), which `invocation` returns so the ledger can
|
|
8
|
+
* carry it.
|
|
9
|
+
*
|
|
10
|
+
* Sessions resume by id from the cwd the session started in, out of the run's shared database
|
|
11
|
+
* (opencodeDbPath). A continue takes the ledger's newest record of this run's kind and resumes
|
|
12
|
+
* only when it is opencode's, has this cwd, and `opencode session export <id> --standalone`
|
|
13
|
+
* actually finds it — `--session` silently creates a missing session, so the existence check is
|
|
14
|
+
* the only thing that stops a follow-up running against an empty session. Anything else mints a
|
|
15
|
+
* fresh id (design §4).
|
|
16
|
+
*
|
|
17
|
+
* Registration lives in src/agent/run-agent.ts beside the pi and claude adapters; a test selects
|
|
18
|
+
* opencode by setting `config.agent = "opencode"` on a run. A run carrying a notebook gets the
|
|
19
|
+
* notes path in its environment (the `tumwater-notes` command on PATH reads it), as the pi and
|
|
20
|
+
* claude adapters do. */
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import os from "node:os";
|
|
23
|
+
import path from "node:path";
|
|
24
|
+
import { loadConfigSafe } from "../../config/config.js";
|
|
25
|
+
import { resolvedAgentConfig } from "../../config/config-views.js";
|
|
26
|
+
import { agentBaseEnv, withNotesPath } from "../../process/run-marker.js";
|
|
27
|
+
import { resolveBackendBin } from "../../pi/pi-bin.js";
|
|
28
|
+
import { opencodeDbPath, teamRootOf } from "../../paths.js";
|
|
29
|
+
import { resumableSession } from "../session-ledger.js";
|
|
30
|
+
import { opencodeArgs } from "./opencode-args.js";
|
|
31
|
+
import { opencodeConfigContent, operatorMcpNames } from "./opencode-config.js";
|
|
32
|
+
import { outputLimits } from "../../pi-extension/bounded-output.js";
|
|
33
|
+
import { seamContextWindow } from "../../pi/pi-models.js";
|
|
34
|
+
import { opencodeModelKey, readOpencodeCache } from "./opencode-catalog.js";
|
|
35
|
+
import { opencodeEnv } from "./opencode-env.js";
|
|
36
|
+
import { mintOpencodeSessionId } from "./opencode-session-id.js";
|
|
37
|
+
import { opencodeExportLines, opencodeTranslator } from "./opencode-stream.js";
|
|
38
|
+
/** How long a post-run `opencode session export` may take before it gives up (design §6): the
|
|
39
|
+
* export measured 0.18 s live, so 10 s is a generous bound on a slow or wedged CLI. Used for
|
|
40
|
+
* both the resume-existence check and the reconciling export. */
|
|
41
|
+
const EXPORT_TIMEOUT_MS = 10_000;
|
|
42
|
+
/** The minimum OpenCode CLI version this harness accepts (plans/opencode-backend.md "Which
|
|
43
|
+
* OpenCode", part 7/7): 2.0.32, the version H1 verified live. V1 is maintenance-only and has
|
|
44
|
+
* the git-repo hang (issue 55037), so doctor fails an installed CLI below this and names
|
|
45
|
+
* `brew install opencode`. */
|
|
46
|
+
export const OPENCODE_MIN_VERSION = "2.0.32";
|
|
47
|
+
/** The repository's root (initial) commit hash, or undefined when git cannot answer (not a
|
|
48
|
+
* repo, or git missing). `git rev-list --max-parents=0 HEAD` is the same read OpenCode makes
|
|
49
|
+
* to key its shell spill by the tree's root commit. Read-only. */
|
|
50
|
+
function rootCommitOf(root) {
|
|
51
|
+
try {
|
|
52
|
+
const res = spawnSync("git", ["-C", root, "rev-list", "--max-parents=0", "HEAD"], {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
timeout: 5_000,
|
|
55
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
56
|
+
});
|
|
57
|
+
const line = res.stdout?.trim();
|
|
58
|
+
return line !== undefined && /^[0-9a-f]{40}$/i.test(line) ? line : undefined;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** The tree's shell spill dir OpenCode writes (finding 17, part 7/7):
|
|
65
|
+
* `~/.local/share/opencode/shell/<root commit>/`, keyed by the repository's root commit hash and
|
|
66
|
+
* OUTSIDE `.tumwater/`, so only a `session delete` or an explicit spill prune reaches it. Empty
|
|
67
|
+
* string when the root commit cannot be read — callers then skip spill work (doctor reports no
|
|
68
|
+
* spill dir; retention prunes no spill files). */
|
|
69
|
+
export function opencodeSpillDir(root) {
|
|
70
|
+
const commit = rootCommitOf(root);
|
|
71
|
+
if (commit === undefined)
|
|
72
|
+
return "";
|
|
73
|
+
return path.join(os.homedir(), ".local", "share", "opencode", "shell", commit);
|
|
74
|
+
}
|
|
75
|
+
/** The catalog cache key for a run's resolved model id (part 6d/7): a bare gateway id gets the
|
|
76
|
+
* `opencode/` provider prefix, a provider-qualified id passes as is — the same rule
|
|
77
|
+
* opencodeArgs uses to build the `--model` arg and opencode-catalog.ts uses to key the cache,
|
|
78
|
+
* so an invocation's lookup always matches the key a run is priced and billed by. */
|
|
79
|
+
function catalogModelKey(model) {
|
|
80
|
+
return opencodeModelKey(model);
|
|
81
|
+
}
|
|
82
|
+
/** Run `opencode session export <id> --standalone` (the same database the run used, bounded at
|
|
83
|
+
* 10 s) and return whether it reported success — the existence check that decides a resume.
|
|
84
|
+
* Nonzero exit, a timeout, or a spawn failure all read as "no such session", which is the safe
|
|
85
|
+
* direction: `--session` would otherwise silently create a missing session. */
|
|
86
|
+
function opencodeSessionExists(id, env, bin, cwd) {
|
|
87
|
+
try {
|
|
88
|
+
const res = spawnSync(bin, ["session", "export", id, "--standalone"], {
|
|
89
|
+
env,
|
|
90
|
+
cwd,
|
|
91
|
+
timeout: EXPORT_TIMEOUT_MS,
|
|
92
|
+
stdio: "ignore",
|
|
93
|
+
});
|
|
94
|
+
return res.status === 0;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Run the reconciling export after a successful run and return its two canonical lines
|
|
101
|
+
* (`run_cost`, `run_usage`) from the session's `info`. Anything that goes wrong — a spawn or
|
|
102
|
+
* timeout, a nonzero exit, unparseable or malformed output — returns `[]`, and the caller
|
|
103
|
+
* (afterRun) reports `opencode_usage_partial` so the run keeps its streamed numbers. */
|
|
104
|
+
function opencodeExport(result) {
|
|
105
|
+
let res;
|
|
106
|
+
try {
|
|
107
|
+
res = spawnSync(result.bin, ["session", "export", result.id, "--standalone"], {
|
|
108
|
+
env: result.env,
|
|
109
|
+
cwd: result.cwd,
|
|
110
|
+
timeout: EXPORT_TIMEOUT_MS,
|
|
111
|
+
encoding: "utf8",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
if (res.status !== 0 || !res.stdout)
|
|
118
|
+
return [];
|
|
119
|
+
let parsed;
|
|
120
|
+
try {
|
|
121
|
+
parsed = JSON.parse(res.stdout);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
return opencodeExportLines(parsed);
|
|
127
|
+
}
|
|
128
|
+
/** The session id this run uses: for a continue, the ledger's newest record of this kind when it
|
|
129
|
+
* is opencode's, has this cwd, and the export finds the session; otherwise a freshly minted id.
|
|
130
|
+
* Returns the id; the runner records it in the ledger. */
|
|
131
|
+
function resolveOpencodeSession(run, env) {
|
|
132
|
+
if (run.continueSession) {
|
|
133
|
+
const record = resumableSession(run.sessionDir, {
|
|
134
|
+
kind: run.kind,
|
|
135
|
+
agent: "opencode",
|
|
136
|
+
cwd: run.cwd,
|
|
137
|
+
});
|
|
138
|
+
if (record !== null && opencodeSessionExists(record.id, env, run.agentBin, run.cwd)) {
|
|
139
|
+
return { sessionId: record.id };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { sessionId: mintOpencodeSessionId() };
|
|
143
|
+
}
|
|
144
|
+
/** Classify opencode's stderr: on a nonzero exit, a free-tier refusal is a provider failure
|
|
145
|
+
* (moves the job to its fallback) and an unknown model or variant is a permanent config error.
|
|
146
|
+
* A run the harness killed has its own cause and is never classified here. The structured
|
|
147
|
+
* signals (the stream's `error` lines) are classified by the translator; this is the stderr
|
|
148
|
+
* half for a CLI that died before emitting one. */
|
|
149
|
+
function classifyOpencodeExit(info) {
|
|
150
|
+
if (info.killed)
|
|
151
|
+
return {};
|
|
152
|
+
if (info.code !== 0 && /FreeTierError/i.test(info.stderr)) {
|
|
153
|
+
return { transientBackend: true, backendKind: "server" };
|
|
154
|
+
}
|
|
155
|
+
if (info.code !== 0 && /Model unavailable|Variant unavailable/i.test(info.stderr)) {
|
|
156
|
+
return { configError: true };
|
|
157
|
+
}
|
|
158
|
+
return {};
|
|
159
|
+
}
|
|
160
|
+
/** The agent-backend seam's opencode adapter. */
|
|
161
|
+
export const opencodeBackend = {
|
|
162
|
+
id: "opencode",
|
|
163
|
+
label: "OpenCode",
|
|
164
|
+
defaultBin: "opencode",
|
|
165
|
+
binEnv: "TUMWATER_OPENCODE_BIN",
|
|
166
|
+
resolveBin(config) {
|
|
167
|
+
return resolveBackendBin(config, this);
|
|
168
|
+
},
|
|
169
|
+
invocation(run) {
|
|
170
|
+
// The shared database and injected config are per-root, built the same way for every run of
|
|
171
|
+
// this team so a resume's export check reads the same OPENCODE_DB the original run wrote.
|
|
172
|
+
const root = teamRootOf(run.sessionDir);
|
|
173
|
+
const dbPath = opencodeDbPath(root);
|
|
174
|
+
// Window-scaled tool-output caps (part 6f/7): OpenCode's `tool_output` default is about
|
|
175
|
+
// three times pi's tuned budgets, so every turn would re-send far more tool output than a
|
|
176
|
+
// pi run — critical on the 200K free model's window. Cap it at pi's `outputLimits(window)
|
|
177
|
+
// .bash` (16,000 at 128k and above, scaled down below), with the ~40 chars/line line cap
|
|
178
|
+
// pi's "12k chars ≈ 300 lines" read budget implies. An unknown window (no catalog entry)
|
|
179
|
+
// keeps the 128k values. The window is part 6e/7's `seamContextWindow` for this run's
|
|
180
|
+
// model.
|
|
181
|
+
const maxBytes = outputLimits(seamContextWindow({ agent: "opencode", model: run.config.model }, root)).bash;
|
|
182
|
+
const configContent = opencodeConfigContent({
|
|
183
|
+
cwd: run.cwd,
|
|
184
|
+
operatorMcpNames: operatorMcpNames(process.env, os.homedir()),
|
|
185
|
+
toolOutput: { maxLines: Math.round(maxBytes / 40), maxBytes },
|
|
186
|
+
});
|
|
187
|
+
// The notebook path rides beside the run marker, as the pi and claude adapters' envs do
|
|
188
|
+
// (withNotesPath); from part 2/4 the run saves its note through the `tumwater-notes` command
|
|
189
|
+
// on PATH, so OpenCode configures no tumwater MCP server for notes.
|
|
190
|
+
const base = withNotesPath(agentBaseEnv(run.runMarker, undefined, run.binDir), run.notesPath);
|
|
191
|
+
const env = opencodeEnv(base, { dbPath, configContent });
|
|
192
|
+
const { sessionId } = resolveOpencodeSession(run, env);
|
|
193
|
+
// The thinking variant (part 6d/7): ride the catalog's effort levels for the resolved
|
|
194
|
+
// model as a `#level` suffix, and drop a level the catalog does not list — an unlisted
|
|
195
|
+
// variant would otherwise be a permanent config error from OpenCode (finding 9).
|
|
196
|
+
const model = run.config.model ?? "";
|
|
197
|
+
const thinking = run.config.thinking;
|
|
198
|
+
const entry = readOpencodeCache(root).models[catalogModelKey(model)];
|
|
199
|
+
const variant = thinking !== undefined && thinking !== "" && entry?.efforts?.includes(thinking) ? thinking : undefined;
|
|
200
|
+
const { args, stdin } = opencodeArgs({
|
|
201
|
+
model,
|
|
202
|
+
...(variant !== undefined ? { variant } : {}),
|
|
203
|
+
sessionId,
|
|
204
|
+
title: run.sessionName,
|
|
205
|
+
args: resolvedAgentConfig(run.config, "opencode").args,
|
|
206
|
+
prompt: run.prompt,
|
|
207
|
+
});
|
|
208
|
+
return { args, stdin, env, sessionId };
|
|
209
|
+
},
|
|
210
|
+
translator() {
|
|
211
|
+
return opencodeTranslator();
|
|
212
|
+
},
|
|
213
|
+
imageInput(model) {
|
|
214
|
+
// Answer from the catalog cache (part 6d/7): a model reads images when its catalog entry
|
|
215
|
+
// says so; a model missing from the cache answers false — the safe direction, so a blind
|
|
216
|
+
// image read is never silently assumed readable. The cache is per-root, found by walking up
|
|
217
|
+
// from the process's working directory to its `.tumwater`, the same root the runs' shared
|
|
218
|
+
// database and cache live under.
|
|
219
|
+
if (model === undefined)
|
|
220
|
+
return false;
|
|
221
|
+
return readOpencodeCache(teamRootOf(process.cwd())).models[catalogModelKey(model)]?.images === true;
|
|
222
|
+
},
|
|
223
|
+
resumable(dir, opts) {
|
|
224
|
+
const record = resumableSession(dir, { kind: opts.kind, agent: "opencode", cwd: opts.cwd });
|
|
225
|
+
if (record === null)
|
|
226
|
+
return false;
|
|
227
|
+
const root = teamRootOf(dir);
|
|
228
|
+
const dbPath = opencodeDbPath(root);
|
|
229
|
+
const env = { ...process.env, OPENCODE_DB: dbPath };
|
|
230
|
+
// Resolve the binary exactly as the run's invocation does — same env → backends.opencode.bin
|
|
231
|
+
// → default precedence — so a configured custom binary's resume guard agrees with the
|
|
232
|
+
// `run.agentBin` the invocation runs (they must not disagree, or a follow-up is silently
|
|
233
|
+
// skipped). The team's config is loaded from the root `dir` lives under.
|
|
234
|
+
const { config } = loadConfigSafe(root);
|
|
235
|
+
const bin = config !== undefined ? opencodeBackend.resolveBin(config).bin : "opencode";
|
|
236
|
+
return opencodeSessionExists(record.id, env, bin, opts.cwd ?? path.dirname(dir));
|
|
237
|
+
},
|
|
238
|
+
afterRun(run, invocation, translator) {
|
|
239
|
+
if (invocation.sessionId === undefined)
|
|
240
|
+
return [];
|
|
241
|
+
const lines = opencodeExport({
|
|
242
|
+
id: invocation.sessionId,
|
|
243
|
+
env: invocation.env,
|
|
244
|
+
bin: run.agentBin,
|
|
245
|
+
cwd: run.cwd,
|
|
246
|
+
});
|
|
247
|
+
if (lines.length === 0) {
|
|
248
|
+
// The export is authoritative, so its loss is worth one warning: the run keeps its
|
|
249
|
+
// streamed numbers and the caller surfaces `opencode_usage_partial` in the event feed.
|
|
250
|
+
translator.noteOpencodeUsagePartial();
|
|
251
|
+
}
|
|
252
|
+
return lines;
|
|
253
|
+
},
|
|
254
|
+
classifyExit: classifyOpencodeExit,
|
|
255
|
+
};
|