tumwater 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* fast on the first unmet precondition (startup-gate.ts's repoNotReady — the repo half of
|
|
3
3
|
* `tumwater run`'s startup gate). One named home beside the argument gate (cli-flag-specs.ts):
|
|
4
4
|
* the dispatcher (cli.ts), the marker commands (cli-marker-commands.ts), the write commands
|
|
5
|
-
* (backlog/backlog-
|
|
5
|
+
* (backlog/backlog-cli.ts), and the read-only command bodies (cli-query-commands.ts) all
|
|
6
6
|
* import it, so a command layer never reaches into a sibling command module for a gate. */
|
|
7
7
|
import { fail } from "./cli-output.js";
|
|
8
8
|
import { repoNotReady } from "../gates/startup-gate.js";
|
package/dist/src/cli/cli-run.js
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* other command it dispatches already delegates to a module (operator/operator-commands.ts,
|
|
4
|
-
* prompt-commands.ts, config-commands.ts, log-commands.ts, doctor.ts, …), and these three were
|
|
5
|
-
* the only implementations living in the dispatcher itself. */
|
|
6
|
-
import { increment } from "../collections.js";
|
|
7
|
-
import { loopIds } from "../roles/loop-ids.js";
|
|
1
|
+
import { compareCodeUnits, increment } from "../collections.js";
|
|
2
|
+
import { loopIds, loopPaused } from "../roles/loop-ids.js";
|
|
8
3
|
import { fail, say } from "./cli-output.js";
|
|
9
4
|
import { durationLabel, failOverDurationCap, failRivalShapes, flagValue, parseBranchFlag, parseDurationFlag, parseRoleFlag, } from "./cli-args.js";
|
|
10
5
|
import { PAUSE_FOR_MAX_MS } from "../operator/operator-intent.js";
|
|
11
6
|
import { parseInitArgs } from "./cli-command-args.js";
|
|
12
|
-
import {
|
|
13
|
-
import { orchestratorAlive, readOrchestratorInfo } from "../
|
|
7
|
+
import { cancelScheduledStop, isTeamPaused, pausedRoles, scheduledStopAt } from "../team/team-state.js";
|
|
8
|
+
import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
14
9
|
import { runStartupCheck, runStartupProblem } from "../gates/startup-gate.js";
|
|
15
10
|
import { initProject } from "../init/init.js";
|
|
16
11
|
import { templateCatalog } from "../init/init-templates.js";
|
|
17
12
|
import { repairInterruptedSwap } from "../build/build-stage.js";
|
|
18
13
|
import { logEvent, subscribeEvents } from "../events/events.js";
|
|
14
|
+
import { notifyTeamDown } from "../events/notify.js";
|
|
19
15
|
import { formatEvent } from "../events/event-format.js";
|
|
20
16
|
import { RESTART_EXIT_CODE } from "../redeploy/redeploy-policy.js";
|
|
17
|
+
import { runStartupMigrations } from "../orchestrator/startup-migration-steps.js";
|
|
21
18
|
import { loadLoopState, stateSkipReason } from "../loop/loop-state.js";
|
|
22
19
|
import { plural } from "../text/phrases.js";
|
|
23
20
|
import { shortSha } from "../text/format.js";
|
|
21
|
+
import { readTickRowsSince, summarizeTickRows } from "../history/history-data.js";
|
|
22
|
+
import { renderSummary } from "../history/history-render.js";
|
|
23
|
+
import { SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
24
24
|
/** `tumwater init`: seed a project directory from the operator's brief (src/init/init.ts does the
|
|
25
25
|
* work; this prints the report). --dry-run prints the would-be actions without writing
|
|
26
26
|
* anything; an already-initialized repo is reported as a no-op, never an error. */
|
|
@@ -60,11 +60,11 @@ export async function cmdInit(root, args) {
|
|
|
60
60
|
}
|
|
61
61
|
say(`created ${result.created.join(", ")}${result.committed ? " (committed)" : ""}`);
|
|
62
62
|
if (result.bootstrap !== null) {
|
|
63
|
-
say(`fresh project — maintenance
|
|
63
|
+
say(`fresh project — maintenance roles wait until ${result.bootstrap} plans are done; remove "bootstrap" from tumwater.json to end that early`);
|
|
64
64
|
}
|
|
65
|
-
say("next: `tumwater
|
|
65
|
+
say("next: `tumwater start` in one terminal, `tumwater tui` in another");
|
|
66
66
|
}
|
|
67
|
-
/** `tumwater run`: boot the
|
|
67
|
+
/** `tumwater run`: boot the team and stream its event feed to this terminal until it stops.
|
|
68
68
|
* Three layers cooperate here: without SUPERVISED_ENV this process becomes the supervisor
|
|
69
69
|
* (superviseRunCommand below), spawning the real orchestrator as a child generation; the child
|
|
70
70
|
* runs runOrchestrator to completion, handing the terminal back via RESTART_EXIT_CODE after a
|
|
@@ -73,14 +73,20 @@ export async function cmdInit(root, args) {
|
|
|
73
73
|
* `--for <duration>` keeps the daemon shape (redeployer, LaunchServicesWatch, supervisor hand-off
|
|
74
74
|
* all stay) but arms a deadline: at the deadline the same graceful stop a Ctrl+C runs fires, and
|
|
75
75
|
* the process exits after printing the summary. A mid-run self-redeploy hands the same args
|
|
76
|
-
* (including `--for`) to the supervisor's next child,
|
|
77
|
-
*
|
|
76
|
+
* (including `--for`) to the supervisor's next child, and the supervisor hands every generation
|
|
77
|
+
* the same window start (RUN_WINDOW_START_ENV), so the deadline and the summary span the whole
|
|
78
|
+
* window; only the final exit prints the summary. */
|
|
78
79
|
export async function cmdRun(root, args) {
|
|
79
80
|
// The one function the self-redeploy asks before swapping onto a successor and the supervisor
|
|
80
81
|
// asks when a generation dies, so the three cannot disagree about what boots. The supervisor
|
|
81
82
|
// half runs it too, so a start that cannot boot fails before any child spawns. The flag
|
|
82
83
|
// vocabulary is validated by the dispatcher (cli.ts, from the same RUN_FLAG_SPECS).
|
|
83
84
|
const once = args.includes("--once");
|
|
85
|
+
// The TUI cannot be hosted by a run that is about to stop, like the dashboard: a --once round
|
|
86
|
+
// drains and exits within the same invocation, so the two shapes are refused together before
|
|
87
|
+
// any boot. --for stays allowed — the TUI runs for the window, like the dashboard.
|
|
88
|
+
if (once && args.includes("--tui"))
|
|
89
|
+
failRivalShapes("--tui", "--once", "a one-round run cannot hold a TUI it is about to kill");
|
|
84
90
|
// The dashboard cannot serve a run that is about to stop: a --once round drains and exits
|
|
85
91
|
// within the same invocation, so the two shapes are refused together before any boot. --for
|
|
86
92
|
// stays allowed — the dashboard serves for the window.
|
|
@@ -102,23 +108,25 @@ export async function cmdRun(root, args) {
|
|
|
102
108
|
const roleFilter = parseRoleFlag(args, loopIds(config));
|
|
103
109
|
if (roleFilter !== null && !once)
|
|
104
110
|
fail("--role is only valid with --once");
|
|
105
|
-
// The refusal an operator hits by trying to start a second
|
|
111
|
+
// The refusal an operator hits by trying to start a second team in one repo: name the
|
|
106
112
|
// running pid and the two ways to stop it, so the fix is in the message instead of a
|
|
107
113
|
// separate `tumwater status`/help lookup. The info file is read once and reused for the
|
|
108
114
|
// liveness check, like snapshot's own poll.
|
|
109
115
|
const running = readOrchestratorInfo(root);
|
|
110
116
|
if (running !== null && orchestratorAlive(root, running))
|
|
111
117
|
fail(`an orchestrator is already running for this repo (pid ${running.pid}) — stop it with \`tumwater stop\` from another terminal, or press Ctrl+C in the terminal that started it`);
|
|
112
|
-
// The
|
|
113
|
-
// module graph (with its
|
|
118
|
+
// The team-booting machinery loads only once a run actually boots: the orchestrator's
|
|
119
|
+
// module graph (with its merge pipeline, review gate, and scheduler) is the CLI's heaviest
|
|
114
120
|
// compile, and the supervisor/redeploy/launch-services pieces ride beside it. Loading them
|
|
115
121
|
// dynamically, after the fail-fast gates above, keeps every other command's spawn — and
|
|
116
122
|
// `run`'s fail-fast paths — off that cost (see cli.ts's matching lazy dispatch).
|
|
117
|
-
const { SUPERVISED_ENV, startParentDeathWatch, expectedSupervisorPid } = await import("../process/supervisor.js");
|
|
123
|
+
const { SUPERVISED_ENV, RUN_WINDOW_START_ENV, ONCE_FAILED_EXIT_CODE, startParentDeathWatch, expectedSupervisorPid } = await import("../process/supervisor.js");
|
|
118
124
|
if (!process.env[SUPERVISED_ENV]) {
|
|
119
125
|
// `args` are the flags after the command token — forward them so the child generation
|
|
120
126
|
// targets the same branch (or whatever else the invocation named).
|
|
121
|
-
|
|
127
|
+
// A stop marker left by a team that died before its deadline must not stop this run.
|
|
128
|
+
cancelScheduledStop(root);
|
|
129
|
+
await superviseRunCommand(root, args, branchArg, forMs);
|
|
122
130
|
return;
|
|
123
131
|
}
|
|
124
132
|
const controller = new AbortController();
|
|
@@ -127,7 +135,7 @@ export async function cmdRun(root, args) {
|
|
|
127
135
|
if (stopping)
|
|
128
136
|
process.exit(130);
|
|
129
137
|
stopping = true;
|
|
130
|
-
say("\nstopping —
|
|
138
|
+
say("\nstopping — finishing current runs (Ctrl+C again to force)");
|
|
131
139
|
controller.abort();
|
|
132
140
|
};
|
|
133
141
|
process.on("SIGINT", stop);
|
|
@@ -135,15 +143,20 @@ export async function cmdRun(root, args) {
|
|
|
135
143
|
// A windowed run's deadline arms the same graceful stop a Ctrl+C runs — the identical `stop`
|
|
136
144
|
// closure, so the drain path is one code path either way. clearTimeout in the finally below
|
|
137
145
|
// retracts it when the run ends first (an early Ctrl+C), so no stray timer outlives the run.
|
|
138
|
-
const
|
|
146
|
+
const windowStart = inheritedWindowStart(process.env[RUN_WINDOW_START_ENV], Date.now());
|
|
147
|
+
const deadlineTimer = forMs !== null ? armDeadlineTimer(Math.max(0, windowStart + forMs - Date.now()), () => {
|
|
139
148
|
say("deadline reached — stopping");
|
|
140
149
|
stop();
|
|
141
150
|
}) : null;
|
|
151
|
+
const scheduledStopWatch = armScheduledStopWatch(root, () => {
|
|
152
|
+
if (!stopping)
|
|
153
|
+
stop(); // A drain already under way must not be turned into a force-kill.
|
|
154
|
+
});
|
|
142
155
|
// A supervisor that dies without forwarding — SIGKILL (the OOM killer, `kill -9`) cannot be
|
|
143
156
|
// trapped or forwarded, and an uncaught supervisor crash forwards nothing either — leaves this
|
|
144
|
-
// generation reparented and ticking the
|
|
157
|
+
// generation reparented and ticking the team unattended. The parent-death watch polls for the
|
|
145
158
|
// reparent and runs the same graceful stop a forwarded SIGTERM would have run, so a killed
|
|
146
|
-
// supervisor takes its
|
|
159
|
+
// supervisor takes its team down with it (BUGS.md 2026-09-30). The spawner hands its pid
|
|
147
160
|
// down (SUPERVISOR_PID_ENV), so a supervisor killed during this child's startup — before the
|
|
148
161
|
// watch's first ppid read — still counts as gone. Everything below runs only in
|
|
149
162
|
// the supervised generation: an unsupervised `run` became the supervisor above and returned.
|
|
@@ -161,22 +174,23 @@ export async function cmdRun(root, args) {
|
|
|
161
174
|
? null
|
|
162
175
|
: await (await import("../redeploy/redeploy.js")).createRedeployer(root, (e) => logEvent(root, e), () => runStartupProblem(root, branchArg));
|
|
163
176
|
const build = redeploy ? ` · build ${shortSha(redeploy.build.sha)}` : "";
|
|
164
|
-
// A long-running
|
|
177
|
+
// A long-running team watches launchservicesd's port count (a no-op off macOS); a once round
|
|
165
178
|
// is over long before a leak could matter.
|
|
166
179
|
const launchServicesWatch = once ? null : new (await import("../process/launch-services.js")).LaunchServicesWatch(root);
|
|
167
|
-
// Name the resolved root when it differs from the cwd: an operator who started the
|
|
180
|
+
// Name the resolved root when it differs from the cwd: an operator who started the team
|
|
168
181
|
// from a subdirectory must see where .tumwater/ actually lives.
|
|
169
182
|
const rootNote = root !== process.cwd() ? ` · root ${root}` : "";
|
|
170
183
|
const forNote = forMs !== null ? ` · for ${durationLabel(forMs)}` : "";
|
|
171
184
|
say(once
|
|
172
185
|
? `tumwater once on branch ${mainBranch}${rootNote} — one round, then exit`
|
|
173
186
|
: `tumwater running on branch ${mainBranch}${forNote}${build}${rootNote} — Ctrl+C to stop`);
|
|
174
|
-
say(`
|
|
187
|
+
say(`roles: ${roles.join(", ")}`);
|
|
175
188
|
say("watch: `tumwater tui` or `tumwater logs -f` in another terminal; events stream below\n");
|
|
176
189
|
const unsubscribe = subscribeEvents((e) => say(formatEvent(e)));
|
|
177
190
|
// Snapshot each role's tick counter so the once summary can tell this round's ticks from
|
|
178
191
|
// the persisted history (the state file accumulates across rounds).
|
|
179
|
-
|
|
192
|
+
await runStartupMigrations(root); // so the snapshot reads a migrated state/<new>.json
|
|
193
|
+
const ticksBefore = new Map(roles.map((role) => [role, loadLoopState(root, role).runs]));
|
|
180
194
|
let exit;
|
|
181
195
|
try {
|
|
182
196
|
exit = await (await import("../orchestrator/orchestrator.js")).runOrchestrator({
|
|
@@ -188,20 +202,56 @@ export async function cmdRun(root, args) {
|
|
|
188
202
|
launchServicesWatch,
|
|
189
203
|
once,
|
|
190
204
|
roleFilter: roleFilter ?? undefined,
|
|
205
|
+
stopsAt: forMs !== null ? windowStart + forMs : undefined,
|
|
191
206
|
});
|
|
192
207
|
}
|
|
193
208
|
finally {
|
|
194
209
|
if (deadlineTimer)
|
|
195
210
|
deadlineTimer.clear();
|
|
211
|
+
scheduledStopWatch.clear();
|
|
196
212
|
unsubscribe();
|
|
197
213
|
}
|
|
198
|
-
if (once
|
|
214
|
+
if (once)
|
|
199
215
|
say(onceSummary(root, roles, ticksBefore, exit.settled, exit.ticksRun));
|
|
216
|
+
else if (forMs !== null && !exit.restart) {
|
|
217
|
+
const { rows, covered } = readTickRowsSince(root, Date.now() - windowStart, null);
|
|
218
|
+
say(windowSummary(durationLabel(forMs), summarizeTickRows(rows), covered));
|
|
219
|
+
}
|
|
220
|
+
if (once) {
|
|
221
|
+
const failures = onceFailures(root, roles, ticksBefore, exit.ticksRun);
|
|
222
|
+
if (failures.length > 0) {
|
|
223
|
+
say(`once: failed — ${failures.map((f) => `${f.role} ${f.result}`).join(", ")}`);
|
|
224
|
+
process.exitCode = ONCE_FAILED_EXIT_CODE;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
200
227
|
// A self-redeploy swapped the new build into dist/: hand the terminal back to the supervisor,
|
|
201
228
|
// which respawns this same script — now the new code — as the next generation.
|
|
202
229
|
if (exit.restart)
|
|
203
230
|
process.exit(RESTART_EXIT_CODE);
|
|
204
231
|
}
|
|
232
|
+
/** The window start a generation adopts: the supervisor's RUN_WINDOW_START_ENV mark when it is a
|
|
233
|
+
* positive integer no later than `now`, else `now` (absent, malformed, or in the future). */
|
|
234
|
+
export function inheritedWindowStart(raw, now) {
|
|
235
|
+
const n = Number(raw);
|
|
236
|
+
return raw !== undefined && Number.isInteger(n) && n > 0 && n <= now ? n : now;
|
|
237
|
+
}
|
|
238
|
+
/** The `run --for` exit summary: a window line, then the `history --summary` table for the ticks
|
|
239
|
+
* that ended since the window opened — every tick counted once by its outcome class. Only the
|
|
240
|
+
* final exit prints it; a mid-window self-redeploy hands off silently and the next generation
|
|
241
|
+
* inherits the window start, so the table spans every generation. */
|
|
242
|
+
export function windowSummary(label, summary, covered) {
|
|
243
|
+
const { total } = summary;
|
|
244
|
+
const lines = [
|
|
245
|
+
total.runs === 0
|
|
246
|
+
? `for ${label}: 0 runs — nothing ran`
|
|
247
|
+
: `for ${label}: ${plural(total.runs, "run")} — ${total.changes} changes, ${total.none} none, ${total.refused} refused, ${total.problems} problems`,
|
|
248
|
+
];
|
|
249
|
+
if (total.runs > 0)
|
|
250
|
+
lines.push(renderSummary(summary));
|
|
251
|
+
if (!covered)
|
|
252
|
+
lines.push(SPARSE_WINDOW_NOTE);
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
205
255
|
/** Arm the windowed run's deadline timer: the one home of the arming `cmdRun` does for
|
|
206
256
|
* `run --for <duration>`. A duration over setTimeout's own 32-bit signed ceiling
|
|
207
257
|
* (2_147_483_647 ms ≈ 24.8 days) is clamped by Node to 1ms with a TimeoutOverflowWarning, so
|
|
@@ -228,6 +278,23 @@ export function armDeadlineTimer(ms, onFired) {
|
|
|
228
278
|
arm(ms);
|
|
229
279
|
return { clear: () => clearTimeout(timer) };
|
|
230
280
|
}
|
|
281
|
+
const SCHEDULED_STOP_POLL_MS = 5000;
|
|
282
|
+
/** Poll for a `tumwater stop --in` marker: once its `until` has passed, consume the marker,
|
|
283
|
+
* stop polling, and call `onStop` (cmdRun's Ctrl+C closure) exactly once. The interval is
|
|
284
|
+
* unref'd; clear() retracts it. Exported for tests. */
|
|
285
|
+
export function armScheduledStopWatch(root, onStop) {
|
|
286
|
+
const timer = setInterval(() => {
|
|
287
|
+
const at = scheduledStopAt(root);
|
|
288
|
+
if (at === undefined || at > Date.now())
|
|
289
|
+
return;
|
|
290
|
+
clearInterval(timer);
|
|
291
|
+
cancelScheduledStop(root);
|
|
292
|
+
say("scheduled stop reached — stopping");
|
|
293
|
+
onStop();
|
|
294
|
+
}, SCHEDULED_STOP_POLL_MS);
|
|
295
|
+
timer.unref();
|
|
296
|
+
return { clear: () => clearInterval(timer) };
|
|
297
|
+
}
|
|
231
298
|
/** Parse `run --for <duration>` (the windowed run): the parsed duration in milliseconds, or null
|
|
232
299
|
* when the flag is absent. The two body-level rules fail fast, before any repo gate or boot: the
|
|
233
300
|
* value must parse as a duration (parseDurationFlag's own wording, the same messages the
|
|
@@ -239,12 +306,49 @@ export function parseRunWindow(args, once) {
|
|
|
239
306
|
const raw = flagValue(args, "--for");
|
|
240
307
|
if (raw === null)
|
|
241
308
|
return null;
|
|
242
|
-
const ms = parseDurationFlag("
|
|
309
|
+
const ms = parseDurationFlag("start --for", raw);
|
|
243
310
|
if (once)
|
|
244
311
|
failRivalShapes("--for", "--once", "a one-round run and a windowed run cannot both apply");
|
|
245
312
|
failOverDurationCap("--for", ms, PAUSE_FOR_MAX_MS);
|
|
246
313
|
return ms;
|
|
247
314
|
}
|
|
315
|
+
/** Results that mean the harness or the project's check failed and the round's work did not
|
|
316
|
+
* merge: a `--once` round with a role ending in one exits ONCE_FAILED_EXIT_CODE. `rejected` and
|
|
317
|
+
* `refused` are the review gate and the agent working as designed; `aborted`, `quiet_killed`, and
|
|
318
|
+
* `user_aborted` only reach a once round on a shutdown or operator abort, which exit
|
|
319
|
+
* their own way. */
|
|
320
|
+
export const ONCE_FAILURE_RESULTS = new Set([
|
|
321
|
+
"error",
|
|
322
|
+
"review_error",
|
|
323
|
+
"merge_conflict",
|
|
324
|
+
"merge_blocked",
|
|
325
|
+
"main_red",
|
|
326
|
+
]);
|
|
327
|
+
function ticksThisRound(s, role, ticksBefore, ticksRun) {
|
|
328
|
+
return ticksRun ? (ticksRun.get(role) ?? 0) : s.runs - (ticksBefore.get(role) ?? 0);
|
|
329
|
+
}
|
|
330
|
+
/** Every role that could have ticked in this once round — `roles` in order plus any role that
|
|
331
|
+
* appears only in `ticksRun` (enabled mid-round after `ticksBefore` was taken) — paired with
|
|
332
|
+
* its loaded loop state and the number of ticks it ran this round. The same per-role setup
|
|
333
|
+
* onceFailures and onceSummary both repeat, folded here so the two summary passes cannot
|
|
334
|
+
* drift on which roles they visit or how they count a round's work. */
|
|
335
|
+
function onceRoundRoles(root, roles, ticksBefore, ticksRun) {
|
|
336
|
+
const extra = ticksRun ? [...ticksRun.keys()].filter((r) => !roles.includes(r)) : [];
|
|
337
|
+
return [...roles, ...extra].map((role) => {
|
|
338
|
+
const state = loadLoopState(root, role);
|
|
339
|
+
return { role, state, ran: ticksThisRound(state, role, ticksBefore, ticksRun) };
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
/** The roles that ticked this round and whose last result is in ONCE_FAILURE_RESULTS. */
|
|
343
|
+
export function onceFailures(root, roles, ticksBefore, ticksRun) {
|
|
344
|
+
const out = [];
|
|
345
|
+
for (const { role, state, ran } of onceRoundRoles(root, roles, ticksBefore, ticksRun)) {
|
|
346
|
+
if (ran > 0 && state.lastResult && ONCE_FAILURE_RESULTS.has(state.lastResult)) {
|
|
347
|
+
out.push({ role, result: state.lastResult });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
248
352
|
/** The once round's one-line summary, read from the runners' persisted loop state plus the
|
|
249
353
|
* orchestrator's own settle reasons — a cron job's log shows what the round did without parsing
|
|
250
354
|
* events. Roles whose tick counter advanced bucket by their last completed result; the rest are
|
|
@@ -268,20 +372,17 @@ export function onceSummary(root, roles, ticksBefore, settled, ticksRun) {
|
|
|
268
372
|
let ticks = 0;
|
|
269
373
|
const outcomes = new Map();
|
|
270
374
|
const skipped = [];
|
|
271
|
-
const
|
|
272
|
-
for (const role of [...roles, ...extra]) {
|
|
273
|
-
const s = loadLoopState(root, role);
|
|
274
|
-
const ran = ticksRun ? (ticksRun.get(role) ?? 0) : s.ticks - (ticksBefore.get(role) ?? 0);
|
|
375
|
+
for (const { role, state, ran } of onceRoundRoles(root, roles, ticksBefore, ticksRun)) {
|
|
275
376
|
if (ran > 0) {
|
|
276
377
|
ticks += ran;
|
|
277
|
-
const key =
|
|
378
|
+
const key = state.lastResult ?? "no_change";
|
|
278
379
|
increment(outcomes, key);
|
|
279
380
|
}
|
|
280
381
|
else {
|
|
281
382
|
const reason = settled?.get(role) ??
|
|
282
|
-
(
|
|
383
|
+
(isTeamPaused(root) || loopPaused(new Set(pausedRoles(root)), role)
|
|
283
384
|
? "paused"
|
|
284
|
-
: stateSkipReason(
|
|
385
|
+
: stateSkipReason(state));
|
|
285
386
|
skipped.push(reason);
|
|
286
387
|
}
|
|
287
388
|
}
|
|
@@ -289,20 +390,24 @@ export function onceSummary(root, roles, ticksBefore, settled, ticksRun) {
|
|
|
289
390
|
// would order `[key, count]` tuples by their "key,count" string form, which happens to
|
|
290
391
|
// agree only while each key is unique.
|
|
291
392
|
const counts = [...outcomes.entries()]
|
|
292
|
-
.sort(([a], [b]) => (a
|
|
393
|
+
.sort(([a], [b]) => compareCodeUnits(a, b))
|
|
293
394
|
.map(([k, n]) => `${n} ${k}`)
|
|
294
395
|
.join(", ");
|
|
295
396
|
const skipNote = skipped.length === 0 ? "" : `, ${skipped.length} skipped (${skipped.join(", ")})`;
|
|
296
|
-
return `once: ${plural(ticks, "
|
|
397
|
+
return `once: ${plural(ticks, "run")} — ${counts || "nothing ran"}${skipNote}`;
|
|
297
398
|
}
|
|
298
|
-
/** Split `run`'s args for `run --gui`: whether the supervisor should also start
|
|
299
|
-
* and
|
|
300
|
-
* generation's own cmdRun never tries to supervise a
|
|
301
|
-
*
|
|
399
|
+
/** Split `run`'s args for `run --gui` and `run --tui`: whether the supervisor should also start
|
|
400
|
+
* the dashboard and/or host the observer TUI, and the args the supervised orchestrator
|
|
401
|
+
* generation gets with both stripped so the generation's own cmdRun never tries to supervise a
|
|
402
|
+
* second dashboard or re-attach a TUI in a terminal the parent owns (a self-redeploy spawns a
|
|
403
|
+
* fresh generation each time). Exported so the unit tests pin the stripping that removing the
|
|
404
|
+
* spawn wiring would otherwise silently drop. */
|
|
302
405
|
export function guiChildPlan(runArgs) {
|
|
303
406
|
return {
|
|
304
407
|
spawnGui: runArgs.includes("--gui"),
|
|
305
|
-
|
|
408
|
+
spawnTui: runArgs.includes("--tui"),
|
|
409
|
+
orchestratorArgs: runArgs.filter((arg) => arg !== "--gui" && arg !== "--tui"),
|
|
410
|
+
childStdio: runArgs.includes("--tui") ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
306
411
|
};
|
|
307
412
|
}
|
|
308
413
|
/** The dashboard half of `run --gui`: keep a `tumwater gui` child alive across its own
|
|
@@ -310,12 +415,12 @@ export function guiChildPlan(runArgs) {
|
|
|
310
415
|
* reload watch (src/redeploy/self-reload.ts) exits RESTART_EXIT_CODE for a fresh sibling on a
|
|
311
416
|
* redeploy and exits on its own if this supervisor dies outright (SIGKILL, a crash) — a killed
|
|
312
417
|
* supervisor never leaves a dashboard holding its port. Any other dashboard exit (a failed bind
|
|
313
|
-
* prints its own message and exits) ends this loop and leaves the
|
|
314
|
-
async function superviseDashboard(signal) {
|
|
418
|
+
* prints its own message and exits) ends this loop and leaves the team running. */
|
|
419
|
+
async function superviseDashboard(signal, stdio = "inherit") {
|
|
315
420
|
const { DASHBOARD_CHILD_ENV } = await import("../redeploy/self-reload.js");
|
|
316
421
|
const { spawnGuiChild } = await import("../process/supervisor.js");
|
|
317
422
|
for (;;) {
|
|
318
|
-
const exit = await spawnGuiChild(signal, { [DASHBOARD_CHILD_ENV]: String(process.pid) });
|
|
423
|
+
const exit = await spawnGuiChild(signal, { [DASHBOARD_CHILD_ENV]: String(process.pid) }, stdio);
|
|
319
424
|
if (signal.aborted || exit.code !== RESTART_EXIT_CODE)
|
|
320
425
|
return;
|
|
321
426
|
}
|
|
@@ -323,14 +428,24 @@ async function superviseDashboard(signal) {
|
|
|
323
428
|
/** The supervisor half of `tumwater run` (src/process/supervisor.ts): spawn the orchestrator as a
|
|
324
429
|
* child generation and respawn it whenever it exits RESTART_EXIT_CODE after redeploying itself.
|
|
325
430
|
* Ctrl+C reaches the child directly from the terminal, so only SIGTERM is forwarded; the
|
|
326
|
-
* supervisor's own exit code is whatever the last generation's was. A
|
|
431
|
+
* supervisor's own exit code is whatever the last generation's was. A team that goes down without
|
|
327
432
|
* the operator asking leaves a `supervisor_exit` event behind (BUGS.md 2026-09-23): the dead
|
|
328
433
|
* generation's stderr reached only this terminal, which nobody may be watching. */
|
|
329
|
-
async function superviseRunCommand(root, runArgs, branchArg) {
|
|
434
|
+
async function superviseRunCommand(root, runArgs, branchArg, forMs) {
|
|
330
435
|
// The supervisor machinery loads lazily, like cmdRun's orchestrator import above: a command
|
|
331
|
-
// that never boots the
|
|
332
|
-
const {
|
|
436
|
+
// that never boots the team never compiles it.
|
|
437
|
+
const { RUN_WINDOW_START_ENV, teamDownEvent, spawnRunChild, superviseRun } = await import("../process/supervisor.js");
|
|
333
438
|
const plan = guiChildPlan(runArgs);
|
|
439
|
+
// A TUI owns the terminal, so it can only be hosted when this supervisor's own stdin and
|
|
440
|
+
// stdout are a TTY — the same guard runTui enforces. Check before booting a team a TUI
|
|
441
|
+
// could never drive, surfacing the actionable tuiTerminalError instead of letting runTui's
|
|
442
|
+
// rejection sit unhandled until superviseRun settles and then crash the supervisor after
|
|
443
|
+
// the team has (pointlessly) started.
|
|
444
|
+
if (plan.spawnTui && (!process.stdin.isTTY || !process.stdout.isTTY)) {
|
|
445
|
+
const { tuiTerminalError } = await import("../ui/tui/tui-input.js");
|
|
446
|
+
fail(tuiTerminalError(Boolean(process.stdin.isTTY), Boolean(process.stdout.isTTY)));
|
|
447
|
+
}
|
|
448
|
+
const windowEnv = forMs !== null ? { [RUN_WINDOW_START_ENV]: String(Date.now()) } : {};
|
|
334
449
|
const controller = new AbortController();
|
|
335
450
|
// The dashboard lives beside the orchestrator generations rather than inside one: it must
|
|
336
451
|
// outlive every generation a redeploy swaps out. Its own abort stops the current dashboard
|
|
@@ -345,18 +460,47 @@ async function superviseRunCommand(root, runArgs, branchArg) {
|
|
|
345
460
|
controller.abort(); // Not delivered to the child by the kernel — forward it.
|
|
346
461
|
guiController.abort();
|
|
347
462
|
});
|
|
348
|
-
|
|
463
|
+
// The TUI is hosted in the supervisor process, beside the orchestrator generations it must
|
|
464
|
+
// outlive. Quitting it (q or Ctrl+C handled by ink's key handler) is the run's Ctrl+C:
|
|
465
|
+
// runTui resolves and the .then sets `stopping` and aborts the controller, so superviseRun
|
|
466
|
+
// drains in-flight ticks and the team shuts down exactly as Ctrl+C on a plain `run` does;
|
|
467
|
+
// the team-down teardown (dashboard abort, onDone, terminal restore) is unchanged.
|
|
468
|
+
// reloadDisabled keeps the hosted TUI off its self-reload watch: the team's own first
|
|
469
|
+
// self-redeploy dist swap must not resolve runTui (which would read as a quit and abort
|
|
470
|
+
// this run's controller) or spawn a re-exec replacement; the host supervisor survives a
|
|
471
|
+
// redeploy by respawning its team children, and the hosted TUI — like the supervisor
|
|
472
|
+
// itself — stays on the build it started, getting fresh code on the next run. The abort
|
|
473
|
+
// signal is how this process ends the TUI when the run ends on its own (a `--for` window
|
|
474
|
+
// closes, or the team stops): runTui resolves on abort and takes its quit teardown, so the
|
|
475
|
+
// `await tui` below cannot hang on a TUI nothing else would ever close.
|
|
476
|
+
const tuiController = new AbortController();
|
|
477
|
+
const tui = plan.spawnTui
|
|
478
|
+
? (await import("../ui/tui/tui.js")).runTui(root, { abortSignal: tuiController.signal, reloadDisabled: true }).then(() => {
|
|
479
|
+
stopping = true; // the TUI quit = the run's Ctrl+C
|
|
480
|
+
controller.abort();
|
|
481
|
+
guiController.abort();
|
|
482
|
+
})
|
|
483
|
+
: Promise.resolve();
|
|
484
|
+
const gui = plan.spawnGui ? superviseDashboard(guiController.signal, plan.childStdio) : Promise.resolve();
|
|
349
485
|
const code = await superviseRun({
|
|
350
|
-
spawnChild: (signal) => spawnRunChild(signal, plan.orchestratorArgs),
|
|
486
|
+
spawnChild: (signal) => spawnRunChild(signal, plan.orchestratorArgs, windowEnv, plan.childStdio),
|
|
351
487
|
stopping: () => stopping,
|
|
352
|
-
onRespawn: (generation) =>
|
|
353
|
-
|
|
488
|
+
onRespawn: (generation) => {
|
|
489
|
+
// With a hosted TUI the supervisor owns no free stdout: its own say() would paint the
|
|
490
|
+
// restart line over the ink full-screen render and corrupt it — the same interleaving
|
|
491
|
+
// childStdio silences for the children. The TUI renders status from on-disk state, so
|
|
492
|
+
// the redeploy is visible there as the team coming back.
|
|
493
|
+
if (!plan.spawnTui)
|
|
494
|
+
say(`\nrestarting onto the update (restart ${generation})\n`);
|
|
495
|
+
},
|
|
496
|
+
onCrashLoop: () => process.stderr.write("tumwater restarted itself too many times in a minute — giving up\n"),
|
|
354
497
|
// Re-ask the startup gate for the reason: a generation that dies right after a respawn
|
|
355
498
|
// most likely met an environment that no longer boots (the 2026-09-22 child found
|
|
356
499
|
// tumwater.json gone). A crash loop's generations all asked for restarts, so the gate
|
|
357
500
|
// has nothing to say about it.
|
|
358
|
-
|
|
359
|
-
logEvent(root,
|
|
501
|
+
onTeamDown: async (down) => {
|
|
502
|
+
const event = logEvent(root, teamDownEvent(down, down.crashLoop ? null : await runStartupProblem(root, branchArg)));
|
|
503
|
+
notifyTeamDown(root, event);
|
|
360
504
|
},
|
|
361
505
|
// The end-of-supervision build repair (see build-stage.repairInterruptedSwap): a swap
|
|
362
506
|
// killed between its two renames leaves dist/ missing, and this process is the only one
|
|
@@ -367,9 +511,14 @@ async function superviseRunCommand(root, runArgs, branchArg) {
|
|
|
367
511
|
// reports its own outcome, so it can never change the exit code.
|
|
368
512
|
onDone: () => repairInterruptedSwap(root),
|
|
369
513
|
}, controller.signal);
|
|
370
|
-
// The orchestrator is gone; take the dashboard with it so a stopped
|
|
371
|
-
// dashboard holding its port
|
|
372
|
-
// loop observe the abort before this process exits.
|
|
514
|
+
// The orchestrator is gone; take the dashboard and the TUI with it so a stopped team never
|
|
515
|
+
// leaves a dashboard holding its port or a TUI holding the terminal. abort() SIGTERMs the
|
|
516
|
+
// current gui child; await lets the spawn loop observe the abort before this process exits.
|
|
517
|
+
// On a plain run the user's own Ctrl+C already resolved the TUI; on a windowed (--for) run
|
|
518
|
+
// the window just closed with the TUI still up, so it is aborted here and its teardown /
|
|
519
|
+
// terminal restore settle before this process exits.
|
|
520
|
+
tuiController.abort();
|
|
521
|
+
await tui; // let the TUI's promise settle / terminal restore
|
|
373
522
|
guiController.abort();
|
|
374
523
|
await gui;
|
|
375
524
|
process.exit(code);
|