tumwater 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -54,6 +54,43 @@ function isCheckScript(s, key) {
|
|
|
54
54
|
const v = s[key];
|
|
55
55
|
return isNonBlankString(v);
|
|
56
56
|
}
|
|
57
|
+
/** Resolve the configured check timeout to cap the run: `check.timeoutSeconds` × 1000 when it is a
|
|
58
|
+
* positive number, else undefined. check.timeoutSeconds bounds the npm walk-up's check too: it is
|
|
59
|
+
* how a repo whose suite outgrows 300 s under load says so (the team's own gate runs were timing
|
|
60
|
+
* out there). Same result as today for every config shape (absent config, absent `check`, a
|
|
61
|
+
* non-number, zero and a positive number). */
|
|
62
|
+
function configuredCheckTimeoutMs(config) {
|
|
63
|
+
const seconds = config?.check?.timeoutSeconds;
|
|
64
|
+
return typeof seconds === "number" && seconds > 0 ? seconds * 1000 : undefined;
|
|
65
|
+
}
|
|
66
|
+
/** A configured `check.command` (plans/portability.md §6/7) wins over the walk-up: the right way to
|
|
67
|
+
* verify a repo is a property of the repo, and the walk cannot know it — a Python, Rust, or Go repo
|
|
68
|
+
* has no npm install for the walk to find, which silently turned every safety gate off there. Null
|
|
69
|
+
* when the command is blank (validation rejects one, but a degraded default config could still
|
|
70
|
+
* carry it), so a blank command falls through to the walk-up instead of disabling the check. */
|
|
71
|
+
function configuredCommandCheck(startDir, check, timeoutMs) {
|
|
72
|
+
if (!isNonBlankString(check.command))
|
|
73
|
+
return null;
|
|
74
|
+
return {
|
|
75
|
+
kind: "command",
|
|
76
|
+
command: check.command,
|
|
77
|
+
cwd: path.resolve(startDir, isNonBlankString(check.cwd) ? check.cwd : "."),
|
|
78
|
+
timeoutMs: timeoutMs ?? BUILD_CHECK_TIMEOUT_MS,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/** The npm walk-up: climb to the nearest ancestor holding both package.json and node_modules (the
|
|
82
|
+
* FIRST qualifying directory is the project — if its package.json has neither script, there is no
|
|
83
|
+
* check, and an unrelated ancestor further up must never be used), prefer `test` then `typecheck`
|
|
84
|
+
* then `build`, and stamp the configured timeout onto the npm result. The walk is required:
|
|
85
|
+
* tumwater worktrees live under `<repo>/.tumwater/worktrees/<role>` with no install of their own
|
|
86
|
+
* (node_modules is gitignored — it exists only where someone ran npm install), so a literal
|
|
87
|
+
* startDir check would silently disable the pre-check forever in dogfood. Returns null when no
|
|
88
|
+
* ancestor within `maxLevels` qualifies or the file is missing/unreadable/malformed. */
|
|
89
|
+
function detectNpmCheck(startDir, maxLevels, timeoutMs) {
|
|
90
|
+
const root = walkUp(startDir, maxLevels, (dir) => (hasInstall(dir) ? dir : null));
|
|
91
|
+
const found = root === null ? null : buildCheckFrom(root);
|
|
92
|
+
return found !== null && found.kind === "npm" && timeoutMs !== undefined ? { ...found, timeoutMs } : found;
|
|
93
|
+
}
|
|
57
94
|
/** Find the project's deterministic check from `startDir` — at most `maxLevels` ancestors
|
|
58
95
|
* (default 5) — to the nearest directory containing BOTH a package.json and a node_modules/
|
|
59
96
|
* directory, then preferring scripts.test over scripts.typecheck and scripts.build (npm convention:
|
|
@@ -71,21 +108,13 @@ function isCheckScript(s, key) {
|
|
|
71
108
|
* be used). Returns null when no ancestor qualifies or the file is missing/unreadable/malformed —
|
|
72
109
|
* detection never throws into the gate. */
|
|
73
110
|
export function detectBuildCheck(startDir, config, maxLevels = WALK_UP_LEVELS) {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
kind: "command",
|
|
81
|
-
command: c.command,
|
|
82
|
-
cwd: path.resolve(startDir, isNonBlankString(c.cwd) ? c.cwd : "."),
|
|
83
|
-
timeoutMs: configuredMs ?? BUILD_CHECK_TIMEOUT_MS,
|
|
84
|
-
};
|
|
111
|
+
const timeoutMs = configuredCheckTimeoutMs(config);
|
|
112
|
+
if (config?.check) {
|
|
113
|
+
const command = configuredCommandCheck(startDir, config.check, timeoutMs);
|
|
114
|
+
if (command !== null)
|
|
115
|
+
return command;
|
|
85
116
|
}
|
|
86
|
-
|
|
87
|
-
const found = root === null ? null : buildCheckFrom(root);
|
|
88
|
-
return found !== null && found.kind === "npm" && configuredMs !== undefined ? { ...found, timeoutMs: configuredMs } : found;
|
|
117
|
+
return detectNpmCheck(startDir, maxLevels, timeoutMs);
|
|
89
118
|
}
|
|
90
119
|
/** The configured per-change gate command (`check.gateCommand`, PLANS.md Land-queue speed 3e) when
|
|
91
120
|
* it is a non-blank string, else undefined — off. Opt-in: when no `mergeCommand` is declared, a
|
|
@@ -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
|
|
@@ -125,6 +125,16 @@ export function failureHeadline(tail) {
|
|
|
125
125
|
export function describeCheck(check) {
|
|
126
126
|
return check.kind === "npm" ? `\`npm run ${check.script}\`` : `\`${check.command}\``;
|
|
127
127
|
}
|
|
128
|
+
/** The single-line headline of a red check — the failureHeadline when the tail has one to
|
|
129
|
+
* offer (a test name or marker plus a non-framing message), else the check's describeCheck
|
|
130
|
+
* command name — the name the gate, merge, and review warnings print for a failed check.
|
|
131
|
+
* One definition for every red that names itself to a human: review-precheck's two
|
|
132
|
+
* flake/failure warnings, merge-to-main's merge-retry quarantine warning, and
|
|
133
|
+
* merge-check-failures's kept-pin headline record and attributeRedCheck absent-tail headline.
|
|
134
|
+
*/
|
|
135
|
+
export function redCheckHeadline(check, tail) {
|
|
136
|
+
return failureHeadline(tail) ?? describeCheck(check);
|
|
137
|
+
}
|
|
128
138
|
/** The machine-generated reasons a red check hands its author: the headline joined to the rest of
|
|
129
139
|
* the clipped tail, so the compiler error sits right after it in the injected next-run note. The
|
|
130
140
|
* headline is failureHeadline's — the first line that is not a stack frame — not outputTail[0]: a
|
|
@@ -32,9 +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) {
|
|
36
|
-
// One permit covers both attempts (the killed-check retry re-runs under the permit it
|
|
37
|
-
// holds); durationMs prices the run itself, not the wait for a permit.
|
|
35
|
+
async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutMs, config, sampleSleep, install, permitWait, cheaper, signal) {
|
|
38
36
|
let durationMs = 0;
|
|
39
37
|
const raw = await withCheckPermit(config, MERGE_SCOPES.has(scope) ? CHECK_TIER.merge : CHECK_TIER.other, async () => {
|
|
40
38
|
// One permit covers both attempts (the killed-check retry re-runs under the permit it
|
|
@@ -45,7 +43,7 @@ async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutM
|
|
|
45
43
|
// outcome stands (runWithCleanRetry). The verdict-less first attempt is priced as its own
|
|
46
44
|
// event (the feed must answer how long a check took), then the final event below records
|
|
47
45
|
// 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)));
|
|
46
|
+
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
47
|
durationMs = d;
|
|
50
48
|
return outcome;
|
|
51
49
|
}, {
|
|
@@ -59,6 +57,23 @@ async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutM
|
|
|
59
57
|
* unverified reject; anything else passes through. Never logs — the caller warns (see
|
|
60
58
|
* warnScopedCheck). Returns the unverified reason only when a rule fired. */
|
|
61
59
|
function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs) {
|
|
60
|
+
const sleepFailed = sleptThroughRun(raw);
|
|
61
|
+
if (!unverifiedSkipFor(scope, raw, sleepFailed))
|
|
62
|
+
return { outcome: raw, unverifiedReason: null };
|
|
63
|
+
const unverifiedReason = unverifiedReasonFor(scope, raw, effectiveMs, load, durationMs, sleepFailed);
|
|
64
|
+
return { outcome: unverifiedFailOutcome(check, raw, unverifiedReason), unverifiedReason };
|
|
65
|
+
}
|
|
66
|
+
/** True when the failed run slept past the tolerance — the slept-through rule. */
|
|
67
|
+
function sleptThroughRun(raw) {
|
|
68
|
+
// A retry that ALSO slept made no clean verdict either: it is recorded as unverified — not
|
|
69
|
+
// attributed to the change, not cached as a red baseline — rather than read as a second
|
|
70
|
+
// deterministic failure (BUGS.md 2026-09-30). A retry that passed stands: the tree went green
|
|
71
|
+
// under the project's own check, even if the host napped through parts of it.
|
|
72
|
+
return raw.status === "failed" && (raw.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS;
|
|
73
|
+
}
|
|
74
|
+
/** Whether a merge scope classifies this raw outcome as a deterministic unverified reject — the
|
|
75
|
+
* merge-scope skip rule. */
|
|
76
|
+
function unverifiedSkipFor(scope, raw, sleepFailed) {
|
|
62
77
|
// A signal kill at a merge scope, or a timeout whose deadline fired on time (real slowness, a
|
|
63
78
|
// genuinely hung or oversized suite), made no verdict about the tree — and this is the check
|
|
64
79
|
// whose whole job is to catch a semantic conflict before it merges, so it rejects
|
|
@@ -67,20 +82,19 @@ function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs)
|
|
|
67
82
|
// slept, so a second verdict-less attempt is recorded as unverified rather than read again as
|
|
68
83
|
// weather (BUGS.md 2026-09-28). no-npm and a broken toolchain still say nothing about the tree,
|
|
69
84
|
// 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;
|
|
85
|
+
// up.
|
|
75
86
|
// A failed dependency install is the same at a merge scope: nothing ran against the tree as its
|
|
76
87
|
// lockfile pins it, so it must not merge (BUGS.md 2026-10-01).
|
|
77
|
-
|
|
78
|
-
((
|
|
88
|
+
return (MERGE_SCOPES.has(scope) &&
|
|
89
|
+
((raw.status === "skipped" &&
|
|
79
90
|
(raw.skipReason === "timeout" || raw.skipReason === "killed" || raw.skipReason === "install")) ||
|
|
80
91
|
sleepFailed));
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
}
|
|
93
|
+
/** The unverified reason phrase for the rule's four arms — the reason chain, with the optional
|
|
94
|
+
* starved-host tail. The reason names the starved host only when the load says the team was the
|
|
95
|
+
* check's competition (a late-deadline wording already names its own cause). */
|
|
96
|
+
function unverifiedReasonFor(scope, raw, effectiveMs, load, durationMs, sleepFailed) {
|
|
97
|
+
return raw.skipReason === "install"
|
|
84
98
|
? installFailedPhrase(SCOPE_WORDS[scope].label, raw.install, UNVERIFIED_TREE)
|
|
85
99
|
: raw.skipReason === "killed"
|
|
86
100
|
? killedPhrase(SCOPE_WORDS[scope].label, raw.killedBy ? { signal: raw.killedBy, durationMs } : undefined, UNVERIFIED_TREE)
|
|
@@ -90,7 +104,11 @@ function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs)
|
|
|
90
104
|
// An on-time kill (the late-deadline wording already names its own cause) names the
|
|
91
105
|
// starved host when the load average says the team was the check's competition.
|
|
92
106
|
(raw.run?.deadlineLateMs ?? 0) > DEADLINE_LATE_TOLERANCE_MS ? "" : loadedHostPhrase(load)}`;
|
|
93
|
-
|
|
107
|
+
}
|
|
108
|
+
/** Assemble the deterministic `failed` outcome that carries the unverified reason — the
|
|
109
|
+
* failed-outcome assembly. */
|
|
110
|
+
function unverifiedFailOutcome(check, raw, unverifiedReason) {
|
|
111
|
+
return {
|
|
94
112
|
status: "failed",
|
|
95
113
|
script: checkScriptName(check),
|
|
96
114
|
outputTail: [unverifiedReason],
|
|
@@ -98,12 +116,16 @@ function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs)
|
|
|
98
116
|
unverified: true,
|
|
99
117
|
...(raw.install ? { install: raw.install } : {}),
|
|
100
118
|
};
|
|
101
|
-
return { outcome, unverifiedReason };
|
|
102
119
|
}
|
|
103
120
|
/** Warn for the two verdict-less roads: a skipped check (buildCheckSkipWarning) or an unverified
|
|
104
121
|
* merge reject. Reads the scope word table and the killed signal. */
|
|
105
122
|
function warnScopedCheck(root, role, scope, outcome, effectiveMs, durationMs, unverifiedReason) {
|
|
106
123
|
if (outcome.status === "skipped") {
|
|
124
|
+
// A check the harness stopped on an abort signal is neither weather nor a skip to warn
|
|
125
|
+
// about: the caller (the gate pre-check) fails closed as aborted on its way out, so no
|
|
126
|
+
// "proceeding to model review" warning may suggest the check still stands.
|
|
127
|
+
if (outcome.skipReason === "aborted")
|
|
128
|
+
return;
|
|
107
129
|
// Environmental — deliberately NOT fail-closed, so a hung build script cannot wedge every
|
|
108
130
|
// code run into the 3-strike discard (gate) or a merge behind the merge lock.
|
|
109
131
|
const w = SCOPE_WORDS[scope];
|
|
@@ -142,11 +164,11 @@ function warnScopedCheck(root, role, scope, outcome, effectiveMs, durationMs, un
|
|
|
142
164
|
* merge and batch scopes, and those events then carry `full: false`. The red-main baseline, which
|
|
143
165
|
* detects on its own, keeps running check.command. `permitWait` hears
|
|
144
166
|
* 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) {
|
|
167
|
+
export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad, signal) {
|
|
146
168
|
const r = resolveScopedCheck(wt, scope, config, timeoutMs, sampleLoad);
|
|
147
169
|
if (!r)
|
|
148
170
|
return null;
|
|
149
|
-
const { raw, durationMs } = await runScopedCheckWithRetry(root, role, scope, wt, r.runCheck, timeoutMs, config, sampleSleep, install, permitWait, r.cheaper);
|
|
171
|
+
const { raw, durationMs } = await runScopedCheckWithRetry(root, role, scope, wt, r.runCheck, timeoutMs, config, sampleSleep, install, permitWait, r.cheaper, signal);
|
|
150
172
|
const { outcome, unverifiedReason } = unverifiedOutcomeFor(scope, r.check, raw, r.effectiveMs, r.load, durationMs);
|
|
151
173
|
logEventBestEffort(root, buildCheckEvent(role, scope, outcome, durationMs, r.cheaper));
|
|
152
174
|
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
|
|
@@ -213,7 +213,8 @@ export function swapDist(root, dist, mainHead) {
|
|
|
213
213
|
* swap can close the window, because the survivor is the supervisor and it exits on the dead
|
|
214
214
|
* generation; the next `tumwater start` then cannot even load `dist/src/cli.js` and fails with
|
|
215
215
|
* MODULE_NOT_FOUND, so the repo is wedged until someone renames `dist.prev` back by hand. The
|
|
216
|
-
* supervisor calls this before it exits (src/cli/cli-start.ts, via superviseStart's
|
|
216
|
+
* supervisor calls this before it exits (src/cli/cli-start-supervisor.ts, via superviseStart's
|
|
217
|
+
* onDone): while
|
|
217
218
|
* it is still up, it renames the preserved old build back into place so the next boot works.
|
|
218
219
|
*
|
|
219
220
|
* Only acts when `dist` is missing and a `dist.prev` exists — both conditions together are the
|
|
@@ -228,11 +229,12 @@ export function recoverInterruptedSwap(root, dist) {
|
|
|
228
229
|
fs.renameSync(prev, dist); // Atomic: dist is either the restored old build or still missing.
|
|
229
230
|
return true;
|
|
230
231
|
}
|
|
231
|
-
/** The supervisor's end-of-supervision repair (src/cli/cli-start.ts wires this into
|
|
232
|
+
/** The supervisor's end-of-supervision repair (src/cli/cli-start-supervisor.ts wires this into
|
|
232
233
|
* superviseStart's onDone): recover dist/ after an interrupted swap and, either way, put the
|
|
233
234
|
* outcome on the event feed. Kept beside recoverInterruptedSwap so the CLI wiring is one call.
|
|
234
|
-
* cli-start.ts imports this module statically, so the code is in memory before a
|
|
235
|
-
* dist/ away — recovering via an import at call time would fail
|
|
235
|
+
* cli-start-supervisor.ts imports this module statically, so the code is in memory before a
|
|
236
|
+
* swap can move dist/ away — recovering via an import at call time would fail
|
|
237
|
+
* ERR_MODULE_NOT_FOUND exactly
|
|
236
238
|
* when it is needed, the window this closes. `dist` defaults to this process's own dist
|
|
237
239
|
* (build-info.distDir, a pure path computation captured at load), overridable so a test can
|
|
238
240
|
* point it at a temp root. A failed restore is reported, never thrown: the hook runs on the
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { knownRoleIdsCached, loadConfigSafe } from "../config/config.js";
|
|
10
10
|
import { aheadOfMain, branchExists, countCommitsInRange, currentBranch, targetBranch } from "../git/git.js";
|
|
11
11
|
import { gitTry } from "../git/git-run.js";
|
|
12
|
-
import { aheadOfMainDiff, aheadOfMainFiles, changedFiles } from "../git/git-diff.js";
|
|
12
|
+
import { aheadOfMainDiff, aheadOfMainFiles, changedFiles, DIFF_MAX_BYTES } from "../git/git-diff.js";
|
|
13
13
|
import { compareCodeUnits } from "../collections.js";
|
|
14
14
|
import { isAbsolute } from "node:path";
|
|
15
15
|
import { branchName } from "../paths.js";
|
|
@@ -44,9 +44,6 @@ export async function collectRoleChanges(root, roles, opts = {}) {
|
|
|
44
44
|
const views = await Promise.all(roles.map((role) => collectRoleChangeAt(root, role, scope, opts)));
|
|
45
45
|
return { mainBranch: scope.mainBranch, roles: views };
|
|
46
46
|
}
|
|
47
|
-
/** The uncommitted patch's cap, matching aheadOfMainDiff's — one shared bound so neither
|
|
48
|
-
* half of the view can print unbounded text. */
|
|
49
|
-
const DIFF_MAX_BYTES = 200_000;
|
|
50
47
|
/** Collect the change the role holds: both halves of the view, or a degraded state
|
|
51
48
|
* when the worktree or the base branch is missing. Never throws on a degraded team — a
|
|
52
49
|
* query about a role that has not run yet must answer, not fail (report's rationale). */
|
|
@@ -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) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { completionConfigKeys, completionModels, completionRoleIds, completionScript, configValueWords, SUPPORTED_SHELLS, } from "./completion.js";
|
|
2
|
+
import { say, fail } from "./cli-output.js";
|
|
3
|
+
import { gotSuffix } from "../text/text.js";
|
|
4
|
+
import { typoSuffix } from "../text/suggest.js";
|
|
5
|
+
/** `tumwater completion`'s command body — the shell-completion script and its --roles/
|
|
6
|
+
* --config-keys/--config-value/--models listing shapes. The completion data lives in
|
|
7
|
+
* src/cli/completion.ts (the spec parser, the completionScript emitter and the read-only
|
|
8
|
+
* listing helpers); this module answers a `completion` invocation, split out of cli.ts so the
|
|
9
|
+
* dispatcher stays the pure shared sequence and this command body sits beside its data the
|
|
10
|
+
* way every other command body does. Imported lazily by cli.ts like each command body. */
|
|
11
|
+
function printCompletionRoles(root) {
|
|
12
|
+
for (const id of completionRoleIds(root))
|
|
13
|
+
say(id);
|
|
14
|
+
}
|
|
15
|
+
function printCompletionConfigKeys(root) {
|
|
16
|
+
for (const key of completionConfigKeys(root))
|
|
17
|
+
say(key);
|
|
18
|
+
}
|
|
19
|
+
function printCompletionConfigValue(key) {
|
|
20
|
+
// The vocabulary `config set <key> <value>` completes for a discrete-valued key
|
|
21
|
+
// (boolean/backend/model/thinking); free-form keys print nothing, so a `set` on one
|
|
22
|
+
// completes nothing instead of offering a wrong vocabulary.
|
|
23
|
+
for (const word of configValueWords(key))
|
|
24
|
+
say(word);
|
|
25
|
+
}
|
|
26
|
+
function printCompletionModels(root) {
|
|
27
|
+
for (const m of completionModels(root))
|
|
28
|
+
say(m);
|
|
29
|
+
}
|
|
30
|
+
const COMPLETION_LISTING_SHAPES = {
|
|
31
|
+
"--roles": "roles",
|
|
32
|
+
"--config-keys": "config-keys",
|
|
33
|
+
"--models": "models",
|
|
34
|
+
};
|
|
35
|
+
function completionListShape(args) {
|
|
36
|
+
if (args.length === 1 && args[0] in COMPLETION_LISTING_SHAPES)
|
|
37
|
+
return COMPLETION_LISTING_SHAPES[args[0]];
|
|
38
|
+
if (args.length === 2 && args[0] === "--config-value")
|
|
39
|
+
return "config-value";
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function completionShell(args) {
|
|
43
|
+
const shell = args[0];
|
|
44
|
+
// A wrong word count names the count, not a bad shell. A single near-miss shell word gets
|
|
45
|
+
// the same typoSuffix `logs --type`/`history --result` give, so `completion zs` reads
|
|
46
|
+
// "did you mean `zsh`?" while a far-off value (`tcsh`) stays hint-free.
|
|
47
|
+
if (args.length === 0)
|
|
48
|
+
fail("completion needs a shell: bash, zsh, or fish");
|
|
49
|
+
if (args.length > 1)
|
|
50
|
+
fail(`completion takes one shell: bash, zsh, or fish (got ${args.length} words: ${args.join(" ")})`);
|
|
51
|
+
if (!SUPPORTED_SHELLS.includes(shell))
|
|
52
|
+
fail(`completion takes one shell: bash, zsh, or fish${gotSuffix(shell)}${typoSuffix(shell ?? "", SUPPORTED_SHELLS)}`);
|
|
53
|
+
return shell;
|
|
54
|
+
}
|
|
55
|
+
/** Answer a `tumwater completion` invocation. Works outside a repo (no requireReadyRepo). */
|
|
56
|
+
export async function cmdCompletion(root, args) {
|
|
57
|
+
const shape = completionListShape(args);
|
|
58
|
+
if (shape === "roles") {
|
|
59
|
+
printCompletionRoles(root);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (shape === "config-keys") {
|
|
63
|
+
printCompletionConfigKeys(root);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (shape === "config-value") {
|
|
67
|
+
printCompletionConfigValue(args[1]);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (shape === "models") {
|
|
71
|
+
printCompletionModels(root);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
say(completionScript(completionShell(args)));
|
|
75
|
+
}
|