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
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { eventsSince } from "../events/event-window.js";
|
|
2
2
|
import { isNotifyFailure } from "../events/notify.js";
|
|
3
|
-
import {
|
|
3
|
+
import { DAY_MS, humanAgo } from "../text/datetime.js";
|
|
4
4
|
import { plural } from "../text/phrases.js";
|
|
5
5
|
import { errorMessage, squash } from "../text/text.js";
|
|
6
6
|
import { okOutcome, warnOutcome } from "./outcome.js";
|
|
7
|
-
/** How far back the `notify` check looks for failed pages.
|
|
8
|
-
|
|
7
|
+
/** How far back the `notify` check looks for failed pages: a day, fixed. An opinionated
|
|
8
|
+
* default with no knob, like STALE_BUG_DAYS: long enough that a transient hook failure shows
|
|
9
|
+
* up, short enough that a page that did not go through is still the operator's business. The
|
|
10
|
+
* always-on notify team alert (src/ui/status-payload.ts notifyFailures) reuses this same
|
|
11
|
+
* threshold and the same isNotifyFailure window, so it cannot drift from the doctor's check
|
|
12
|
+
* — the STALE_BUG_DAYS precedent. */
|
|
13
|
+
export const NOTIFY_CHECK_WINDOW_MS = DAY_MS;
|
|
9
14
|
/** The `notify` check: whether a notify command is configured, and a warning when it failed to
|
|
10
15
|
* page within the last day (a nonzero exit, a kill, or a failed start — the warnings
|
|
11
16
|
* spawnNotifyCommand logs). Warns but never fails, like the hold rows: a dead webhook must not
|
|
@@ -18,8 +23,7 @@ export function checkNotify(root, config, now = Date.now()) {
|
|
|
18
23
|
if (failures.length === 0)
|
|
19
24
|
return okOutcome("configured — no failed pages in the last 24h");
|
|
20
25
|
const latest = failures[failures.length - 1];
|
|
21
|
-
|
|
22
|
-
return warnOutcome(`${plural(failures.length, "failed page")} in the last 24h; latest ${age} ago: ${squash(String(latest.message), 200)}`);
|
|
26
|
+
return warnOutcome(`${plural(failures.length, "failed page")} in the last 24h; latest ${humanAgo(latest.ts, now)}: ${squash(String(latest.message), 200)}`);
|
|
23
27
|
}
|
|
24
28
|
catch (err) {
|
|
25
29
|
return warnOutcome(`could not read the event log for failed pages: ${errorMessage(err)}`);
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { childEnv } from "../process/process.js";
|
|
4
|
+
import { AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS, boundedProbe } from "./probe.js";
|
|
5
|
+
import { opencodeBackend, opencodeSpillDir } from "../agent/opencode/opencode-backend.js";
|
|
6
|
+
import { opencodeModelKey, readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
7
|
+
import { opencodeDbPath } from "../paths.js";
|
|
8
|
+
import { envPath, isDirectory, readTextOrNull, statOrNull, treeBytes } from "../files/files.js";
|
|
9
|
+
import { formatBytes, tokenWindowLabel } from "../text/format.js";
|
|
10
|
+
import { escapeRegExp } from "../text/text.js";
|
|
11
|
+
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
12
|
+
import { loadConfigForCheck, opencodeSeamsInUse, seamedModels, } from "./doctor-model-checks.js";
|
|
13
|
+
/** The doctor's opencode credential and leak readiness check, split out of doctor-model-checks.ts
|
|
14
|
+
* (whose shared seam/model framework and the pi, tier-models and image-input checks stay there):
|
|
15
|
+
* checkOpencode and its helpers (opencodeProviderOf, opencodeAuthLists, jsonKeyPresent,
|
|
16
|
+
* opencodeConfigLeaks). This is the doctor's only check that spawns the `opencode` CLI and reads
|
|
17
|
+
* its catalog and the operator's global opencode config — a distinct concern from the
|
|
18
|
+
* filesystem/git checks beside it, kept together so a change to how doctor reads opencode does
|
|
19
|
+
* not touch the other model checks. The shared report contract (CheckOutcome) and the
|
|
20
|
+
* seam/model framework (loadConfigForCheck, opencodeSeamsInUse, seamPairs) come from
|
|
21
|
+
* doctor-model-checks.ts, the AUTH_CHECK_* probe budgets from probe.ts, and the shared report
|
|
22
|
+
* contract (CheckOutcome) from outcome.ts, as the other doctor-* modules do. */
|
|
23
|
+
/** The provider of a non-gateway opencode model id (`opencode/<provider>/<model>`), or
|
|
24
|
+
* undefined for a gateway id (a bare `opencode/<model>` or `<model>`, served by OpenCode's
|
|
25
|
+
* own gateway with no credential). A model id with more than the `opencode/` namespace prefix
|
|
26
|
+
* is provider-qualified and needs a credential OpenCode lists. */
|
|
27
|
+
function opencodeProviderOf(model) {
|
|
28
|
+
const parts = model.split("/");
|
|
29
|
+
if (parts[0] === "opencode" && parts.length >= 3)
|
|
30
|
+
return parts[1];
|
|
31
|
+
if (parts[0] !== "opencode" && parts.length >= 2)
|
|
32
|
+
return parts[0];
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Whether `opencode auth list --format json` (bounded) lists `provider` exactly. A probe that
|
|
36
|
+
* cannot run, exits nonzero, or omits the provider reads as NOT listed — the safe direction,
|
|
37
|
+
* since doctor is warning the operator to add the credential, never confirming one it did not
|
|
38
|
+
* see. The list is matched EXACTLY, not as a raw substring: when the output parses as an array
|
|
39
|
+
* (of names or of per-credential objects), a credential listed as `anthropic` must never vouch
|
|
40
|
+
* for `anthropic-foo` twice over; when it cannot be parsed as JSON (a shape the CLI may change),
|
|
41
|
+
* the text is matched word-bounded so a truncated or prose line still reads. */
|
|
42
|
+
async function opencodeAuthLists(config, provider, pathEnv, timeoutMs) {
|
|
43
|
+
const resolved = opencodeBackend.resolveBin(config);
|
|
44
|
+
const result = await boundedProbe(resolved.bin, ["auth", "list", "--format", "json"], childEnv({ PATH: pathEnv }), timeoutMs, AUTH_CHECK_MAX_OUTPUT_BYTES);
|
|
45
|
+
if (result?.code !== 0)
|
|
46
|
+
return false;
|
|
47
|
+
const text = result.stdout;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(text);
|
|
50
|
+
if (Array.isArray(parsed)) {
|
|
51
|
+
return parsed.some((item) => {
|
|
52
|
+
if (typeof item === "string")
|
|
53
|
+
return item === provider;
|
|
54
|
+
return (item !== null &&
|
|
55
|
+
typeof item === "object" &&
|
|
56
|
+
Object.values(item).some((v) => v === provider));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Not an array-shaped JSON payload — fall through to the word-bounded text match.
|
|
62
|
+
}
|
|
63
|
+
const esc = escapeRegExp(provider);
|
|
64
|
+
return new RegExp(`(^|[^A-Za-z0-9_-])${esc}([^A-Za-z0-9_-]|$)`).test(text);
|
|
65
|
+
}
|
|
66
|
+
/** Whether a JSON-ish key (optionally quoted, followed by a colon) appears in a config file's
|
|
67
|
+
* text — a loose match that tolerates JSONC comments without parsing them. */
|
|
68
|
+
function jsonKeyPresent(text, key) {
|
|
69
|
+
return new RegExp(`["']?${key}["']?\\s*:`).test(text);
|
|
70
|
+
}
|
|
71
|
+
/** The operator's global opencode config files and AGENTS.md (plans/opencode-backend.md §7
|
|
72
|
+
* "Leaks", part 7/7): the operator's own config and instructions still load into team runs,
|
|
73
|
+
* so each MCP server (disabled for team runs) or plugin/agent/instructions entry or the
|
|
74
|
+
* AGENTS.md file itself produces one warning naming the file and what leaks in: an MCP server
|
|
75
|
+
* is switched off by the harness, while a plugin/agent/instructions entry or AGENTS.md is NOT
|
|
76
|
+
* switched off and still loads. `configHome` is the config root to scan — the XDG config home
|
|
77
|
+
* (when set) or `~/.config`, the SAME folder opencode's own operatorMcpNames reads, so the leak
|
|
78
|
+
* scan matches the config a run actually loads. Config files
|
|
79
|
+
* are JSON/C, so the keys are matched loosely (a word followed by a colon) rather than fully
|
|
80
|
+
* parsed — enough to name what still loads before a run inherits it. */
|
|
81
|
+
function opencodeConfigLeaks(configHome) {
|
|
82
|
+
const dir = path.join(configHome, "opencode");
|
|
83
|
+
const warns = [];
|
|
84
|
+
for (const file of ["config.json", "opencode.json", "opencode.jsonc"]) {
|
|
85
|
+
const text = readTextOrNull(path.join(dir, file));
|
|
86
|
+
if (text === null)
|
|
87
|
+
continue;
|
|
88
|
+
const parts = [];
|
|
89
|
+
let mcp = false;
|
|
90
|
+
if (jsonKeyPresent(text, "mcp") && jsonKeyPresent(text, "servers")) {
|
|
91
|
+
parts.push("an MCP server");
|
|
92
|
+
mcp = true;
|
|
93
|
+
}
|
|
94
|
+
for (const key of ["plugin", "agent", "instructions"]) {
|
|
95
|
+
if (jsonKeyPresent(text, key))
|
|
96
|
+
parts.push(`a ${key} entry`);
|
|
97
|
+
}
|
|
98
|
+
if (parts.length === 0)
|
|
99
|
+
continue;
|
|
100
|
+
const joined = parts.join(" and ");
|
|
101
|
+
// Only the MCP server is disabled for team runs; everything else still loads.
|
|
102
|
+
const verdict = mcp && parts.length === 1 ? " — disabled for team runs" : " — still loads into team runs";
|
|
103
|
+
warns.push(`${path.join(dir, file)} carries ${joined}${verdict}`);
|
|
104
|
+
}
|
|
105
|
+
const agentsMd = path.join(dir, "AGENTS.md");
|
|
106
|
+
if (statOrNull(agentsMd)?.isFile() === true)
|
|
107
|
+
warns.push(`${agentsMd} still loads into team runs`);
|
|
108
|
+
return warns;
|
|
109
|
+
}
|
|
110
|
+
/** OpenCode model, credential and leak readiness (plans/opencode-backend.md §7, part 7/7):
|
|
111
|
+
* every model routed to opencode must be in the catalog cache — an unknown model fails, a
|
|
112
|
+
* cached one reads "free" or its per-M prices and context window, and a thinking level with no
|
|
113
|
+
* catalog effort is noted as dropped; a non-gateway provider (`opencode/<provider>/…`) must
|
|
114
|
+
* have a credential `opencode auth list` lists, else warns; and the operator's global opencode
|
|
115
|
+
* config and AGENTS.md must not leak an MCP server or a plugin/agent/instructions entry into
|
|
116
|
+
* team runs, else warns naming the file and key. Reports the team's opencode database and spill
|
|
117
|
+
* sizes as information. Stands down (ok) when no seam routes to opencode, so a pi-only team
|
|
118
|
+
* spawns nothing and reads nothing. `pathEnv` and `home` are injectable so tests need no real
|
|
119
|
+
* PATH or HOME. The version half of the opencode pre-flight is the sibling checkOpencodeVersion
|
|
120
|
+
* in doctor-checks.ts. */
|
|
121
|
+
export async function checkOpencode(root, pathEnv = envPath(), home = os.homedir(), xdgConfigHome = process.env.XDG_CONFIG_HOME ?? "", timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
122
|
+
const config = loadConfigForCheck(root);
|
|
123
|
+
if ("level" in config)
|
|
124
|
+
return config;
|
|
125
|
+
if (!opencodeSeamsInUse(config))
|
|
126
|
+
return okOutcome("no model jobs run on opencode");
|
|
127
|
+
const models = seamedModels(config, "opencode");
|
|
128
|
+
const cache = readOpencodeCache(root);
|
|
129
|
+
const fails = [];
|
|
130
|
+
const warns = [];
|
|
131
|
+
const notes = [];
|
|
132
|
+
for (const pair of models) {
|
|
133
|
+
const key = opencodeModelKey(pair.model);
|
|
134
|
+
const entry = cache.models[key];
|
|
135
|
+
if (entry === undefined) {
|
|
136
|
+
fails.push(`${pair.model} is not in the opencode catalog cache`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const price = entry.free ? "free" : `$${entry.inputPerM ?? 0}/$${entry.outputPerM ?? 0} per M`;
|
|
140
|
+
const window = entry.context !== undefined ? `, ${tokenWindowLabel(entry.context)}` : "";
|
|
141
|
+
notes.push(`${pair.model} — ${price}${window}`);
|
|
142
|
+
// A model with no effort list at all supports no thinking level, so a requested level is
|
|
143
|
+
// dropped just the same as one the list omits.
|
|
144
|
+
if (pair.thinking !== undefined &&
|
|
145
|
+
pair.thinking !== "" &&
|
|
146
|
+
!entry.efforts?.includes(pair.thinking)) {
|
|
147
|
+
warns.push(`${pair.model} has no ${pair.thinking} effort — that thinking level is dropped`);
|
|
148
|
+
}
|
|
149
|
+
const provider = opencodeProviderOf(pair.model);
|
|
150
|
+
if (provider !== undefined && !(await opencodeAuthLists(config, provider, pathEnv, timeoutMs))) {
|
|
151
|
+
warns.push(`${pair.model} is a non-gateway provider opencode does not list a credential for`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
warns.push(...opencodeConfigLeaks(xdgConfigHome !== "" ? xdgConfigHome : path.join(home, ".config")));
|
|
155
|
+
const db = statOrNull(opencodeDbPath(root));
|
|
156
|
+
const spillDir = opencodeSpillDir(root);
|
|
157
|
+
const spill = spillDir !== "" && isDirectory(spillDir) ? treeBytes(spillDir) : null;
|
|
158
|
+
const sizeNotes = `db ${formatBytes(db?.size ?? 0)}${spill !== null ? `, spill ${formatBytes(spill)}` : ", no spill dir"}`;
|
|
159
|
+
const found = [...fails, ...warns];
|
|
160
|
+
const base = found.length === 0 ? `opencode ready — ${notes.join("; ")}; ${sizeNotes}` : found.join("; ");
|
|
161
|
+
if (fails.length > 0)
|
|
162
|
+
return failOutcome(base);
|
|
163
|
+
return (warns.length > 0 ? warnOutcome : okOutcome)(base);
|
|
164
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
import fs from "node:fs";
|
|
3
2
|
import path from "node:path";
|
|
4
3
|
import { worktreesDir } from "../paths.js";
|
|
5
4
|
import { pidAlive } from "../process/process.js";
|
|
5
|
+
import { GROUP_POLL_MS, KILL_GRACE_MS } from "../process/process-group.js";
|
|
6
6
|
import { systemProcessProbe } from "../process/process-table.js";
|
|
7
|
+
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
7
8
|
import { errorMessage, truncate } from "../text/text.js";
|
|
8
9
|
import { plural } from "../text/phrases.js";
|
|
9
10
|
import { failOutcome, okOutcome, warnOutcome } from "./outcome.js";
|
|
@@ -71,10 +72,10 @@ function relativeArgvIsOurs(command, cwd, root, dirs) {
|
|
|
71
72
|
}
|
|
72
73
|
return false;
|
|
73
74
|
}
|
|
74
|
-
/**
|
|
75
|
+
/** The processes that descend from `pid`, given the table's parent → children map — an
|
|
75
76
|
* orphaned test runner's workers keep the runner as their parent, so they are the orphan's
|
|
76
77
|
* tree, not orphans of their own, and killing the runner alone leaves them running. */
|
|
77
|
-
function
|
|
78
|
+
function descendantPids(children, pid) {
|
|
78
79
|
const seen = new Set();
|
|
79
80
|
const stack = [...(children.get(pid) ?? [])];
|
|
80
81
|
for (let p = stack.pop(); p !== undefined; p = stack.pop()) {
|
|
@@ -83,52 +84,43 @@ function descendantCount(children, pid) {
|
|
|
83
84
|
seen.add(p);
|
|
84
85
|
stack.push(...(children.get(p) ?? []));
|
|
85
86
|
}
|
|
86
|
-
return seen
|
|
87
|
+
return [...seen];
|
|
87
88
|
}
|
|
88
|
-
/**
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
*
|
|
117
|
-
* Cheap by construction: one `ps`, then one cwd lookup covering only the parentless
|
|
118
|
-
* processes argv did not already settle (and only those this user can inspect — lsof and
|
|
119
|
-
* /proc cannot read anyone else's, root reads all). An unreadable table degrades to a warn,
|
|
120
|
-
* never a crashed doctor; unreadable cwds degrade to the argv match and say so. Any orphan is
|
|
121
|
-
* a fail — the exit code is the point: a scripted doctor must notice. `probe` is injectable so
|
|
122
|
-
* tests run against a fake table. */
|
|
123
|
-
export async function checkOrphans(root, probe = systemProcessProbe) {
|
|
89
|
+
/** A mark's harness is dead (true), alive (false), or the mark names no harness (null — a torn
|
|
90
|
+
* or foreign value judges nothing rather than counting as dead). */
|
|
91
|
+
function harnessGone(marker) {
|
|
92
|
+
const m = /^(\d+)-/.exec(marker);
|
|
93
|
+
return m ? !pidAlive(Number(m[1])) : null;
|
|
94
|
+
}
|
|
95
|
+
/** A parent pid → child pids map of the table. */
|
|
96
|
+
function childrenOf(rows) {
|
|
97
|
+
const children = new Map();
|
|
98
|
+
for (const r of rows) {
|
|
99
|
+
const siblings = children.get(r.ppid);
|
|
100
|
+
if (siblings)
|
|
101
|
+
siblings.push(r.pid);
|
|
102
|
+
else
|
|
103
|
+
children.set(r.ppid, [r.pid]);
|
|
104
|
+
}
|
|
105
|
+
return children;
|
|
106
|
+
}
|
|
107
|
+
/** An orphan's command with the repo root cut out, longest spelling first: the resolved one can
|
|
108
|
+
* contain the given one (/private/var/… holds /var/…) — then trimmed to ORPHAN_COMMAND_MAX. */
|
|
109
|
+
function shownCommand(command, root) {
|
|
110
|
+
const cut = [...rootSpellings(root)].sort((a, b) => b.length - a.length);
|
|
111
|
+
return truncate(cut.reduce((c, rt) => c.split(`${rt}/`).join(""), command), ORPHAN_COMMAND_MAX);
|
|
112
|
+
}
|
|
113
|
+
/** The detection half of checkOrphans, shared with killOrphans so the check and the kill cannot
|
|
114
|
+
* disagree about the set. */
|
|
115
|
+
async function findOrphans(root, probe) {
|
|
116
|
+
const roots = rootSpellings(root);
|
|
124
117
|
let rows;
|
|
125
118
|
try {
|
|
126
119
|
rows = await probe.list();
|
|
127
120
|
}
|
|
128
121
|
catch (err) {
|
|
129
|
-
return
|
|
122
|
+
return { error: errorMessage(err) };
|
|
130
123
|
}
|
|
131
|
-
const roots = rootSpellings(root);
|
|
132
124
|
const dirs = roots.map(worktreesDir);
|
|
133
125
|
// Never doctor itself, should it be run parentless from a worktree's build.
|
|
134
126
|
const parentless = rows.filter((r) => r.ppid === 1 && r.pid !== process.pid);
|
|
@@ -156,12 +148,6 @@ export async function checkOrphans(root, probe = systemProcessProbe) {
|
|
|
156
148
|
// Best-effort per the probe contract: argv and cwd still carry the check alone.
|
|
157
149
|
}
|
|
158
150
|
}
|
|
159
|
-
/** A mark's harness is dead (true), alive (false), or the mark names no harness (null —
|
|
160
|
-
* a torn or foreign value judges nothing rather than counting as dead). */
|
|
161
|
-
const harnessGone = (marker) => {
|
|
162
|
-
const m = /^(\d+)-/.exec(marker);
|
|
163
|
-
return m ? !pidAlive(Number(m[1])) : null;
|
|
164
|
-
};
|
|
165
151
|
const byMark = new Set();
|
|
166
152
|
for (const r of parentless) {
|
|
167
153
|
if (byArgv.has(r.pid))
|
|
@@ -176,35 +162,168 @@ export async function checkOrphans(root, probe = systemProcessProbe) {
|
|
|
176
162
|
const cwd = cwds.get(r.pid);
|
|
177
163
|
return (cwd !== undefined && isUnder(cwd, dirs)) || relativeArgvIsOurs(r.command, cwd, root, dirs);
|
|
178
164
|
});
|
|
165
|
+
return { rows, orphans, marks, cwdProblem };
|
|
166
|
+
}
|
|
167
|
+
/** Orphaned worktree processes — the detector half of the grandchild-leak fix (BUGS.md
|
|
168
|
+
* 2026-09-21): a process reparented to PID 1 that belongs to one of this repo's worktrees.
|
|
169
|
+
* Leaks of this kind (a killed run's tool-call grandchildren, a timed-out build check's test
|
|
170
|
+
* tree, a pi run's backgrounded server, a stray orchestrator an orphaned suite started) hold
|
|
171
|
+
* no lock, write no event and touch no state file, so nothing else in the harness can see
|
|
172
|
+
* them. A process is this repo's when its argv names a path under `.tumwater/worktrees/`
|
|
173
|
+
* (absolute; or relative, confirmed by cwd where it can be — relativeArgvIsOurs) or when its
|
|
174
|
+
* cwd lies under it: a leaked `node dist/test/test-runner.js` names no worktree at all.
|
|
175
|
+
*
|
|
176
|
+
* PPID 1 is what keeps the live team out: a running orchestrator's pi runs and build checks
|
|
177
|
+
* have the orchestrator as parent, and the orchestrator and its supervisor run from the repo
|
|
178
|
+
* root, never from a worktree, even when a detached `nohup` launch leaves them parentless.
|
|
179
|
+
* (macOS always reparents an orphan to launchd, PID 1; on Linux a process under a child
|
|
180
|
+
* subreaper — `systemd --user` in a desktop session — is reparented to that instead, and is
|
|
181
|
+
* not seen here.)
|
|
182
|
+
*
|
|
183
|
+
* A second kind of evidence closes the gap the 2026-09-30 sweep fix left (BUGS.md, part
|
|
184
|
+
* 2/2): every process a run starts carries `TUMWATER_RUN=<harness pid>-<nonce>` in its
|
|
185
|
+
* environment (appended to any inherited mark), wherever its cwd is — exactly where the
|
|
186
|
+
* prompts send agents (scratch dirs under the system temp). A parentless process whose
|
|
187
|
+
* every mark names a harness pid that is no longer alive is this team's orphan: nothing
|
|
188
|
+
* will reap it, because the run sweep that would have dies with its harness. A mark naming
|
|
189
|
+
* ANY live harness is not reported — that harness's own sweep kills everything carrying the
|
|
190
|
+
* mark when its run folds. A pid that died and was recycled by an unrelated process reads
|
|
191
|
+
* as live (pidAlive cannot tell), so such a straggler stays unseen here — the same limit the
|
|
192
|
+
* merge lock's stale-holder check carries. An unparseable mark (a torn or foreign value
|
|
193
|
+
* without a `<pid>-` prefix) judges nothing; on macOS ps -E hides platform binaries'
|
|
194
|
+
* environments, so a marked sleep/sh leak stays unseen there, as the sweep's own scan does.
|
|
195
|
+
*
|
|
196
|
+
* Cheap by construction: one `ps`, then one cwd lookup covering only the parentless
|
|
197
|
+
* processes argv did not already settle (and only those this user can inspect — lsof and
|
|
198
|
+
* /proc cannot read anyone else's, root reads all). An unreadable table degrades to a warn,
|
|
199
|
+
* never a crashed doctor; unreadable cwds degrade to the argv match and say so. Any orphan is
|
|
200
|
+
* a fail — the exit code is the point: a scripted doctor must notice. `probe` is injectable so
|
|
201
|
+
* tests run against a fake table. */
|
|
202
|
+
export async function checkOrphans(root, probe = systemProcessProbe) {
|
|
203
|
+
const scan = await findOrphans(root, probe);
|
|
204
|
+
if ("error" in scan)
|
|
205
|
+
return warnOutcome(`could not scan the process table — ${scan.error}`);
|
|
206
|
+
const { rows, orphans, marks, cwdProblem } = scan;
|
|
179
207
|
if (orphans.length === 0) {
|
|
180
208
|
if (cwdProblem !== null)
|
|
181
209
|
return warnOutcome(`none named in argv, but process cwds are unreadable (${cwdProblem}) — an orphan started inside a worktree, like a leaked test runner, would be missed`);
|
|
182
210
|
return okOutcome("none — no process reparented to PID 1 runs from .tumwater/worktrees/ or carries only dead runs' marks");
|
|
183
211
|
}
|
|
184
|
-
const children =
|
|
185
|
-
for (const r of rows) {
|
|
186
|
-
const siblings = children.get(r.ppid);
|
|
187
|
-
if (siblings)
|
|
188
|
-
siblings.push(r.pid);
|
|
189
|
-
else
|
|
190
|
-
children.set(r.ppid, [r.pid]);
|
|
191
|
-
}
|
|
192
|
-
// The root is cut from each shown command, longest spelling first: the resolved one can
|
|
193
|
-
// contain the given one (/private/var/… holds /var/…).
|
|
194
|
-
const cut = [...roots].sort((a, b) => b.length - a.length);
|
|
212
|
+
const children = childrenOf(rows);
|
|
195
213
|
const listed = orphans.slice(0, ORPHANS_LISTED_MAX).map((r) => {
|
|
196
|
-
const n =
|
|
214
|
+
const n = descendantPids(children, r.pid).length;
|
|
197
215
|
const tree = n > 0 ? `, +${plural(n, "descendant")}` : "";
|
|
198
|
-
const command = cut.reduce((c, rt) => c.split(`${rt}/`).join(""), r.command);
|
|
199
216
|
// A mark-caught orphan says which run marked it: the operator killing it wants to know
|
|
200
217
|
// nothing else will, and the dead harness's pid is the evidence argv and cwd lacked.
|
|
201
218
|
const mark = (marks.get(r.pid) ?? []).find((m) => harnessGone(m) === true);
|
|
202
219
|
const marked = mark !== undefined ? ` (run ${mark}; tumwater ${mark.split("-")[0]} exited)` : "";
|
|
203
|
-
return `pid ${r.pid} (age ${r.etime}, cpu ${r.time}${tree}) ${
|
|
220
|
+
return `pid ${r.pid} (age ${r.etime}, cpu ${r.time}${tree}) ${shownCommand(r.command, root)}${marked}`;
|
|
204
221
|
});
|
|
205
222
|
const more = orphans.length - listed.length;
|
|
206
223
|
const count = `${plural(orphans.length, "orphaned process", "orphaned processes")} (PPID 1)`;
|
|
207
224
|
return failOutcome(`${count}: ${listed.join("; ")}${more > 0 ? `; and ${more} more` : ""}` +
|
|
208
|
-
" — nothing reaps these; kill each with its descendants" +
|
|
225
|
+
" — nothing reaps these; `tumwater doctor --kill-orphans` (or the dashboard Health card's Kill orphans, or the TUI Health tab's Ctrl+K pressed twice) kills each with its descendants" +
|
|
209
226
|
(cwdProblem !== null ? ` (process cwds unreadable — ${cwdProblem}; argv matched only)` : ""));
|
|
210
227
|
}
|
|
228
|
+
const systemSignals = {
|
|
229
|
+
kill: (pid, signal) => void process.kill(pid, signal),
|
|
230
|
+
alive: pidAlive,
|
|
231
|
+
graceMs: KILL_GRACE_MS,
|
|
232
|
+
pollMs: GROUP_POLL_MS,
|
|
233
|
+
};
|
|
234
|
+
/** The lines `doctor --kill-orphans` prints for a report, shared with the dashboard's Kill
|
|
235
|
+
* orphans toast. */
|
|
236
|
+
export function orphanKillLines(report) {
|
|
237
|
+
if ("error" in report)
|
|
238
|
+
return [`could not scan the process table — ${report.error}; nothing killed`];
|
|
239
|
+
const lines = report.killed.map((k) => {
|
|
240
|
+
const tree = k.descendants > 0 ? ` (+${plural(k.descendants, "descendant")})` : "";
|
|
241
|
+
return `killed pid ${k.pid}${tree} ${k.command}`;
|
|
242
|
+
});
|
|
243
|
+
for (const f of report.failed)
|
|
244
|
+
lines.push(`could not kill pid ${f.pid}: ${f.reason}`);
|
|
245
|
+
if (lines.length === 0)
|
|
246
|
+
lines.push("no orphaned processes to kill");
|
|
247
|
+
return lines;
|
|
248
|
+
}
|
|
249
|
+
/** Kill the processes checkOrphans lists, each with its descendants from the same scan:
|
|
250
|
+
* SIGTERM all, wait up to `graceMs` for them to go, SIGKILL what remains. A pid already gone
|
|
251
|
+
* (ESRCH) counts as killed; any other refusal (EPERM) is reported and the rest are still
|
|
252
|
+
* signalled. An orphan with a refused pid in its tree is reported failed, not killed. */
|
|
253
|
+
export async function killOrphans(root, probe = systemProcessProbe, signals = systemSignals) {
|
|
254
|
+
const scan = await findOrphans(root, probe);
|
|
255
|
+
if ("error" in scan)
|
|
256
|
+
return { error: scan.error };
|
|
257
|
+
return killTrees(root, scan.rows, scan.orphans, signals);
|
|
258
|
+
}
|
|
259
|
+
/** The signal-and-wait half of killOrphans: SIGTERM each chosen row's tree from `rows`, wait,
|
|
260
|
+
* SIGKILL the rest, and report what went. */
|
|
261
|
+
async function killTrees(root, rows, chosen, signals) {
|
|
262
|
+
const children = childrenOf(rows);
|
|
263
|
+
const refused = new Map();
|
|
264
|
+
const send = (pid, signal) => {
|
|
265
|
+
try {
|
|
266
|
+
signals.kill(pid, signal);
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
if (err.code !== "ESRCH")
|
|
270
|
+
refused.set(pid, errorMessage(err));
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const trees = chosen.map((r) => ({ r, pids: [r.pid, ...descendantPids(children, r.pid)] }));
|
|
274
|
+
const all = [...new Set(trees.flatMap((t) => t.pids))];
|
|
275
|
+
for (const pid of all)
|
|
276
|
+
send(pid, "SIGTERM");
|
|
277
|
+
const stillAlive = () => all.filter((pid) => !refused.has(pid) && signals.alive(pid));
|
|
278
|
+
const deadline = Date.now() + signals.graceMs;
|
|
279
|
+
while (stillAlive().length > 0 && Date.now() < deadline)
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, signals.pollMs));
|
|
281
|
+
for (const pid of stillAlive())
|
|
282
|
+
send(pid, "SIGKILL");
|
|
283
|
+
const killed = [];
|
|
284
|
+
const failed = [];
|
|
285
|
+
for (const { r, pids } of trees) {
|
|
286
|
+
const bad = pids.find((pid) => refused.has(pid));
|
|
287
|
+
if (bad === undefined)
|
|
288
|
+
killed.push({ pid: r.pid, command: shownCommand(r.command, root), descendants: pids.length - 1 });
|
|
289
|
+
else
|
|
290
|
+
failed.push({ pid: bad, reason: refused.get(bad) ?? "" });
|
|
291
|
+
}
|
|
292
|
+
return { killed, failed };
|
|
293
|
+
}
|
|
294
|
+
/** Team start's reap of a crashed predecessor's leaks: kill the parentless processes whose run
|
|
295
|
+
* marks all name dead harnesses and at least one names `previousPid`, the orchestrator the
|
|
296
|
+
* info file recorded. Safe without an operator's judgment: a mark proves a tumwater start
|
|
297
|
+
* started the process, the previous-pid match scopes it to this repo, and the dead-harness
|
|
298
|
+
* test excludes any run whose own sweep is still coming. A live `previousPid` (a hand-off
|
|
299
|
+
* whose old orchestrator is draining) scans nothing. Orphans caught only by argv or cwd carry
|
|
300
|
+
* no mark read (findOrphans asks marks only of processes argv did not settle) and stay for
|
|
301
|
+
* `doctor --kill-orphans`. Returns the killed count; never throws. */
|
|
302
|
+
export async function reapPreviousTeamOrphans(root, previousPid, probe = systemProcessProbe, signals = systemSignals) {
|
|
303
|
+
try {
|
|
304
|
+
if (pidAlive(previousPid))
|
|
305
|
+
return 0;
|
|
306
|
+
const scan = await findOrphans(root, probe);
|
|
307
|
+
if ("error" in scan)
|
|
308
|
+
return 0;
|
|
309
|
+
const chosen = scan.orphans.filter((r) => {
|
|
310
|
+
const marks = scan.marks.get(r.pid) ?? [];
|
|
311
|
+
return marks.length > 0 && marks.every((m) => harnessGone(m) === true) &&
|
|
312
|
+
marks.some((m) => m.startsWith(`${previousPid}-`));
|
|
313
|
+
});
|
|
314
|
+
if (chosen.length === 0)
|
|
315
|
+
return 0;
|
|
316
|
+
const { killed, failed } = await killTrees(root, scan.rows, chosen, signals);
|
|
317
|
+
const items = killed.map((k) => {
|
|
318
|
+
const tree = k.descendants > 0 ? ` (+${plural(k.descendants, "descendant")})` : "";
|
|
319
|
+
return `pid ${k.pid}${tree} ${k.command}`;
|
|
320
|
+
});
|
|
321
|
+
const refused = failed.map((f) => `could not kill pid ${f.pid}: ${f.reason}`);
|
|
322
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `killed ${plural(killed.length, "orphaned process", "orphaned processes")} left by tumwater ` +
|
|
323
|
+
`${previousPid}'s runs at team start: ${[...items, ...refused].join("; ")}`);
|
|
324
|
+
return killed.length;
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
return 0;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { childEnv } from "../process/process.js";
|
|
2
|
+
import { boundedProbe, AUTH_CHECK_MAX_OUTPUT_BYTES, AUTH_CHECK_TIMEOUT_MS } from "./probe.js";
|
|
3
|
+
import { parseJsonObject } from "../files/json-object.js";
|
|
4
|
+
import { envPath } from "../files/files.js";
|
|
5
|
+
import { resolveBackendBin } from "../pi/pi-bin.js";
|
|
6
|
+
import { parsePiModelImages, parsePiModelTable, writePiListedModels } from "../pi/pi-listed-models.js";
|
|
7
|
+
/** The default credential probe: run the resolved agent binary's own auth check for one provider
|
|
8
|
+
* and read `status` out of its JSON. The binary resolves exactly as the spawn does
|
|
9
|
+
* (resolveBackendBin — the same source a real run would launch), so doctor tests the auth of
|
|
10
|
+
* the pi it would actually run. pi's contract is `{"status": "ready"|"not_ready"|"invalid",
|
|
11
|
+
* provider, reason?, authType?}` with exit codes 0/1/2 — a non-zero exit there is a verdict, not
|
|
12
|
+
* a crash, so readiness is read from the body alone and a reply that did not run or does not
|
|
13
|
+
* parse is the only "unknown" (never not-ready, which would blame credentials). Never throws;
|
|
14
|
+
* every failure mode ends on "unknown". The bounded subprocess is the shared boundedProbe
|
|
15
|
+
* (probe.ts), also used by the claude probes. */
|
|
16
|
+
export async function piProviderAuth(config, provider, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS) {
|
|
17
|
+
const resolved = resolveBackendBin(config);
|
|
18
|
+
const result = await boundedProbe(resolved.bin, ["auth", "check", "--provider", provider, "--json"], childEnv({ PATH: pathEnv }), timeoutMs, AUTH_CHECK_MAX_OUTPUT_BYTES);
|
|
19
|
+
if (result === null)
|
|
20
|
+
return "unknown";
|
|
21
|
+
// An unreadable reply is unknown, never not-ready (which would blame the credentials).
|
|
22
|
+
const doc = parseJsonObject(result.stdout);
|
|
23
|
+
if (doc === null)
|
|
24
|
+
return "unknown";
|
|
25
|
+
if (doc.status === "ready")
|
|
26
|
+
return "ready";
|
|
27
|
+
// `invalid` (a bad provider name, exit 2) is likewise not a healthy credential: it reads
|
|
28
|
+
// not-ready so the warning blames the setup, not the probe.
|
|
29
|
+
if (doc.status === "not_ready" || doc.status === "invalid")
|
|
30
|
+
return "not-ready";
|
|
31
|
+
return "unknown";
|
|
32
|
+
}
|
|
33
|
+
/** Read the table `pi --list-models` prints into a set of `provider/model` keys. Null unless the
|
|
34
|
+
* first non-blank line is the `provider model …` header, so a "No models matching" reply or any
|
|
35
|
+
* other text is never taken for an empty catalog. */
|
|
36
|
+
export function parsePiModelList(stdout) {
|
|
37
|
+
const table = parsePiModelTable(stdout);
|
|
38
|
+
if (table === null)
|
|
39
|
+
return null;
|
|
40
|
+
const listed = new Set();
|
|
41
|
+
for (const [provider, model] of table.rows)
|
|
42
|
+
if (model !== undefined)
|
|
43
|
+
listed.add(`${provider}/${model}`);
|
|
44
|
+
return listed;
|
|
45
|
+
}
|
|
46
|
+
/** The models pi itself lists (`pi --list-models`), which include its built-in catalog that
|
|
47
|
+
* models.json does not repeat. pi lists only the providers it has credentials for. Null when the
|
|
48
|
+
* probe could not run or failed; never throws. Given a `root`, it also records the images column
|
|
49
|
+
* for `seamImageInput` (src/pi/pi-listed-models.ts). */
|
|
50
|
+
export async function piListedModels(config, pathEnv = envPath(), timeoutMs = AUTH_CHECK_TIMEOUT_MS, root) {
|
|
51
|
+
const result = await boundedProbe(resolveBackendBin(config).bin, ["--list-models"], childEnv({ PATH: pathEnv }), timeoutMs, AUTH_CHECK_MAX_OUTPUT_BYTES);
|
|
52
|
+
if (result?.code !== 0)
|
|
53
|
+
return null;
|
|
54
|
+
const images = root === undefined ? null : parsePiModelImages(result.stdout);
|
|
55
|
+
if (root !== undefined && images !== null) {
|
|
56
|
+
try {
|
|
57
|
+
writePiListedModels(root, images);
|
|
58
|
+
}
|
|
59
|
+
catch { } // A failed write never changes the check.
|
|
60
|
+
}
|
|
61
|
+
return parsePiModelList(result.stdout);
|
|
62
|
+
}
|