tumwater 0.4.2 → 0.4.4
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -2,13 +2,11 @@ import { baseRoleOf, runnerIds } from "../roles/runner-ids.js";
|
|
|
2
2
|
import { knownRoleIds } from "../config/config.js";
|
|
3
3
|
import { unknownRoleMessage } from "../roles/roles.js";
|
|
4
4
|
import { fail, say, sayJson } from "./cli-output.js";
|
|
5
|
-
import { durationLabel, failRivalShapes, parseBranchFlag,
|
|
5
|
+
import { durationLabel, failRivalShapes, parseBranchFlag, parseRoleListFlag, tokenValueProblem, } from "./cli-args.js";
|
|
6
6
|
import { cancelScheduledStop, scheduledStopAt } from "../team/team-state.js";
|
|
7
|
-
import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
8
|
-
import { runStartupCheck, runStartupProblem } from "../gates/startup-
|
|
9
|
-
import { repairInterruptedSwap } from "../build/build-stage.js";
|
|
7
|
+
import { orchestratorAlive, readOrchestratorInfo } from "../team/team-orchestrator-info.js";
|
|
8
|
+
import { runStartupCheck, runStartupProblem } from "../gates/startup-gates.js";
|
|
10
9
|
import { logEvent, subscribeEvents } from "../events/events.js";
|
|
11
|
-
import { notifyTeamDown } from "../events/notify.js";
|
|
12
10
|
import { formatEvent } from "../events/event-format.js";
|
|
13
11
|
import { RESTART_EXIT_CODE } from "../redeploy/redeploy-policy.js";
|
|
14
12
|
import { runStartupMigrations } from "../orchestrator/startup-migration-steps.js";
|
|
@@ -16,14 +14,8 @@ import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
|
16
14
|
import { shortSha } from "../text/format.js";
|
|
17
15
|
import { readRunRowsSince, summarizeRunRows } from "../history/history-data.js";
|
|
18
16
|
import { ONCE_FAILURE_RESULTS, inheritedWindowStart, onceFailures, onceReport, onceSummary, parseRunWindow, renderOnceSummary, windowReport, windowSummary, } from "./cli-start-summary.js";
|
|
19
|
-
/** The env var that carries the dashboard gate token between a supervisor and its dashboard
|
|
20
|
-
* child (and across a re-exec'd dashboard generation), for the `run --gui`/`start --gui`
|
|
21
|
-
* supervisor's own use. Defined here rather than imported from gui-command.js so this module
|
|
22
|
-
* (and every `start`/`run` invocation) never pulls the GUI server graph onto the page; it
|
|
23
|
-
* must stay the same literal as gui-command's GUI_TOKEN_ENV, and a merge that renames one
|
|
24
|
-
* renames the other. */
|
|
25
|
-
const GUI_TOKEN_ENV = "TUMWATER_GUI_TOKEN";
|
|
26
17
|
export { ONCE_FAILURE_RESULTS, inheritedWindowStart, onceFailures, onceReport, onceSummary, parseRunWindow, renderOnceSummary, windowReport, windowSummary, };
|
|
18
|
+
import { GUI_TOKEN_ENV, superviseStartCommand } from "./cli-start-supervisor.js";
|
|
27
19
|
/** Refuse the --once run-mode rivals: a TUI and a dashboard each cannot be hosted by a run that
|
|
28
20
|
* is about to stop, so both are refused together before any boot. --for stays allowed — both
|
|
29
21
|
* serve for the window. Owns the run-mode rival refusals. */
|
|
@@ -151,13 +143,11 @@ async function parseStartInvocation(root, args) {
|
|
|
151
143
|
assertNoRunningOrchestrator(root);
|
|
152
144
|
return { once, json, forMs, branchArg, roleFilter, config, mainBranch };
|
|
153
145
|
}
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
const { root, once, json, forMs, branchArg, roleFilter, config, mainBranch } = input;
|
|
160
|
-
const { RUN_WINDOW_START_ENV, ONCE_FAILED_EXIT_CODE, startParentDeathWatch, expectedSupervisorPid } = await import("../process/supervisor.js");
|
|
146
|
+
/** Create the abort handle and the graceful-stop closure a supervised generation passes to
|
|
147
|
+
* every watchdog, and register the SIGINT/SIGTERM handlers that trigger it. Returns an
|
|
148
|
+
* `isStopping` query so the later once-exit check reads the live flag instead of a captured
|
|
149
|
+
* boolean. */
|
|
150
|
+
function installStopHandlers(json) {
|
|
161
151
|
const controller = new AbortController();
|
|
162
152
|
let stopping = false;
|
|
163
153
|
const stop = () => {
|
|
@@ -173,6 +163,12 @@ async function runSupervisedGeneration(input) {
|
|
|
173
163
|
};
|
|
174
164
|
process.on("SIGINT", stop);
|
|
175
165
|
process.on("SIGTERM", stop);
|
|
166
|
+
return { controller, stop, isStopping: () => stopping };
|
|
167
|
+
}
|
|
168
|
+
/** Arm the windowed run's deadline timer and the scheduled-stop watch, both of which run the
|
|
169
|
+
* same graceful `stop` a Ctrl+C runs. Returns the window start and the two clearable handles. */
|
|
170
|
+
async function armRunWatchdogs(root, json, forMs, stop, isStopping) {
|
|
171
|
+
const { RUN_WINDOW_START_ENV } = await import("../process/supervisor.js");
|
|
176
172
|
// A windowed run's deadline arms the same graceful stop a Ctrl+C runs — the identical `stop`
|
|
177
173
|
// closure, so the drain path is one code path either way. clearTimeout in the finally below
|
|
178
174
|
// retracts it when the run ends first (an early Ctrl+C), so no stray timer outlives the run.
|
|
@@ -185,22 +181,13 @@ async function runSupervisedGeneration(input) {
|
|
|
185
181
|
stop();
|
|
186
182
|
}) : null;
|
|
187
183
|
const scheduledStopWatch = armScheduledStopWatch(root, () => {
|
|
188
|
-
if (!
|
|
184
|
+
if (!isStopping())
|
|
189
185
|
stop(); // A drain already under way must not be turned into a force-kill.
|
|
190
186
|
}, json);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// supervisor takes its team down with it (BUGS.md 2026-09-30). The spawner hands its pid
|
|
196
|
-
// down (SUPERVISOR_PID_ENV), so a supervisor killed during this child's startup — before the
|
|
197
|
-
// watch's first ppid read — still counts as gone. Everything below runs only in
|
|
198
|
-
// the supervised generation: an unsupervised `run` became the supervisor above and returned.
|
|
199
|
-
const expectedPpid = expectedSupervisorPid();
|
|
200
|
-
startParentDeathWatch(stop, expectedPpid !== undefined ? { expectedPpid } : {});
|
|
201
|
-
const enabled = runnerIds(config);
|
|
202
|
-
// A scoped round's role list is exactly the filter; otherwise every enabled role runs.
|
|
203
|
-
const roles = roleFilter ?? enabled;
|
|
187
|
+
return { windowStart, deadlineTimer, scheduledStopWatch };
|
|
188
|
+
}
|
|
189
|
+
/** Persist the baseline verdicts and boot the once-gated redeployer and launch-services watch. */
|
|
190
|
+
async function bootGenerationParts(root, branchArg, once) {
|
|
204
191
|
// Main's green verdicts outlive this generation: the redeployer and baseline's surviving readers
|
|
205
192
|
// (`mainTipVerdict`, the ci hand-off's `mainRedHealNote`) start from the ones the previous
|
|
206
193
|
// generation verified instead of re-running the suite.
|
|
@@ -210,10 +197,13 @@ async function runSupervisedGeneration(input) {
|
|
|
210
197
|
const redeploy = once
|
|
211
198
|
? null
|
|
212
199
|
: await (await import("../redeploy/redeploy.js")).createRedeployer(root, (e) => logEvent(root, e), () => runStartupProblem(root, branchArg));
|
|
213
|
-
const build = redeploy ? ` · build ${shortSha(redeploy.build.sha)}` : "";
|
|
214
200
|
// A long-running team watches launchservicesd's port count (a no-op off macOS); a once round
|
|
215
201
|
// is over long before a leak could matter.
|
|
216
202
|
const launchServicesWatch = once ? null : new (await import("../process/launch-services.js")).LaunchServicesWatch(root);
|
|
203
|
+
return { redeploy, launchServicesWatch };
|
|
204
|
+
}
|
|
205
|
+
/** Print the supervised generation's banner and subscribe to its event stream on non-json runs. */
|
|
206
|
+
function announceGeneration(roles, mainBranch, once, json, forMs, root, build) {
|
|
217
207
|
// Name the resolved root when it differs from the cwd: an operator who started the team
|
|
218
208
|
// from a subdirectory must see where .tumwater/ actually lives.
|
|
219
209
|
const rootNote = root !== process.cwd() ? ` · root ${root}` : "";
|
|
@@ -229,38 +219,17 @@ async function runSupervisedGeneration(input) {
|
|
|
229
219
|
say(`roles: ${roles.join(", ")}`);
|
|
230
220
|
say("watch: `tumwater tui` or `tumwater logs -f` in another terminal; events stream below\n");
|
|
231
221
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const runsBefore = new Map(roles.map((role) => [role, loadRunnerState(root, role).runs]));
|
|
237
|
-
let exit;
|
|
238
|
-
try {
|
|
239
|
-
exit = await (await import("../orchestrator/orchestrator.js")).runOrchestrator({
|
|
240
|
-
root,
|
|
241
|
-
config,
|
|
242
|
-
mainBranch,
|
|
243
|
-
signal: controller.signal,
|
|
244
|
-
redeploy,
|
|
245
|
-
launchServicesWatch,
|
|
246
|
-
once,
|
|
247
|
-
roleFilter: roleFilter ?? undefined,
|
|
248
|
-
stopsAt: forMs !== null ? windowStart + forMs : undefined,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
finally {
|
|
252
|
-
if (deadlineTimer)
|
|
253
|
-
deadlineTimer.clear();
|
|
254
|
-
scheduledStopWatch.clear();
|
|
255
|
-
unsubscribe();
|
|
256
|
-
}
|
|
222
|
+
return json ? () => { } : subscribeEvents((e) => say(formatEvent(e)));
|
|
223
|
+
}
|
|
224
|
+
/** Print the round summary: the once report/summary, or a windowed run's window report. */
|
|
225
|
+
function printRoundSummary(root, roles, runsBefore, once, json, forMs, windowStart, settled, runsDone, restart) {
|
|
257
226
|
if (once) {
|
|
258
227
|
if (json)
|
|
259
|
-
sayJson(onceReport(root, roles, runsBefore,
|
|
228
|
+
sayJson(onceReport(root, roles, runsBefore, settled, runsDone));
|
|
260
229
|
else
|
|
261
|
-
say(onceSummary(root, roles, runsBefore,
|
|
230
|
+
say(onceSummary(root, roles, runsBefore, settled, runsDone));
|
|
262
231
|
}
|
|
263
|
-
else if (forMs !== null && !
|
|
232
|
+
else if (forMs !== null && !restart) {
|
|
264
233
|
const { rows, covered } = readRunRowsSince(root, Date.now() - windowStart, null);
|
|
265
234
|
const summary = summarizeRunRows(rows);
|
|
266
235
|
if (json)
|
|
@@ -268,8 +237,12 @@ async function runSupervisedGeneration(input) {
|
|
|
268
237
|
else
|
|
269
238
|
say(windowSummary(durationLabel(forMs), summary, covered));
|
|
270
239
|
}
|
|
240
|
+
}
|
|
241
|
+
/** Set the process exit code for a once round that failed or was cut short before completion. */
|
|
242
|
+
async function applyOnceExitCode(root, roles, runsBefore, runsDone, once, json, isStopping) {
|
|
243
|
+
const { ONCE_FAILED_EXIT_CODE } = await import("../process/supervisor.js");
|
|
271
244
|
if (once) {
|
|
272
|
-
const failures = onceFailures(root, roles, runsBefore,
|
|
245
|
+
const failures = onceFailures(root, roles, runsBefore, runsDone);
|
|
273
246
|
if (failures.length > 0) {
|
|
274
247
|
// In --json mode the failure facts already ride in the report's `failures`; the exit
|
|
275
248
|
// code is the contract a CI caller needs, so only the prose line is suppressed.
|
|
@@ -281,27 +254,80 @@ async function runSupervisedGeneration(input) {
|
|
|
281
254
|
// not read as success (0) to the CI caller parsing the JSON: the document still carries
|
|
282
255
|
// the round's partial facts, but the truncated round exits non-zero. Non-json once keeps
|
|
283
256
|
// its historical behavior (prose + 0 unless a role failed).
|
|
284
|
-
if (json &&
|
|
257
|
+
if (json && isStopping())
|
|
285
258
|
process.exitCode = ONCE_FAILED_EXIT_CODE;
|
|
286
259
|
}
|
|
260
|
+
}
|
|
261
|
+
/** Boot the supervised generation: the stop closure and SIGINT/SIGTERM handlers, the --for
|
|
262
|
+
* deadline timer and scheduled-stop watch, the parent-death watch, the baseline/redeploy/
|
|
263
|
+
* launch-services imports, the banner, the event subscription, runOrchestrator, and the
|
|
264
|
+
* once/window summaries and exit codes. */
|
|
265
|
+
async function runSupervisedGeneration(input) {
|
|
266
|
+
const { root, once, json, forMs, branchArg, roleFilter, config, mainBranch } = input;
|
|
267
|
+
const { startParentDeathWatch, expectedSupervisorPid } = await import("../process/supervisor.js");
|
|
268
|
+
const { controller, stop, isStopping } = installStopHandlers(json);
|
|
269
|
+
const { windowStart, deadlineTimer, scheduledStopWatch } = await armRunWatchdogs(root, json, forMs, stop, isStopping);
|
|
270
|
+
// A supervisor that dies without forwarding — SIGKILL (the OOM killer, `kill -9`) cannot be
|
|
271
|
+
// trapped or forwarded, and an uncaught supervisor crash forwards nothing either — leaves this
|
|
272
|
+
// generation reparented and running the team unattended. The parent-death watch polls for the
|
|
273
|
+
// reparent and runs the same graceful stop a forwarded SIGTERM would have run, so a killed
|
|
274
|
+
// supervisor takes its team down with it (BUGS.md 2026-09-30). The spawner hands its pid
|
|
275
|
+
// down (SUPERVISOR_PID_ENV), so a supervisor killed during this child's startup — before the
|
|
276
|
+
// watch's first ppid read — still counts as gone. Everything below runs only in
|
|
277
|
+
// the supervised generation: an unsupervised `run` became the supervisor above and returned.
|
|
278
|
+
const expectedPpid = expectedSupervisorPid();
|
|
279
|
+
startParentDeathWatch(stop, expectedPpid !== undefined ? { expectedPpid } : {});
|
|
280
|
+
const enabled = runnerIds(config);
|
|
281
|
+
// A scoped round's role list is exactly the filter; otherwise every enabled role runs.
|
|
282
|
+
const roles = roleFilter ?? enabled;
|
|
283
|
+
const { redeploy, launchServicesWatch } = await bootGenerationParts(root, branchArg, once);
|
|
284
|
+
const build = redeploy ? ` · build ${shortSha(redeploy.build.sha)}` : "";
|
|
285
|
+
const unsubscribe = announceGeneration(roles, mainBranch, once, json, forMs, root, build);
|
|
286
|
+
// Snapshot each role's run counter so the once summary can tell this round's runs from
|
|
287
|
+
// the persisted history (the state file accumulates across rounds).
|
|
288
|
+
await runStartupMigrations(root); // so the snapshot reads a migrated state/<new>.json
|
|
289
|
+
const runsBefore = new Map(roles.map((role) => [role, loadRunnerState(root, role).runs]));
|
|
290
|
+
let exit;
|
|
291
|
+
try {
|
|
292
|
+
exit = await (await import("../orchestrator/orchestrator.js")).runOrchestrator({
|
|
293
|
+
root,
|
|
294
|
+
config,
|
|
295
|
+
mainBranch,
|
|
296
|
+
signal: controller.signal,
|
|
297
|
+
redeploy,
|
|
298
|
+
launchServicesWatch,
|
|
299
|
+
once,
|
|
300
|
+
roleFilter: roleFilter ?? undefined,
|
|
301
|
+
stopsAt: forMs !== null ? windowStart + forMs : undefined,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
if (deadlineTimer)
|
|
306
|
+
deadlineTimer.clear();
|
|
307
|
+
scheduledStopWatch.clear();
|
|
308
|
+
unsubscribe();
|
|
309
|
+
}
|
|
310
|
+
printRoundSummary(root, roles, runsBefore, once, json, forMs, windowStart, exit.settled, exit.runsDone, exit.restart);
|
|
311
|
+
await applyOnceExitCode(root, roles, runsBefore, exit.runsDone, once, json, isStopping);
|
|
287
312
|
// A self-redeploy swapped the new build into dist/: hand the terminal back to the supervisor,
|
|
288
313
|
// which respawns this same script — now the new code — as the next generation.
|
|
289
314
|
if (exit.restart)
|
|
290
315
|
process.exit(RESTART_EXIT_CODE);
|
|
291
316
|
// A windowed (--for) run that did not self-redeploy is done the moment its summary is out:
|
|
292
317
|
// exit deterministically instead of trusting the event loop to drain. This supervised
|
|
293
|
-
// generation runs a whole team, and any ref'd handle a shutdown
|
|
318
|
+
// generation runs a whole team, and any ref'd handle a shutdown missed (a watcher, marker,
|
|
294
319
|
// or notifier handle a join missed) would otherwise pin this process's loop open, so the
|
|
295
320
|
// supervisor — which waits on this child's exit — never exits and the run reads as a hang:
|
|
296
321
|
// summary printed, exit code null (BUGS.md 2026-10-26). The summary above was the last
|
|
297
|
-
// output and nothing after depends on this loop, so an explicit exit is equivalent to
|
|
298
|
-
// natural
|
|
322
|
+
// output and nothing after depends on this loop, so an explicit exit is equivalent to the
|
|
323
|
+
// natural exit 0 this replaces, just guaranteed.
|
|
299
324
|
if (forMs !== null)
|
|
300
325
|
process.exit(0);
|
|
301
326
|
}
|
|
302
327
|
/** `tumwater start`: boot the team and stream its event feed to this terminal until it stops.
|
|
303
328
|
* Three layers cooperate here: without SUPERVISED_ENV this process becomes the supervisor
|
|
304
|
-
* (superviseStartCommand
|
|
329
|
+
* (src/cli/cli-start-supervisor.ts's superviseStartCommand), spawning the real orchestrator as a
|
|
330
|
+
* child generation; the child
|
|
305
331
|
* runs runOrchestrator to completion, handing the terminal back via RESTART_EXIT_CODE after a
|
|
306
332
|
* self-redeploy so the supervisor respawns on the new build; --once collapses the whole thing
|
|
307
333
|
* into one round of runs and a summary line (onceSummary below), for cron-style invocations.
|
|
@@ -379,179 +405,3 @@ export function armScheduledStopWatch(root, onStop, quiet = false) {
|
|
|
379
405
|
timer.unref();
|
|
380
406
|
return { clear: () => clearInterval(timer) };
|
|
381
407
|
}
|
|
382
|
-
/** Split `run`'s args for `run --gui` and `run --tui`: whether the supervisor should also start
|
|
383
|
-
* the dashboard and/or host the observer TUI, and the args the supervised orchestrator
|
|
384
|
-
* generation gets with both stripped so the generation's own cmdStart never tries to supervise a
|
|
385
|
-
* second dashboard or re-attach a TUI in a terminal the parent owns (a self-redeploy spawns a
|
|
386
|
-
* fresh generation each time). Exported so the unit tests pin the stripping that removing the
|
|
387
|
-
* spawn wiring would otherwise silently drop. */
|
|
388
|
-
export function guiChildPlan(runArgs) {
|
|
389
|
-
const portIdx = runArgs.indexOf("--port");
|
|
390
|
-
const portRaw = portIdx >= 0 ? runArgs[portIdx + 1] : undefined;
|
|
391
|
-
const port = portRaw !== undefined ? parsePortFlag(portRaw) : null;
|
|
392
|
-
const tokenIdx = runArgs.indexOf("--token");
|
|
393
|
-
const tokenRaw = tokenIdx >= 0 ? runArgs[tokenIdx + 1] : undefined;
|
|
394
|
-
// Drop --port/--token and their value tokens from the supervized orchestrator generation
|
|
395
|
-
// along with --gui/--tui (and --all-interfaces by name): the child's own cmdStart would
|
|
396
|
-
// otherwise hit the dashboard-only-flag refusals (its args no longer carry --gui) or supervise
|
|
397
|
-
// a second dashboard, and a self-redeploy hand-off must never forward a dashboard flag it is
|
|
398
|
-
// not serving itself. --all-interfaces and --token have no value token to skip, so the name
|
|
399
|
-
// filter below drops them; the valued pair is skipped by index like --port's.
|
|
400
|
-
const skip = new Set();
|
|
401
|
-
if (portIdx >= 0) {
|
|
402
|
-
skip.add(portIdx);
|
|
403
|
-
if (portIdx + 1 < runArgs.length)
|
|
404
|
-
skip.add(portIdx + 1);
|
|
405
|
-
}
|
|
406
|
-
if (tokenIdx >= 0) {
|
|
407
|
-
skip.add(tokenIdx);
|
|
408
|
-
if (tokenIdx + 1 < runArgs.length)
|
|
409
|
-
skip.add(tokenIdx + 1);
|
|
410
|
-
}
|
|
411
|
-
const guiArgs = [];
|
|
412
|
-
if (port !== null)
|
|
413
|
-
guiArgs.push("--port", String(port));
|
|
414
|
-
if (runArgs.includes("--all-interfaces"))
|
|
415
|
-
guiArgs.push("--all-interfaces");
|
|
416
|
-
// The token is returned separately (guiToken) and reaches the child through its env, never
|
|
417
|
-
// guiArgs/argv: a secret on the command line is world-readable (/proc/<pid>/cmdline) and is
|
|
418
|
-
// re-run verbatim by a self-reload onto each new generation. The skip set still drops the
|
|
419
|
-
// operator's --token pair from the orchestrator generation (its cmdStart would refuse a
|
|
420
|
-
// dashboard flag without --gui, and a self-redeploy hand-off must never forward it).
|
|
421
|
-
return {
|
|
422
|
-
spawnGui: runArgs.includes("--gui"),
|
|
423
|
-
spawnTui: runArgs.includes("--tui"),
|
|
424
|
-
guiArgs,
|
|
425
|
-
guiToken: tokenRaw,
|
|
426
|
-
orchestratorArgs: runArgs.filter((arg, i) => arg !== "--gui" && arg !== "--tui" && arg !== "--all-interfaces" && !skip.has(i)),
|
|
427
|
-
childStdio: runArgs.includes("--tui") ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
/** The env a supervised dashboard child is spawned with: it marks the child as owned by this
|
|
431
|
-
* supervisor (`childMarkerEnv`, DASHBOARD_CHILD_ENV, names the host pid so the child's reload
|
|
432
|
-
* watch at src/redeploy/self-reload.ts exits RESTART_EXIT_CODE / dies with the supervisor) and,
|
|
433
|
-
* when the operator gave a gate token, carries it under GUI_TOKEN_ENV. The token deliberately
|
|
434
|
-
* never reaches the child's argv: /proc/<pid>/cmdline is world-readable, and the child's own
|
|
435
|
-
* self-reload (reexecSelf, which propagates env via childEnv) would re-run a command-line secret
|
|
436
|
-
* onto each fresh generation. Kept out of superviseDashboard so this env wiring is unit-testable
|
|
437
|
-
* without spawning a live dashboard child (BUGS-FIXED.md's dashboard gate-token fix). */
|
|
438
|
-
export function dashboardChildEnv(childMarkerEnv, guiToken, hostPid) {
|
|
439
|
-
const env = { [childMarkerEnv]: String(hostPid) };
|
|
440
|
-
if (guiToken !== undefined)
|
|
441
|
-
env[GUI_TOKEN_ENV] = guiToken;
|
|
442
|
-
return env;
|
|
443
|
-
}
|
|
444
|
-
/** The dashboard half of `run --gui`: keep a `tumwater gui` child alive across its own
|
|
445
|
-
* self-reloads. The child is marked with DASHBOARD_CHILD_ENV naming this supervisor, so its
|
|
446
|
-
* reload watch (src/redeploy/self-reload.ts) exits RESTART_EXIT_CODE for a fresh sibling on a
|
|
447
|
-
* redeploy and exits on its own if this supervisor dies outright (SIGKILL, a crash) — a killed
|
|
448
|
-
* supervisor never leaves a dashboard holding its port. Any other dashboard exit (a failed bind
|
|
449
|
-
* prints its own message and exits) ends this loop and leaves the team running. */
|
|
450
|
-
async function superviseDashboard(signal, guiToken, stdio = "inherit", guiArgs = []) {
|
|
451
|
-
const { DASHBOARD_CHILD_ENV } = await import("../redeploy/self-reload.js");
|
|
452
|
-
const { spawnGuiChild } = await import("../process/supervisor.js");
|
|
453
|
-
const env = dashboardChildEnv(DASHBOARD_CHILD_ENV, guiToken, process.pid);
|
|
454
|
-
for (;;) {
|
|
455
|
-
const exit = await spawnGuiChild(signal, env, stdio, guiArgs);
|
|
456
|
-
if (signal.aborted || exit.code !== RESTART_EXIT_CODE)
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
/** The supervisor half of `tumwater start` (src/process/supervisor.ts): spawn the orchestrator as a
|
|
461
|
-
* child generation and respawn it whenever it exits RESTART_EXIT_CODE after redeploying itself.
|
|
462
|
-
* Ctrl+C reaches the child directly from the terminal, so only SIGTERM is forwarded; the
|
|
463
|
-
* supervisor's own exit code is whatever the last generation's was. A team that goes down without
|
|
464
|
-
* the operator asking leaves a `supervisor_exit` event behind (BUGS.md 2026-09-23): the dead
|
|
465
|
-
* generation's stderr reached only this terminal, which nobody may be watching. */
|
|
466
|
-
async function superviseStartCommand(root, runArgs, branchArg, forMs) {
|
|
467
|
-
// The supervisor machinery loads lazily, like cmdStart's orchestrator import above: a command
|
|
468
|
-
// that never boots the team never compiles it.
|
|
469
|
-
const { RUN_WINDOW_START_ENV, teamDownEvent, spawnStartChild, superviseStart } = await import("../process/supervisor.js");
|
|
470
|
-
const plan = guiChildPlan(runArgs);
|
|
471
|
-
// A TUI owns the terminal, so it can only be hosted when this supervisor's own stdin and
|
|
472
|
-
// stdout are a TTY — the same guard runTui enforces. Check before booting a team a TUI
|
|
473
|
-
// could never drive, surfacing the actionable tuiTerminalError instead of letting runTui's
|
|
474
|
-
// rejection sit unhandled until superviseStart settles and then crash the supervisor after
|
|
475
|
-
// the team has (pointlessly) started.
|
|
476
|
-
if (plan.spawnTui && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
477
|
-
const { tuiTerminalError } = await import("../ui/tui/tui-input.js");
|
|
478
|
-
fail(tuiTerminalError(Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY)));
|
|
479
|
-
}
|
|
480
|
-
const windowEnv = forMs !== null ? { [RUN_WINDOW_START_ENV]: String(Date.now()) } : {};
|
|
481
|
-
const controller = new AbortController();
|
|
482
|
-
// The dashboard lives beside the orchestrator generations rather than inside one: it must
|
|
483
|
-
// outlive every generation a redeploy swaps out. Its own abort stops the current dashboard
|
|
484
|
-
// child, which spawnGuiChild's abort wiring SIGTERMs.
|
|
485
|
-
const guiController = new AbortController();
|
|
486
|
-
let stopping = false;
|
|
487
|
-
process.on("SIGINT", () => {
|
|
488
|
-
stopping = true; // The child got the same SIGINT from the terminal and stops on its own.
|
|
489
|
-
});
|
|
490
|
-
process.on("SIGTERM", () => {
|
|
491
|
-
stopping = true;
|
|
492
|
-
controller.abort(); // Not delivered to the child by the kernel — forward it.
|
|
493
|
-
guiController.abort();
|
|
494
|
-
});
|
|
495
|
-
// The TUI is hosted in the supervisor process, beside the orchestrator generations it must
|
|
496
|
-
// outlive. Quitting it (q or Ctrl+C handled by ink's key handler) is the run's Ctrl+C:
|
|
497
|
-
// runTui resolves and the .then sets `stopping` and aborts the controller, so superviseStart
|
|
498
|
-
// drains in-flight runs and the team shuts down exactly as Ctrl+C on a plain `run` does;
|
|
499
|
-
// the team-down teardown (dashboard abort, onDone, terminal restore) is unchanged.
|
|
500
|
-
// reloadDisabled keeps the hosted TUI off its self-reload watch: the team's own first
|
|
501
|
-
// self-redeploy dist swap must not resolve runTui (which would read as a quit and abort
|
|
502
|
-
// this run's controller) or spawn a re-exec replacement; the host supervisor survives a
|
|
503
|
-
// redeploy by respawning its team children, and the hosted TUI — like the supervisor
|
|
504
|
-
// itself — stays on the build it started, getting fresh code on the next run. The abort
|
|
505
|
-
// signal is how this process ends the TUI when the run ends on its own (a `--for` window
|
|
506
|
-
// closes, or the team stops): runTui resolves on abort and takes its quit teardown, so the
|
|
507
|
-
// `await tui` below cannot hang on a TUI nothing else would ever close.
|
|
508
|
-
const tuiController = new AbortController();
|
|
509
|
-
const tui = plan.spawnTui
|
|
510
|
-
? (await import("../ui/tui/tui.js")).runTui(root, { abortSignal: tuiController.signal, reloadDisabled: true }).then(() => {
|
|
511
|
-
stopping = true; // the TUI quit = the run's Ctrl+C
|
|
512
|
-
controller.abort();
|
|
513
|
-
guiController.abort();
|
|
514
|
-
})
|
|
515
|
-
: Promise.resolve();
|
|
516
|
-
const gui = plan.spawnGui ? superviseDashboard(guiController.signal, plan.guiToken, plan.childStdio, plan.guiArgs) : Promise.resolve();
|
|
517
|
-
const code = await superviseStart({
|
|
518
|
-
spawnChild: (signal) => spawnStartChild(signal, plan.orchestratorArgs, windowEnv, plan.childStdio),
|
|
519
|
-
stopping: () => stopping,
|
|
520
|
-
onRespawn: (generation) => {
|
|
521
|
-
// With a hosted TUI the supervisor owns no free stdout: its own say() would paint the
|
|
522
|
-
// restart line over the ink full-screen render and corrupt it — the same interleaving
|
|
523
|
-
// childStdio silences for the children. The TUI renders status from on-disk state, so
|
|
524
|
-
// the redeploy is visible there as the team coming back.
|
|
525
|
-
if (!plan.spawnTui && !runArgs.includes("--json"))
|
|
526
|
-
say(`\nrestarting onto the update (restart ${generation})\n`);
|
|
527
|
-
},
|
|
528
|
-
onCrashLoop: () => process.stderr.write("tumwater restarted itself too many times in a minute — giving up\n"),
|
|
529
|
-
// Re-ask the startup gate for the reason: a generation that dies right after a respawn
|
|
530
|
-
// most likely met an environment that no longer boots (the 2026-09-22 child found
|
|
531
|
-
// tumwater.json gone). A crash loop's generations all asked for restarts, so the gate
|
|
532
|
-
// has nothing to say about it.
|
|
533
|
-
onTeamDown: async (down) => {
|
|
534
|
-
const event = logEvent(root, teamDownEvent(down, down.crashLoop ? null : await runStartupProblem(root, branchArg)));
|
|
535
|
-
notifyTeamDown(root, event);
|
|
536
|
-
},
|
|
537
|
-
// The end-of-supervision build repair (see build-stage.repairInterruptedSwap): a swap
|
|
538
|
-
// killed between its two renames leaves dist/ missing, and this process is the only one
|
|
539
|
-
// still able to rename the preserved old build back before it exits — the next `tumwater
|
|
540
|
-
// run` would otherwise fail to load its own dist/src/cli.js. repairInterruptedSwap is a
|
|
541
|
-
// static import, so its code is in memory before any swap can move dist/ away: an import
|
|
542
|
-
// at call time would fail ERR_MODULE_NOT_FOUND exactly when it is needed. The repair
|
|
543
|
-
// reports its own outcome, so it can never change the exit code.
|
|
544
|
-
onDone: () => repairInterruptedSwap(root),
|
|
545
|
-
}, controller.signal);
|
|
546
|
-
// The orchestrator is gone; take the dashboard and the TUI with it so a stopped team never
|
|
547
|
-
// leaves a dashboard holding its port or a TUI holding the terminal. abort() SIGTERMs the
|
|
548
|
-
// current gui child; await lets the spawn loop observe the abort before this process exits.
|
|
549
|
-
// On a plain run the user's own Ctrl+C already resolved the TUI; on a windowed (--for) run
|
|
550
|
-
// the window just closed with the TUI still up, so it is aborted here and its teardown /
|
|
551
|
-
// terminal restore settle before this process exits.
|
|
552
|
-
tuiController.abort();
|
|
553
|
-
await tui; // let the TUI's promise settle / terminal restore
|
|
554
|
-
guiController.abort();
|
|
555
|
-
await gui;
|
|
556
|
-
process.exit(code);
|
|
557
|
-
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { customRoleNames, knownRoleIds, loadConfig } from "../config/config.js";
|
|
5
5
|
import { BACKEND_IDS, BACKEND_KEYS, CLAUDE_AGENT_KEYS, CODEX_BACKEND_KEYS, MODEL_TIERS, ROLE_ENTRY_KEYS, THINKING_LEVELS, TOP_LEVEL_KEYS } from "../config/config-schema.js";
|
|
6
6
|
import { DOTTED_MAP_KEYS, DOTTED_SECTION_FIELDS } from "../config/config-keys.js";
|
|
7
|
-
import { compareCodeUnits } from "../collections.js";
|
|
7
|
+
import { compareCodeUnits, distinctOrdered } from "../collections.js";
|
|
8
8
|
import { HISTORY_MAX_RUNS, HISTORY_RESULT_FLAG_NAMES, HISTORY_SUMMARY_GROUPS, readRunRows } from "../history/history-data.js";
|
|
9
9
|
import { templateIds } from "../init/init-templates.js";
|
|
10
10
|
import { DOCTOR_CHECK_NAMES } from "../doctor/doctor-check-names.js";
|
|
@@ -13,44 +13,71 @@ import { EVENT_TYPE_LABELS } from "../events/event-labels.js";
|
|
|
13
13
|
import { POST_REVIEW_STATES } from "../review/post-merge-review-render.js";
|
|
14
14
|
import { HELP, helpStanzas } from "./help.js";
|
|
15
15
|
export const SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
|
|
16
|
-
/** The
|
|
17
|
-
* description column (two or more spaces).
|
|
18
|
-
|
|
19
|
-
* a single-space description (`role <id> --clear-notes Clear that notes`) adds nothing. */
|
|
20
|
-
function usageWords(firstLine, command) {
|
|
16
|
+
/** The usage text of a stanza first line: the part after `tumwater <command>` up to the
|
|
17
|
+
* description column (two or more spaces). */
|
|
18
|
+
function usagePortion(firstLine, command) {
|
|
21
19
|
const rest = firstLine.replace(new RegExp(`^ {2}tumwater ${command}`), "");
|
|
22
20
|
// Usage arguments follow the command after a single space; two or more spaces start the
|
|
23
21
|
// description.
|
|
24
|
-
|
|
22
|
+
return /^ {2,}/.test(rest) ? "" : (rest.split(/ {2,}/)[0] ?? "");
|
|
23
|
+
}
|
|
24
|
+
/** An angle-bracket alternation of plain words — `<bash|zsh|fish>`, the completion command's
|
|
25
|
+
* shell positional — expands to each alternative as a subcommand word, so `tumwater completion
|
|
26
|
+
* <TAB>` lists exactly the shells the command accepts. A bare placeholder like `<id>` or `<k>`
|
|
27
|
+
* carries no `|` and so stays a placeholder, skipped as before (its `<`/`>` never leave the
|
|
28
|
+
* token, so it never looked like a word). The expansion only fires while the leading run is
|
|
29
|
+
* open; the returned words contribute as the leading words of the usage part. */
|
|
30
|
+
function alternationWords(token, leading) {
|
|
31
|
+
const alternation = /^<([a-z][a-z-]*(?:\|[a-z][a-z-]*)+)>$/.exec(token);
|
|
32
|
+
return alternation && leading ? alternation[1].split("|") : null;
|
|
33
|
+
}
|
|
34
|
+
/** Whether a token counts as a completable bare word: it is a plain lowercase word and either
|
|
35
|
+
* sits inside brackets, opens a bracket itself, or is still within the leading run. */
|
|
36
|
+
function isCompletableWord(bare, token, depth, leading) {
|
|
37
|
+
return /^[a-z][a-z-]*$/.test(bare) && (depth > 0 || token.startsWith("[") || leading);
|
|
38
|
+
}
|
|
39
|
+
/** Whether a token ends the leading run: outside brackets, not opening a bracket, and not
|
|
40
|
+
* blank once its brackets and bars are stripped (so an angle-bracket placeholder closes it). */
|
|
41
|
+
function endsLeadingRun(bare, token, depth) {
|
|
42
|
+
return depth === 0 && !token.startsWith("[") && bare !== "";
|
|
43
|
+
}
|
|
44
|
+
/** The bracket-depth delta of a token: `[` +1 and `]` -1 per character, everything else 0. */
|
|
45
|
+
function bracketDelta(token) {
|
|
46
|
+
let delta = 0;
|
|
47
|
+
for (const ch of token)
|
|
48
|
+
delta += ch === "[" ? 1 : ch === "]" ? -1 : 0;
|
|
49
|
+
return delta;
|
|
50
|
+
}
|
|
51
|
+
/** The completable words of a usage text: the angle-bracket alternation expands, bare lowercase
|
|
52
|
+
* words count inside `[...]` and as the leading run directly after the command, and outside
|
|
53
|
+
* brackets the first other token ends the run. */
|
|
54
|
+
function usageWordsIn(usage) {
|
|
25
55
|
const words = [];
|
|
26
56
|
let depth = 0;
|
|
27
57
|
let leading = true;
|
|
28
58
|
for (const token of usage.split(/\s+/).filter(Boolean)) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// placeholder like `<id>` or `<k>` carries no `|` and so stays a placeholder, skipped
|
|
33
|
-
// as before (its `<`/`>` never leave the token, so it never looked like a word).
|
|
34
|
-
const alternation = /^<([a-z][a-z-]*(?:\|[a-z][a-z-]*)+)>$/.exec(token);
|
|
35
|
-
if (alternation && leading) {
|
|
36
|
-
for (const alt of alternation[1].split("|"))
|
|
37
|
-
words.push(alt);
|
|
59
|
+
const alternates = alternationWords(token, leading);
|
|
60
|
+
if (alternates) {
|
|
61
|
+
words.push(...alternates);
|
|
38
62
|
continue;
|
|
39
63
|
}
|
|
40
64
|
const bare = token.replace(/[[\]|]/g, "");
|
|
41
|
-
|
|
42
|
-
if (isWord && (depth > 0 || token.startsWith("[") || leading))
|
|
65
|
+
if (isCompletableWord(bare, token, depth, leading))
|
|
43
66
|
words.push(bare);
|
|
44
|
-
else if (
|
|
67
|
+
else if (endsLeadingRun(bare, token, depth))
|
|
45
68
|
leading = false;
|
|
46
|
-
|
|
47
|
-
depth += ch === "[" ? 1 : ch === "]" ? -1 : 0;
|
|
69
|
+
depth += bracketDelta(token);
|
|
48
70
|
}
|
|
49
71
|
return words;
|
|
50
72
|
}
|
|
73
|
+
/** The completable words of a stanza's usage part: the text after `tumwater <command>` up to the
|
|
74
|
+
* description column (two or more spaces). */
|
|
75
|
+
function usageWords(firstLine, command) {
|
|
76
|
+
return usageWordsIn(usagePortion(firstLine, command));
|
|
77
|
+
}
|
|
51
78
|
export function completionSpec(help = HELP) {
|
|
52
79
|
const stanzas = helpStanzas(help);
|
|
53
|
-
const commands =
|
|
80
|
+
const commands = distinctOrdered(stanzas.map((s) => s.command));
|
|
54
81
|
const subcommands = {};
|
|
55
82
|
const flags = {};
|
|
56
83
|
for (const command of commands) {
|
|
@@ -113,7 +140,7 @@ export function completionConfigKeys(root) {
|
|
|
113
140
|
...Object.entries(DOTTED_SECTION_FIELDS).flatMap(([section, fields]) => fields.map((field) => `${section}.${field}`)),
|
|
114
141
|
...roleIds.flatMap((id) => ROLE_ENTRY_KEYS.map((field) => `roles.${id}.${field}`)),
|
|
115
142
|
];
|
|
116
|
-
return
|
|
143
|
+
return distinctOrdered(keys);
|
|
117
144
|
}
|
|
118
145
|
/** The value vocabulary `config set <key> <value>` completes for one key, or [] when the
|
|
119
146
|
* field is free-form. The discrete-valued keys source the SAME closed constants the config
|
|
@@ -152,6 +179,15 @@ export function configValueWords(key) {
|
|
|
152
179
|
const ROLE_POSITIONAL_COMMANDS = ["role", "history", "revert"];
|
|
153
180
|
/** Flags whose value is a file path: completion offers file names after them. */
|
|
154
181
|
const PATH_FLAGS = ["--file", "--attach", "--notes-file", "--instructions-file", "--path"];
|
|
182
|
+
/** The constant fish value suffix for the flags whose candidates are a fixed `__tumwater_*` list:
|
|
183
|
+
* `--role`, `--result`, `--type` and `--status` take comma lists, so they complete per item through
|
|
184
|
+
* `__fish_complete_list`, skipping the items already listed. */
|
|
185
|
+
const FISH_LIST_FLAG_SUFFIXES = {
|
|
186
|
+
"--role": ` -x -a "(__fish_complete_list , __tumwater_roles)"`,
|
|
187
|
+
"--result": ` -x -a "(__fish_complete_list , __tumwater_results)"`,
|
|
188
|
+
"--type": ` -x -a "(__fish_complete_list , __tumwater_types)"`,
|
|
189
|
+
"--status": ` -x -a "(__fish_complete_list , __tumwater_review_states)"`,
|
|
190
|
+
};
|
|
155
191
|
/** The event-type codes `logs --type` filters by: the keys of the labels map, so a new event
|
|
156
192
|
* type completes the moment it gains a label. Sorted for a stable, alphabetical offer of a
|
|
157
193
|
* vocabulary too large to recall offhand — the same reason `--role`, `--template`, and
|
|
@@ -289,37 +325,37 @@ function bashScript(help) {
|
|
|
289
325
|
``,
|
|
290
326
|
].join("\n");
|
|
291
327
|
}
|
|
292
|
-
/** The value suffix
|
|
293
|
-
*
|
|
294
|
-
* `
|
|
295
|
-
|
|
296
|
-
function fishFlagValue(command, flag) {
|
|
297
|
-
if (flag === "--role")
|
|
298
|
-
return ` -x -a "(__fish_complete_list , __tumwater_roles)"`;
|
|
328
|
+
/** The fish value suffix for the flags whose value depends on the command: `--transcript` has role
|
|
329
|
+
* ids only under `logs`, and `--model` completes the closed tier set under `role` but the log's
|
|
330
|
+
* selectors under `history`. Undefined for a flag this helper does not own. */
|
|
331
|
+
function fishScopedFlagSuffix(command, flag) {
|
|
299
332
|
if (flag === "--transcript" && command === "logs")
|
|
300
|
-
return
|
|
301
|
-
if (PATH_FLAGS.includes(flag))
|
|
302
|
-
return ` -r -F`;
|
|
303
|
-
if (flag === "--template")
|
|
304
|
-
return ` -x -a "${templateIds().join(" ")}"`;
|
|
305
|
-
if (flag === "--result")
|
|
306
|
-
return ` -x -a "(__fish_complete_list , __tumwater_results)"`;
|
|
307
|
-
if (flag === "--type")
|
|
308
|
-
return ` -x -a "(__fish_complete_list , __tumwater_types)"`;
|
|
309
|
-
if (flag === "--status")
|
|
310
|
-
return ` -x -a "(__fish_complete_list , __tumwater_review_states)"`;
|
|
311
|
-
if (flag === "--thinking")
|
|
312
|
-
return ` -x -a "${THINKING_LEVEL_WORDS.join(" ")}"`;
|
|
313
|
-
if (flag === "--backend")
|
|
314
|
-
return ` -x -a "${BACKEND_IDS.join(" ")}"`;
|
|
315
|
-
if (flag === "--by")
|
|
316
|
-
return ` -x -a "${HISTORY_SUMMARY_GROUPS.join(" ")}"`;
|
|
317
|
-
// `role --model` completes the closed tier set; `history --model` completes the log's selectors.
|
|
333
|
+
return FISH_LIST_FLAG_SUFFIXES["--role"];
|
|
318
334
|
if (flag === "--model" && command === "role")
|
|
319
335
|
return ` -x -a "${MODEL_TIERS.join(" ")}"`;
|
|
320
336
|
if (flag === "--model" && command === "history")
|
|
321
337
|
return ` -x -a "(__fish_complete_list , __tumwater_models)"`;
|
|
322
|
-
return
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
/** The fish value suffix for the flags whose candidates are a joined literal list. Undefined for a
|
|
341
|
+
* flag this helper does not own. */
|
|
342
|
+
function fishWordSuffix(flag) {
|
|
343
|
+
switch (flag) {
|
|
344
|
+
case "--template": return ` -x -a "${templateIds().join(" ")}"`;
|
|
345
|
+
case "--thinking": return ` -x -a "${THINKING_LEVEL_WORDS.join(" ")}"`;
|
|
346
|
+
case "--backend": return ` -x -a "${BACKEND_IDS.join(" ")}"`;
|
|
347
|
+
case "--by": return ` -x -a "${HISTORY_SUMMARY_GROUPS.join(" ")}"`;
|
|
348
|
+
default: return undefined;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** The value suffix a fish flag line carries: what completes after the flag. `--role`, `--result`
|
|
352
|
+
* `--type` and `--status` take comma lists, so they complete per item through
|
|
353
|
+
* `__fish_complete_list`,
|
|
354
|
+
* skipping the items already listed. Tries the command-scoped flags, then the fixed
|
|
355
|
+
* `__tumwater_*` list table, then the joined literal-word flags, and finally the path flags, in
|
|
356
|
+
* the same order the ladder did. */
|
|
357
|
+
function fishFlagValue(command, flag) {
|
|
358
|
+
return fishScopedFlagSuffix(command, flag) ?? FISH_LIST_FLAG_SUFFIXES[flag] ?? fishWordSuffix(flag) ?? (PATH_FLAGS.includes(flag) ? " -r -F" : "");
|
|
323
359
|
}
|
|
324
360
|
function fishScript(help) {
|
|
325
361
|
const spec = completionSpec(help);
|