tumwater 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +1 -8
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +9 -0
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +5 -20
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +153 -102
- package/dist/src/config/config-schema.js +9 -11
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +120 -20
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +3 -3
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +27 -68
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +51 -33
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +1 -1
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +135 -84
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +119 -50
- package/dist/src/run/stage-check.js +49 -32
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +1 -7
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +159 -92
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +3 -3
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +11 -7
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
|
@@ -4,11 +4,11 @@ import { unknownRoleMessage } from "../roles/roles.js";
|
|
|
4
4
|
import { fail, say, sayJson } from "./cli-output.js";
|
|
5
5
|
import { durationLabel, failRivalShapes, parseBranchFlag, parsePortFlag, parseRoleListFlag, tokenValueProblem, } from "./cli-args.js";
|
|
6
6
|
import { cancelScheduledStop, scheduledStopAt } from "../team/team-state.js";
|
|
7
|
-
import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
8
|
-
import { runStartupCheck, runStartupProblem } from "../gates/startup-
|
|
7
|
+
import { orchestratorAlive, readOrchestratorInfo } from "../team/team-orchestrator-info.js";
|
|
8
|
+
import { runStartupCheck, runStartupProblem } from "../gates/startup-gates.js";
|
|
9
9
|
import { repairInterruptedSwap } from "../build/build-stage.js";
|
|
10
10
|
import { logEvent, subscribeEvents } from "../events/events.js";
|
|
11
|
-
import { notifyTeamDown } from "../events/notify.js";
|
|
11
|
+
import { notifyTeamDown } from "../events/event-notify.js";
|
|
12
12
|
import { formatEvent } from "../events/event-format.js";
|
|
13
13
|
import { RESTART_EXIT_CODE } from "../redeploy/redeploy-policy.js";
|
|
14
14
|
import { runStartupMigrations } from "../orchestrator/startup-migration-steps.js";
|
|
@@ -151,13 +151,11 @@ async function parseStartInvocation(root, args) {
|
|
|
151
151
|
assertNoRunningOrchestrator(root);
|
|
152
152
|
return { once, json, forMs, branchArg, roleFilter, config, mainBranch };
|
|
153
153
|
}
|
|
154
|
-
/**
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
const { root, once, json, forMs, branchArg, roleFilter, config, mainBranch } = input;
|
|
160
|
-
const { RUN_WINDOW_START_ENV, ONCE_FAILED_EXIT_CODE, startParentDeathWatch, expectedSupervisorPid } = await import("../process/supervisor.js");
|
|
154
|
+
/** Create the abort handle and the graceful-stop closure a supervised generation passes to
|
|
155
|
+
* every watchdog, and register the SIGINT/SIGTERM handlers that trigger it. Returns an
|
|
156
|
+
* `isStopping` query so the later once-exit check reads the live flag instead of a captured
|
|
157
|
+
* boolean. */
|
|
158
|
+
function installStopHandlers(json) {
|
|
161
159
|
const controller = new AbortController();
|
|
162
160
|
let stopping = false;
|
|
163
161
|
const stop = () => {
|
|
@@ -173,6 +171,12 @@ async function runSupervisedGeneration(input) {
|
|
|
173
171
|
};
|
|
174
172
|
process.on("SIGINT", stop);
|
|
175
173
|
process.on("SIGTERM", stop);
|
|
174
|
+
return { controller, stop, isStopping: () => stopping };
|
|
175
|
+
}
|
|
176
|
+
/** Arm the windowed run's deadline timer and the scheduled-stop watch, both of which run the
|
|
177
|
+
* same graceful `stop` a Ctrl+C runs. Returns the window start and the two clearable handles. */
|
|
178
|
+
async function armRunWatchdogs(root, json, forMs, stop, isStopping) {
|
|
179
|
+
const { RUN_WINDOW_START_ENV } = await import("../process/supervisor.js");
|
|
176
180
|
// A windowed run's deadline arms the same graceful stop a Ctrl+C runs — the identical `stop`
|
|
177
181
|
// closure, so the drain path is one code path either way. clearTimeout in the finally below
|
|
178
182
|
// retracts it when the run ends first (an early Ctrl+C), so no stray timer outlives the run.
|
|
@@ -185,22 +189,13 @@ async function runSupervisedGeneration(input) {
|
|
|
185
189
|
stop();
|
|
186
190
|
}) : null;
|
|
187
191
|
const scheduledStopWatch = armScheduledStopWatch(root, () => {
|
|
188
|
-
if (!
|
|
192
|
+
if (!isStopping())
|
|
189
193
|
stop(); // A drain already under way must not be turned into a force-kill.
|
|
190
194
|
}, json);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
// supervisor takes its team down with it (BUGS.md 2026-09-30). The spawner hands its pid
|
|
196
|
-
// down (SUPERVISOR_PID_ENV), so a supervisor killed during this child's startup — before the
|
|
197
|
-
// watch's first ppid read — still counts as gone. Everything below runs only in
|
|
198
|
-
// the supervised generation: an unsupervised `run` became the supervisor above and returned.
|
|
199
|
-
const expectedPpid = expectedSupervisorPid();
|
|
200
|
-
startParentDeathWatch(stop, expectedPpid !== undefined ? { expectedPpid } : {});
|
|
201
|
-
const enabled = runnerIds(config);
|
|
202
|
-
// A scoped round's role list is exactly the filter; otherwise every enabled role runs.
|
|
203
|
-
const roles = roleFilter ?? enabled;
|
|
195
|
+
return { windowStart, deadlineTimer, scheduledStopWatch };
|
|
196
|
+
}
|
|
197
|
+
/** Persist the baseline verdicts and boot the once-gated redeployer and launch-services watch. */
|
|
198
|
+
async function bootGenerationParts(root, branchArg, once) {
|
|
204
199
|
// Main's green verdicts outlive this generation: the redeployer and baseline's surviving readers
|
|
205
200
|
// (`mainTipVerdict`, the ci hand-off's `mainRedHealNote`) start from the ones the previous
|
|
206
201
|
// generation verified instead of re-running the suite.
|
|
@@ -210,10 +205,13 @@ async function runSupervisedGeneration(input) {
|
|
|
210
205
|
const redeploy = once
|
|
211
206
|
? null
|
|
212
207
|
: await (await import("../redeploy/redeploy.js")).createRedeployer(root, (e) => logEvent(root, e), () => runStartupProblem(root, branchArg));
|
|
213
|
-
const build = redeploy ? ` · build ${shortSha(redeploy.build.sha)}` : "";
|
|
214
208
|
// A long-running team watches launchservicesd's port count (a no-op off macOS); a once round
|
|
215
209
|
// is over long before a leak could matter.
|
|
216
210
|
const launchServicesWatch = once ? null : new (await import("../process/launch-services.js")).LaunchServicesWatch(root);
|
|
211
|
+
return { redeploy, launchServicesWatch };
|
|
212
|
+
}
|
|
213
|
+
/** Print the supervised generation's banner and subscribe to its event stream on non-json runs. */
|
|
214
|
+
function announceGeneration(roles, mainBranch, once, json, forMs, root, build) {
|
|
217
215
|
// Name the resolved root when it differs from the cwd: an operator who started the team
|
|
218
216
|
// from a subdirectory must see where .tumwater/ actually lives.
|
|
219
217
|
const rootNote = root !== process.cwd() ? ` · root ${root}` : "";
|
|
@@ -229,38 +227,17 @@ async function runSupervisedGeneration(input) {
|
|
|
229
227
|
say(`roles: ${roles.join(", ")}`);
|
|
230
228
|
say("watch: `tumwater tui` or `tumwater logs -f` in another terminal; events stream below\n");
|
|
231
229
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const runsBefore = new Map(roles.map((role) => [role, loadRunnerState(root, role).runs]));
|
|
237
|
-
let exit;
|
|
238
|
-
try {
|
|
239
|
-
exit = await (await import("../orchestrator/orchestrator.js")).runOrchestrator({
|
|
240
|
-
root,
|
|
241
|
-
config,
|
|
242
|
-
mainBranch,
|
|
243
|
-
signal: controller.signal,
|
|
244
|
-
redeploy,
|
|
245
|
-
launchServicesWatch,
|
|
246
|
-
once,
|
|
247
|
-
roleFilter: roleFilter ?? undefined,
|
|
248
|
-
stopsAt: forMs !== null ? windowStart + forMs : undefined,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
finally {
|
|
252
|
-
if (deadlineTimer)
|
|
253
|
-
deadlineTimer.clear();
|
|
254
|
-
scheduledStopWatch.clear();
|
|
255
|
-
unsubscribe();
|
|
256
|
-
}
|
|
230
|
+
return json ? () => { } : subscribeEvents((e) => say(formatEvent(e)));
|
|
231
|
+
}
|
|
232
|
+
/** Print the round summary: the once report/summary, or a windowed run's window report. */
|
|
233
|
+
function printRoundSummary(root, roles, runsBefore, once, json, forMs, windowStart, settled, runsDone, restart) {
|
|
257
234
|
if (once) {
|
|
258
235
|
if (json)
|
|
259
|
-
sayJson(onceReport(root, roles, runsBefore,
|
|
236
|
+
sayJson(onceReport(root, roles, runsBefore, settled, runsDone));
|
|
260
237
|
else
|
|
261
|
-
say(onceSummary(root, roles, runsBefore,
|
|
238
|
+
say(onceSummary(root, roles, runsBefore, settled, runsDone));
|
|
262
239
|
}
|
|
263
|
-
else if (forMs !== null && !
|
|
240
|
+
else if (forMs !== null && !restart) {
|
|
264
241
|
const { rows, covered } = readRunRowsSince(root, Date.now() - windowStart, null);
|
|
265
242
|
const summary = summarizeRunRows(rows);
|
|
266
243
|
if (json)
|
|
@@ -268,8 +245,12 @@ async function runSupervisedGeneration(input) {
|
|
|
268
245
|
else
|
|
269
246
|
say(windowSummary(durationLabel(forMs), summary, covered));
|
|
270
247
|
}
|
|
248
|
+
}
|
|
249
|
+
/** Set the process exit code for a once round that failed or was cut short before completion. */
|
|
250
|
+
async function applyOnceExitCode(root, roles, runsBefore, runsDone, once, json, isStopping) {
|
|
251
|
+
const { ONCE_FAILED_EXIT_CODE } = await import("../process/supervisor.js");
|
|
271
252
|
if (once) {
|
|
272
|
-
const failures = onceFailures(root, roles, runsBefore,
|
|
253
|
+
const failures = onceFailures(root, roles, runsBefore, runsDone);
|
|
273
254
|
if (failures.length > 0) {
|
|
274
255
|
// In --json mode the failure facts already ride in the report's `failures`; the exit
|
|
275
256
|
// code is the contract a CI caller needs, so only the prose line is suppressed.
|
|
@@ -281,21 +262,73 @@ async function runSupervisedGeneration(input) {
|
|
|
281
262
|
// not read as success (0) to the CI caller parsing the JSON: the document still carries
|
|
282
263
|
// the round's partial facts, but the truncated round exits non-zero. Non-json once keeps
|
|
283
264
|
// its historical behavior (prose + 0 unless a role failed).
|
|
284
|
-
if (json &&
|
|
265
|
+
if (json && isStopping())
|
|
285
266
|
process.exitCode = ONCE_FAILED_EXIT_CODE;
|
|
286
267
|
}
|
|
268
|
+
}
|
|
269
|
+
/** Boot the supervised generation: the stop closure and SIGINT/SIGTERM handlers, the --for
|
|
270
|
+
* deadline timer and scheduled-stop watch, the parent-death watch, the baseline/redeploy/
|
|
271
|
+
* launch-services imports, the banner, the event subscription, runOrchestrator, and the
|
|
272
|
+
* once/window summaries and exit codes. */
|
|
273
|
+
async function runSupervisedGeneration(input) {
|
|
274
|
+
const { root, once, json, forMs, branchArg, roleFilter, config, mainBranch } = input;
|
|
275
|
+
const { startParentDeathWatch, expectedSupervisorPid } = await import("../process/supervisor.js");
|
|
276
|
+
const { controller, stop, isStopping } = installStopHandlers(json);
|
|
277
|
+
const { windowStart, deadlineTimer, scheduledStopWatch } = await armRunWatchdogs(root, json, forMs, stop, isStopping);
|
|
278
|
+
// A supervisor that dies without forwarding — SIGKILL (the OOM killer, `kill -9`) cannot be
|
|
279
|
+
// trapped or forwarded, and an uncaught supervisor crash forwards nothing either — leaves this
|
|
280
|
+
// generation reparented and running the team unattended. The parent-death watch polls for the
|
|
281
|
+
// reparent and runs the same graceful stop a forwarded SIGTERM would have run, so a killed
|
|
282
|
+
// supervisor takes its team down with it (BUGS.md 2026-09-30). The spawner hands its pid
|
|
283
|
+
// down (SUPERVISOR_PID_ENV), so a supervisor killed during this child's startup — before the
|
|
284
|
+
// watch's first ppid read — still counts as gone. Everything below runs only in
|
|
285
|
+
// the supervised generation: an unsupervised `run` became the supervisor above and returned.
|
|
286
|
+
const expectedPpid = expectedSupervisorPid();
|
|
287
|
+
startParentDeathWatch(stop, expectedPpid !== undefined ? { expectedPpid } : {});
|
|
288
|
+
const enabled = runnerIds(config);
|
|
289
|
+
// A scoped round's role list is exactly the filter; otherwise every enabled role runs.
|
|
290
|
+
const roles = roleFilter ?? enabled;
|
|
291
|
+
const { redeploy, launchServicesWatch } = await bootGenerationParts(root, branchArg, once);
|
|
292
|
+
const build = redeploy ? ` · build ${shortSha(redeploy.build.sha)}` : "";
|
|
293
|
+
const unsubscribe = announceGeneration(roles, mainBranch, once, json, forMs, root, build);
|
|
294
|
+
// Snapshot each role's run counter so the once summary can tell this round's runs from
|
|
295
|
+
// the persisted history (the state file accumulates across rounds).
|
|
296
|
+
await runStartupMigrations(root); // so the snapshot reads a migrated state/<new>.json
|
|
297
|
+
const runsBefore = new Map(roles.map((role) => [role, loadRunnerState(root, role).runs]));
|
|
298
|
+
let exit;
|
|
299
|
+
try {
|
|
300
|
+
exit = await (await import("../orchestrator/orchestrator.js")).runOrchestrator({
|
|
301
|
+
root,
|
|
302
|
+
config,
|
|
303
|
+
mainBranch,
|
|
304
|
+
signal: controller.signal,
|
|
305
|
+
redeploy,
|
|
306
|
+
launchServicesWatch,
|
|
307
|
+
once,
|
|
308
|
+
roleFilter: roleFilter ?? undefined,
|
|
309
|
+
stopsAt: forMs !== null ? windowStart + forMs : undefined,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
if (deadlineTimer)
|
|
314
|
+
deadlineTimer.clear();
|
|
315
|
+
scheduledStopWatch.clear();
|
|
316
|
+
unsubscribe();
|
|
317
|
+
}
|
|
318
|
+
printRoundSummary(root, roles, runsBefore, once, json, forMs, windowStart, exit.settled, exit.runsDone, exit.restart);
|
|
319
|
+
await applyOnceExitCode(root, roles, runsBefore, exit.runsDone, once, json, isStopping);
|
|
287
320
|
// A self-redeploy swapped the new build into dist/: hand the terminal back to the supervisor,
|
|
288
321
|
// which respawns this same script — now the new code — as the next generation.
|
|
289
322
|
if (exit.restart)
|
|
290
323
|
process.exit(RESTART_EXIT_CODE);
|
|
291
324
|
// A windowed (--for) run that did not self-redeploy is done the moment its summary is out:
|
|
292
325
|
// 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
|
|
326
|
+
// generation runs a whole team, and any ref'd handle a shutdown missed (a watcher, marker,
|
|
294
327
|
// or notifier handle a join missed) would otherwise pin this process's loop open, so the
|
|
295
328
|
// supervisor — which waits on this child's exit — never exits and the run reads as a hang:
|
|
296
329
|
// summary printed, exit code null (BUGS.md 2026-10-26). The summary above was the last
|
|
297
|
-
// output and nothing after depends on this loop, so an explicit exit is equivalent to
|
|
298
|
-
// natural
|
|
330
|
+
// output and nothing after depends on this loop, so an explicit exit is equivalent to the
|
|
331
|
+
// natural exit 0 this replaces, just guaranteed.
|
|
299
332
|
if (forMs !== null)
|
|
300
333
|
process.exit(0);
|
|
301
334
|
}
|
|
@@ -13,41 +13,68 @@ import { EVENT_TYPE_LABELS } from "../events/event-labels.js";
|
|
|
13
13
|
import { POST_REVIEW_STATES } from "../review/post-merge-review-render.js";
|
|
14
14
|
import { HELP, helpStanzas } from "./help.js";
|
|
15
15
|
export const SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
|
|
16
|
-
/** The
|
|
17
|
-
* description column (two or more spaces).
|
|
18
|
-
|
|
19
|
-
* a single-space description (`role <id> --clear-notes Clear that notes`) adds nothing. */
|
|
20
|
-
function usageWords(firstLine, command) {
|
|
16
|
+
/** The usage text of a stanza first line: the part after `tumwater <command>` up to the
|
|
17
|
+
* description column (two or more spaces). */
|
|
18
|
+
function usagePortion(firstLine, command) {
|
|
21
19
|
const rest = firstLine.replace(new RegExp(`^ {2}tumwater ${command}`), "");
|
|
22
20
|
// Usage arguments follow the command after a single space; two or more spaces start the
|
|
23
21
|
// description.
|
|
24
|
-
|
|
22
|
+
return /^ {2,}/.test(rest) ? "" : (rest.split(/ {2,}/)[0] ?? "");
|
|
23
|
+
}
|
|
24
|
+
/** An angle-bracket alternation of plain words — `<bash|zsh|fish>`, the completion command's
|
|
25
|
+
* shell positional — expands to each alternative as a subcommand word, so `tumwater completion
|
|
26
|
+
* <TAB>` lists exactly the shells the command accepts. A bare placeholder like `<id>` or `<k>`
|
|
27
|
+
* carries no `|` and so stays a placeholder, skipped as before (its `<`/`>` never leave the
|
|
28
|
+
* token, so it never looked like a word). The expansion only fires while the leading run is
|
|
29
|
+
* open; the returned words contribute as the leading words of the usage part. */
|
|
30
|
+
function alternationWords(token, leading) {
|
|
31
|
+
const alternation = /^<([a-z][a-z-]*(?:\|[a-z][a-z-]*)+)>$/.exec(token);
|
|
32
|
+
return alternation && leading ? alternation[1].split("|") : null;
|
|
33
|
+
}
|
|
34
|
+
/** Whether a token counts as a completable bare word: it is a plain lowercase word and either
|
|
35
|
+
* sits inside brackets, opens a bracket itself, or is still within the leading run. */
|
|
36
|
+
function isCompletableWord(bare, token, depth, leading) {
|
|
37
|
+
return /^[a-z][a-z-]*$/.test(bare) && (depth > 0 || token.startsWith("[") || leading);
|
|
38
|
+
}
|
|
39
|
+
/** Whether a token ends the leading run: outside brackets, not opening a bracket, and not
|
|
40
|
+
* blank once its brackets and bars are stripped (so an angle-bracket placeholder closes it). */
|
|
41
|
+
function endsLeadingRun(bare, token, depth) {
|
|
42
|
+
return depth === 0 && !token.startsWith("[") && bare !== "";
|
|
43
|
+
}
|
|
44
|
+
/** The bracket-depth delta of a token: `[` +1 and `]` -1 per character, everything else 0. */
|
|
45
|
+
function bracketDelta(token) {
|
|
46
|
+
let delta = 0;
|
|
47
|
+
for (const ch of token)
|
|
48
|
+
delta += ch === "[" ? 1 : ch === "]" ? -1 : 0;
|
|
49
|
+
return delta;
|
|
50
|
+
}
|
|
51
|
+
/** The completable words of a usage text: the angle-bracket alternation expands, bare lowercase
|
|
52
|
+
* words count inside `[...]` and as the leading run directly after the command, and outside
|
|
53
|
+
* brackets the first other token ends the run. */
|
|
54
|
+
function usageWordsIn(usage) {
|
|
25
55
|
const words = [];
|
|
26
56
|
let depth = 0;
|
|
27
57
|
let leading = true;
|
|
28
58
|
for (const token of usage.split(/\s+/).filter(Boolean)) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// placeholder like `<id>` or `<k>` carries no `|` and so stays a placeholder, skipped
|
|
33
|
-
// as before (its `<`/`>` never leave the token, so it never looked like a word).
|
|
34
|
-
const alternation = /^<([a-z][a-z-]*(?:\|[a-z][a-z-]*)+)>$/.exec(token);
|
|
35
|
-
if (alternation && leading) {
|
|
36
|
-
for (const alt of alternation[1].split("|"))
|
|
37
|
-
words.push(alt);
|
|
59
|
+
const alternates = alternationWords(token, leading);
|
|
60
|
+
if (alternates) {
|
|
61
|
+
words.push(...alternates);
|
|
38
62
|
continue;
|
|
39
63
|
}
|
|
40
64
|
const bare = token.replace(/[[\]|]/g, "");
|
|
41
|
-
|
|
42
|
-
if (isWord && (depth > 0 || token.startsWith("[") || leading))
|
|
65
|
+
if (isCompletableWord(bare, token, depth, leading))
|
|
43
66
|
words.push(bare);
|
|
44
|
-
else if (
|
|
67
|
+
else if (endsLeadingRun(bare, token, depth))
|
|
45
68
|
leading = false;
|
|
46
|
-
|
|
47
|
-
depth += ch === "[" ? 1 : ch === "]" ? -1 : 0;
|
|
69
|
+
depth += bracketDelta(token);
|
|
48
70
|
}
|
|
49
71
|
return words;
|
|
50
72
|
}
|
|
73
|
+
/** The completable words of a stanza's usage part: the text after `tumwater <command>` up to the
|
|
74
|
+
* description column (two or more spaces). */
|
|
75
|
+
function usageWords(firstLine, command) {
|
|
76
|
+
return usageWordsIn(usagePortion(firstLine, command));
|
|
77
|
+
}
|
|
51
78
|
export function completionSpec(help = HELP) {
|
|
52
79
|
const stanzas = helpStanzas(help);
|
|
53
80
|
const commands = [...new Set(stanzas.map((s) => s.command))];
|
|
@@ -152,6 +179,15 @@ export function configValueWords(key) {
|
|
|
152
179
|
const ROLE_POSITIONAL_COMMANDS = ["role", "history", "revert"];
|
|
153
180
|
/** Flags whose value is a file path: completion offers file names after them. */
|
|
154
181
|
const PATH_FLAGS = ["--file", "--attach", "--notes-file", "--instructions-file", "--path"];
|
|
182
|
+
/** The constant fish value suffix for the flags whose candidates are a fixed `__tumwater_*` list:
|
|
183
|
+
* `--role`, `--result`, `--type` and `--status` take comma lists, so they complete per item through
|
|
184
|
+
* `__fish_complete_list`, skipping the items already listed. */
|
|
185
|
+
const FISH_LIST_FLAG_SUFFIXES = {
|
|
186
|
+
"--role": ` -x -a "(__fish_complete_list , __tumwater_roles)"`,
|
|
187
|
+
"--result": ` -x -a "(__fish_complete_list , __tumwater_results)"`,
|
|
188
|
+
"--type": ` -x -a "(__fish_complete_list , __tumwater_types)"`,
|
|
189
|
+
"--status": ` -x -a "(__fish_complete_list , __tumwater_review_states)"`,
|
|
190
|
+
};
|
|
155
191
|
/** The event-type codes `logs --type` filters by: the keys of the labels map, so a new event
|
|
156
192
|
* type completes the moment it gains a label. Sorted for a stable, alphabetical offer of a
|
|
157
193
|
* vocabulary too large to recall offhand — the same reason `--role`, `--template`, and
|
|
@@ -289,37 +325,37 @@ function bashScript(help) {
|
|
|
289
325
|
``,
|
|
290
326
|
].join("\n");
|
|
291
327
|
}
|
|
292
|
-
/** The value suffix
|
|
293
|
-
*
|
|
294
|
-
* `
|
|
295
|
-
|
|
296
|
-
function fishFlagValue(command, flag) {
|
|
297
|
-
if (flag === "--role")
|
|
298
|
-
return ` -x -a "(__fish_complete_list , __tumwater_roles)"`;
|
|
328
|
+
/** The fish value suffix for the flags whose value depends on the command: `--transcript` has role
|
|
329
|
+
* ids only under `logs`, and `--model` completes the closed tier set under `role` but the log's
|
|
330
|
+
* selectors under `history`. Undefined for a flag this helper does not own. */
|
|
331
|
+
function fishScopedFlagSuffix(command, flag) {
|
|
299
332
|
if (flag === "--transcript" && command === "logs")
|
|
300
|
-
return
|
|
301
|
-
if (PATH_FLAGS.includes(flag))
|
|
302
|
-
return ` -r -F`;
|
|
303
|
-
if (flag === "--template")
|
|
304
|
-
return ` -x -a "${templateIds().join(" ")}"`;
|
|
305
|
-
if (flag === "--result")
|
|
306
|
-
return ` -x -a "(__fish_complete_list , __tumwater_results)"`;
|
|
307
|
-
if (flag === "--type")
|
|
308
|
-
return ` -x -a "(__fish_complete_list , __tumwater_types)"`;
|
|
309
|
-
if (flag === "--status")
|
|
310
|
-
return ` -x -a "(__fish_complete_list , __tumwater_review_states)"`;
|
|
311
|
-
if (flag === "--thinking")
|
|
312
|
-
return ` -x -a "${THINKING_LEVEL_WORDS.join(" ")}"`;
|
|
313
|
-
if (flag === "--backend")
|
|
314
|
-
return ` -x -a "${BACKEND_IDS.join(" ")}"`;
|
|
315
|
-
if (flag === "--by")
|
|
316
|
-
return ` -x -a "${HISTORY_SUMMARY_GROUPS.join(" ")}"`;
|
|
317
|
-
// `role --model` completes the closed tier set; `history --model` completes the log's selectors.
|
|
333
|
+
return FISH_LIST_FLAG_SUFFIXES["--role"];
|
|
318
334
|
if (flag === "--model" && command === "role")
|
|
319
335
|
return ` -x -a "${MODEL_TIERS.join(" ")}"`;
|
|
320
336
|
if (flag === "--model" && command === "history")
|
|
321
337
|
return ` -x -a "(__fish_complete_list , __tumwater_models)"`;
|
|
322
|
-
return
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
/** The fish value suffix for the flags whose candidates are a joined literal list. Undefined for a
|
|
341
|
+
* flag this helper does not own. */
|
|
342
|
+
function fishWordSuffix(flag) {
|
|
343
|
+
switch (flag) {
|
|
344
|
+
case "--template": return ` -x -a "${templateIds().join(" ")}"`;
|
|
345
|
+
case "--thinking": return ` -x -a "${THINKING_LEVEL_WORDS.join(" ")}"`;
|
|
346
|
+
case "--backend": return ` -x -a "${BACKEND_IDS.join(" ")}"`;
|
|
347
|
+
case "--by": return ` -x -a "${HISTORY_SUMMARY_GROUPS.join(" ")}"`;
|
|
348
|
+
default: return undefined;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
/** The value suffix a fish flag line carries: what completes after the flag. `--role`, `--result`
|
|
352
|
+
* `--type` and `--status` take comma lists, so they complete per item through
|
|
353
|
+
* `__fish_complete_list`,
|
|
354
|
+
* skipping the items already listed. Tries the command-scoped flags, then the fixed
|
|
355
|
+
* `__tumwater_*` list table, then the joined literal-word flags, and finally the path flags, in
|
|
356
|
+
* the same order the ladder did. */
|
|
357
|
+
function fishFlagValue(command, flag) {
|
|
358
|
+
return fishScopedFlagSuffix(command, flag) ?? FISH_LIST_FLAG_SUFFIXES[flag] ?? fishWordSuffix(flag) ?? (PATH_FLAGS.includes(flag) ? " -r -F" : "");
|
|
323
359
|
}
|
|
324
360
|
function fishScript(help) {
|
|
325
361
|
const spec = completionSpec(help);
|
|
@@ -59,16 +59,11 @@ function screenOperableKey(root, key) {
|
|
|
59
59
|
fail(fieldError);
|
|
60
60
|
}
|
|
61
61
|
else if (parsed.kind === "map") {
|
|
62
|
-
//
|
|
63
|
-
// keyed by provider, not a role id, and `config set` writes it without the role check —
|
|
64
|
-
// so get/unset must agree, or set would succeed while get/unset refused the provider as
|
|
65
|
-
// an "unknown role". A typo'd role id in the other two maps is refused the same way set's
|
|
62
|
+
// Role-keyed maps check the role catalog: a typo'd role id is refused the way set's
|
|
66
63
|
// validator treats it.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
fail(unknownRoleMessage(parsed.role, ids));
|
|
71
|
-
}
|
|
64
|
+
const ids = knownRoleIds(config);
|
|
65
|
+
if (!ids.includes(parsed.role))
|
|
66
|
+
fail(unknownRoleMessage(parsed.role, ids));
|
|
72
67
|
}
|
|
73
68
|
return { parsed, config };
|
|
74
69
|
}
|
|
@@ -89,72 +84,74 @@ function resolvedConfigValue(config, parsed, key) {
|
|
|
89
84
|
? record.roles?.[parsed.id]?.[parsed.field]
|
|
90
85
|
: record[canonicalConfigKey(key)];
|
|
91
86
|
}
|
|
92
|
-
/** The `
|
|
93
|
-
*
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
87
|
+
/** The `config get` verb: screen the key through screenOperableKey, then print its resolved
|
|
88
|
+
* value (defaults merged in) as JSON, or JSON null when nothing sets it. */
|
|
89
|
+
async function configGet(root, key) {
|
|
90
|
+
const { parsed, config } = screenOperableKey(root, key ?? "");
|
|
91
|
+
const value = resolvedConfigValue(config, parsed, key ?? "");
|
|
92
|
+
sayJsonLine(value === undefined ? null : value); // Absent optional key → JSON null.
|
|
93
|
+
}
|
|
94
|
+
/** The `config unset` verb: screen the key, remove it, and confirm what, if anything, was
|
|
95
|
+
* removed (or what the key now resolves to). */
|
|
96
|
+
async function configUnset(root, key) {
|
|
97
|
+
// cli.ts's arity gate requires exactly two args (`unset <key>`) before cmdConfig runs;
|
|
98
|
+
// this is the defensive tail for a direct caller, matching the bare-config tail below.
|
|
99
|
+
if (key === undefined)
|
|
100
|
+
fail(CONFIG_USAGE);
|
|
101
|
+
const k = canonicalConfigKey(key);
|
|
102
|
+
// The same screens get runs first, so a typo'd top-level key, role id, or role field
|
|
103
|
+
// fails identically — unsetConfigKey's own parse/field checks are the mutation-side
|
|
104
|
+
// backstop, not the wording the operator sees.
|
|
105
|
+
const { parsed } = screenOperableKey(root, k);
|
|
106
|
+
const result = unsetConfigKey(root, k);
|
|
107
|
+
if (!result.ok)
|
|
108
|
+
fail(result.error);
|
|
109
|
+
// The reload only feeds the message, so a failed one leaves `now` undefined.
|
|
110
|
+
const reloaded = loadConfigSafe(root).config;
|
|
111
|
+
const now = reloaded === undefined ? undefined : resolvedConfigValue(reloaded, parsed, k);
|
|
112
|
+
const resolves = now === undefined ? "" : JSON.stringify(now);
|
|
113
|
+
if (result.oldValue === undefined) {
|
|
114
|
+
say(`${k} is not set in tumwater.json, so nothing was removed` +
|
|
115
|
+
(now === undefined ? "" : ` (it resolves to ${resolves})`));
|
|
101
116
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (key === undefined)
|
|
106
|
-
fail(CONFIG_USAGE);
|
|
107
|
-
const k = canonicalConfigKey(key);
|
|
108
|
-
// The same screens get runs first, so a typo'd top-level key, role id, or role field
|
|
109
|
-
// fails identically — unsetConfigKey's own parse/field checks are the mutation-side
|
|
110
|
-
// backstop, not the wording the operator sees.
|
|
111
|
-
const { parsed } = screenOperableKey(root, k);
|
|
112
|
-
const result = unsetConfigKey(root, k);
|
|
113
|
-
if (!result.ok)
|
|
114
|
-
fail(result.error);
|
|
115
|
-
// The reload only feeds the message, so a failed one leaves `now` undefined.
|
|
116
|
-
const reloaded = loadConfigSafe(root).config;
|
|
117
|
-
const now = reloaded === undefined ? undefined : resolvedConfigValue(reloaded, parsed, k);
|
|
118
|
-
const resolves = now === undefined ? "" : JSON.stringify(now);
|
|
119
|
-
if (result.oldValue === undefined) {
|
|
120
|
-
say(`${k} is not set in tumwater.json, so nothing was removed` +
|
|
121
|
-
(now === undefined ? "" : ` (it resolves to ${resolves})`));
|
|
122
|
-
}
|
|
123
|
-
else {
|
|
124
|
-
const was = JSON.stringify(result.oldValue);
|
|
125
|
-
say(`unset ${k} (was ${was}${now === undefined ? "" : `; now resolves to ${resolves}`})`);
|
|
126
|
-
}
|
|
127
|
-
return;
|
|
117
|
+
else {
|
|
118
|
+
const was = JSON.stringify(result.oldValue);
|
|
119
|
+
say(`unset ${k} (was ${was}${now === undefined ? "" : `; now resolves to ${resolves}`})`);
|
|
128
120
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
say(
|
|
148
|
-
const note = result.note;
|
|
149
|
-
if (note)
|
|
150
|
-
say(`note: ${note}`);
|
|
151
|
-
// A bare `roles` set replaces the FILE's section, but the built-in catalog always merges
|
|
152
|
-
// per entry at load, so the replacement cannot disable a role by omission. Say so here
|
|
153
|
-
// rather than let the confirmation imply the role is gone.
|
|
154
|
-
if (key === "roles")
|
|
155
|
-
say("note: the built-in role catalog still merges per entry — disable a role with `tumwater config set roles.<id>.enabled false`, not by omitting it");
|
|
121
|
+
}
|
|
122
|
+
/** The `config set` verb: write one value through setConfigKey (refusing a flag-looking
|
|
123
|
+
* value first) and confirm the write or that it was already that value. */
|
|
124
|
+
async function configSet(root, key, rest) {
|
|
125
|
+
// A flag-looking value is a dropped `--` in disguise: `config set model --json` would
|
|
126
|
+
// otherwise take the literal string "--json" as the value (the JSON-parse fallback
|
|
127
|
+
// swallows any token) and validateConfig's string keys accept it silently — the same
|
|
128
|
+
// shape gui/gui-command.ts's --token gate refuses, where the value the next flag holds is never
|
|
129
|
+
// what the operator meant. Refuse before setConfigKey reads it; a value that really
|
|
130
|
+
// does begin with `--` has no legitimate shape among the config keys' values
|
|
131
|
+
// (identifiers, durations, windows, JSON scalars and arrays), so nothing is lost.
|
|
132
|
+
const raw = rest[0] ?? "";
|
|
133
|
+
if (raw.startsWith("--"))
|
|
134
|
+
fail(`config set got the flag-looking value ${JSON.stringify(raw)} instead of a value — is a flag missing its value, or did a token slip into the value's position? (e.g. \`tumwater config set model gpt-5\`)`);
|
|
135
|
+
const result = setConfigKey(root, key ?? "", raw);
|
|
136
|
+
if (!result.ok)
|
|
137
|
+
fail(result.error);
|
|
138
|
+
if (result.unchanged) {
|
|
139
|
+
say(`${canonicalConfigKey(key ?? "")} is already ${JSON.stringify(result.value)}, so tumwater.json was not written`);
|
|
156
140
|
return;
|
|
157
141
|
}
|
|
142
|
+
say(`set ${canonicalConfigKey(key ?? "")} to ${JSON.stringify(result.value)}`);
|
|
143
|
+
const note = result.note;
|
|
144
|
+
if (note)
|
|
145
|
+
say(`note: ${note}`);
|
|
146
|
+
// A bare `roles` set replaces the FILE's section, but the built-in catalog always merges
|
|
147
|
+
// per entry at load, so the replacement cannot disable a role by omission. Say so here
|
|
148
|
+
// rather than let the confirmation imply the role is gone.
|
|
149
|
+
if (key === "roles")
|
|
150
|
+
say("note: the built-in role catalog still merges per entry — disable a role with `tumwater config set roles.<id>.enabled false`, not by omitting it");
|
|
151
|
+
}
|
|
152
|
+
/** The bare-config tail: the whole resolved config as pretty JSON, refusing a surplus
|
|
153
|
+
* subcommand the arity gate somehow missed. */
|
|
154
|
+
async function configDump(root, sub) {
|
|
158
155
|
// Bare config, or anything else: the whole-config dump is the default, and cli.ts's arity
|
|
159
156
|
// check has already rejected a malformed get/set — this usage line is the defensive tail.
|
|
160
157
|
if (sub !== undefined)
|
|
@@ -164,3 +161,17 @@ export async function cmdConfig(root, args = []) {
|
|
|
164
161
|
fail(error); // validateConfig's message, via the standard fail()
|
|
165
162
|
sayJson(config);
|
|
166
163
|
}
|
|
164
|
+
/** The `tumwater config` command body: bare prints the resolved config; `get` and `unset`
|
|
165
|
+
* screen their key through screenOperableKey, while `set` writes through setConfigKey. Each
|
|
166
|
+
* verb's guards and confirmation wording live in one named helper — configGet, configUnset,
|
|
167
|
+
* configSet, configDump — that this dispatcher delegates to. */
|
|
168
|
+
export async function cmdConfig(root, args = []) {
|
|
169
|
+
const [sub, key, ...rest] = args;
|
|
170
|
+
if (sub === "get")
|
|
171
|
+
return configGet(root, key);
|
|
172
|
+
if (sub === "unset")
|
|
173
|
+
return configUnset(root, key);
|
|
174
|
+
if (sub === "set")
|
|
175
|
+
return configSet(root, key, rest);
|
|
176
|
+
return configDump(root, sub);
|
|
177
|
+
}
|
package/dist/src/cli/help.js
CHANGED
|
@@ -313,9 +313,8 @@ Usage:
|
|
|
313
313
|
non-blank line is the symptom, the rest is the body kept
|
|
314
314
|
line for line; wakes the bugfix role
|
|
315
315
|
tumwater bug --release <n> [--json]
|
|
316
|
-
Lift the Refused or
|
|
317
|
-
Needs
|
|
318
|
-
(one per run) from the Nth entry in BUGS.md ## Open (as
|
|
316
|
+
Lift the Refused or the
|
|
317
|
+
Needs review notes from the Nth entry in BUGS.md ## Open (as
|
|
319
318
|
\`tumwater bug --list\` lists it), commit it on main, and wake
|
|
320
319
|
the bugfix role; --json prints {file, title, lifted}
|
|
321
320
|
tumwater bug --top <n> [--json]
|
|
@@ -370,9 +369,8 @@ Usage:
|
|
|
370
369
|
the first non-blank line is the title, the rest is the body
|
|
371
370
|
kept line for line; wakes the feature role
|
|
372
371
|
tumwater plan --release <n> [--json]
|
|
373
|
-
Lift the Refused or
|
|
374
|
-
Needs
|
|
375
|
-
(one per run) from the Nth entry in PLANS.md ## Planned
|
|
372
|
+
Lift the Refused or the
|
|
373
|
+
Needs review notes from the Nth entry in PLANS.md ## Planned
|
|
376
374
|
(as \`tumwater plan --list\` lists it), commit it on main,
|
|
377
375
|
and wake the feature role; --json prints {file, title, lifted}
|
|
378
376
|
tumwater plan --top <n> [--json]
|