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
|
@@ -13,8 +13,8 @@ import { logEventBestEffort } from "../events/events.js";
|
|
|
13
13
|
* full-suite check. The one vocabulary the three predicates that recognize a full-suite check
|
|
14
14
|
* select on — isPassingFullCheck, isRedFullCheck, and the status polls' newest-merge-check
|
|
15
15
|
* scan (status-polls.ts) — so they cannot drift on which scopes count. The deadline-killed
|
|
16
|
-
* failure costing in time-spend.ts is a narrower pair (merge/batch only, with a status
|
|
17
|
-
* timeout required), so it keeps its own check. `full` is the event's `full` field: false
|
|
16
|
+
* failure costing in failure-time-spend.ts is a narrower pair (merge/batch only, with a status
|
|
17
|
+
* and a timeout required), so it keeps its own check. `full` is the event's `full` field: false
|
|
18
18
|
* (a check.mergeCommand run) is never a full-suite check. */
|
|
19
19
|
export function isFullCheckScope(scope, full) {
|
|
20
20
|
// A merge or batch run of check.mergeCommand carries `full: false` (buildCheckEvent): it ran
|
|
@@ -32,7 +32,7 @@ function resolveScopedCheck(wt, scope, config, timeoutMs, sampleLoad) {
|
|
|
32
32
|
/** Run the resolved check under one permit, with the one clean retry for a verdict-less death —
|
|
33
33
|
* the one-permit-two-attempt rule. Prices the first attempt as its own build_check event and
|
|
34
34
|
* returns the final outcome and its run duration. */
|
|
35
|
-
async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutMs, config, sampleSleep, install, permitWait, cheaper) {
|
|
35
|
+
async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutMs, config, sampleSleep, install, permitWait, cheaper, signal) {
|
|
36
36
|
// One permit covers both attempts (the killed-check retry re-runs under the permit it
|
|
37
37
|
// holds); durationMs prices the run itself, not the wait for a permit.
|
|
38
38
|
let durationMs = 0;
|
|
@@ -45,7 +45,7 @@ async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutM
|
|
|
45
45
|
// outcome stands (runWithCleanRetry). The verdict-less first attempt is priced as its own
|
|
46
46
|
// event (the feed must answer how long a check took), then the final event below records
|
|
47
47
|
// the retry's classified outcome.
|
|
48
|
-
const { outcome, durationMs: d } = await runWithCleanRetry(() => runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install), (first, dur) => logEventBestEffort(root, buildCheckEvent(role, scope, first, dur, cheaper)));
|
|
48
|
+
const { outcome, durationMs: d } = await runWithCleanRetry(() => runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install, signal), (first, dur) => logEventBestEffort(root, buildCheckEvent(role, scope, first, dur, cheaper)));
|
|
49
49
|
durationMs = d;
|
|
50
50
|
return outcome;
|
|
51
51
|
}, {
|
|
@@ -59,6 +59,23 @@ async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutM
|
|
|
59
59
|
* unverified reject; anything else passes through. Never logs — the caller warns (see
|
|
60
60
|
* warnScopedCheck). Returns the unverified reason only when a rule fired. */
|
|
61
61
|
function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs) {
|
|
62
|
+
const sleepFailed = sleptThroughRun(raw);
|
|
63
|
+
if (!unverifiedSkipFor(scope, raw, sleepFailed))
|
|
64
|
+
return { outcome: raw, unverifiedReason: null };
|
|
65
|
+
const unverifiedReason = unverifiedReasonFor(scope, raw, effectiveMs, load, durationMs, sleepFailed);
|
|
66
|
+
return { outcome: unverifiedFailOutcome(check, raw, unverifiedReason), unverifiedReason };
|
|
67
|
+
}
|
|
68
|
+
/** True when the failed run slept past the tolerance — the slept-through rule. */
|
|
69
|
+
function sleptThroughRun(raw) {
|
|
70
|
+
// A retry that ALSO slept made no clean verdict either: it is recorded as unverified — not
|
|
71
|
+
// attributed to the change, not cached as a red baseline — rather than read as a second
|
|
72
|
+
// deterministic failure (BUGS.md 2026-09-30). A retry that passed stands: the tree went green
|
|
73
|
+
// under the project's own check, even if the host napped through parts of it.
|
|
74
|
+
return raw.status === "failed" && (raw.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS;
|
|
75
|
+
}
|
|
76
|
+
/** Whether a merge scope classifies this raw outcome as a deterministic unverified reject — the
|
|
77
|
+
* merge-scope skip rule. */
|
|
78
|
+
function unverifiedSkipFor(scope, raw, sleepFailed) {
|
|
62
79
|
// A signal kill at a merge scope, or a timeout whose deadline fired on time (real slowness, a
|
|
63
80
|
// genuinely hung or oversized suite), made no verdict about the tree — and this is the check
|
|
64
81
|
// whose whole job is to catch a semantic conflict before it merges, so it rejects
|
|
@@ -67,20 +84,19 @@ function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs)
|
|
|
67
84
|
// slept, so a second verdict-less attempt is recorded as unverified rather than read again as
|
|
68
85
|
// weather (BUGS.md 2026-09-28). no-npm and a broken toolchain still say nothing about the tree,
|
|
69
86
|
// and the gate scope still proceeds to the model reviewer, which the merge path's own check backs
|
|
70
|
-
// up.
|
|
71
|
-
// attributed to the change, not cached as a red baseline — rather than read as a second
|
|
72
|
-
// deterministic failure (BUGS.md 2026-09-30). A retry that passed stands: the tree went green
|
|
73
|
-
// under the project's own check, even if the host napped through parts of it.
|
|
74
|
-
const sleepFailed = raw.status === "failed" && (raw.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS;
|
|
87
|
+
// up.
|
|
75
88
|
// A failed dependency install is the same at a merge scope: nothing ran against the tree as its
|
|
76
89
|
// lockfile pins it, so it must not merge (BUGS.md 2026-10-01).
|
|
77
|
-
|
|
78
|
-
((
|
|
90
|
+
return (MERGE_SCOPES.has(scope) &&
|
|
91
|
+
((raw.status === "skipped" &&
|
|
79
92
|
(raw.skipReason === "timeout" || raw.skipReason === "killed" || raw.skipReason === "install")) ||
|
|
80
93
|
sleepFailed));
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
}
|
|
95
|
+
/** The unverified reason phrase for the rule's four arms — the reason chain, with the optional
|
|
96
|
+
* starved-host tail. The reason names the starved host only when the load says the team was the
|
|
97
|
+
* check's competition (a late-deadline wording already names its own cause). */
|
|
98
|
+
function unverifiedReasonFor(scope, raw, effectiveMs, load, durationMs, sleepFailed) {
|
|
99
|
+
return raw.skipReason === "install"
|
|
84
100
|
? installFailedPhrase(SCOPE_WORDS[scope].label, raw.install, UNVERIFIED_TREE)
|
|
85
101
|
: raw.skipReason === "killed"
|
|
86
102
|
? killedPhrase(SCOPE_WORDS[scope].label, raw.killedBy ? { signal: raw.killedBy, durationMs } : undefined, UNVERIFIED_TREE)
|
|
@@ -90,7 +106,11 @@ function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs)
|
|
|
90
106
|
// An on-time kill (the late-deadline wording already names its own cause) names the
|
|
91
107
|
// starved host when the load average says the team was the check's competition.
|
|
92
108
|
(raw.run?.deadlineLateMs ?? 0) > DEADLINE_LATE_TOLERANCE_MS ? "" : loadedHostPhrase(load)}`;
|
|
93
|
-
|
|
109
|
+
}
|
|
110
|
+
/** Assemble the deterministic `failed` outcome that carries the unverified reason — the
|
|
111
|
+
* failed-outcome assembly. */
|
|
112
|
+
function unverifiedFailOutcome(check, raw, unverifiedReason) {
|
|
113
|
+
return {
|
|
94
114
|
status: "failed",
|
|
95
115
|
script: checkScriptName(check),
|
|
96
116
|
outputTail: [unverifiedReason],
|
|
@@ -98,12 +118,16 @@ function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs)
|
|
|
98
118
|
unverified: true,
|
|
99
119
|
...(raw.install ? { install: raw.install } : {}),
|
|
100
120
|
};
|
|
101
|
-
return { outcome, unverifiedReason };
|
|
102
121
|
}
|
|
103
122
|
/** Warn for the two verdict-less roads: a skipped check (buildCheckSkipWarning) or an unverified
|
|
104
123
|
* merge reject. Reads the scope word table and the killed signal. */
|
|
105
124
|
function warnScopedCheck(root, role, scope, outcome, effectiveMs, durationMs, unverifiedReason) {
|
|
106
125
|
if (outcome.status === "skipped") {
|
|
126
|
+
// A check the harness stopped on an abort signal is neither weather nor a skip to warn
|
|
127
|
+
// about: the caller (the gate pre-check) fails closed as aborted on its way out, so no
|
|
128
|
+
// "proceeding to model review" warning may suggest the check still stands.
|
|
129
|
+
if (outcome.skipReason === "aborted")
|
|
130
|
+
return;
|
|
107
131
|
// Environmental — deliberately NOT fail-closed, so a hung build script cannot wedge every
|
|
108
132
|
// code run into the 3-strike discard (gate) or a merge behind the merge lock.
|
|
109
133
|
const w = SCOPE_WORDS[scope];
|
|
@@ -142,11 +166,11 @@ function warnScopedCheck(root, role, scope, outcome, effectiveMs, durationMs, un
|
|
|
142
166
|
* merge and batch scopes, and those events then carry `full: false`. The red-main baseline, which
|
|
143
167
|
* detects on its own, keeps running check.command. `permitWait` hears
|
|
144
168
|
* about a wait for the check permit (the merge cell's "waiting for a check slot"). Never throws. */
|
|
145
|
-
export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad) {
|
|
169
|
+
export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad, signal) {
|
|
146
170
|
const r = resolveScopedCheck(wt, scope, config, timeoutMs, sampleLoad);
|
|
147
171
|
if (!r)
|
|
148
172
|
return null;
|
|
149
|
-
const { raw, durationMs } = await runScopedCheckWithRetry(root, role, scope, wt, r.runCheck, timeoutMs, config, sampleSleep, install, permitWait, r.cheaper);
|
|
173
|
+
const { raw, durationMs } = await runScopedCheckWithRetry(root, role, scope, wt, r.runCheck, timeoutMs, config, sampleSleep, install, permitWait, r.cheaper, signal);
|
|
150
174
|
const { outcome, unverifiedReason } = unverifiedOutcomeFor(scope, r.check, raw, r.effectiveMs, r.load, durationMs);
|
|
151
175
|
logEventBestEffort(root, buildCheckEvent(role, scope, outcome, durationMs, r.cheaper));
|
|
152
176
|
warnScopedCheck(root, role, scope, outcome, r.effectiveMs, durationMs, unverifiedReason);
|
|
@@ -72,7 +72,7 @@ export function checkScriptName(check) {
|
|
|
72
72
|
* branch state — which is what this check exists for; build-check.test.ts pins the
|
|
73
73
|
* no-node_modules-worktree resolution. A configured command's cwd is resolved by
|
|
74
74
|
* detectBuildCheck against the worktree the detection started from. */
|
|
75
|
-
export async function runBuildCheck(wt, check, timeoutMs = BUILD_CHECK_TIMEOUT_MS, killGraceMs = KILL_GRACE_MS, sampleSleep = sampleSleepClock, install = npmInstall) {
|
|
75
|
+
export async function runBuildCheck(wt, check, timeoutMs = BUILD_CHECK_TIMEOUT_MS, killGraceMs = KILL_GRACE_MS, sampleSleep = sampleSleepClock, install = npmInstall, signal) {
|
|
76
76
|
const script = checkScriptName(check);
|
|
77
77
|
const effectiveMs = checkTimeoutMs(check, timeoutMs);
|
|
78
78
|
// Environmental probe first: a toolchain broken below the project (git exiting 69 on an
|
|
@@ -105,13 +105,13 @@ export async function runBuildCheck(wt, check, timeoutMs = BUILD_CHECK_TIMEOUT_M
|
|
|
105
105
|
if (r)
|
|
106
106
|
installed = { packages: r.packages, durationMs: r.durationMs };
|
|
107
107
|
}
|
|
108
|
-
const outcome = await runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sampleSleep);
|
|
108
|
+
const outcome = await runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sampleSleep, signal);
|
|
109
109
|
return installed ? { ...outcome, install: installed } : outcome;
|
|
110
110
|
}
|
|
111
111
|
/** runBuildCheck's run-and-classify step, once the probe and any owed install are done: spawn
|
|
112
|
-
* the declared check as a process group under its deadline,
|
|
113
|
-
* classify what it did. */
|
|
114
|
-
async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sampleSleep) {
|
|
112
|
+
* the declared check as a process group under its deadline — or, when `signal` fires, stopped
|
|
113
|
+
* by the harness as its own abort — bracketed by sleep samples, and classify what it did. */
|
|
114
|
+
async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sampleSleep, signal) {
|
|
115
115
|
// Sleep evidence brackets the spawn: the opening sample is taken while the host is provably
|
|
116
116
|
// awake (this code is running), so any sleep the closing sample can attribute began inside
|
|
117
117
|
// the run's window. A platform with no readable clock leaves run.sleptMs unset — no evidence
|
|
@@ -123,12 +123,14 @@ async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sam
|
|
|
123
123
|
timeoutMs: effectiveMs,
|
|
124
124
|
killGraceMs,
|
|
125
125
|
maxBuffer: EXEC_MAX_BUFFER,
|
|
126
|
+
signal,
|
|
126
127
|
})
|
|
127
128
|
: await runScriptGroup("npm", ["run", check.script], {
|
|
128
129
|
cwd: wt,
|
|
129
130
|
timeoutMs: effectiveMs,
|
|
130
131
|
killGraceMs,
|
|
131
132
|
maxBuffer: EXEC_MAX_BUFFER,
|
|
133
|
+
signal,
|
|
132
134
|
});
|
|
133
135
|
// Spawn failed before anything ran — the runner is missing from PATH.
|
|
134
136
|
if (r.spawnError)
|
|
@@ -143,6 +145,12 @@ async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sam
|
|
|
143
145
|
// deadline actually fired, which the warning reports when it was late.
|
|
144
146
|
if (r.timedOut)
|
|
145
147
|
return { status: "skipped", script, skipReason: "timeout", run };
|
|
148
|
+
// The harness stopped the tree on an abort signal (a user abort or a shutdown) — its own
|
|
149
|
+
// stop, not weather and not a verdict about the tree: classify it separately from a timeout
|
|
150
|
+
// and from an external killed signal so runScopedBuildCheck neither retries it nor warns it
|
|
151
|
+
// as a skip.
|
|
152
|
+
if (r.aborted)
|
|
153
|
+
return { status: "skipped", script, skipReason: "aborted", run };
|
|
146
154
|
// The tree died on a signal the harness did NOT send (runScriptGroup resolves its own
|
|
147
155
|
// timeout with timedOut already set, so a signal reaching this line is external — another
|
|
148
156
|
// run's `pkill`, an operator's cleanup): the outcome says nothing about the tree, but it is
|
|
@@ -72,6 +72,15 @@ function failStrayArg(args, reason, ...claimed) {
|
|
|
72
72
|
if (extra !== undefined)
|
|
73
73
|
fail(`unexpected argument ${JSON.stringify(extra)} — ${reason}`);
|
|
74
74
|
}
|
|
75
|
+
/** The two positions a valued flag claims for a free-form-prompt parser's stray-argument
|
|
76
|
+
* accounting: the flag token and its value — the flag and its value are never prompt content.
|
|
77
|
+
* Empty when the flag is absent, so a negative index never over-claims a real token. The one
|
|
78
|
+
* shape every valued-flag pair claims for the parsePromptFlags/parsePromptArgs stray-argument
|
|
79
|
+
* accounting — `--role`, `--grep`, `--at` in parsePromptFlags and `--file` in editPromptArgs
|
|
80
|
+
* ("claimed like --role's": each site is this same pair) — so no site can drift from the others. */
|
|
81
|
+
function pairClaim(flagIndex) {
|
|
82
|
+
return flagIndex >= 0 ? [flagIndex, flagIndex + 1] : [];
|
|
83
|
+
}
|
|
75
84
|
/** Read a `--file <path>` prompt file: fail naming the file when it cannot be read. A raw
|
|
76
85
|
* ENOENT/EISDIR names the path but not its role, so the message says this was the --file
|
|
77
86
|
* prompt. Shared by the init and prompt parsers' path branches; the prompt parser's stdin
|
|
@@ -141,25 +150,22 @@ function rejectDuplicateFlags(args, flags) {
|
|
|
141
150
|
for (const flag of flags)
|
|
142
151
|
rejectRepeatFlag(args.filter((a) => a === flag).length > 1, flag);
|
|
143
152
|
}
|
|
144
|
-
/** `tumwater init
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
* `--branch`, `--template`, `--adopt`, `--dry-run` or `--list-templates`), each given at most
|
|
148
|
-
* once; with `--file` present nothing but the other flags may accompany it; a `--branch
|
|
149
|
-
* <name>` pair and the valueless flags are never prompt content; single-dash positionals are
|
|
150
|
-
* prompt content, not flags. Without these checks a misspelled --file would be baked into the
|
|
151
|
-
* brief text — injected into every run of every role until someone edits the project
|
|
153
|
+
/** The flag-vocabulary gate for `tumwater init`: every double-dash token must be a known
|
|
154
|
+
* INIT_FLAG_SPECS flag, each given at most once. Without it a misspelled --file would be baked
|
|
155
|
+
* into the brief text — injected into every run of every role until someone edits the project
|
|
152
156
|
* brief. */
|
|
153
|
-
|
|
157
|
+
function assertInitFlags(args) {
|
|
154
158
|
const known = INIT_FLAG_SPECS.flatMap((s) => s.names);
|
|
155
159
|
// The unknown-double-dash gate reads the specs, so the list its error names admits exactly
|
|
156
160
|
// what the duplicate-flag check below accepts — one vocabulary, one message renderer.
|
|
157
161
|
rejectUnknownDoubleDash("init", args, INIT_FLAG_SPECS);
|
|
158
162
|
rejectDuplicateFlags(args, known);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
}
|
|
164
|
+
/** Resolve `--template <id>` from the argument list, refusing an empty id, an id unknown to
|
|
165
|
+
* the catalog, and a `--template` given alongside `--list-templates`. Validated here so an
|
|
166
|
+
* unknown id fails before initProject runs any side effect — before a half-seeded repo can
|
|
167
|
+
* result from a typo'd id. */
|
|
168
|
+
function resolveInitTemplate(args, listTemplates) {
|
|
163
169
|
// Validate the template id here, so an unknown one fails before initProject runs any side
|
|
164
170
|
// effect — and before a half-seeded repo can result from a typo'd id.
|
|
165
171
|
const templateFlag = args.indexOf("--template");
|
|
@@ -175,29 +181,40 @@ export function parseInitArgs(args) {
|
|
|
175
181
|
if (listTemplates && templateFlag >= 0) {
|
|
176
182
|
fail("--list-templates takes no --template — it lists the catalog, it does not use one");
|
|
177
183
|
}
|
|
184
|
+
return template;
|
|
185
|
+
}
|
|
186
|
+
/** The `--file` branch of `parseInitArgs`: when the flag is present the prompt comes from the
|
|
187
|
+
* file, so every other positional is claimed (refused as stray) and the file's exact contents
|
|
188
|
+
* are read as the prompt — unless `--list-templates` conflicts, which takes no prompt at all.
|
|
189
|
+
* Returns null when no `--file` flag is present. */
|
|
190
|
+
function resolveInitFileArg(args, base) {
|
|
178
191
|
const fileFlag = args.indexOf("--file");
|
|
179
|
-
if (fileFlag
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
192
|
+
if (fileFlag < 0)
|
|
193
|
+
return null;
|
|
194
|
+
const file = promptFileValue(args[fileFlag + 1]);
|
|
195
|
+
const claimed = [fileFlag, fileFlag + 1];
|
|
196
|
+
const branchFlag = args.indexOf("--branch");
|
|
197
|
+
if (branchFlag >= 0)
|
|
198
|
+
claimed.push(branchFlag, branchFlag + 1);
|
|
199
|
+
const templateFlag = args.indexOf("--template");
|
|
200
|
+
if (templateFlag >= 0)
|
|
201
|
+
claimed.push(templateFlag, templateFlag + 1);
|
|
202
|
+
for (const flag of INIT_BOOLEAN_FLAGS) {
|
|
203
|
+
if (args.includes(flag))
|
|
204
|
+
claimed.push(args.indexOf(flag));
|
|
205
|
+
}
|
|
206
|
+
failStrayArg(args, "with --file the prompt comes from the file", ...claimed);
|
|
207
|
+
const contents = readPromptFileChecked(file);
|
|
208
|
+
if (base.listTemplates) {
|
|
209
|
+
fail(LIST_TEMPLATES_PROMPT_ERROR);
|
|
197
210
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
211
|
+
return { ...base, prompt: contents };
|
|
212
|
+
}
|
|
213
|
+
/** Reconstruct the positional prompt text for `tumwater init`: everything except the
|
|
214
|
+
* `--branch`/`--template` pairs and the valueless flags is prompt text. The join keeps
|
|
215
|
+
* single-dash tokens as content, exactly as before — with no flag present this is the old
|
|
216
|
+
* args.join(" "). */
|
|
217
|
+
function joinInitPrompt(args) {
|
|
201
218
|
const promptTokens = [];
|
|
202
219
|
for (let i = 0; i < args.length; i++) {
|
|
203
220
|
const arg = args[i];
|
|
@@ -206,11 +223,29 @@ export function parseInitArgs(args) {
|
|
|
206
223
|
else if (!INIT_BOOLEAN_FLAGS.includes(arg))
|
|
207
224
|
promptTokens.push(arg);
|
|
208
225
|
}
|
|
209
|
-
|
|
210
|
-
|
|
226
|
+
return promptTokens.join(" ");
|
|
227
|
+
}
|
|
228
|
+
/** `tumwater init` argument handling. Init's positionals are free-form prompt text, so
|
|
229
|
+
* rejectUnknownArgs (which rejects ANY unconsumed token) can't be used wholesale. The rules
|
|
230
|
+
* instead: a double-dash token must be a known flag from INIT_FLAG_SPECS, each given at most
|
|
231
|
+
* once; with `--file` present nothing but the other flags may accompany it; a `--branch
|
|
232
|
+
* <name>` pair and the valueless flags are never prompt content; single-dash positionals are
|
|
233
|
+
* prompt content, not flags. */
|
|
234
|
+
export function parseInitArgs(args) {
|
|
235
|
+
assertInitFlags(args);
|
|
236
|
+
const branch = parseBranchFlag(args);
|
|
237
|
+
const adopt = args.includes("--adopt");
|
|
238
|
+
const dryRun = args.includes("--dry-run");
|
|
239
|
+
const listTemplates = args.includes("--list-templates");
|
|
240
|
+
const template = resolveInitTemplate(args, listTemplates);
|
|
241
|
+
const base = { branch, template, listTemplates, adopt, dryRun };
|
|
242
|
+
const fromFile = resolveInitFileArg(args, base);
|
|
243
|
+
if (fromFile !== null)
|
|
244
|
+
return fromFile;
|
|
245
|
+
const prompt = joinInitPrompt(args);
|
|
246
|
+
if (listTemplates && prompt.trim() !== "")
|
|
211
247
|
fail(LIST_TEMPLATES_PROMPT_ERROR);
|
|
212
|
-
}
|
|
213
|
-
return { prompt, branch, template, listTemplates, adopt, dryRun };
|
|
248
|
+
return { ...base, prompt };
|
|
214
249
|
}
|
|
215
250
|
/** The stdin prompt read once per process: cli.ts pre-parses prompt's args and cmdPrompt
|
|
216
251
|
* re-parses them, and a second readFileSync(0) on a drained pipe would see an empty prompt. */
|
|
@@ -306,7 +341,7 @@ function parsePromptRole(args, roleFlag) {
|
|
|
306
341
|
const role = roleRaw ?? null;
|
|
307
342
|
// The flag and its value are never prompt content: --role is a scope, not text. Only claimed
|
|
308
343
|
// when actually present — negative indexes would over-claim real tokens.
|
|
309
|
-
const roleClaim = roleFlag
|
|
344
|
+
const roleClaim = pairClaim(roleFlag);
|
|
310
345
|
return { role, roleClaim };
|
|
311
346
|
}
|
|
312
347
|
/** The `--json` list-only refusal: in enqueue or cancel mode it must never silently ride
|
|
@@ -325,7 +360,7 @@ function parsePromptGrep(args, scan) {
|
|
|
325
360
|
if (scan.grepFlag >= 0 && (!grepRaw || grepRaw.startsWith("--"))) {
|
|
326
361
|
fail("--grep needs text (usage: tumwater prompt --list --grep <text>)");
|
|
327
362
|
}
|
|
328
|
-
const grepClaim = scan.grepFlag
|
|
363
|
+
const grepClaim = pairClaim(scan.grepFlag);
|
|
329
364
|
return { grepRaw, grepClaim };
|
|
330
365
|
}
|
|
331
366
|
/** The queue-only-mode refusals: --file and --at have nothing to mean except in the enqueue
|
|
@@ -365,7 +400,7 @@ function parsePromptDeferral(args, atFlag) {
|
|
|
365
400
|
// An edit keeps the target prompt's existing deferral exactly as it was (the marker is
|
|
366
401
|
// plumbing the edit carries over), so a --at alongside it has nothing to mean.
|
|
367
402
|
// The pair is claimed like --role's: the flag and its value are never prompt content.
|
|
368
|
-
const atClaim = atFlag
|
|
403
|
+
const atClaim = pairClaim(atFlag);
|
|
369
404
|
let atDelayMs = null;
|
|
370
405
|
if (atFlag >= 0) {
|
|
371
406
|
atDelayMs = parseDurationFlag("--at", args[atFlag + 1]);
|
|
@@ -415,13 +450,19 @@ function listPromptArgs(args, f) {
|
|
|
415
450
|
failStrayArg(args, "with --list there is no prompt text", f.listFlag, ...f.jsonFlags, ...f.roleClaim, ...f.grepClaim);
|
|
416
451
|
return { mode: "list", role: f.role, json: f.jsonFlags.length > 0, grep: f.grepRaw ?? null };
|
|
417
452
|
}
|
|
453
|
+
/** The shared `--cancel <n>` / `--top <n>` mode: each takes exactly one 1-based position
|
|
454
|
+
* and refuses any other token — these modes have no prompt text. Shared so the two cannot
|
|
455
|
+
* drift; `mode` drives both the refusal wording and the returned mode. */
|
|
456
|
+
function positionOnlyPromptArgs(args, f, mode, flag, flagIndex) {
|
|
457
|
+
const n = parsePositionFlag(flag, args[flagIndex + 1], `${flag} 2`);
|
|
458
|
+
// The position is the only token this mode may carry; anything else alongside it would be
|
|
459
|
+
// prompt text, and this mode has none.
|
|
460
|
+
failStrayArg(args, `with ${flag} there is no prompt text`, flagIndex, flagIndex + 1, ...f.roleClaim);
|
|
461
|
+
return { mode, role: f.role, position: n };
|
|
462
|
+
}
|
|
418
463
|
/** `--cancel <n>`: remove one entry by its 1-based queue position. */
|
|
419
464
|
function cancelPromptArgs(args, f) {
|
|
420
|
-
|
|
421
|
-
// The position is the only token --cancel may carry; anything else alongside it would be
|
|
422
|
-
// prompt text, and this mode has none.
|
|
423
|
-
failStrayArg(args, "with --cancel there is no prompt text", f.cancelFlag, f.cancelFlag + 1, ...f.roleClaim);
|
|
424
|
-
return { mode: "cancel", role: f.role, position: n };
|
|
465
|
+
return positionOnlyPromptArgs(args, f, "cancel", "--cancel", f.cancelFlag);
|
|
425
466
|
}
|
|
426
467
|
/** `--edit <n>`: rewrite one entry's text in place — the replacement is the --file contents,
|
|
427
468
|
* the tokens after the position, or (with neither) an editor open in the caller. */
|
|
@@ -432,7 +473,7 @@ function editPromptArgs(args, f) {
|
|
|
432
473
|
// a mistyped command would edit prompt N with only the trailing fragment — so they are
|
|
433
474
|
// refused here by the same stray-argument wording the sibling modes use; tokens after the
|
|
434
475
|
// position are the replacement text, minus any claimed attach pair (none: edit in $EDITOR).
|
|
435
|
-
const fileClaim = f.fileFlag
|
|
476
|
+
const fileClaim = pairClaim(f.fileFlag);
|
|
436
477
|
for (let i = 0; i < f.editFlag; i++) {
|
|
437
478
|
if (f.roleClaim.includes(i) || f.attachClaims.includes(i) || fileClaim.includes(i))
|
|
438
479
|
continue;
|
|
@@ -457,11 +498,7 @@ function editPromptArgs(args, f) {
|
|
|
457
498
|
}
|
|
458
499
|
/** `--top <n>`: move one entry to the head of its role's queue. */
|
|
459
500
|
function topPromptArgs(args, f) {
|
|
460
|
-
|
|
461
|
-
// The position is the only token --top may carry; anything else alongside it would be
|
|
462
|
-
// prompt text, and this mode has none.
|
|
463
|
-
failStrayArg(args, "with --top there is no prompt text", f.topFlag, f.topFlag + 1, ...f.roleClaim);
|
|
464
|
-
return { mode: "top", role: f.role, position: n };
|
|
501
|
+
return positionOnlyPromptArgs(args, f, "top", "--top", f.topFlag);
|
|
465
502
|
}
|
|
466
503
|
/** `--file <path>`: enqueue the file's (or stdin's) whole contents as the prompt. */
|
|
467
504
|
function filePromptArgs(args, f) {
|
|
@@ -465,35 +465,55 @@ export const GUI_FLAG_SPECS = [PORT_FLAG, ALL_INTERFACES_FLAG, TOKEN_FLAG];
|
|
|
465
465
|
export function rejectUnknownArgs(command, args, specs) {
|
|
466
466
|
if (args.length === 0)
|
|
467
467
|
return;
|
|
468
|
-
const claim =
|
|
469
|
-
for (const spec of specs)
|
|
470
|
-
for (const name of spec.names)
|
|
471
|
-
claim.set(name, spec);
|
|
468
|
+
const claim = flagClaims(specs);
|
|
472
469
|
const seen = new Set();
|
|
473
470
|
const consumed = new Array(args.length).fill(false);
|
|
474
471
|
for (let i = 0; i < args.length; i++) {
|
|
475
472
|
if (consumed[i])
|
|
476
473
|
continue;
|
|
477
474
|
const arg = args[i] ?? ""; // Unreachable fallback: the loop bound guarantees a token here.
|
|
478
|
-
const spec = claim.get(arg);
|
|
479
|
-
if (spec === undefined) {
|
|
480
|
-
// An equals-form token names a real flag; refuse it with its own message before the
|
|
481
|
-
// generic "unknown argument" misreports `--role=feature` as a misspelling.
|
|
482
|
-
rejectEqualsForm(arg, specs);
|
|
483
|
-
if (specs.length === 0)
|
|
484
|
-
fail(`tumwater ${command} takes no arguments${gotSuffix(arg)}`);
|
|
485
|
-
failUnknownArgument(command, arg, specs);
|
|
486
|
-
}
|
|
475
|
+
const spec = claim.get(arg) ?? rejectUnknownFlag(command, arg, specs);
|
|
487
476
|
rejectRepeatFlag(seen.has(spec), spec.names[0]);
|
|
488
477
|
seen.add(spec);
|
|
489
|
-
|
|
490
|
-
fail(spec.missingValue ?? `${spec.names[0]} needs a value`);
|
|
491
|
-
// The value exists (the trailing-no-value case failed above): name a malformed one now,
|
|
492
|
-
// before the ready-repo gate or any environment check can mask it.
|
|
493
|
-
if (spec.value)
|
|
494
|
-
spec.validate?.(args[i + 1] ?? "");
|
|
495
|
-
const n = spec.value ? 2 : 1;
|
|
496
|
-
for (let j = 0; j < n && i + j < args.length; j++)
|
|
497
|
-
consumed[i + j] = true;
|
|
478
|
+
consumeFlagOccurrence(spec, args, i, consumed);
|
|
498
479
|
}
|
|
499
480
|
}
|
|
481
|
+
/** The spelling-to-spec claim map rejectUnknownArgs keyed its loop on: every accepted flag
|
|
482
|
+
* spelling maps to the spec declaring it, alias spellings (-f and --follow) sharing one spec
|
|
483
|
+
* entry. Built once up front so a token look-up is a map get, not a scan. */
|
|
484
|
+
function flagClaims(specs) {
|
|
485
|
+
const claim = new Map();
|
|
486
|
+
for (const spec of specs)
|
|
487
|
+
for (const name of spec.names)
|
|
488
|
+
claim.set(name, spec);
|
|
489
|
+
return claim;
|
|
490
|
+
}
|
|
491
|
+
/** The one refusal for a token naming no known flag: an equals-form token first reroutes
|
|
492
|
+
* through rejectEqualsForm, which names a REAL flag's offending `--flag=value` spelling
|
|
493
|
+
* before the generic "unknown argument" misreports `--role=feature` as a misspelling; then
|
|
494
|
+
* a command with no flags at all fails with the takes-no-arguments wording; otherwise the
|
|
495
|
+
* shared unknown-argument wording (with its did-you-mean hint) fires. Returns never, so the
|
|
496
|
+
* caller's `claim.get(arg) ?? rejectUnknownFlag(...)` narrows `spec` to a known FlagSpec. */
|
|
497
|
+
function rejectUnknownFlag(command, arg, specs) {
|
|
498
|
+
// An equals-form token names a real flag; refuse it with its own message before the
|
|
499
|
+
// generic "unknown argument" misreports `--role=feature` as a misspelling.
|
|
500
|
+
rejectEqualsForm(arg, specs);
|
|
501
|
+
if (specs.length === 0)
|
|
502
|
+
fail(`tumwater ${command} takes no arguments${gotSuffix(arg)}`);
|
|
503
|
+
failUnknownArgument(command, arg, specs);
|
|
504
|
+
}
|
|
505
|
+
/** Consume one recognised flag occurrence: fail a valued flag left without its value, name a
|
|
506
|
+
* malformed value via the spec's validate call, and mark the one or two tokens (the flag and,
|
|
507
|
+
* for a valued flag, its value) as consumed so a later loop iteration skips them. The value
|
|
508
|
+
* is marked only after the value checks pass, so a failed value is never silently swallowed. */
|
|
509
|
+
function consumeFlagOccurrence(spec, args, i, consumed) {
|
|
510
|
+
if (spec.value && i === args.length - 1)
|
|
511
|
+
fail(spec.missingValue ?? `${spec.names[0]} needs a value`);
|
|
512
|
+
// The value exists (the trailing-no-value case failed above): name a malformed one now,
|
|
513
|
+
// before the ready-repo gate or any environment check can mask it.
|
|
514
|
+
if (spec.value)
|
|
515
|
+
spec.validate?.(args[i + 1] ?? "");
|
|
516
|
+
const n = spec.value ? 2 : 1;
|
|
517
|
+
for (let j = 0; j < n && i + j < args.length; j++)
|
|
518
|
+
consumed[i + j] = true;
|
|
519
|
+
}
|
package/dist/src/cli/cli-init.js
CHANGED
|
@@ -12,8 +12,7 @@ import { templateCatalog } from "../init/init-templates.js";
|
|
|
12
12
|
export async function cmdInit(root, args) {
|
|
13
13
|
const { prompt, branch, template, listTemplates, adopt, dryRun } = parseInitArgs(args);
|
|
14
14
|
if (listTemplates) {
|
|
15
|
-
|
|
16
|
-
say(`${id} — ${description}`);
|
|
15
|
+
printTemplateCatalog();
|
|
17
16
|
return;
|
|
18
17
|
}
|
|
19
18
|
const result = await initProject(root, prompt, branch ?? undefined, {
|
|
@@ -21,21 +20,44 @@ export async function cmdInit(root, args) {
|
|
|
21
20
|
dryRun,
|
|
22
21
|
template: template ?? undefined,
|
|
23
22
|
});
|
|
23
|
+
printInitIntro(result);
|
|
24
|
+
if (result.dryRun) {
|
|
25
|
+
printDryRun(result);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
printInitReport(result);
|
|
29
|
+
}
|
|
30
|
+
/** Print the `tumwater init --list-templates` catalog: one line per template. */
|
|
31
|
+
function printTemplateCatalog() {
|
|
32
|
+
for (const { id, description } of templateCatalog())
|
|
33
|
+
say(`${id} — ${description}`);
|
|
34
|
+
}
|
|
35
|
+
/** Print the `template:`/adopt intro lines shared by the dry-run and completed paths. */
|
|
36
|
+
function printInitIntro(result) {
|
|
24
37
|
if (result.template !== "blank")
|
|
25
38
|
say(`template: ${result.template}`);
|
|
26
39
|
if (result.adopted) {
|
|
27
40
|
say(`${result.dryRun ? "would adopt" : "adopting"} an existing repo: the project brief goes in TUMWATER.md and README.md is left untouched`);
|
|
28
41
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
say(`dry run — would
|
|
35
|
-
say(`would leave alone: ${list(result.leftAlone)}`);
|
|
36
|
-
say("nothing written; re-run without --dry-run to apply");
|
|
37
|
-
return;
|
|
42
|
+
}
|
|
43
|
+
/** Print the dry-run preview: repo line, the would-create/would-leave-alone lists, and the
|
|
44
|
+
* "nothing written" line. */
|
|
45
|
+
function printDryRun(result) {
|
|
46
|
+
if (result.repoInitialized && result.branch) {
|
|
47
|
+
say(`dry run — would initialize a new git repository on branch ${result.branch}`);
|
|
38
48
|
}
|
|
49
|
+
const list = (names) => (names.length > 0 ? names.join(", ") : "nothing");
|
|
50
|
+
say(`dry run — would create: ${list(result.created)}`);
|
|
51
|
+
say(`would leave alone: ${list(result.leftAlone)}`);
|
|
52
|
+
say("nothing written; re-run without --dry-run to apply");
|
|
53
|
+
}
|
|
54
|
+
/** Format the created-files line, appending `(committed)` when the repo was seeded. */
|
|
55
|
+
function createdLine(result) {
|
|
56
|
+
return `created ${result.created.join(", ")}${result.committed ? " (committed)" : ""}`;
|
|
57
|
+
}
|
|
58
|
+
/** Print the completed-init report: the already-initialized no-op, the repo line, the
|
|
59
|
+
* created-files line, the bootstrap note, and the "next:" line. */
|
|
60
|
+
function printInitReport(result) {
|
|
39
61
|
if (result.created.length === 0) {
|
|
40
62
|
say("already initialized; nothing to do");
|
|
41
63
|
return;
|
|
@@ -43,7 +65,7 @@ export async function cmdInit(root, args) {
|
|
|
43
65
|
if (result.repoInitialized && result.branch) {
|
|
44
66
|
say(`initialized a new git repository on branch ${result.branch}`);
|
|
45
67
|
}
|
|
46
|
-
say(
|
|
68
|
+
say(createdLine(result));
|
|
47
69
|
if (result.bootstrap !== null) {
|
|
48
70
|
say(`fresh project — maintenance roles wait until ${result.bootstrap} plans are done; remove "bootstrap" from tumwater.json to end that early`);
|
|
49
71
|
}
|
|
@@ -25,7 +25,7 @@ import { renderMergeQueue, scopeMergeQueue } from "../merge/merge-queue-render.j
|
|
|
25
25
|
import { mergeChanges } from "../merge/merge-slot.js";
|
|
26
26
|
import { shortSha } from "../text/format.js";
|
|
27
27
|
import { roleLabel } from "../roles/role-labels.js";
|
|
28
|
-
import { orchestratorAlive } from "../team/orchestrator-info.js";
|
|
28
|
+
import { orchestratorAlive } from "../team/team-orchestrator-info.js";
|
|
29
29
|
import { queueInFlight } from "../status/status-polls.js";
|
|
30
30
|
import { roleInScope } from "../roles/runner-ids.js";
|
|
31
31
|
/** `tumwater status [-f/--follow]`: the team snapshot as rendered text or as the JSON
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/** The repo-readiness gate every repo-bound command runs before its body reads state: fail
|
|
2
|
-
* fast on the first unmet precondition (startup-
|
|
2
|
+
* fast on the first unmet precondition (startup-gates.ts's repoNotReady — the repo half of
|
|
3
3
|
* `tumwater start`'s startup gate). One named home beside the argument gate (cli-flag-specs.ts):
|
|
4
4
|
* the dispatcher (cli.ts), the marker commands (cli-marker-commands.ts), the write commands
|
|
5
5
|
* (backlog/backlog-cli.ts), and the read-only command bodies (cli-query-commands.ts) all
|
|
6
6
|
* import it, so a command layer never reaches into a sibling command module for a gate. */
|
|
7
7
|
import { fail } from "./cli-output.js";
|
|
8
8
|
import { teamWorkspaceMessage } from "../gates/readiness.js";
|
|
9
|
-
import { repoNotReady } from "../gates/startup-
|
|
9
|
+
import { repoNotReady } from "../gates/startup-gates.js";
|
|
10
10
|
import { owningTeamRoot } from "../paths.js";
|
|
11
11
|
/** Fail (exit 1) on the first unmet repo precondition, naming the fix; return once the repo is
|
|
12
12
|
* ready. */
|