tumwater 0.2.0 → 0.4.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 +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +55 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +6 -4
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +404 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +281 -55
- package/dist/src/backlog/backlog-cli-view.js +255 -0
- package/dist/src/backlog/backlog-cli.js +91 -335
- package/dist/src/backlog/backlog-commit.js +20 -13
- package/dist/src/backlog/backlog-eligibility.js +193 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +264 -0
- package/dist/src/backlog/backlog-release.js +134 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-guardrails.js +136 -0
- package/dist/src/baseline/main-red.js +153 -36
- package/dist/src/baseline/main-watch.js +232 -0
- package/dist/src/brief/brief-edit.js +12 -12
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +31 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +65 -3
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +181 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +183 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +291 -598
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +525 -0
- package/dist/src/cli/cli-start-summary.js +167 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +193 -237
- package/dist/src/cli/completion.js +169 -22
- package/dist/src/cli/config-commands.js +58 -26
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +258 -82
- package/dist/src/cli/log-commands.js +164 -85
- package/dist/src/cli/question-commands.js +211 -10
- package/dist/src/cli.js +173 -45
- package/dist/src/collections.js +56 -13
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +16 -1
- package/dist/src/config/config-fallbacks.js +94 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +169 -222
- package/dist/src/config/config-views.js +136 -54
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +164 -265
- package/dist/src/config/config.js +53 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +330 -0
- package/dist/src/doctor/doctor-backlog-plans.js +398 -0
- package/dist/src/doctor/doctor-backlog.js +249 -512
- package/dist/src/doctor/doctor-check-names.js +85 -0
- package/dist/src/doctor/doctor-checks.js +61 -593
- package/dist/src/doctor/doctor-claude-checks.js +142 -0
- package/dist/src/doctor/doctor-codex-checks.js +276 -0
- package/dist/src/doctor/doctor-contention-checks.js +506 -0
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +80 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +216 -537
- package/dist/src/doctor/doctor-notify.js +9 -5
- package/dist/src/doctor/doctor-opencode-checks.js +164 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +397 -0
- package/dist/src/doctor/doctor.js +126 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +21 -12
- package/dist/src/events/event-format.js +185 -89
- package/dist/src/events/event-labels.js +11 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +52 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +80 -26
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +65 -60
- package/dist/src/failure/failure-render.js +29 -21
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +35 -35
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +88 -225
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +101 -143
- package/dist/src/gates/maintenance-quota.js +37 -30
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +89 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +54 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +43 -19
- package/dist/src/gui/gui-backlog-endpoints.js +156 -0
- package/dist/src/gui/gui-command.js +26 -13
- package/dist/src/gui/gui-config-commands.js +168 -80
- package/dist/src/gui/gui-endpoint-commands.js +5 -4
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +235 -13
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +97 -0
- package/dist/src/gui/gui-server.js +26 -5
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +155 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +190 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +28 -17
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +58 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +45 -19
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +214 -0
- package/dist/src/merge/merge-queue-render.js +48 -11
- package/dist/src/merge/merge-queue.js +52 -15
- package/dist/src/merge/merge-requeue.js +103 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +27 -11
- package/dist/src/merge/merge-to-main.js +77 -26
- package/dist/src/merge/merge-top.js +83 -0
- package/dist/src/merge/merge-verify.js +75 -18
- package/dist/src/notes-cli.js +35 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +352 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +71 -370
- package/dist/src/orchestrator/orchestrator.js +129 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +245 -0
- package/dist/src/orchestrator/scheduling-skip.js +195 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +126 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +26 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +150 -217
- package/dist/src/prompt/notes-backlog.js +184 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +131 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +188 -124
- package/dist/src/report/report-fold.js +130 -54
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +136 -65
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +23 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +112 -1
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +239 -0
- package/dist/src/review/protected-edits.js +24 -10
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +180 -401
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +127 -12
- package/dist/src/roles/role-guidance.js +65 -20
- package/dist/src/roles/role-labels.js +7 -5
- package/dist/src/roles/role-render.js +108 -56
- package/dist/src/roles/role-view.js +95 -56
- package/dist/src/roles/roles.js +33 -33
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/run/qa-coverage.js +125 -0
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +175 -0
- package/dist/src/run/run-detail-render.js +91 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +73 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +122 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +170 -83
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +105 -57
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +178 -0
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +231 -0
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +216 -0
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +191 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +109 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +39 -18
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +236 -245
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +103 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +49 -17
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +43 -28
- package/dist/src/status/status-roles.js +50 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +7 -8
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +65 -18
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +60 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +78 -13
- package/dist/src/ui/badges.js +99 -38
- package/dist/src/ui/gui/gui-client-backlog.js +314 -0
- package/dist/src/ui/gui/gui-client-boot.js +89 -61
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +254 -0
- package/dist/src/ui/gui/gui-client-drawer.js +155 -289
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +437 -68
- package/dist/src/ui/gui/gui-client-labels.js +6 -3
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +60 -52
- package/dist/src/ui/gui/gui-client-pending.js +405 -24
- package/dist/src/ui/gui/gui-client-report-chart.js +492 -0
- package/dist/src/ui/gui/gui-client-report.js +105 -430
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +163 -5
- package/dist/src/ui/gui/gui-client-team.js +36 -465
- package/dist/src/ui/gui/gui-client.js +38 -26
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +10 -7
- package/dist/src/ui/gui/gui-styles.js +38 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +33 -32
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +364 -49
- package/dist/src/ui/status-render.js +109 -82
- package/dist/src/ui/team-alerts.js +728 -62
- package/dist/src/ui/tone.js +12 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +53 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +4 -12
- package/dist/src/ui/tui/tui-editor-backends.js +127 -0
- package/dist/src/ui/tui/tui-editor-brief.js +14 -36
- package/dist/src/ui/tui/tui-editor-budget.js +36 -39
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +25 -0
- package/dist/src/ui/tui/tui-editor-config.js +99 -0
- package/dist/src/ui/tui/tui-editor-entry.js +39 -64
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +46 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +117 -0
- package/dist/src/ui/tui/tui-editor-interval.js +29 -16
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +9 -29
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +95 -41
- package/dist/src/ui/tui/tui-editor-retire.js +5 -13
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +71 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +101 -34
- package/dist/src/ui/tui/tui-editor-slots.js +24 -0
- package/dist/src/ui/tui/tui-editors.js +44 -2
- package/dist/src/ui/tui/tui-frame.js +68 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +33 -9
- package/dist/src/ui/tui/tui-pane-activity.js +35 -19
- package/dist/src/ui/tui/tui-pane-backlog.js +198 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +168 -8
- package/dist/src/ui/tui/tui-pane-history.js +446 -82
- package/dist/src/ui/tui/tui-pane-pending.js +539 -15
- package/dist/src/ui/tui/tui-pane-queued.js +78 -57
- package/dist/src/ui/tui/tui-pane-report.js +54 -16
- package/dist/src/ui/tui/tui-pane-session.js +34 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +91 -28
- package/dist/src/ui/tui/tui-panes.js +153 -34
- package/dist/src/ui/tui/tui.js +18 -15
- package/dist/src/verdict/fix-claim.js +129 -33
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +77 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/leftover.js +0 -130
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/qa-coverage.js +0 -108
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/** codex's half of the write guard (src/agent/write-guard.ts holds the rule): a PreToolUse hook on
|
|
2
|
+
* `apply_patch`, installed by part 6/9, refuses an `apply_patch` that writes under the team root
|
|
3
|
+
* but outside the run's worktree. The entry point is write-guard-hook.ts beside this file.
|
|
4
|
+
*
|
|
5
|
+
* Codex's hook takes no argument naming the worktree, unlike claude's: codex trusts a hook by a
|
|
6
|
+
* hash of its whole definition, so a command constant per build needs one trust lookup, while a
|
|
7
|
+
* per-worktree argument would need one per slot (plans/codex-write-guard.md finding 3). The run's
|
|
8
|
+
* directory is the payload's `cwd`, which is codex's session cwd and cannot be moved by a shell
|
|
9
|
+
* `cd`, since every shell call is its own `zsh -lc`; with no `cwd` the hook returns null and lets
|
|
10
|
+
* the patch run. */
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { shellQuote, strayWriteRefusal } from "../write-guard.js";
|
|
13
|
+
import { patchWritePaths } from "./codex-patch-paths.js";
|
|
14
|
+
/** The tool a codex PreToolUse hook sees for file edits; anything else is not this hook's
|
|
15
|
+
* business. */
|
|
16
|
+
const APPLY_PATCH = "apply_patch";
|
|
17
|
+
/** The refusal for one codex PreToolUse payload (the hook's stdin, parsed), or null to let the
|
|
18
|
+
* patch run. It acts only when `tool_name` is `"apply_patch"` and `tool_input.command` is a
|
|
19
|
+
* string; each path the patch writes is refused with `strayWriteRefusal(path, cwd)` in order if it
|
|
20
|
+
* is under the team root but outside the worktree, a relative path resolving from `cwd` as
|
|
21
|
+
* codex resolves it. A payload naming no `cwd`, or too broken to read, returns null and the patch
|
|
22
|
+
* runs. */
|
|
23
|
+
export function codexWriteRefusal(payload) {
|
|
24
|
+
if (typeof payload !== "object" || payload === null)
|
|
25
|
+
return null;
|
|
26
|
+
const { tool_name: tool, tool_input: input, cwd } = payload;
|
|
27
|
+
if (tool !== APPLY_PATCH)
|
|
28
|
+
return null;
|
|
29
|
+
if (typeof cwd !== "string" || cwd === "")
|
|
30
|
+
return null;
|
|
31
|
+
if (typeof input !== "object" || input === null)
|
|
32
|
+
return null;
|
|
33
|
+
const command = input.command;
|
|
34
|
+
if (typeof command !== "string")
|
|
35
|
+
return null;
|
|
36
|
+
for (const target of patchWritePaths(command)) {
|
|
37
|
+
const refusal = strayWriteRefusal(target, cwd);
|
|
38
|
+
if (refusal !== null)
|
|
39
|
+
return refusal;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
/** Seconds codex waits for the hook before running the tool anyway (the same timeout claude's
|
|
44
|
+
* hook gets). */
|
|
45
|
+
const HOOK_TIMEOUT_SECONDS = 30;
|
|
46
|
+
/** The matcher the PreToolUse hook is registered with; codex sends only `apply_patch` here. */
|
|
47
|
+
const HOOK_MATCHER = "^apply_patch$";
|
|
48
|
+
/** `s` as a TOML basic string, escaping `\`, `"` and every control character (U+0000–U+001F
|
|
49
|
+
* and DEL), so a nodeBin or hook path with any of them still round-trips through a `-c` value. */
|
|
50
|
+
export function tomlString(s) {
|
|
51
|
+
let out = '"';
|
|
52
|
+
for (const ch of s) {
|
|
53
|
+
const code = ch.codePointAt(0);
|
|
54
|
+
if (ch === "\\")
|
|
55
|
+
out += "\\\\";
|
|
56
|
+
else if (ch === '"')
|
|
57
|
+
out += '\\"';
|
|
58
|
+
else if (code < 0x20 || code === 0x7f)
|
|
59
|
+
out += `\\u${code.toString(16).padStart(4, "0")}`;
|
|
60
|
+
else
|
|
61
|
+
out += ch;
|
|
62
|
+
}
|
|
63
|
+
return out + '"';
|
|
64
|
+
}
|
|
65
|
+
/** The codex write guard's definition: the PreToolUse hook on `apply_patch` that runs the
|
|
66
|
+
* compiled write-guard-hook.js with this process's node, falling back to `node` on PATH when
|
|
67
|
+
* that binary has moved (as claude's and the notes shim do). The hook path resolves from this
|
|
68
|
+
* module, so a staged build (.tumwater/build/<sha>) runs its own copy and gets its own hash. */
|
|
69
|
+
export function codexWriteGuardDefinition(nodeBin = process.execPath) {
|
|
70
|
+
const hook = fileURLToPath(new URL("./write-guard-hook.js", import.meta.url));
|
|
71
|
+
const command = `n=${shellQuote(nodeBin)}; [ -x "$n" ] || n=node; exec "$n" ${shellQuote(hook)}`;
|
|
72
|
+
return { matcher: HOOK_MATCHER, command, timeout: HOOK_TIMEOUT_SECONDS };
|
|
73
|
+
}
|
|
74
|
+
/** The `-c` value that installs `definition` as the session's PreToolUse hook, in codex's
|
|
75
|
+
* `hooks.PreToolUse=[{matcher=…,hooks=[{type="command",command=…,timeout=…}]}]` inline-table
|
|
76
|
+
* shape (finding 1). */
|
|
77
|
+
export function codexHookArg(definition) {
|
|
78
|
+
return `hooks.PreToolUse=[{matcher=${tomlString(definition.matcher)},hooks=[{type="command",command=${tomlString(definition.command)},timeout=${definition.timeout}}]}]`;
|
|
79
|
+
}
|
|
80
|
+
/** The `-c` value that trusts the hook identified by `trust.key` at `trust.hash`, in the
|
|
81
|
+
* inline-table spelling `hooks.state={<key>={trusted_hash=<hash>}}`. The dotted
|
|
82
|
+
* `hooks.state."<key>".trusted_hash=` spelling is ignored by codex because the key contains
|
|
83
|
+
* dots (finding 2). */
|
|
84
|
+
export function codexHookTrustArg(trust) {
|
|
85
|
+
return `hooks.state={${tomlString(trust.key)}={trusted_hash=${tomlString(trust.hash)}}}`;
|
|
86
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The PreToolUse hook codex runs before each `apply_patch` in a team run (part 6/9 installs it
|
|
2
|
+
* through `-c`): `node write-guard-hook.js` with codex's hook payload on stdin. To block the patch
|
|
3
|
+
* it prints a `deny` decision with the refusal as its reason, in the shape finding 5 showed codex
|
|
4
|
+
* accepts: the model reads only the reason, so a denial by exit code would not carry it. It prints
|
|
5
|
+
* nothing to let the patch run, and exits 0 either way. Any failure of its own (bad JSON, a throw)
|
|
6
|
+
* prints nothing, so a broken hook lets the patch run and the stray-write warning in runner-run.ts
|
|
7
|
+
* still sees a stray write. The stdin read and the deny-decision printing are runGuardHook's
|
|
8
|
+
* (src/agent/write-guard.ts), shared with the claude PreToolUse hook; only the refusal computation
|
|
9
|
+
* differs. */
|
|
10
|
+
import { runGuardHook } from "../write-guard.js";
|
|
11
|
+
import { codexWriteRefusal } from "./codex-write-guard.js";
|
|
12
|
+
runGuardHook((payload) => codexWriteRefusal(payload));
|
|
@@ -9,34 +9,37 @@ import { BORROW_ORDER, resolveTierFallbacks } from "../config/config-fallbacks.j
|
|
|
9
9
|
import { seamAgent } from "../config/model-selector.js";
|
|
10
10
|
import { modelLabel } from "./backend-traits.js";
|
|
11
11
|
import { piModelImageInput, piPairFree, piModelsPath, readPiProviders } from "../pi/pi-models.js";
|
|
12
|
-
import {
|
|
12
|
+
import { piListedImageInput } from "../pi/pi-listed-models.js";
|
|
13
|
+
import { backendById } from "./agent-backend.js";
|
|
13
14
|
import { imageProbeEntryFresh, imageProbeKey } from "../doctor/image-probe.js";
|
|
14
15
|
/** One seam's image-input capability (plans/image-input.md "Capability"): `true` when the
|
|
15
16
|
* seam's backend declares image support, `false` when it is text-only, `null` when it cannot
|
|
16
|
-
* be answered from what the team knows (
|
|
17
|
-
*
|
|
17
|
+
* be answered from what the team knows (a pair neither models.json nor the pi listing names, a
|
|
18
|
+
* backend with no hook and no catalog entry).
|
|
18
19
|
*
|
|
19
20
|
* The backend bookends: a backend with an `imageInput` hook (claude → true) owns the answer;
|
|
20
21
|
* 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)
|
|
22
|
-
*
|
|
22
|
+
* (pi's own default for a custom model); a pair absent from models.json answers what
|
|
23
|
+
* `pi --list-models` last said (src/pi/pi-listed-models.ts), else null.
|
|
24
|
+
* `probes` is the doctor's live-probe cache (part 2/5): a claimed pi seam whose probe FAILED
|
|
25
|
+
* reads false — a claim is only as good as the verified
|
|
23
26
|
* answer — while a cached pass (or an unprobed claim) stands on the catalog. claude seams are
|
|
24
27
|
* never probed, so the hook answer is never overridden. */
|
|
25
|
-
export function seamImageInput(seam, providers, probes) {
|
|
26
|
-
const
|
|
27
|
-
const backend =
|
|
28
|
+
export function seamImageInput(seam, providers, probes, listed = null) {
|
|
29
|
+
const backendId = seamAgent(seam);
|
|
30
|
+
const backend = backendById(backendId);
|
|
28
31
|
// A backend with an imageInput hook owns the answer outright (claude: true for every model).
|
|
29
32
|
if (backend?.imageInput !== undefined)
|
|
30
33
|
return backend.imageInput(seam.model);
|
|
31
34
|
// An unknown backend without a hook has no catalog to consult — unknown, never assume.
|
|
32
|
-
if (
|
|
35
|
+
if (backendId !== "pi")
|
|
33
36
|
return null;
|
|
34
|
-
const base = piModelImageInput(providers, seam.provider, seam.model);
|
|
37
|
+
const base = piModelImageInput(providers, seam.provider, seam.model) ?? piListedImageInput(listed, seam.provider, seam.model);
|
|
35
38
|
if (base !== true)
|
|
36
39
|
return base;
|
|
37
40
|
// 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
|
|
39
|
-
const entry = probes?.[imageProbeKey(
|
|
41
|
+
// probed, so the probe key carries the pi backend.
|
|
42
|
+
const entry = probes?.[imageProbeKey(backendId, seam.provider, seam.model)];
|
|
40
43
|
if (entry?.verdict === "fail" && imageProbeEntryFresh(entry))
|
|
41
44
|
return false;
|
|
42
45
|
return true;
|
|
@@ -48,13 +51,13 @@ export function seamImageInput(seam, providers, probes) {
|
|
|
48
51
|
* and takes the first tier whose model reads images. `regime` picks the pair set (see
|
|
49
52
|
* RerouteRegime); `modelsPath` and `account` are injectable so tests need no real pi install
|
|
50
53
|
* or account. */
|
|
51
|
-
export function imageReroute(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, probes, providers = null) {
|
|
54
|
+
export function imageReroute(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, probes, providers = null, listed = null) {
|
|
52
55
|
const tier = roleSeamTier(config, role);
|
|
53
56
|
const candidates = BORROW_ORDER[tier].filter((t) => t !== tier);
|
|
54
57
|
if (candidates.length === 0)
|
|
55
58
|
return null;
|
|
56
59
|
const providerMap = providers ?? readPiProviders(modelsPath);
|
|
57
|
-
const capable = (rc) => seamImageInput(rc, providerMap, probes) === true;
|
|
60
|
+
const capable = (rc) => seamImageInput(rc, providerMap, probes, listed) === true;
|
|
58
61
|
if (regime === "primary") {
|
|
59
62
|
for (const t of candidates) {
|
|
60
63
|
const rc = withModelOverrides(config, t, {});
|
|
@@ -65,7 +68,9 @@ export function imageReroute(config, role, regime = "primary", modelsPath = piMo
|
|
|
65
68
|
}
|
|
66
69
|
// Fallback regime: only the tiers' free fallback pairs qualify (resolveTierFallbacks over the
|
|
67
70
|
// price check), never a priced primary model — a reroute must not spend past the cap.
|
|
68
|
-
const resolved = resolveTierFallbacks(config, (p) =>
|
|
71
|
+
const resolved = resolveTierFallbacks(config, (p) =>
|
|
72
|
+
// The free-ness lookup reads the one claude account held here (part 2a/9).
|
|
73
|
+
piPairFree(providerMap, p, (b) => (b === "claude" ? account : null)));
|
|
69
74
|
for (const t of candidates) {
|
|
70
75
|
const pair = resolved[t].pair;
|
|
71
76
|
if (!pair)
|
|
@@ -81,25 +86,25 @@ export function imageReroute(config, role, regime = "primary", modelsPath = piMo
|
|
|
81
86
|
return null;
|
|
82
87
|
}
|
|
83
88
|
/** 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;
|
|
89
|
+
export function imageRerouteTarget(config, role, regime = "primary", modelsPath = piModelsPath(), account = null, probes, providers = null, listed = null) {
|
|
90
|
+
return imageReroute(config, role, regime, modelsPath, account, probes, providers, listed)?.config ?? null;
|
|
86
91
|
}
|
|
87
92
|
/** A seam's model as one readable token, the same label the doctor's `image input` check uses
|
|
88
93
|
* (`claude/<id>` for a claude seam, `provider/model` for pi). The one home of the label, so
|
|
89
94
|
* the surface strings (the images alert, the role page, the status rows) and the doctor report
|
|
90
95
|
* cannot drift, and delegates to backend-traits' modelLabel. */
|
|
91
|
-
export function seamImageLabel(
|
|
92
|
-
return modelLabel(
|
|
96
|
+
export function seamImageLabel(backend, provider, model) {
|
|
97
|
+
return modelLabel(backend, provider, model);
|
|
93
98
|
}
|
|
94
|
-
/** Compute RoleImageInfo for one
|
|
99
|
+
/** Compute RoleImageInfo for one role. `modelsPath` and `probes` are injectable; the providers
|
|
95
100
|
* catalog is read (stat-cached) here and also handed to imageReroute so a poll that asks many
|
|
96
|
-
*
|
|
101
|
+
* roles still parses the catalog once. All reads mirror the doctor's `image input` check, so
|
|
97
102
|
* the surface and the check agree about capability. */
|
|
98
|
-
export function roleImageInfo(config, role, modelsPath = piModelsPath(), probes) {
|
|
103
|
+
export function roleImageInfo(config, role, modelsPath = piModelsPath(), probes, listed = null) {
|
|
99
104
|
const providers = readPiProviders(modelsPath);
|
|
100
105
|
const rc = configForRole(config, role);
|
|
101
|
-
const imageInput = seamImageInput(rc, providers, probes);
|
|
102
|
-
const rr = imageReroute(config, role, "primary", modelsPath, null, probes, providers);
|
|
106
|
+
const imageInput = seamImageInput(rc, providers, probes, listed);
|
|
107
|
+
const rr = imageReroute(config, role, "primary", modelsPath, null, probes, providers, listed);
|
|
103
108
|
const imageRerouteStr = rr
|
|
104
109
|
? `${rr.tier} (${seamImageLabel(rr.config.agent, rr.config.provider, rr.config.model)})`
|
|
105
110
|
: null;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** Installing the per-team `tumwater-notes` shim every backend's run finds on its PATH
|
|
3
2
|
* (plans/notes-command.md "Reaching it from a run"): run-agent calls `ensureNotesShim(root)`
|
|
4
3
|
* before building each invocation, and the backend prepends the returned dir to the run's
|
|
5
|
-
* PATH (
|
|
4
|
+
* PATH (backendBaseEnv's `binDir`, src/process/run-marker.ts). The shim is a two-line `sh`
|
|
6
5
|
* script that execs the compiled notes CLI, so a model's shell — pi's, Claude Code's or
|
|
7
6
|
* OpenCode's — runs the same command on every backend.
|
|
8
7
|
*
|
|
@@ -14,7 +13,7 @@
|
|
|
14
13
|
import fs from "node:fs";
|
|
15
14
|
import path from "node:path";
|
|
16
15
|
import { distDir } from "../build/build-info.js";
|
|
17
|
-
import { writeTextAtomic } from "../files/files.js";
|
|
16
|
+
import { readTextOrNull, writeTextAtomic } from "../files/files.js";
|
|
18
17
|
import { notesShimDir } from "../paths.js";
|
|
19
18
|
/** The shim's file name — the command name every run sees on its PATH. */
|
|
20
19
|
export const NOTES_SHIM_NAME = "tumwater-notes";
|
|
@@ -22,17 +21,17 @@ export const NOTES_SHIM_NAME = "tumwater-notes";
|
|
|
22
21
|
* The shim is written atomically with mode 0755, only when its content differs from what is
|
|
23
22
|
* already there (a redeploy that changes `<dist>` rewrites it). Never throws: a failure to
|
|
24
23
|
* write is the caller's to absorb — run-agent logs one warning and runs without the shim. */
|
|
25
|
-
export function ensureNotesShim(root) {
|
|
24
|
+
export function ensureNotesShim(root, execPath = process.execPath) {
|
|
26
25
|
const dir = notesShimDir(root);
|
|
27
26
|
const target = path.join(dir, NOTES_SHIM_NAME);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
27
|
+
// Prefer the recorded node binary, falling back to `node` on PATH when it is gone — a
|
|
28
|
+
// Homebrew node upgrade under the running team deletes the old keg while the team keeps
|
|
29
|
+
// running on it, so `process.execPath` names a file that no longer exists (BUGS.md
|
|
30
|
+
// 2026-10-21). The `[ -x "$n" ]` check answers at run time, so a shim written long ago
|
|
31
|
+
// still survives an upgrade, and the running orchestrator's repeated rewrites stop
|
|
32
|
+
// restoring the broken path.
|
|
33
|
+
const content = `#!/bin/sh\nn="${execPath}"; [ -x "$n" ] || n=node; exec "$n" "${path.join(distDir(), "src", "notes-cli.js")}" "$@"\n`;
|
|
34
|
+
const needsWrite = readTextOrNull(target) !== content; // absent or unreadable — write it
|
|
36
35
|
if (needsWrite) {
|
|
37
36
|
writeTextAtomic(target, content);
|
|
38
37
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** Building one OpenCode run's command line (plans/opencode-backend.md "The invocation") —
|
|
3
2
|
* pure argv construction plus the prompt-on-stdin pairing, with no subprocess, backend
|
|
4
3
|
* registration, or session database. Part 5/7 wires this into the adapter; this module turns
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The opencode adapter for the agent seam (plans/opencode-backend.md "The invocation",
|
|
3
2
|
* "Sessions", §6). It shapes one run's argv/stdin/env, translates opencode's `run --format
|
|
4
3
|
* json` into the canonical stream, runs the authoritative session export after the process
|
|
@@ -15,16 +14,17 @@
|
|
|
15
14
|
* fresh id (design §4).
|
|
16
15
|
*
|
|
17
16
|
* 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
|
|
17
|
+
* opencode by setting `config.agent = "opencode"` on a run. A run carrying a note gets the
|
|
19
18
|
* notes path in its environment (the `tumwater-notes` command on PATH reads it), as the pi and
|
|
20
19
|
* claude adapters do. */
|
|
21
20
|
import { spawnSync } from "node:child_process";
|
|
22
21
|
import os from "node:os";
|
|
23
22
|
import path from "node:path";
|
|
23
|
+
import { ensureParentDir } from "../../files/files.js";
|
|
24
24
|
import { loadConfigSafe } from "../../config/config.js";
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
25
|
+
import { resolvedBackendConfig } from "../../config/config-views.js";
|
|
26
|
+
import { backendRunEnv } from "../../process/run-marker.js";
|
|
27
|
+
import { resolveBinForBackend } from "../../pi/pi-bin.js";
|
|
28
28
|
import { opencodeDbPath, teamRootOf } from "../../paths.js";
|
|
29
29
|
import { resumableSession } from "../session-ledger.js";
|
|
30
30
|
import { opencodeArgs } from "./opencode-args.js";
|
|
@@ -164,13 +164,15 @@ export const opencodeBackend = {
|
|
|
164
164
|
defaultBin: "opencode",
|
|
165
165
|
binEnv: "TUMWATER_OPENCODE_BIN",
|
|
166
166
|
resolveBin(config) {
|
|
167
|
-
return
|
|
167
|
+
return resolveBinForBackend(config, this);
|
|
168
168
|
},
|
|
169
169
|
invocation(run) {
|
|
170
170
|
// The shared database and injected config are per-root, built the same way for every run of
|
|
171
171
|
// this team so a resume's export check reads the same OPENCODE_DB the original run wrote.
|
|
172
172
|
const root = teamRootOf(run.sessionDir);
|
|
173
173
|
const dbPath = opencodeDbPath(root);
|
|
174
|
+
// opencode will not create the database's parent directory and its server dies without it.
|
|
175
|
+
ensureParentDir(dbPath);
|
|
174
176
|
// Window-scaled tool-output caps (part 6f/7): OpenCode's `tool_output` default is about
|
|
175
177
|
// three times pi's tuned budgets, so every turn would re-send far more tool output than a
|
|
176
178
|
// pi run — critical on the 200K free model's window. Cap it at pi's `outputLimits(window)
|
|
@@ -184,10 +186,10 @@ export const opencodeBackend = {
|
|
|
184
186
|
operatorMcpNames: operatorMcpNames(process.env, os.homedir()),
|
|
185
187
|
toolOutput: { maxLines: Math.round(maxBytes / 40), maxBytes },
|
|
186
188
|
});
|
|
187
|
-
// The
|
|
189
|
+
// The notes path rides beside the run marker, as the pi and claude adapters' envs do
|
|
188
190
|
// (withNotesPath); from part 2/4 the run saves its note through the `tumwater-notes` command
|
|
189
191
|
// on PATH, so OpenCode configures no tumwater MCP server for notes.
|
|
190
|
-
const base =
|
|
192
|
+
const base = backendRunEnv(run);
|
|
191
193
|
const env = opencodeEnv(base, { dbPath, configContent });
|
|
192
194
|
const { sessionId } = resolveOpencodeSession(run, env);
|
|
193
195
|
// The thinking variant (part 6d/7): ride the catalog's effort levels for the resolved
|
|
@@ -202,7 +204,7 @@ export const opencodeBackend = {
|
|
|
202
204
|
...(variant !== undefined ? { variant } : {}),
|
|
203
205
|
sessionId,
|
|
204
206
|
title: run.sessionName,
|
|
205
|
-
args:
|
|
207
|
+
args: resolvedBackendConfig(run.config, "opencode").args,
|
|
206
208
|
prompt: run.prompt,
|
|
207
209
|
});
|
|
208
210
|
return { args, stdin, env, sessionId };
|
|
@@ -226,6 +228,7 @@ export const opencodeBackend = {
|
|
|
226
228
|
return false;
|
|
227
229
|
const root = teamRootOf(dir);
|
|
228
230
|
const dbPath = opencodeDbPath(root);
|
|
231
|
+
ensureParentDir(dbPath);
|
|
229
232
|
const env = { ...process.env, OPENCODE_DB: dbPath };
|
|
230
233
|
// Resolve the binary exactly as the run's invocation does — same env → backends.opencode.bin
|
|
231
234
|
// → default precedence — so a configured custom binary's resume guard agrees with the
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The data half of OpenCode free-ness (plans/opencode-backend.md §7; part 6a/7): a bounded
|
|
3
2
|
* fetch of models.dev's `api.json` — the catalog OpenCode itself caches (finding 10) — a parse
|
|
4
3
|
* that keeps only the providers the config routes through OpenCode, a cache at
|
|
@@ -8,7 +7,10 @@
|
|
|
8
7
|
* part 7/7 (a startup-cache seed and doctor), so this module's only callers today are its own
|
|
9
8
|
* tests. */
|
|
10
9
|
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { isJsonObject, validEntries } from "../../files/json-object.js";
|
|
11
11
|
import { readJsonFile, writeJsonAtomic } from "../../files/json-files.js";
|
|
12
|
+
import { errorMessage } from "../../text/text.js";
|
|
13
|
+
import { DAY_MS } from "../../text/datetime.js";
|
|
12
14
|
import { opencodeModelsPath } from "../../paths.js";
|
|
13
15
|
import { logEventBestEffort } from "../../events/events.js";
|
|
14
16
|
import { opencodeQualifiedModel } from "./opencode-args.js";
|
|
@@ -18,7 +20,7 @@ const CATALOG_URL = "https://models.dev/api.json";
|
|
|
18
20
|
* most once a day, so a slow or wedged network must not hold the team up. */
|
|
19
21
|
const CATALOG_FETCH_TIMEOUT_MS = 30_000;
|
|
20
22
|
/** How long a fetched cache is trusted before it is refreshed. */
|
|
21
|
-
const CATALOG_CACHE_TTL_MS =
|
|
23
|
+
const CATALOG_CACHE_TTL_MS = DAY_MS;
|
|
22
24
|
const defaultFetch = async (url, timeoutMs) => {
|
|
23
25
|
const res = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
24
26
|
if (!res.ok)
|
|
@@ -37,26 +39,25 @@ export async function fetchOpencodeCatalog(fetchFn = defaultFetch) {
|
|
|
37
39
|
* the provider map at the top level; OpenCode's cached copy wraps that same map under a `body`
|
|
38
40
|
* key (the 2.0.32 capture in test/fixtures), so both shapes are accepted. Each kept model is
|
|
39
41
|
* keyed `<provider>/<model>`. A model never present yields no entry, so an unknown model is
|
|
40
|
-
* automatically "not free" to opencodeModelFree.
|
|
42
|
+
* automatically "not free" to opencodeModelFree. The body is untrusted network data: a malformed
|
|
43
|
+
* provider or model is skipped, never thrown on. */
|
|
41
44
|
export function parseOpencodeCatalog(body, providers) {
|
|
42
45
|
const out = {};
|
|
43
|
-
const root = body;
|
|
46
|
+
const root = isJsonObject(body) ? body : {};
|
|
44
47
|
// models.dev's api.json IS the provider map; OpenCode's cached copy wraps that map under `body`.
|
|
45
|
-
const wrapped = root
|
|
46
|
-
const catalog =
|
|
47
|
-
|
|
48
|
-
: (body ?? {});
|
|
49
|
-
for (const [provider, providerRaw] of Object.entries(catalog)) {
|
|
48
|
+
const wrapped = root.body;
|
|
49
|
+
const catalog = isJsonObject(wrapped) ? wrapped : root;
|
|
50
|
+
for (const [provider, providerInfo] of Object.entries(catalog)) {
|
|
50
51
|
if (provider !== "opencode" && !providers.includes(provider))
|
|
51
52
|
continue;
|
|
52
|
-
|
|
53
|
-
if (providerInfo?.models === undefined)
|
|
53
|
+
if (!isJsonObject(providerInfo) || !isJsonObject(providerInfo.models))
|
|
54
54
|
continue;
|
|
55
|
-
for (const [id,
|
|
56
|
-
|
|
55
|
+
for (const [id, m] of Object.entries(providerInfo.models)) {
|
|
56
|
+
if (!isJsonObject(m))
|
|
57
|
+
continue;
|
|
57
58
|
// Only a present cost block marks a model free: a missing cost block is unknown price data,
|
|
58
59
|
// and treating it as $0 is the unsafe direction for a budget gate.
|
|
59
|
-
const cost = m
|
|
60
|
+
const cost = isJsonObject(m.cost) ? m.cost : undefined;
|
|
60
61
|
const input = cost?.input ?? 0;
|
|
61
62
|
const output = cost?.output ?? 0;
|
|
62
63
|
const model = { free: cost !== undefined && input === 0 && output === 0 };
|
|
@@ -64,16 +65,19 @@ export function parseOpencodeCatalog(body, providers) {
|
|
|
64
65
|
model.inputPerM = input;
|
|
65
66
|
model.outputPerM = output;
|
|
66
67
|
}
|
|
67
|
-
if (m
|
|
68
|
+
if (isJsonObject(m.limit) && m.limit.context !== undefined)
|
|
68
69
|
model.context = m.limit.context;
|
|
69
|
-
if (m
|
|
70
|
+
if (isJsonObject(m.modalities) && Array.isArray(m.modalities.input)) {
|
|
70
71
|
model.images = m.modalities.input.includes("image");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(m.reasoning_options)) {
|
|
74
|
+
const efforts = m.reasoning_options
|
|
75
|
+
.flatMap((r) => isJsonObject(r) && r.type === "effort" && Array.isArray(r.values) ? r.values : []);
|
|
76
|
+
if (efforts.length > 0)
|
|
77
|
+
model.efforts = efforts;
|
|
78
|
+
}
|
|
79
|
+
if (isOpencodeCatalogModel(model))
|
|
80
|
+
out[`${provider}/${id}`] = model;
|
|
77
81
|
}
|
|
78
82
|
}
|
|
79
83
|
return out;
|
|
@@ -113,20 +117,32 @@ export function observeOpencodeRun(root, run, opts) {
|
|
|
113
117
|
log({ source: opts.source, type: "opencode_free_model_billed", model: run.model, costUsd: run.costUsd });
|
|
114
118
|
return cache;
|
|
115
119
|
}
|
|
120
|
+
function isOpencodeCatalogModel(value) {
|
|
121
|
+
if (!isJsonObject(value) || typeof value.free !== "boolean")
|
|
122
|
+
return false;
|
|
123
|
+
const numbers = [value.inputPerM, value.outputPerM, value.context, value.billedAt];
|
|
124
|
+
if (!numbers.every((n) => n === undefined || (typeof n === "number" && Number.isFinite(n))))
|
|
125
|
+
return false;
|
|
126
|
+
if (value.images !== undefined && typeof value.images !== "boolean")
|
|
127
|
+
return false;
|
|
128
|
+
return (value.efforts === undefined ||
|
|
129
|
+
(Array.isArray(value.efforts) && value.efforts.every((e) => typeof e === "string")));
|
|
130
|
+
}
|
|
116
131
|
/** Read the cached catalog, or an empty one (fetchedAt 0, no models) when the file is missing
|
|
117
132
|
* or malformed — the harness's no-data policy, so a torn write reads as "needs a refresh".
|
|
118
133
|
* Exported so the opencode backend's adapter (part 6d/7) and doctor can read the same cache
|
|
119
134
|
* without triggering a network fetch. The synchronous read the sync poll surfaces
|
|
120
135
|
* (pollBudgetGate, collectBudget) use to thread the catalog-cache into their free-ness reads
|
|
121
136
|
* (part 6b/7); the async refresh (which runs once per process at the startup gate) is what
|
|
122
|
-
* fills the file in.
|
|
137
|
+
* fills the file in. Entries that are not well-formed catalog models are dropped, so a foreign
|
|
138
|
+
* entry reads as a model missing from the cache — not free. */
|
|
123
139
|
export function readOpencodeCache(root) {
|
|
124
140
|
const existing = readJsonFile(opencodeModelsPath(root));
|
|
125
|
-
if (existing !== null && typeof existing.fetchedAt === "number" &&
|
|
141
|
+
if (existing !== null && typeof existing.fetchedAt === "number" && isJsonObject(existing.models)) {
|
|
126
142
|
return {
|
|
127
143
|
fetchedAt: existing.fetchedAt,
|
|
128
144
|
cliVersion: typeof existing.cliVersion === "string" ? existing.cliVersion : undefined,
|
|
129
|
-
models: existing.models,
|
|
145
|
+
models: validEntries(existing.models, isOpencodeCatalogModel),
|
|
130
146
|
};
|
|
131
147
|
}
|
|
132
148
|
return { fetchedAt: 0, models: {} };
|
|
@@ -150,16 +166,21 @@ function readCliVersion(bin = "opencode") {
|
|
|
150
166
|
return undefined;
|
|
151
167
|
}
|
|
152
168
|
}
|
|
169
|
+
/** Whether the cache is missing or older than CATALOG_CACHE_TTL_MS at `now`. The age rule
|
|
170
|
+
* refreshOpencodeCatalog and the poll-driven refresh (newOpencodeCatalogRefresh) share. */
|
|
171
|
+
export function opencodeCatalogStale(cache, now) {
|
|
172
|
+
return cache.fetchedAt === 0 || now - cache.fetchedAt >= CATALOG_CACHE_TTL_MS;
|
|
173
|
+
}
|
|
153
174
|
/** Return a cache fresh enough to price by, fetching and persisting when the current one is
|
|
154
175
|
* missing, stale (older than 24 h) or fetched by a different `opencode --version`. A failed
|
|
155
176
|
* fetch keeps the old cache (logging one warning) so the team still prices by the last good
|
|
156
|
-
* data; `billedAt` from the old cache survives a successful refresh (part 6c/7).
|
|
177
|
+
* data; `billedAt` from the old cache survives a successful refresh (part 6c/7). A failed cache
|
|
178
|
+
* write warns and returns the fetched catalog, so the next start fetches again. */
|
|
157
179
|
export async function refreshOpencodeCatalog(root, opts) {
|
|
158
180
|
const now = opts.now !== undefined ? opts.now() : Date.now();
|
|
159
181
|
const version = opts.cliVersion !== undefined ? opts.cliVersion : readCliVersion(opts.cliBin);
|
|
160
182
|
const current = readOpencodeCache(root);
|
|
161
|
-
const fresh = current
|
|
162
|
-
now - current.fetchedAt < CATALOG_CACHE_TTL_MS &&
|
|
183
|
+
const fresh = !opencodeCatalogStale(current, now) &&
|
|
163
184
|
(version === undefined || current.cliVersion === version);
|
|
164
185
|
if (fresh)
|
|
165
186
|
return current;
|
|
@@ -179,6 +200,11 @@ export async function refreshOpencodeCatalog(root, opts) {
|
|
|
179
200
|
fresh.billedAt = old.billedAt;
|
|
180
201
|
}
|
|
181
202
|
const cache = { fetchedAt: now, cliVersion: version, models: parsed };
|
|
182
|
-
|
|
203
|
+
try {
|
|
204
|
+
writeOpencodeCache(root, cache);
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
opts.warn?.(`could not save the open code model catalog: ${errorMessage(err)}`);
|
|
208
|
+
}
|
|
183
209
|
return cache;
|
|
184
210
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** Building one OpenCode run's injected config (`OPENCODE_CONFIG_CONTENT`) (plans/opencode-
|
|
3
2
|
* backend.md "The invocation") — pure JSON construction, with no subprocess. The config
|
|
4
3
|
* applies after the operator's global and project files, so only managed config wins over it:
|
|
5
4
|
* the gate-safe isolation switches (snapshots off, updates off, six denials), `enabled →
|
|
6
5
|
* disabled: true` for every operator MCP server (config merges, so disabling is the only way
|
|
7
|
-
* to remove one),
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* to remove one), an `instructions` entry mirroring claude's AGENTS.md handling, and tumwater's
|
|
7
|
+
* write-guard plugin (write-guard-plugin/index.ts), which refuses a write outside the run's
|
|
8
|
+
* worktree. No tool is disabled beyond the six gate-safe denials — denying `read` trips the
|
|
9
|
+
* free-tier gate (finding 11). Part 5/7 wires this string into the run's environment.
|
|
10
10
|
*
|
|
11
11
|
* From part 3/4 tumwater no longer adds an MCP server of its own: run notes ride the
|
|
12
12
|
* `tumwater-notes` command on PATH, so every operator MCP server is disabled alike — an
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
* here, and doctor reports it in part 7/7. */
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import path from "node:path";
|
|
20
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
21
|
+
import { readTextOrNull } from "../../files/files.js";
|
|
22
|
+
import { isJsonObject } from "../../files/json-object.js";
|
|
20
23
|
/** The operator's global opencode config lives in `$XDG_CONFIG_HOME/opencode/` (or
|
|
21
24
|
* `~/.config/opencode/`), in `config.json`, `opencode.json` or `opencode.jsonc`. */
|
|
22
25
|
const OPERATOR_CONFIG_FILES = ["config.json", "opencode.json", "opencode.jsonc"];
|
|
@@ -123,13 +126,9 @@ export function operatorMcpNames(env, homeDir) {
|
|
|
123
126
|
const configHome = env.XDG_CONFIG_HOME ?? path.join(homeDir, ".config");
|
|
124
127
|
const dir = path.join(configHome, "opencode");
|
|
125
128
|
for (const file of OPERATOR_CONFIG_FILES) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
text = fs.readFileSync(path.join(dir, file), "utf8");
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
129
|
+
const text = readTextOrNull(path.join(dir, file));
|
|
130
|
+
if (text === null)
|
|
131
131
|
continue;
|
|
132
|
-
}
|
|
133
132
|
let parsed;
|
|
134
133
|
try {
|
|
135
134
|
parsed = JSON.parse(file.endsWith(".jsonc") ? stripJsonc(text) : text);
|
|
@@ -138,13 +137,11 @@ export function operatorMcpNames(env, homeDir) {
|
|
|
138
137
|
return [];
|
|
139
138
|
}
|
|
140
139
|
const mcp = parsed?.mcp;
|
|
141
|
-
if (mcp
|
|
142
|
-
return [];
|
|
143
|
-
if (typeof mcp !== "object" || Array.isArray(mcp))
|
|
140
|
+
if (!isJsonObject(mcp))
|
|
144
141
|
return [];
|
|
145
142
|
const names = new Set();
|
|
146
143
|
const servers = mcp.servers;
|
|
147
|
-
if (
|
|
144
|
+
if (isJsonObject(servers)) {
|
|
148
145
|
for (const key of Object.keys(servers))
|
|
149
146
|
names.add(key);
|
|
150
147
|
}
|
|
@@ -163,6 +160,13 @@ export function operatorMcpNames(env, homeDir) {
|
|
|
163
160
|
function shouldInjectClaudeMd(cwd) {
|
|
164
161
|
return fs.existsSync(path.join(cwd, "CLAUDE.md")) && !fs.existsSync(path.join(cwd, "AGENTS.md"));
|
|
165
162
|
}
|
|
163
|
+
/** The write-guard plugin's `plugins` entry: a `file://` URL of the directory holding its
|
|
164
|
+
* compiled `index.js`, since OpenCode 2.x loads a configured plugin path only as a directory.
|
|
165
|
+
* Resolved from this module, so a staged build (.tumwater/build/<sha>) loads its own copy. */
|
|
166
|
+
export function writeGuardPluginEntry() {
|
|
167
|
+
const entry = fileURLToPath(new URL("./write-guard-plugin/index.js", import.meta.url));
|
|
168
|
+
return pathToFileURL(path.dirname(entry)).href;
|
|
169
|
+
}
|
|
166
170
|
/** Build the injected opencode config JSON string. Never reads the worktree beyond the
|
|
167
171
|
* CLAUDE.md/AGENTS.md probe. */
|
|
168
172
|
export function opencodeConfigContent(opts) {
|
|
@@ -170,6 +174,7 @@ export function opencodeConfigContent(opts) {
|
|
|
170
174
|
snapshots: false,
|
|
171
175
|
update: "disable",
|
|
172
176
|
permissions: DENIED_PERMISSIONS,
|
|
177
|
+
plugins: [writeGuardPluginEntry()],
|
|
173
178
|
};
|
|
174
179
|
if (opts.toolOutput !== undefined) {
|
|
175
180
|
config.tool_output = { max_lines: opts.toolOutput.maxLines, max_bytes: opts.toolOutput.maxBytes };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** Building the environment for one opencode run (plans/opencode-backend.md "The invocation")
|
|
3
2
|
* — pure scrub-and-set over a base environment, with no subprocess. The caller layers the
|
|
4
3
|
* run-marker and the macOS preload on first (claude's adapter shows the pie pattern), then
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** Minting an OpenCode session id (plans/opencode-backend.md "Sessions", finding 3) — a pure
|
|
3
2
|
* reproduction of OpenCode's `Identifier.descending()` (packages/schema/src/identifier.ts), so
|
|
4
3
|
* a fresh run's `--session` id is accepted by the free-tier gate. A `ses_…` id in any other
|
|
@@ -12,7 +11,7 @@ import crypto from "node:crypto";
|
|
|
12
11
|
const ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
13
12
|
/** Per-process minting state, mirroring OpenCode's per-millisecond counter: the counter
|
|
14
13
|
* restarts at 1 for each new millisecond, and increments within the same millisecond so two
|
|
15
|
-
* ids minted in one
|
|
14
|
+
* ids minted in one millisecond still differ in their hex. */
|
|
16
15
|
let lastMintMs = -1;
|
|
17
16
|
let counter = 0;
|
|
18
17
|
/** Mint an OpenCode session id. `now` is a Unix-millisecond epoch (injected for tests);
|
|
@@ -251,7 +251,7 @@ export function opencodeTranslator(opts = {}) {
|
|
|
251
251
|
* the run's real cost and token totals. Anything malformed returns `[]`; part 5/7 logs
|
|
252
252
|
* `opencode_usage_partial` when this is empty. */
|
|
253
253
|
export function opencodeExportLines(exportJson) {
|
|
254
|
-
if (
|
|
254
|
+
if (!isJsonObject(exportJson))
|
|
255
255
|
return [];
|
|
256
256
|
const info = exportJson.info;
|
|
257
257
|
if (typeof info !== "object" || info === null)
|