tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -16,7 +16,7 @@ import { renderStatus } from "../ui/status-render.js";
|
|
|
16
16
|
import { backlogPayload } from "../backlog/backlog.js";
|
|
17
17
|
import { backlogHolds, backlogInProgress } from "../backlog/backlog-eligibility.js";
|
|
18
18
|
import { readyBacklog, grepBacklog, renderBacklogMarkdown } from "../backlog/backlog-render.js";
|
|
19
|
-
import { loadQuarantine } from "../review/known-flakes.js";
|
|
19
|
+
import { loadQuarantine, releaseQuarantinedTest } from "../review/known-flakes.js";
|
|
20
20
|
import { quarantineRows, renderQuarantine } from "../review/quarantine-render.js";
|
|
21
21
|
import { collectChangeOverlaps, scopeChangeOverlaps, scopeChangeOverlapsToPath, collectTeamChanges, collectRoleChange, collectRoleChanges, normalizeDiffPath } from "../change/change-data.js";
|
|
22
22
|
import { renderChangeOverlaps, renderTeamChange, renderRoleChange, renderRoleChanges } from "../change/change-render.js";
|
|
@@ -338,14 +338,49 @@ export async function cmdBacklog(root, args, seams = {}) {
|
|
|
338
338
|
const result = readBacklog();
|
|
339
339
|
sayJsonOrRender(rest, result.view, () => renderBacklogText(result));
|
|
340
340
|
}
|
|
341
|
-
// `tumwater quarantine [--json]` — the
|
|
342
|
-
// quarantine
|
|
343
|
-
//
|
|
344
|
-
//
|
|
345
|
-
//
|
|
341
|
+
// `tumwater quarantine [--release <n>] [--json]` — the view of the review gate's flake
|
|
342
|
+
// quarantine, read-only except the one destructive `--release <n>` mode that removes the Nth
|
|
343
|
+
// entry the list prints at the operator's request. The read path keeps its no-gate
|
|
344
|
+
// refuseTeamWorkspace stance: loadQuarantine degrades an absent or malformed file to the empty
|
|
345
|
+
// list, so the command prints the empty line in any directory and the read never writes
|
|
346
|
+
// quarantine.json (a test otherwise leaves the list through releaseQuarantine). --json serves
|
|
347
|
+
// the same positioned rows to scripts.
|
|
346
348
|
export async function cmdQuarantine(root, args) {
|
|
349
|
+
// `--release <n>` is the read-only command's one destructive mode: route it to its own
|
|
350
|
+
// branch before the read gate, exactly as cmdReviews routes `--accept`.
|
|
351
|
+
if (args.includes("--release")) {
|
|
352
|
+
await cmdQuarantineRelease(root, args);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
347
355
|
rejectUnknownArgs("quarantine", args, [JSON_FLAG]);
|
|
348
356
|
refuseTeamWorkspace(root);
|
|
349
357
|
const rows = quarantineRows(loadQuarantine(root));
|
|
350
358
|
sayJsonOrRender(args, { tests: rows }, () => renderQuarantine(rows, Date.now()));
|
|
351
359
|
}
|
|
360
|
+
/** The `tumwater quarantine --release <n>` branch: remove the Nth entry `tumwater quarantine`
|
|
361
|
+
* lists (the same oldest-first 1..N row `quarantineRows` prints) at the operator's request,
|
|
362
|
+
* the manual twin of the auto-release that fires when a filed bug leaves `## Open`. Parsed the
|
|
363
|
+
* way cmdReviewsAccept parses `--accept`: `--release <n>` the one valued flag, `--json` the one
|
|
364
|
+
* boolean (at most once), `--help`/`-h` tolerated, anything else refused. An out-of-range
|
|
365
|
+
* position fails naming the current depth; a concurrent removal answers `false`. */
|
|
366
|
+
async function cmdQuarantineRelease(root, args) {
|
|
367
|
+
const { json, position, rest } = scanPositionArgs(args, "--release", "--release 2");
|
|
368
|
+
rejectUnknownArgs("quarantine", rest, []);
|
|
369
|
+
if (position === null)
|
|
370
|
+
fail("--release needs a position (usage: tumwater quarantine --release <n>)");
|
|
371
|
+
await requireReadyRepo(root);
|
|
372
|
+
const rows = quarantineRows(loadQuarantine(root));
|
|
373
|
+
if (position < 1 || position > rows.length) {
|
|
374
|
+
fail(`no quarantined test at position ${position} (flake quarantine holds ${rows.length} tests)`);
|
|
375
|
+
}
|
|
376
|
+
const test = rows[position - 1].test;
|
|
377
|
+
if (!releaseQuarantinedTest(root, test)) {
|
|
378
|
+
fail(`quarantined test at position ${position} is already gone`);
|
|
379
|
+
}
|
|
380
|
+
if (json) {
|
|
381
|
+
sayJson({ released: { position, test } });
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
say(`released quarantined test ${position}: ${test} — the gate no longer excuses it`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* `readPostReviews` reads a missing, torn or hand-edited file as the well-formed subset (or `[]`),
|
|
5
5
|
* so the command prints the empty line in any directory. Read-only except the one destructive
|
|
6
6
|
* `--accept <n>` mode, which removes the Nth entry the operator deliberately accepts unreviewed
|
|
7
|
-
* (the action the at-risk
|
|
7
|
+
* (the action the doctor's at-risk post-review row's advice names); that mode adds no new
|
|
8
|
+
* ready-repo requirement to
|
|
8
9
|
* the read path and gates the write with `requireReadyRepo`. Unlike `cmdQueue`'s `--cancel`, the
|
|
9
10
|
* read case still writes no file. */
|
|
10
11
|
import { JSON_FLAG, ROLE_FLAG, rejectUnknownArgs, statusFlagSpec } from "./cli-flag-specs.js";
|
|
@@ -17,7 +17,7 @@ import { renderRoleMarkdown } from "../roles/role-render.js";
|
|
|
17
17
|
import { DIRECTOR_ROLE, unknownRoleMessage } from "../roles/roles.js";
|
|
18
18
|
import { saveRoleNote } from "../agent/role-notes-write.js";
|
|
19
19
|
import { roleNotesPath } from "../paths.js";
|
|
20
|
-
import { errorMessage } from "../text/text.js";
|
|
20
|
+
import { errorMessage, isExponentSpelling } from "../text/text.js";
|
|
21
21
|
import { readPromptFileArg } from "./cli-command-args.js";
|
|
22
22
|
import { canonicalRoleId } from "../roles/legacy-role-ids.js";
|
|
23
23
|
import { INSTANCE_ROLES, baseRoleOf } from "../roles/runner-ids.js";
|
|
@@ -44,12 +44,6 @@ const CAP_SPEC = { names: ["--cap"], value: true, valueName: "<usd>" };
|
|
|
44
44
|
function plainDecimalToJson(raw) {
|
|
45
45
|
return /^\d*\.?\d+$/.test(raw) ? String(Number(raw)) : null;
|
|
46
46
|
}
|
|
47
|
-
/** Detect exponent spellings that the cap, interval and instances writes must reject
|
|
48
|
-
* before JSON parsing can turn them into otherwise valid numeric config values. Other
|
|
49
|
-
* invalid spellings fall through to the config validator, which names the key. */
|
|
50
|
-
function isExponentSpelling(raw) {
|
|
51
|
-
return raw.trim() !== "" && plainDecimalToJson(raw) === null && /^\d+(\.\d+)?[eE][+-]?\d+$/.test(raw.trim());
|
|
52
|
-
}
|
|
53
47
|
/** `role`'s min-run-interval write flags: clear it, or set it to a second count (0 is
|
|
54
48
|
* back-to-back). */
|
|
55
49
|
const CLEAR_MIN_RUN_INTERVAL_SPEC = { names: ["--clear-min-run-interval"] };
|
|
@@ -119,6 +113,19 @@ function requireKnownRole(root, id) {
|
|
|
119
113
|
if (!knownRoleIds(cfg).includes(id))
|
|
120
114
|
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
121
115
|
}
|
|
116
|
+
/** Refuse the two mistakes a per-role config write must reject up front: handing both a
|
|
117
|
+
* set flag and its clear twin (e.g. `--model` with `--clear-model`), and passing --json,
|
|
118
|
+
* which only the read/render path honors. `setFlag`/`clearFlag` are the exact spellings the
|
|
119
|
+
* refusal names (the clear twin is not always `--clear` + the set flag — `--notes-file`'s
|
|
120
|
+
* twin is `--clear-notes`), and `writeNoun` is the "<a|an> X write" phrase the --json
|
|
121
|
+
* refusal needs. Every set/clear write branch opens with the same two-line prelude, so
|
|
122
|
+
* this keeps it in one place. */
|
|
123
|
+
function rejectSetClearAndJson(rest, setFlag, clearFlag, writeNoun) {
|
|
124
|
+
if (rest.includes(setFlag) && rest.includes(clearFlag))
|
|
125
|
+
fail(`give ${clearFlag} or ${setFlag}, not both`);
|
|
126
|
+
if (rest.includes("--json"))
|
|
127
|
+
fail(`--json does not apply to ${writeNoun}`);
|
|
128
|
+
}
|
|
122
129
|
/** The config-key write the seven per-role hold branches (quiet hours, daily cap, min run
|
|
123
130
|
* interval, provider, model, thinking and backend) share: with `clear`, or an empty typed
|
|
124
131
|
* value, unset `key` under `base`; else set it. `serialize` shapes the value `setConfigKey`
|
|
@@ -237,10 +244,7 @@ export async function cmdRole(root, args) {
|
|
|
237
244
|
if (writes.length > 1)
|
|
238
245
|
fail(`${writes.join(" and ")} writes cannot share one invocation`);
|
|
239
246
|
if (notesWrite) {
|
|
240
|
-
|
|
241
|
-
fail("give --clear-notes or --notes-file, not both");
|
|
242
|
-
if (rest.includes("--json"))
|
|
243
|
-
fail("--json does not apply to a notes write");
|
|
247
|
+
rejectSetClearAndJson(rest, "--notes-file", "--clear-notes", "a notes write");
|
|
244
248
|
if (id === DIRECTOR_ROLE)
|
|
245
249
|
fail("the director has no notes");
|
|
246
250
|
requireKnownRole(root, id);
|
|
@@ -259,10 +263,7 @@ export async function cmdRole(root, args) {
|
|
|
259
263
|
// enforces the 4096-char cap and the write is atomic. The director is allowed here (its
|
|
260
264
|
// override reaches its own runs), unlike the notes branch's director refusal.
|
|
261
265
|
if (instructionsWrite) {
|
|
262
|
-
|
|
263
|
-
fail("give --clear-instructions or --instructions-file, not both");
|
|
264
|
-
if (rest.includes("--json"))
|
|
265
|
-
fail("--json does not apply to an instructions write");
|
|
266
|
+
rejectSetClearAndJson(rest, "--instructions-file", "--clear-instructions", "an instructions write");
|
|
266
267
|
requireKnownRole(root, id);
|
|
267
268
|
const base = baseRoleOf(id);
|
|
268
269
|
if (clearInst) {
|
|
@@ -287,10 +288,7 @@ export async function cmdRole(root, args) {
|
|
|
287
288
|
// The director is exempt — its quietHoursPerRole.director entry is inert (the gate and
|
|
288
289
|
// doctor both exempt it), like the notes branch's director refusal.
|
|
289
290
|
if (quietWrite) {
|
|
290
|
-
|
|
291
|
-
fail("give --clear-quiet-hours or --quiet-hours, not both");
|
|
292
|
-
if (rest.includes("--json"))
|
|
293
|
-
fail("--json does not apply to a quiet-hours write");
|
|
291
|
+
rejectSetClearAndJson(rest, "--quiet-hours", "--clear-quiet-hours", "a quiet-hours write");
|
|
294
292
|
requireKnownRole(root, id);
|
|
295
293
|
if (id === DIRECTOR_ROLE)
|
|
296
294
|
fail("the director has no quiet hours");
|
|
@@ -307,10 +305,7 @@ export async function cmdRole(root, args) {
|
|
|
307
305
|
// atomic. The director is exempt — it has no cap, like the notes and quiet-hours branches'
|
|
308
306
|
// director refusals.
|
|
309
307
|
if (capWrite) {
|
|
310
|
-
|
|
311
|
-
fail("give --clear-cap or --cap, not both");
|
|
312
|
-
if (rest.includes("--json"))
|
|
313
|
-
fail("--json does not apply to a cap write");
|
|
308
|
+
rejectSetClearAndJson(rest, "--cap", "--clear-cap", "a cap write");
|
|
314
309
|
requireKnownRole(root, id);
|
|
315
310
|
if (id === DIRECTOR_ROLE)
|
|
316
311
|
fail("the director has no daily cap");
|
|
@@ -326,7 +321,7 @@ export async function cmdRole(root, args) {
|
|
|
326
321
|
// already a plain decimal, so the exponent form is exactly the silent-write set; hex,
|
|
327
322
|
// negative and non-numeric spellings fall through to the write path, whose
|
|
328
323
|
// validateConfig message names the key.
|
|
329
|
-
if (isExponentSpelling(cap))
|
|
324
|
+
if (isExponentSpelling(cap.trim()))
|
|
330
325
|
fail(`${cfgKey} must be a plain dollar amount like 25, 12.50, or .5 — no +/-, exponent, or hex notation`);
|
|
331
326
|
writeRoleConfigKey(root, base, clearCap, cap, cfgKey, "daily cap", (v) => plainDecimalToJson(v) ?? v, (v) => v);
|
|
332
327
|
return;
|
|
@@ -339,10 +334,7 @@ export async function cmdRole(root, args) {
|
|
|
339
334
|
// write is atomic. Unlike the notes/quiet-hours/cap branches there is no director refusal:
|
|
340
335
|
// the director runs on the same clock.
|
|
341
336
|
if (intervalWrite) {
|
|
342
|
-
|
|
343
|
-
fail("give --clear-min-run-interval or --min-run-interval, not both");
|
|
344
|
-
if (rest.includes("--json"))
|
|
345
|
-
fail("--json does not apply to a min-run-interval write");
|
|
337
|
+
rejectSetClearAndJson(rest, "--min-run-interval", "--clear-min-run-interval", "a min-run-interval write");
|
|
346
338
|
requireKnownRole(root, id);
|
|
347
339
|
const base = baseRoleOf(id);
|
|
348
340
|
const cfgKey = `roles.${base}.minRunIntervalSeconds`;
|
|
@@ -356,7 +348,7 @@ export async function cmdRole(root, args) {
|
|
|
356
348
|
// plain decimal, so the exponent form is exactly the silent-write set; hex, negative and
|
|
357
349
|
// non-numeric spellings fall through to the write path, whose validateConfig message
|
|
358
350
|
// names the key.
|
|
359
|
-
if (isExponentSpelling(secs))
|
|
351
|
+
if (isExponentSpelling(secs.trim()))
|
|
360
352
|
fail(`${cfgKey} must be a plain second count like 300, 0.5, or .5 — no +/-, exponent, or hex notation`);
|
|
361
353
|
writeRoleConfigKey(root, base, clearInterval, secs, cfgKey, "min run interval", (v) => plainDecimalToJson(v) ?? v, (v) => `${v}s`);
|
|
362
354
|
return;
|
|
@@ -369,10 +361,7 @@ export async function cmdRole(root, args) {
|
|
|
369
361
|
// validateConfig's `!INSTANCE_ROLES.has(id)` branch answers for the director and every
|
|
370
362
|
// custom role, and an unset/empty value clears the key.
|
|
371
363
|
if (instancesWrite) {
|
|
372
|
-
|
|
373
|
-
fail("give --clear-instances or --instances, not both");
|
|
374
|
-
if (rest.includes("--json"))
|
|
375
|
-
fail("--json does not apply to an instances write");
|
|
364
|
+
rejectSetClearAndJson(rest, "--instances", "--clear-instances", "an instances write");
|
|
376
365
|
const loaded = loadConfigSafe(root);
|
|
377
366
|
const cfg = loaded.config ?? defaultConfig();
|
|
378
367
|
if (!knownRoleIds(cfg).includes(id))
|
|
@@ -389,7 +378,7 @@ export async function cmdRole(root, args) {
|
|
|
389
378
|
// or out ("9e0" → 9 too) — is refused here, while fractional, hex, negative and plain
|
|
390
379
|
// out-of-range integer spellings fall through to the write path, whose validateConfig
|
|
391
380
|
// message names the key.
|
|
392
|
-
if (isExponentSpelling(n))
|
|
381
|
+
if (isExponentSpelling(n.trim()))
|
|
393
382
|
fail(`${cfgKey} must be a plain integer from 1 to 8 — no exponent notation`);
|
|
394
383
|
// An absent count already means 1 (instanceCount's default), so asking for 1 is a no-op —
|
|
395
384
|
// but only for a role that may split; any other role falls through to validateConfig's refusal.
|
|
@@ -401,65 +390,30 @@ export async function cmdRole(root, args) {
|
|
|
401
390
|
writeRoleConfigKey(root, base, clearInstances, n, `roles.${base}.instances`, "instances", (v) => v, (v) => v);
|
|
402
391
|
return;
|
|
403
392
|
}
|
|
404
|
-
// The model-
|
|
405
|
-
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
if (clearModel && rest.includes("--model"))
|
|
425
|
-
fail("give --clear-model or --model, not both");
|
|
426
|
-
if (rest.includes("--json"))
|
|
427
|
-
fail("--json does not apply to a model write");
|
|
428
|
-
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
429
|
-
if (!knownRoleIds(cfg).includes(id))
|
|
430
|
-
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
431
|
-
const base = baseRoleOf(id);
|
|
432
|
-
const model = flagValue(rest, "--model") ?? "";
|
|
433
|
-
writeRoleConfigKey(root, base, clearModel, model, `roles.${base}.model`, "model", (v) => JSON.stringify(v), (v) => v);
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
if (thinkingWrite) {
|
|
437
|
-
if (clearThinking && rest.includes("--thinking"))
|
|
438
|
-
fail("give --clear-thinking or --thinking, not both");
|
|
439
|
-
if (rest.includes("--json"))
|
|
440
|
-
fail("--json does not apply to a thinking write");
|
|
441
|
-
const cfg = loadConfigSafe(root).config ?? defaultConfig();
|
|
442
|
-
if (!knownRoleIds(cfg).includes(id))
|
|
443
|
-
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
444
|
-
const base = baseRoleOf(id);
|
|
445
|
-
const level = flagValue(rest, "--thinking") ?? "";
|
|
446
|
-
writeRoleConfigKey(root, base, clearThinking, level, `roles.${base}.thinking`, "thinking", (v) => JSON.stringify(v), (v) => v);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
// The backend write: the twin of the provider/model/thinking writes, landing the role's
|
|
450
|
-
// backend (`pi`, `claude`, `opencode` or `codex`) in roles.<base>.backend through the same
|
|
451
|
-
// setConfigKey/unsetConfigKey core, so validateConfig's checkEntryBackend owns the id
|
|
452
|
-
// (rejecting an unknown one, and a `model` that cannot pair with the named backend) and
|
|
453
|
-
// the write stays atomic. An instance id writes its base role's key, like the others.
|
|
454
|
-
if (backendWrite) {
|
|
455
|
-
if (clearBackend && rest.includes("--backend"))
|
|
456
|
-
fail("give --clear-backend or --backend, not both");
|
|
457
|
-
if (rest.includes("--json"))
|
|
458
|
-
fail("--json does not apply to a backend write");
|
|
393
|
+
// The model-writing branches — provider, model, thinking and backend — are one shape: each
|
|
394
|
+
// lands the role's field in roles.<base>.<field> (the terminal twin of `config set
|
|
395
|
+
// roles.<base>.<field> <value>` and `config unset`) through the same setConfigKey/unsetConfigKey
|
|
396
|
+
// core, so validateConfig owns the id — checkModelTripleField ("tier-name" shape: model
|
|
397
|
+
// accepts a tier name, thinking a THINKING_LEVELS level) for the provider/model/thinking
|
|
398
|
+
// fields that configForRole resolves through seamOverrides, checkEntryBackend for backend
|
|
399
|
+
// (`pi`, `claude`, `opencode` or `codex`, refusing an unknown one and a model that cannot
|
|
400
|
+
// pair with it) — and each write is atomic. No director refusal: each is accepted exactly as
|
|
401
|
+
// --min-run-interval is, and an instance id writes its base role's key. Every branch calls
|
|
402
|
+
// writeRoleConfigKey with the same JSON-encode/identity shaping, differing only in the flag
|
|
403
|
+
// spellings, the key field and the noun the confirmation line names — one table, one body.
|
|
404
|
+
for (const { flag, clearFlag, write, clear, field, noun } of [
|
|
405
|
+
{ flag: "--provider", clearFlag: "--clear-provider", write: providerWrite, clear: clearProvider, field: "provider", noun: "provider" },
|
|
406
|
+
{ flag: "--model", clearFlag: "--clear-model", write: modelWrite, clear: clearModel, field: "model", noun: "model" },
|
|
407
|
+
{ flag: "--thinking", clearFlag: "--clear-thinking", write: thinkingWrite, clear: clearThinking, field: "thinking", noun: "thinking" },
|
|
408
|
+
{ flag: "--backend", clearFlag: "--clear-backend", write: backendWrite, clear: clearBackend, field: "backend", noun: "backend" },
|
|
409
|
+
]) {
|
|
410
|
+
if (!write)
|
|
411
|
+
continue;
|
|
412
|
+
rejectSetClearAndJson(rest, flag, clearFlag, `a ${noun} write`);
|
|
459
413
|
requireKnownRole(root, id);
|
|
460
414
|
const base = baseRoleOf(id);
|
|
461
|
-
const
|
|
462
|
-
writeRoleConfigKey(root, base,
|
|
415
|
+
const value = flagValue(rest, flag) ?? "";
|
|
416
|
+
writeRoleConfigKey(root, base, clear, value, `roles.${base}.${field}`, noun, (v) => JSON.stringify(v), (v) => v);
|
|
463
417
|
return;
|
|
464
418
|
}
|
|
465
419
|
// The enable/disable write: the scoped write for the enabled state `tumwater role <id>`
|
|
@@ -489,7 +443,7 @@ export async function cmdRole(root, args) {
|
|
|
489
443
|
say(`${base}: ${state}`);
|
|
490
444
|
return;
|
|
491
445
|
}
|
|
492
|
-
// The review-after-merge write: the one command the post-merge churn
|
|
446
|
+
// The review-after-merge write: the one command the post-merge churn row's remedy
|
|
493
447
|
// needs — add the role to, or remove it from, review.afterMerge for the base id, the
|
|
494
448
|
// membership test reviewedAfterMerge shares. The whole-list value lands through the same
|
|
495
449
|
// setConfigKey core `config set review.afterMerge <json>` uses, so validateConfig owns the
|
|
@@ -24,25 +24,10 @@ import { ONCE_FAILURE_RESULTS, inheritedWindowStart, onceFailures, onceReport, o
|
|
|
24
24
|
* renames the other. */
|
|
25
25
|
const GUI_TOKEN_ENV = "TUMWATER_GUI_TOKEN";
|
|
26
26
|
export { ONCE_FAILURE_RESULTS, inheritedWindowStart, onceFailures, onceReport, onceSummary, parseRunWindow, renderOnceSummary, windowReport, windowSummary, };
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
31
|
-
* self-redeploy so the supervisor respawns on the new build; --once collapses the whole thing
|
|
32
|
-
* into one round of runs and a summary line (onceSummary below), for cron-style invocations.
|
|
33
|
-
* `--for <duration>` keeps the daemon shape (redeployer, LaunchServicesWatch, supervisor hand-off
|
|
34
|
-
* all stay) but arms a deadline: at the deadline the same graceful stop a Ctrl+C runs fires, and
|
|
35
|
-
* the process exits after printing the summary. A mid-run self-redeploy hands the same args
|
|
36
|
-
* (including `--for`) to the supervisor's next child, and the supervisor hands every generation
|
|
37
|
-
* the same window start (RUN_WINDOW_START_ENV), so the deadline and the summary span the whole
|
|
38
|
-
* window; only the final exit prints the summary. */
|
|
39
|
-
export async function cmdStart(root, args) {
|
|
40
|
-
// The one function the self-redeploy asks before swapping onto a successor and the supervisor
|
|
41
|
-
// asks when a generation dies, so the three cannot disagree about what boots. The supervisor
|
|
42
|
-
// half runs it too, so a start that cannot boot fails before any child spawns. The flag
|
|
43
|
-
// vocabulary is validated by the dispatcher (cli.ts, from the same START_FLAG_SPECS).
|
|
44
|
-
const once = args.includes("--once");
|
|
45
|
-
const json = args.includes("--json");
|
|
27
|
+
/** Refuse the --once run-mode rivals: a TUI and a dashboard each cannot be hosted by a run that
|
|
28
|
+
* is about to stop, so both are refused together before any boot. --for stays allowed — both
|
|
29
|
+
* serve for the window. Owns the run-mode rival refusals. */
|
|
30
|
+
function assertOnceRivalFlags(args, once) {
|
|
46
31
|
// The TUI cannot be hosted by a run that is about to stop, like the dashboard: a --once round
|
|
47
32
|
// drains and exits within the same invocation, so the two shapes are refused together before
|
|
48
33
|
// any boot. --for stays allowed — the TUI runs for the window, like the dashboard.
|
|
@@ -53,6 +38,10 @@ export async function cmdStart(root, args) {
|
|
|
53
38
|
// stays allowed — the dashboard serves for the window.
|
|
54
39
|
if (once && args.includes("--gui"))
|
|
55
40
|
failRivalShapes("--gui", "--once", "a one-round run cannot serve a dashboard it is about to kill");
|
|
41
|
+
}
|
|
42
|
+
/** Refuse the dashboard-only flags that name nothing without --gui. Owns the dashboard-only
|
|
43
|
+
* refusals. */
|
|
44
|
+
function assertDashboardOnlyFlags(args) {
|
|
56
45
|
// `--port` chooses the `--gui` dashboard's port; without `--gui` it names no port for this
|
|
57
46
|
// command, so refuse it (a valid value already passed the spec's shape gate) before any boot.
|
|
58
47
|
if (args.includes("--port") && !args.includes("--gui"))
|
|
@@ -65,6 +54,10 @@ export async function cmdStart(root, args) {
|
|
|
65
54
|
fail("--all-interfaces is only valid with --gui (e.g. `tumwater start --gui --all-interfaces`); the standalone dashboard is `tumwater gui --all-interfaces`");
|
|
66
55
|
if (args.includes("--token") && !args.includes("--gui"))
|
|
67
56
|
fail("--token is only valid with --gui (e.g. `tumwater start --gui --token s3cret`); the standalone dashboard is `tumwater gui --token <secret>`");
|
|
57
|
+
}
|
|
58
|
+
/** Check the --token value and scrub `--token <value>` off this process's argv. Owns the --token
|
|
59
|
+
* value check and the argv scrub. */
|
|
60
|
+
function handleTokenFlag(args) {
|
|
68
61
|
// A malformed --token (valueless, empty, or flag-looking) must fail here before any boot, so
|
|
69
62
|
// a bad secret never leaves the team running behind a dashboard that exited. The same
|
|
70
63
|
// tokenValueProblem `tumwater gui` runs, named for the one-command form the operator typed.
|
|
@@ -82,12 +75,10 @@ export async function cmdStart(root, args) {
|
|
|
82
75
|
process.argv.splice(argvToken, 2);
|
|
83
76
|
say(`removed --token from this process's command line; prefer the ${GUI_TOKEN_ENV} env var`);
|
|
84
77
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
fail(startup.problem);
|
|
90
|
-
const { config, mainBranch } = startup;
|
|
78
|
+
}
|
|
79
|
+
/** Parse the --role filter against the live config and guard it to --once rounds. Owns the
|
|
80
|
+
* --role parse and its --once guard; the disabled/unknown guards stay separate (see below). */
|
|
81
|
+
function resolveRoleFilter(args, config, once) {
|
|
91
82
|
// A scoped once round: `--role <id>[,<id>...]` names the runners that run. Parsed here, where the
|
|
92
83
|
// live config is readable. The parser accepts every known id (custom roles included, enabled
|
|
93
84
|
// or not) so a typo still fails with the did-you-mean hint; an id that is known but has no
|
|
@@ -97,6 +88,11 @@ export async function cmdStart(root, args) {
|
|
|
97
88
|
const roleFilter = parseRoleListFlag(args, knownRoleIds(config));
|
|
98
89
|
if (roleFilter !== null && !once)
|
|
99
90
|
fail("--role is only valid with --once");
|
|
91
|
+
return roleFilter;
|
|
92
|
+
}
|
|
93
|
+
/** Refuse --json without --once or --for, and the --gui/--tui rivals of a --json round/window.
|
|
94
|
+
* Owns the --json mode refusals. */
|
|
95
|
+
function assertJsonMode(args, json, once, forMs) {
|
|
100
96
|
if (json && !once && forMs === null)
|
|
101
97
|
fail("--json is only valid with --once or --for");
|
|
102
98
|
// Both interactive modes write their own banner to the stdout a JSON document must own, so a
|
|
@@ -105,6 +101,10 @@ export async function cmdStart(root, args) {
|
|
|
105
101
|
failRivalShapes("--json", "--gui", "the dashboard writes its banner to the stdout a JSON document must own");
|
|
106
102
|
if (json && args.includes("--tui"))
|
|
107
103
|
failRivalShapes("--json", "--tui", "a TUI owns the terminal");
|
|
104
|
+
}
|
|
105
|
+
/** Guard a non-null --role filter against a disabled or unknown id. Owns the disabled/unknown
|
|
106
|
+
* --role guards, firing after the --json mode refusals so the fire order upstream is unchanged. */
|
|
107
|
+
function assertRoleFilterRunnable(roleFilter, config) {
|
|
108
108
|
if (roleFilter !== null) {
|
|
109
109
|
const runners = runnerIds(config);
|
|
110
110
|
const missing = roleFilter.find((id) => !runners.includes(id));
|
|
@@ -115,6 +115,10 @@ export async function cmdStart(root, args) {
|
|
|
115
115
|
fail(unknownRoleMessage(missing, runners));
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
}
|
|
119
|
+
/** Refuse a second team in one repo by naming the running pid and the two ways to stop it. Owns
|
|
120
|
+
* the already-running-orchestrator refusal. */
|
|
121
|
+
function assertNoRunningOrchestrator(root) {
|
|
118
122
|
// The refusal an operator hits by trying to start a second team in one repo: name the
|
|
119
123
|
// running pid and the two ways to stop it, so the fix is in the message instead of a
|
|
120
124
|
// separate `tumwater status`/help lookup. The info file is read once and reused for the
|
|
@@ -122,20 +126,38 @@ export async function cmdStart(root, args) {
|
|
|
122
126
|
const running = readOrchestratorInfo(root);
|
|
123
127
|
if (running !== null && orchestratorAlive(root, running))
|
|
124
128
|
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`);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
129
|
+
}
|
|
130
|
+
/** Parse and validate the invocation before any team boots: the --tui/--gui/--json/--port/
|
|
131
|
+
* --all-interfaces/--token/--role/--for/--branch guards, the --token argv scrub, runStartupCheck,
|
|
132
|
+
* and the already-running-orchestrator refusal. Returns the resolved invocation. The per-job
|
|
133
|
+
* guard groups each live in a named helper — assertOnceRivalFlags, assertDashboardOnlyFlags,
|
|
134
|
+
* handleTokenFlag, resolveRoleFilter, assertJsonMode, assertRoleFilterRunnable and
|
|
135
|
+
* assertNoRunningOrchestrator — called here in fire order. */
|
|
136
|
+
async function parseStartInvocation(root, args) {
|
|
137
|
+
const once = args.includes("--once");
|
|
138
|
+
const json = args.includes("--json");
|
|
139
|
+
assertOnceRivalFlags(args, once);
|
|
140
|
+
assertDashboardOnlyFlags(args);
|
|
141
|
+
handleTokenFlag(args);
|
|
142
|
+
const forMs = parseRunWindow(args, once);
|
|
143
|
+
const branchArg = parseBranchFlag(args);
|
|
144
|
+
const startup = await runStartupCheck(root, branchArg);
|
|
145
|
+
if ("problem" in startup)
|
|
146
|
+
fail(startup.problem);
|
|
147
|
+
const { config, mainBranch } = startup;
|
|
148
|
+
const roleFilter = resolveRoleFilter(args, config, once);
|
|
149
|
+
assertJsonMode(args, json, once, forMs);
|
|
150
|
+
assertRoleFilterRunnable(roleFilter, config);
|
|
151
|
+
assertNoRunningOrchestrator(root);
|
|
152
|
+
return { once, json, forMs, branchArg, roleFilter, config, mainBranch };
|
|
153
|
+
}
|
|
154
|
+
/** Boot the supervised generation: the stop closure and SIGINT/SIGTERM handlers, the --for
|
|
155
|
+
* deadline timer and scheduled-stop watch, the parent-death watch, the baseline/redeploy/
|
|
156
|
+
* launch-services imports, the banner, the event subscription, runOrchestrator, and the
|
|
157
|
+
* once/window summaries and exit codes. */
|
|
158
|
+
async function runSupervisedGeneration(input) {
|
|
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");
|
|
139
161
|
const controller = new AbortController();
|
|
140
162
|
let stopping = false;
|
|
141
163
|
const stop = () => {
|
|
@@ -179,8 +201,9 @@ export async function cmdStart(root, args) {
|
|
|
179
201
|
const enabled = runnerIds(config);
|
|
180
202
|
// A scoped round's role list is exactly the filter; otherwise every enabled role runs.
|
|
181
203
|
const roles = roleFilter ?? enabled;
|
|
182
|
-
// Main's green verdicts outlive this generation: the redeployer and
|
|
183
|
-
//
|
|
204
|
+
// Main's green verdicts outlive this generation: the redeployer and baseline's surviving readers
|
|
205
|
+
// (`mainTipVerdict`, the ci hand-off's `mainRedHealNote`) start from the ones the previous
|
|
206
|
+
// generation verified instead of re-running the suite.
|
|
184
207
|
(await import("../baseline/main-baseline.js")).persistBaselineVerdicts(root);
|
|
185
208
|
// A once round never self-redeploys: it runs one round on the build it booted and exits,
|
|
186
209
|
// so the hand-off machinery (and its build stamp) stays daemon-only.
|
|
@@ -265,6 +288,50 @@ export async function cmdStart(root, args) {
|
|
|
265
288
|
// which respawns this same script — now the new code — as the next generation.
|
|
266
289
|
if (exit.restart)
|
|
267
290
|
process.exit(RESTART_EXIT_CODE);
|
|
291
|
+
// A windowed (--for) run that did not self-redeploy is done the moment its summary is out:
|
|
292
|
+
// 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 misses (a watcher, marker,
|
|
294
|
+
// or notifier handle a join missed) would otherwise pin this process's loop open, so the
|
|
295
|
+
// supervisor — which waits on this child's exit — never exits and the run reads as a hang:
|
|
296
|
+
// 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 a
|
|
298
|
+
// natural one with code 0, just guaranteed.
|
|
299
|
+
if (forMs !== null)
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
/** `tumwater start`: boot the team and stream its event feed to this terminal until it stops.
|
|
303
|
+
* Three layers cooperate here: without SUPERVISED_ENV this process becomes the supervisor
|
|
304
|
+
* (superviseStartCommand below), spawning the real orchestrator as a child generation; the child
|
|
305
|
+
* runs runOrchestrator to completion, handing the terminal back via RESTART_EXIT_CODE after a
|
|
306
|
+
* self-redeploy so the supervisor respawns on the new build; --once collapses the whole thing
|
|
307
|
+
* into one round of runs and a summary line (onceSummary below), for cron-style invocations.
|
|
308
|
+
* `--for <duration>` keeps the daemon shape (redeployer, LaunchServicesWatch, supervisor hand-off
|
|
309
|
+
* all stay) but arms a deadline: at the deadline the same graceful stop a Ctrl+C runs fires, and
|
|
310
|
+
* the process exits after printing the summary. A mid-run self-redeploy hands the same args
|
|
311
|
+
* (including `--for`) to the supervisor's next child, and the supervisor hands every generation
|
|
312
|
+
* the same window start (RUN_WINDOW_START_ENV), so the deadline and the summary span the whole
|
|
313
|
+
* window; only the final exit prints the summary. */
|
|
314
|
+
export async function cmdStart(root, args) {
|
|
315
|
+
// The one function the self-redeploy asks before swapping onto a successor and the supervisor
|
|
316
|
+
// asks when a generation dies, so the three cannot disagree about what boots. The supervisor
|
|
317
|
+
// half runs it too, so a start that cannot boot fails before any child spawns. The flag
|
|
318
|
+
// vocabulary is validated by the dispatcher (cli.ts, from the same START_FLAG_SPECS).
|
|
319
|
+
const { once, json, forMs, branchArg, roleFilter, config, mainBranch } = await parseStartInvocation(root, args);
|
|
320
|
+
// The team-booting machinery loads only once a run actually boots: the orchestrator's
|
|
321
|
+
// module graph (with its merge pipeline, review gate, and scheduler) is the CLI's heaviest
|
|
322
|
+
// compile, and the supervisor/redeploy/launch-services pieces ride beside it. Loading them
|
|
323
|
+
// dynamically, after the fail-fast gates above, keeps every other command's spawn — and
|
|
324
|
+
// `run`'s fail-fast paths — off that cost (see cli.ts's matching lazy dispatch).
|
|
325
|
+
const supervisor = await import("../process/supervisor.js");
|
|
326
|
+
if (!process.env[supervisor.SUPERVISED_ENV]) {
|
|
327
|
+
// `args` are the flags after the command token — forward them so the child generation
|
|
328
|
+
// targets the same branch (or whatever else the invocation named).
|
|
329
|
+
// A stop marker left by a team that died before its deadline must not stop this run.
|
|
330
|
+
cancelScheduledStop(root);
|
|
331
|
+
await superviseStartCommand(root, args, branchArg, forMs);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
await runSupervisedGeneration({ root, config, mainBranch, branchArg, once, json, forMs, roleFilter });
|
|
268
335
|
}
|
|
269
336
|
/** Arm the windowed run's deadline timer: the one home of the arming `cmdStart` does for
|
|
270
337
|
* `run --for <duration>`. A duration over setTimeout's own 32-bit signed ceiling
|
|
@@ -360,6 +427,20 @@ export function guiChildPlan(runArgs) {
|
|
|
360
427
|
childStdio: runArgs.includes("--tui") ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
361
428
|
};
|
|
362
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
|
+
}
|
|
363
444
|
/** The dashboard half of `run --gui`: keep a `tumwater gui` child alive across its own
|
|
364
445
|
* self-reloads. The child is marked with DASHBOARD_CHILD_ENV naming this supervisor, so its
|
|
365
446
|
* reload watch (src/redeploy/self-reload.ts) exits RESTART_EXIT_CODE for a fresh sibling on a
|
|
@@ -369,12 +450,7 @@ export function guiChildPlan(runArgs) {
|
|
|
369
450
|
async function superviseDashboard(signal, guiToken, stdio = "inherit", guiArgs = []) {
|
|
370
451
|
const { DASHBOARD_CHILD_ENV } = await import("../redeploy/self-reload.js");
|
|
371
452
|
const { spawnGuiChild } = await import("../process/supervisor.js");
|
|
372
|
-
const env =
|
|
373
|
-
// The gate token rides in the child's env, not its argv: /proc/<pid>/cmdline is world-readable,
|
|
374
|
-
// and the child's self-reload (reexecSelf, which propagates env via childEnv) would otherwise
|
|
375
|
-
// re-run the secret onto each fresh generation.
|
|
376
|
-
if (guiToken !== undefined)
|
|
377
|
-
env[GUI_TOKEN_ENV] = guiToken;
|
|
453
|
+
const env = dashboardChildEnv(DASHBOARD_CHILD_ENV, guiToken, process.pid);
|
|
378
454
|
for (;;) {
|
|
379
455
|
const exit = await spawnGuiChild(signal, env, stdio, guiArgs);
|
|
380
456
|
if (signal.aborted || exit.code !== RESTART_EXIT_CODE)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Ctrl+C or the process that started the command exits. Lifted out of
|
|
4
4
|
* cli-query-commands.ts when `report -f` landed so the follow views have one leaf
|
|
5
5
|
* home instead of a per-command copy. */
|
|
6
|
-
import { startParentDeathWatch } from "../process/supervisor.js";
|
|
6
|
+
import { expectedSupervisorPid, startParentDeathWatch } from "../process/supervisor.js";
|
|
7
7
|
import { stripTerminalControls } from "../text/text.js";
|
|
8
8
|
/** The default repaint interval for a follow view — a fixed cadence, no knob. */
|
|
9
9
|
const FOLLOW_VIEW_INTERVAL_MS = 2000;
|
|
@@ -47,6 +47,7 @@ export async function followView(frameText, seams) {
|
|
|
47
47
|
// would also leave that read's rejections unhandled. Sync callers never set `painting`, so
|
|
48
48
|
// the check is a no-op for them: each of their ticks still paints exactly one frame.
|
|
49
49
|
let painting = false;
|
|
50
|
+
const expectedSupervisor = expectedSupervisorPid();
|
|
50
51
|
await new Promise((resolve) => {
|
|
51
52
|
const timer = setInterval(() => {
|
|
52
53
|
if (painting)
|
|
@@ -68,6 +69,7 @@ export async function followView(frameText, seams) {
|
|
|
68
69
|
clearInterval(timer);
|
|
69
70
|
resolve();
|
|
70
71
|
}, {
|
|
72
|
+
...(expectedSupervisor !== undefined ? { expectedPpid: expectedSupervisor } : {}),
|
|
71
73
|
...(seams.ppid ? { ppid: seams.ppid } : {}),
|
|
72
74
|
...(seams.intervalMs !== undefined ? { intervalMs: seams.intervalMs } : {}),
|
|
73
75
|
});
|