tumwater 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,37 +1,35 @@
|
|
|
1
1
|
import { BUILD_CHECK_TIMEOUT_MS, detectBuildCheck, gateCommandOf, } from "./build-check-detect.js";
|
|
2
2
|
import { runBuildCheck, checkScriptName, checkTimeoutMs, } from "./build-check.js";
|
|
3
3
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
4
|
-
import { MERGE_SCOPES, SCOPE_WORDS, buildCheckEvent, buildCheckSkipWarning,
|
|
4
|
+
import { MERGE_SCOPES, SCOPE_WORDS, buildCheckEvent, buildCheckSkipWarning, runWithCleanRetry, installFailedPhrase, killedPhrase, loadedHostPhrase, timedOutPhrase, DEADLINE_LATE_TOLERANCE_MS, SLEEP_SPAN_TOLERANCE_MS, sleptPhrase, starvedDeadlineMs, UNVERIFIED_TREE, } from "./build-check-events.js";
|
|
5
5
|
import { CHECK_TIER, withCheckPermit } from "../concurrency/check-permit.js";
|
|
6
6
|
import { sampleHostLoad, sampleSleepClock } from "./host-sleep.js";
|
|
7
7
|
import { npmInstall } from "./dep-install.js";
|
|
8
|
-
/** Run the project's declared check for a named scope — the detect → run → build_check
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* must be answerable from the feed, not by timing it by hand. Returns null when no check is
|
|
8
|
+
/** Run the project's declared check for a named scope — the detect → run → build_check event →
|
|
9
|
+
* skip-warning sequence the review gate's pre-check (scope "gate"), the merge path's in-lock
|
|
10
|
+
* re-check (scope "merge"), and the batch merger's one check over the whole stacked tree (scope
|
|
11
|
+
* "batch") previously each ran inline, kept in one place so the event's shape and the skip warning
|
|
12
|
+
* cannot drift between them. Split out of build-check.ts — which keeps the check's execution and
|
|
13
|
+
* outcome classification (runBuildCheck, BuildCheckOutcome) — because orchestration is its own
|
|
14
|
+
* concern: the permit, the event emission, the verdict-less retry, the merge-scope unverified
|
|
15
|
+
* remapping, and the skip warning. Every run is an event with its cost: the deterministic checks
|
|
16
|
+
* are where the team's compute goes after the authoring run, and "how long does npm test take per
|
|
17
|
+
* merge" must be answerable from the feed, not by timing it by hand. Returns null when no check is
|
|
19
18
|
* declared (nothing to run — the caller passes, exactly as before this split), otherwise the
|
|
20
|
-
* declared check plus its classified outcome. A "skipped" outcome also logs its standard
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* landing cell's "waiting for a check slot"). Never throws. */
|
|
19
|
+
* declared check plus its classified outcome. A "skipped" outcome also logs its standard warning
|
|
20
|
+
* here (wording keyed on the scope, the timeout as actually set — and, when its deadline fired
|
|
21
|
+
* late, as actually enforced); "failed" and "passed" are the caller's to decide (deterministic
|
|
22
|
+
* reject vs. verifiedHead / baseline seeding). A timeout or signal kill at a merge scope is
|
|
23
|
+
* remapped to a deterministic "failed" — the tree is unverified, so it must not merge. A check
|
|
24
|
+
* killed by a signal the harness did not send is retried once at any scope — the first run's death
|
|
25
|
+
* says nothing about the tree (it is another run's `pkill`), so one verdict from a clean attempt is
|
|
26
|
+
* owed before the skip is honored; a timeout whose deadline demonstrably fired late (the harness's
|
|
27
|
+
* own host-sleep evidence) earns that same one retry at any scope, and a failed run the host slept
|
|
28
|
+
* through does too; each attempt is priced as its own build_check event, carrying when the check
|
|
29
|
+
* itself ran (buildCheckRunFields). A configured `check.gateCommand` replaces the command at scope
|
|
30
|
+
* "gate" only (same cwd and timeout); the merge and batch scopes — and the red-main baseline, which
|
|
31
|
+
* detects on its own — keep running check.command. `permitWait` hears about a wait for the check
|
|
32
|
+
* permit (the merge cell's "waiting for a check slot"). Never throws. */
|
|
35
33
|
export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad) {
|
|
36
34
|
const gateCommand = scope === "gate" ? gateCommandOf(config) : undefined;
|
|
37
35
|
const check = detectBuildCheck(wt, gateCommand === undefined ? config : { check: { ...config?.check, command: gateCommand } });
|
|
@@ -41,7 +39,7 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
|
|
|
41
39
|
// caller's bound. Effective here so the reason text and the skip warning agree with what
|
|
42
40
|
// runBuildCheck enforced.
|
|
43
41
|
// A merge scope under a starved host gets a longer deadline (starvedDeadlineMs): the check
|
|
44
|
-
// runs as long as the
|
|
42
|
+
// runs as long as the team's load stretches it, and a kill at the unstretched bound would
|
|
45
43
|
// only measure the load (BUGS.md 2026-10-12). The load sampled here is also what the
|
|
46
44
|
// unverified reason names.
|
|
47
45
|
const load = sampleLoad();
|
|
@@ -53,42 +51,33 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
|
|
|
53
51
|
// holds); durationMs prices the run itself, not the wait for a permit.
|
|
54
52
|
let durationMs = 0;
|
|
55
53
|
const raw = await withCheckPermit(config, MERGE_SCOPES.has(scope) ? CHECK_TIER.merge : CHECK_TIER.other, async () => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// below records the retry's classified outcome.
|
|
68
|
-
if (!owesCleanRetry(first))
|
|
69
|
-
return first;
|
|
70
|
-
logEventBestEffort(root, buildCheckEvent(role, scope, first, durationMs));
|
|
71
|
-
const retryStart = Date.now();
|
|
72
|
-
const retry = await runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install);
|
|
73
|
-
durationMs = Date.now() - retryStart;
|
|
74
|
-
return retry;
|
|
54
|
+
// One permit covers both attempts (the killed-check retry re-runs under the permit it
|
|
55
|
+
// holds); durationMs prices the run itself, not the wait for a permit. A check the
|
|
56
|
+
// harness did not stop itself says nothing about the tree; a timeout whose deadline
|
|
57
|
+
// demonstrably fired late, and a failed run the host slept through, are the same weather
|
|
58
|
+
// — at every scope each owes one retry from a clean attempt, and the second attempt's
|
|
59
|
+
// outcome stands (runWithCleanRetry). The verdict-less first attempt is priced as its own
|
|
60
|
+
// event (the feed must answer how long a check took), then the final event below records
|
|
61
|
+
// the retry's classified outcome.
|
|
62
|
+
const { outcome, durationMs: d } = await runWithCleanRetry(() => runBuildCheck(wt, runCheck, timeoutMs, undefined, sampleSleep, install), (first, dur) => logEventBestEffort(root, buildCheckEvent(role, scope, first, dur)));
|
|
63
|
+
durationMs = d;
|
|
64
|
+
return outcome;
|
|
75
65
|
}, permitWait);
|
|
76
|
-
// A signal kill at a merge scope, or a timeout whose deadline fired on time (real slowness,
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
// deterministically — the author keeps its commit and retries. A timeout whose deadline
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// A retry that ALSO slept made no clean verdict either: it is recorded as unverified — not
|
|
66
|
+
// A signal kill at a merge scope, or a timeout whose deadline fired on time (real slowness, a
|
|
67
|
+
// genuinely hung or oversized suite), made no verdict about the tree — and this is the check
|
|
68
|
+
// whose whole job is to catch a semantic conflict before it merges, so it rejects
|
|
69
|
+
// deterministically — the author keeps its commit and retries. A timeout whose deadline fired
|
|
70
|
+
// late has already had its retry above: the lateness is the harness's own evidence the host
|
|
71
|
+
// slept, so a second verdict-less attempt is recorded as unverified rather than read again as
|
|
72
|
+
// weather (BUGS.md 2026-09-28). no-npm and a broken toolchain still say nothing about the tree,
|
|
73
|
+
// and the gate scope still proceeds to the model reviewer, which the merge path's own check backs
|
|
74
|
+
// up. A retry that ALSO slept made no clean verdict either: it is recorded as unverified — not
|
|
86
75
|
// attributed to the change, not cached as a red baseline — rather than read as a second
|
|
87
|
-
// deterministic failure (BUGS.md 2026-09-30). A retry that passed stands: the tree went
|
|
88
|
-
//
|
|
76
|
+
// deterministic failure (BUGS.md 2026-09-30). A retry that passed stands: the tree went green
|
|
77
|
+
// under the project's own check, even if the host napped through parts of it.
|
|
89
78
|
const sleepFailed = raw.status === "failed" && (raw.run?.sleptMs ?? 0) > SLEEP_SPAN_TOLERANCE_MS;
|
|
90
|
-
// A failed dependency install is the same at a merge scope: nothing ran against the tree as
|
|
91
|
-
//
|
|
79
|
+
// A failed dependency install is the same at a merge scope: nothing ran against the tree as its
|
|
80
|
+
// lockfile pins it, so it must not merge (BUGS.md 2026-10-01).
|
|
92
81
|
const unverifiedSkip = MERGE_SCOPES.has(scope) &&
|
|
93
82
|
(((raw.status === "skipped" &&
|
|
94
83
|
(raw.skipReason === "timeout" || raw.skipReason === "killed" || raw.skipReason === "install")) ||
|
|
@@ -101,7 +90,7 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
|
|
|
101
90
|
? sleptPhrase(SCOPE_WORDS[scope].label, raw.run.sleptMs, UNVERIFIED_TREE)
|
|
102
91
|
: `${SCOPE_WORDS[scope].label} ${timedOutPhrase(effectiveMs, raw.run)}; ${UNVERIFIED_TREE}${
|
|
103
92
|
// An on-time kill (the late-deadline wording already names its own cause) names the
|
|
104
|
-
// starved host when the load average says the
|
|
93
|
+
// starved host when the load average says the team was the check's competition.
|
|
105
94
|
(raw.run?.deadlineLateMs ?? 0) > DEADLINE_LATE_TOLERANCE_MS ? "" : loadedHostPhrase(load)}`;
|
|
106
95
|
const outcome = unverifiedSkip
|
|
107
96
|
? {
|
|
@@ -41,10 +41,11 @@ const TOOLCHAIN_ERROR_PATTERNS = [
|
|
|
41
41
|
function toolchainErrorInOutput(output) {
|
|
42
42
|
return TOOLCHAIN_ERROR_PATTERNS.some((p) => p.test(output));
|
|
43
43
|
}
|
|
44
|
-
/** The timeout a check actually runs under: `check.timeoutSeconds` when configured
|
|
45
|
-
* carries it as timeoutMs on both kinds), else a configured command's
|
|
46
|
-
* check's caller-given bound. One resolution so the run, the event,
|
|
47
|
-
*
|
|
44
|
+
/** The timeout a check actually runs under: `check.timeoutSeconds` when configured
|
|
45
|
+
* (detectBuildCheck carries it as timeoutMs on both kinds), else a configured command's
|
|
46
|
+
* built-in 300 s and an npm check's caller-given bound. One resolution so the run, the event,
|
|
47
|
+
* and the skip warning
|
|
48
|
+
* cannot disagree.
|
|
48
49
|
*
|
|
49
50
|
* checkTimeoutMs and checkScriptName are both shared with build-check-scoped.ts's
|
|
50
51
|
* runScopedBuildCheck, whose reason text and event must agree with what runBuildCheck
|
|
@@ -81,16 +82,16 @@ export async function runBuildCheck(wt, check, timeoutMs = BUILD_CHECK_TIMEOUT_M
|
|
|
81
82
|
if ((await probeToolchain()) === "broken") {
|
|
82
83
|
return { status: "skipped", script, skipReason: "toolchain" };
|
|
83
84
|
}
|
|
84
|
-
// A tree whose lockfile pins direct dependencies the walk-up does not resolve (a change that
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
85
|
+
// A tree whose lockfile pins direct dependencies the walk-up does not resolve (a change that adds
|
|
86
|
+
// or bumps one — node_modules is gitignored, so the worktree resolves through a root install that
|
|
87
|
+
// predates it) is installed first, into the tree itself: without it the check fails TS2307 on a
|
|
88
|
+
// correct change and rejects it before any reviewer sees it (BUGS.md 2026-10-01). An npm check
|
|
89
|
+
// only — a configured command owns its own environment. A failed install made no verdict about
|
|
90
|
+
// the tree; runScopedBuildCheck treats it as unverified at the merge scopes, so an uninstallable
|
|
91
|
+
// tree never merges. Never answer a missing dependency by skipping instead (classifying a `Cannot
|
|
92
|
+
// find module` failure as environmental): that was tried and reverted twice on 2026-10-01 by the
|
|
93
|
+
// user's decision — see BUGS.md's "Do not reintroduce a missing-install skip" note for why it
|
|
94
|
+
// switches the gate off team-wide.
|
|
94
95
|
let installed;
|
|
95
96
|
if (check.kind === "npm") {
|
|
96
97
|
const r = await syncInstall(wt, install);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { gitTry } from "../git/git-run.js";
|
|
@@ -40,7 +41,7 @@ export async function stampBuild(root, dist, sha) {
|
|
|
40
41
|
const info = { sha: head, builtAt: Date.now(), root: path.resolve(root) };
|
|
41
42
|
// Atomic: a process killed mid-write (a redeploy swap, a timeout-killed `npm test`) would
|
|
42
43
|
// otherwise leave a torn stamp, and readBuildInfo reads that as no provenance at all —
|
|
43
|
-
// createRedeployer then returns null and the
|
|
44
|
+
// createRedeployer then returns null and the team never self-redeploys onto newer main,
|
|
44
45
|
// the exact blind spot the stamp exists to close.
|
|
45
46
|
writeJsonAtomic(buildInfoPath(dist), info);
|
|
46
47
|
return info;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { distDir, stampBuild } from "./build-info.js";
|
|
@@ -6,7 +7,7 @@ import { resolveFromNodeModules } from "./build-check-detect.js";
|
|
|
6
7
|
import { ensureDir, removeTree } from "../files/files.js";
|
|
7
8
|
import { stagingDir, stagingRootDir } from "../paths.js";
|
|
8
9
|
import { execFileAsync } from "../process/process.js";
|
|
9
|
-
import { warnEvent, warnEventBestEffort } from "../events/events.js";
|
|
10
|
+
import { warnEvent, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
10
11
|
import { errorMessage } from "../text/text.js";
|
|
11
12
|
import { shortSha } from "../text/format.js";
|
|
12
13
|
/** Producing and swapping the compiled tree behind a self-redeploy (redeploy.ts): compile a
|
|
@@ -16,14 +17,14 @@ import { shortSha } from "../text/format.js";
|
|
|
16
17
|
* reads without the staging and rename detail. Split out of redeploy.ts for that reason. */
|
|
17
18
|
/** Hard cap on one compile of the harness; tsc on this codebase takes well under a minute. */
|
|
18
19
|
const COMPILE_TIMEOUT_MS = 5 * 60_000;
|
|
19
|
-
/** How old a superseded staged build may get before a later compile deletes it. A completed
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
20
|
+
/** How old a superseded staged build may get before a later compile deletes it. A completed compile
|
|
21
|
+
* stays consumable for one episode's tail — the drain hold is capped at RESTART_DRAIN_MAX_MS (30
|
|
22
|
+
* min) — so anything older than this grace cannot be swapped by any live episode and is pure disk:
|
|
23
|
+
* a team whose restarts stay blocked (red main, a failing boot gate) recompiles every new head it
|
|
24
|
+
* merges, and without a prune each merged commit left a full staged tree behind until the NEXT
|
|
25
|
+
* successful swap cleaned them all up — ~750 MB across one busy day (observed 2026-09-30: 128
|
|
26
|
+
* staged builds, none swapped). A successful swap already wipes every staging; this bounds the
|
|
27
|
+
* accumulation between swaps. */
|
|
27
28
|
export const STAGED_PRUNE_AFTER_MS = 45 * 60_000;
|
|
28
29
|
/** Delete staged builds other than `keep` that no live episode can still swap: directories
|
|
29
30
|
* named for a head (40-hex SHA) whose mtime is older than STAGED_PRUNE_AFTER_MS. Everything
|
|
@@ -79,7 +80,7 @@ function rejectedCompile(detail) {
|
|
|
79
80
|
* ancestor node_modules, and the mirror lives under <root>/.tumwater/. The compiler itself is
|
|
80
81
|
* found the same way (resolveFromNodeModules): a checkout that never ran `npm install` — every
|
|
81
82
|
* tumwater worktree — still has the install of an ancestor to borrow, and demanding a local one
|
|
82
|
-
* is what left the
|
|
83
|
+
* is what left the team unable to rebuild itself on 2026-09-08 (BUGS.md). Never throws. */
|
|
83
84
|
export async function compileStaged(root, mirrorWt, mainHead, timeoutMs = COMPILE_TIMEOUT_MS,
|
|
84
85
|
/** Test seam: the node binary the primary spawn runs tsc under. Production always uses
|
|
85
86
|
* process.execPath; a test injects a stale path to reproduce an interpreter an upgrade
|
|
@@ -109,7 +110,7 @@ execPath = process.execPath) {
|
|
|
109
110
|
// 2026-09-29) into a diagnosis instead of a mystery.
|
|
110
111
|
const missing = missingSpawnInputs(execPath, mirrorWt, tsc);
|
|
111
112
|
// The one spawn failure a retry can beat: the running build's own node binary vanished
|
|
112
|
-
// under it (an upgrade while the
|
|
113
|
+
// under it (an upgrade while the team stays up leaves process.execPath naming a file that
|
|
113
114
|
// no longer exists, while every PATH-resolved spawn — git, npm, pi — keeps working). tsc's
|
|
114
115
|
// own shebang resolves a live node through PATH at spawn time, so run it directly.
|
|
115
116
|
if (fs.existsSync(execPath) || !fs.existsSync(mirrorWt))
|
|
@@ -128,13 +129,13 @@ execPath = process.execPath) {
|
|
|
128
129
|
const stamped = await stampBuild(root, staged, mainHead);
|
|
129
130
|
if (stamped)
|
|
130
131
|
pruneStaleStagings(root, staged); // Superseded stagings die with the new build's success.
|
|
131
|
-
return stamped ? { ok: true, detail: "" } : { ok: false, detail: "could not stamp the compiled
|
|
132
|
+
return stamped ? { ok: true, detail: "" } : { ok: false, detail: "could not stamp the compiled update" };
|
|
132
133
|
}
|
|
133
134
|
/** Classify one failed compile spawn into its terminal CompileResult, or null when the error
|
|
134
135
|
* carries no exit at all — a spawn failure the caller still has to diagnose. Shared by the
|
|
135
136
|
* primary and shebang-fallback spawns so the two classification ladders cannot drift: a
|
|
136
137
|
* numeric exit code is the one shape that is a verdict about the tree (tsc ran and chose it),
|
|
137
|
-
* so latching anything else as a verdict pins the
|
|
138
|
+
* so latching anything else as a verdict pins the team on the stale build even after the
|
|
138
139
|
* environment recovers (BUGS.md 2026-09-28), and misclassifying a real exit as a rejection
|
|
139
140
|
* makes the redeployer drop a genuinely failing head and re-attempt it forever instead of
|
|
140
141
|
* blocking on it (review objection 2026-09-29). A timeout (killed) and a death by signal are
|
|
@@ -173,7 +174,7 @@ function missingSpawnInputs(execPath, cwd, tsc) {
|
|
|
173
174
|
export function swapDist(root, dist, mainHead) {
|
|
174
175
|
const staged = stagingDir(root, mainHead);
|
|
175
176
|
if (!fs.existsSync(staged))
|
|
176
|
-
throw new Error(`no staged
|
|
177
|
+
throw new Error(`no staged update for ${shortSha(mainHead)}`);
|
|
177
178
|
const prev = path.join(stagingRootDir(root), "dist.prev");
|
|
178
179
|
removeTree(prev);
|
|
179
180
|
const hadDist = fs.existsSync(dist);
|
|
@@ -190,21 +191,21 @@ export function swapDist(root, dist, mainHead) {
|
|
|
190
191
|
catch (restoreErr) {
|
|
191
192
|
// A restore failure leaves dist/ MISSING — the operator must hear that plus where
|
|
192
193
|
// the old build sits, not just the raw ENOENT/ENOSPC of whichever rename lost.
|
|
193
|
-
throw new Error(`could not restore the previous dist after a failed
|
|
194
|
-
`: ${errorMessage(err)}; dist is missing and the old
|
|
194
|
+
throw new Error(`could not restore the previous dist after a failed update swap for ${shortSha(mainHead)}` +
|
|
195
|
+
`: ${errorMessage(err)}; dist is missing and the old update is preserved at ${prev}` +
|
|
195
196
|
` (restore failed: ${errorMessage(restoreErr)})`);
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
// The raw error alone ("ENOTDIR", "EACCES") never says which move failed or which
|
|
199
200
|
// staged build was lost: name the head and the dist path the swap was serving.
|
|
200
|
-
throw new Error(`could not move the staged
|
|
201
|
+
throw new Error(`could not move the staged update for ${shortSha(mainHead)} into ${dist}: ${errorMessage(err)}`);
|
|
201
202
|
}
|
|
202
203
|
// The swap is committed: the rename above put the new build at dist/. Everything below is
|
|
203
204
|
// cleanup of superseded trees — the old build stepped aside at dist.prev, and the stagings
|
|
204
205
|
// of heads that moved on before their own swap. It is dead weight a later swap or compile
|
|
205
206
|
// collects, so a removal that fails (a directory surviving removeTree's retries, an
|
|
206
207
|
// unreadable staging root) must not be reported as a failed swap: the redeployer would
|
|
207
|
-
// block the restart while dist/ already holds the new build, leaving the
|
|
208
|
+
// block the restart while dist/ already holds the new build, leaving the team running the
|
|
208
209
|
// old code under a misleading `restart BLOCKED: swapping the new build into place failed`.
|
|
209
210
|
// Warn and continue, the same best-effort policy pruneStaleStagings applies so pruning
|
|
210
211
|
// never fails the work it follows.
|
|
@@ -244,10 +245,10 @@ export function recoverInterruptedSwap(root, dist) {
|
|
|
244
245
|
export function repairInterruptedSwap(root, dist = distDir()) {
|
|
245
246
|
try {
|
|
246
247
|
if (recoverInterruptedSwap(root, dist))
|
|
247
|
-
warnEventBestEffort(root,
|
|
248
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, "an interrupted update swap left dist/ missing — restored it from .tumwater/build/dist.prev");
|
|
248
249
|
}
|
|
249
250
|
catch (err) {
|
|
250
|
-
warnEventBestEffort(root,
|
|
251
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `dist/ is missing after an interrupted update swap and could not be restored from .tumwater/build/dist.prev: ${errorMessage(err)}`);
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
254
|
/** Remove one post-swap leftover tree, warning instead of throwing: the swap is already
|
|
@@ -257,7 +258,7 @@ function removeSupersededQuiet(root, dir) {
|
|
|
257
258
|
removeTree(dir);
|
|
258
259
|
}
|
|
259
260
|
catch (err) {
|
|
260
|
-
warnCleanup(root, `could not remove superseded
|
|
261
|
+
warnCleanup(root, `could not remove superseded update tree ${dir}: ${errorMessage(err)}`);
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
264
|
/** Best-effort sweep of the staging root after a successful swap: every directory left there
|
|
@@ -282,7 +283,7 @@ function sweepSupersededStagings(root) {
|
|
|
282
283
|
* event log is unwritable too, the new dist/ is already in place and stays the outcome. */
|
|
283
284
|
function warnCleanup(root, message) {
|
|
284
285
|
try {
|
|
285
|
-
warnEvent(root,
|
|
286
|
+
warnEvent(root, TUMWATER_SOURCE, message);
|
|
286
287
|
}
|
|
287
288
|
catch {
|
|
288
289
|
// Cleanup must not throw; the swap already succeeded.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { resolveFromNodeModules } from "./build-check-detect.js";
|
|
3
4
|
import { readJsonFile } from "../files/json-files.js";
|
|
@@ -6,14 +7,14 @@ import { KILL_GRACE_MS, runScriptGroup } from "../process/process-group.js";
|
|
|
6
7
|
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
7
8
|
import { errorMessage } from "../text/text.js";
|
|
8
9
|
/** Keeping a tree's install in step with its lockfile (BUGS.md 2026-10-01). node_modules is
|
|
9
|
-
* gitignored, so a tumwater worktree has none of its own: its toolchain and every import
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
* gitignored, so a tumwater worktree has none of its own: its toolchain and every import resolve by
|
|
11
|
+
* walking UP to the root checkout's install (detectBuildCheck, resolveFromNodeModules). That
|
|
12
|
+
* install predates any dependency a change adds, and no loop ever ran `npm install` — so a
|
|
13
|
+
* legitimate dependency-adding change failed its gate check with TS2307 every time, and had it
|
|
14
|
+
* merged, every tree after it would have resolved against a root install missing the package. Two
|
|
15
|
+
* callers close both halves: runBuildCheck installs into the checked tree when its lockfile's
|
|
16
|
+
* direct dependencies are not what the walk-up resolves, and the merge path re-syncs the root
|
|
17
|
+
* checkout after a fast-forward moves main's lockfile. */
|
|
17
18
|
/** Hard cap on one install run — a registry fetch of a handful of packages takes seconds; the
|
|
18
19
|
* cap only bounds a hung network. Same bound as the check itself. */
|
|
19
20
|
const DEP_INSTALL_TIMEOUT_MS = 300_000;
|
|
@@ -53,7 +54,7 @@ function installedVersion(dir, name) {
|
|
|
53
54
|
/** The production installer: `npm install` in `dir` from its own lockfile, run as a process
|
|
54
55
|
* group under the timeout. `--no-save` keeps package.json and package-lock.json byte-identical,
|
|
55
56
|
* so neither a worktree nor the root checkout goes dirty. Not `npm ci`: ci deletes node_modules
|
|
56
|
-
* first, and at the root that is the install every in-flight check of a live
|
|
57
|
+
* first, and at the root that is the install every in-flight check of a live team resolves
|
|
57
58
|
* through. `--ignore-scripts` runs no package's (or the project's own) lifecycle scripts inside
|
|
58
59
|
* the harness. */
|
|
59
60
|
export const npmInstall = async (dir, timeoutMs) => {
|
|
@@ -91,22 +92,22 @@ export async function syncInstall(dir, install = npmInstall, timeoutMs = DEP_INS
|
|
|
91
92
|
return { packages, ok: true, durationMs };
|
|
92
93
|
}
|
|
93
94
|
/** Re-sync the root checkout's install after a fast-forward moved main's lockfile (BUGS.md
|
|
94
|
-
* 2026-10-01). Every
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
95
|
+
* 2026-10-01). Every team worktree, the redeploy compile, and the running harness resolve through
|
|
96
|
+
* `root/node_modules`, so a merged dependency the root never installed would leave each of them
|
|
97
|
+
* resolving against the old tree. Called under the merge lock right after the ff, so the next
|
|
98
|
+
* merge's in-lock check already sees it. A `dep_install` event prices a run; a failure also warns,
|
|
99
|
+
* and is retried by the next merge's call (the drift is still there) — meanwhile each check
|
|
100
|
+
* installs into its own tree, so nothing is rejected for it. No drift (the common case — the merge
|
|
101
|
+
* did not touch dependencies) spawns nothing and logs nothing. */
|
|
101
102
|
export async function syncRootInstall(root, role, install = npmInstall) {
|
|
102
103
|
const r = await syncInstall(root, install);
|
|
103
104
|
if (!r)
|
|
104
105
|
return;
|
|
105
|
-
// Best-effort: the root install already ran and the ff that prompted it already moved main,
|
|
106
|
-
//
|
|
107
|
-
//
|
|
106
|
+
// Best-effort: the root install already ran and the ff that prompted it already moved main, so an
|
|
107
|
+
// unwritable feed must not reject the merge that awaited this (merge-to-main.ts's tryMerge,
|
|
108
|
+
// merge-stack.ts's ffStackToMain).
|
|
108
109
|
logEventBestEffort(root, {
|
|
109
|
-
|
|
110
|
+
source: role,
|
|
110
111
|
type: "dep_install",
|
|
111
112
|
packages: r.packages,
|
|
112
113
|
status: r.ok ? "passed" : "failed",
|
|
@@ -114,5 +115,5 @@ export async function syncRootInstall(root, role, install = npmInstall) {
|
|
|
114
115
|
...(r.detail ? { error: r.detail } : {}),
|
|
115
116
|
});
|
|
116
117
|
if (!r.ok)
|
|
117
|
-
warnEventBestEffort(root, role, `the root install did not pick up main's dependencies (${r.packages.join(", ")})${r.detail ? `: ${r.detail}` : ""}; checks install into their own trees until a later
|
|
118
|
+
warnEventBestEffort(root, role, `the root install did not pick up main's dependencies (${r.packages.join(", ")})${r.detail ? `: ${r.detail}` : ""}; checks install into their own trees until a later merge re-syncs it`);
|
|
118
119
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Measured host sleep inside a time window — the evidence that lets the harness tell "the
|
|
2
|
-
* build failed" from "the host was asleep while the build ran". The
|
|
2
|
+
* build failed" from "the host was asleep while the build ran". The team's host is a laptop
|
|
3
3
|
* in clamshell maintenance sleep (BUGS.md 2026-09-21 logged 569 sleeps over three days), and
|
|
4
4
|
* a sleep shorter than a check's remaining deadline but longer than a test's own wall-clock
|
|
5
5
|
* wait expires that wait at the next wake: the suite exits nonzero INSIDE the deadline, so
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/** The collector behind `tumwater diff --role <id>`: what change a loop holds between its commit
|
|
2
|
-
* and its merge. The committed half is the loop branch's
|
|
3
|
-
*
|
|
2
|
+
* and its merge. The committed half is the loop branch's unmerged work (commits ahead of the
|
|
3
|
+
* team's main branch, with the patch); the uncommitted half is the loop worktree's dirty state.
|
|
4
4
|
* Everything is read-only git plumbing already used elsewhere — this module only wires the pieces
|
|
5
|
-
* into one operator view, degrading like `report` does when the
|
|
5
|
+
* into one operator view, degrading like `report` does when the team (or just this role's
|
|
6
6
|
* worktree) does not exist yet. Lives beside the other collectors (src/report/report-data.ts,
|
|
7
7
|
* history-data.ts, status/status-data.ts); the terminal rendering of these views is
|
|
8
8
|
* change-render.ts's half. */
|
|
9
9
|
import { knownRoleIdsCached, loadConfigSafe } from "../config/config.js";
|
|
10
|
-
import { aheadOfMain, branchExists, currentBranch, targetBranch } from "../git/git.js";
|
|
10
|
+
import { aheadOfMain, branchExists, countCommitsInRange, currentBranch, targetBranch } from "../git/git.js";
|
|
11
11
|
import { gitTry } from "../git/git-run.js";
|
|
12
12
|
import { aheadOfMainDiff, changedFiles } from "../git/git-diff.js";
|
|
13
13
|
import { branchName } from "../paths.js";
|
|
@@ -17,14 +17,14 @@ import { roleWorktreeDir } from "../git/slots-state.js";
|
|
|
17
17
|
* order — disabled roles included, since a loop stopped mid-flight still holds its branch).
|
|
18
18
|
* Each entry goes through collectRoleChange, which keeps the absent/no-base degradation
|
|
19
19
|
* logic single-homed and only computes the patch git-diffs for roles actually holding work
|
|
20
|
-
* (both are "" when a role holds nothing); the
|
|
21
|
-
export async function
|
|
20
|
+
* (both are "" when a role holds nothing); the team view then drops the patch fields. */
|
|
21
|
+
export async function collectTeamChanges(root) {
|
|
22
22
|
const roles = await Promise.all(knownRoleIdsCached(root).map(async (role) => {
|
|
23
23
|
const view = await collectRoleChange(root, role);
|
|
24
|
-
const { diff: _diff, uncommittedDiff: _uncommittedDiff, ...
|
|
25
|
-
return
|
|
24
|
+
const { diff: _diff, uncommittedDiff: _uncommittedDiff, ...team } = view;
|
|
25
|
+
return team;
|
|
26
26
|
}));
|
|
27
|
-
// mainBranch is
|
|
27
|
+
// mainBranch is team-wide: collectRoleChange resolves it once per role through the same
|
|
28
28
|
// targetBranch call, so the first entry's value is every entry's value.
|
|
29
29
|
return { mainBranch: roles[0]?.mainBranch ?? "main", roles };
|
|
30
30
|
}
|
|
@@ -32,9 +32,9 @@ export async function collectFleetChanges(root) {
|
|
|
32
32
|
* half of the view can print unbounded text. */
|
|
33
33
|
const DIFF_MAX_BYTES = 200_000;
|
|
34
34
|
/** Collect the change the role's loop holds: both halves of the view, or a degraded state
|
|
35
|
-
* when the worktree or the base branch is missing. Never throws on a degraded
|
|
35
|
+
* when the worktree or the base branch is missing. Never throws on a degraded team — a
|
|
36
36
|
* query about a loop that has not run yet must answer, not fail (report's rationale). */
|
|
37
|
-
export async function collectRoleChange(root, role) {
|
|
37
|
+
export async function collectRoleChange(root, role, opts = {}) {
|
|
38
38
|
const branch = branchName(role);
|
|
39
39
|
const { config } = loadConfigSafe(root);
|
|
40
40
|
const mainBranch = targetBranch(config?.baseBranch, await currentBranch(root));
|
|
@@ -43,6 +43,7 @@ export async function collectRoleChange(root, role) {
|
|
|
43
43
|
branch,
|
|
44
44
|
mainBranch,
|
|
45
45
|
ahead: 0,
|
|
46
|
+
behind: 0,
|
|
46
47
|
commits: [],
|
|
47
48
|
diff: "",
|
|
48
49
|
dirtyFiles: [],
|
|
@@ -58,11 +59,21 @@ export async function collectRoleChange(root, role) {
|
|
|
58
59
|
if (wt === null || !(await isUsableWorktree(wt)))
|
|
59
60
|
return { ...empty, state: "absent" };
|
|
60
61
|
const ahead = await aheadOfMain(wt, mainBranch);
|
|
62
|
+
const behind = await countCommitsInRange(wt, `HEAD..${mainBranch}`);
|
|
61
63
|
const commits = parseOnelineLog(await gitTry(wt, "log", "--oneline", `${mainBranch}..HEAD`));
|
|
62
|
-
|
|
64
|
+
// Under opts.stat both halves hold the uncapped `git diff --stat` summary (one line per file).
|
|
65
|
+
const diff = ahead === 0
|
|
66
|
+
? ""
|
|
67
|
+
: opts.stat
|
|
68
|
+
? ((await gitTry(wt, "diff", "--stat", `${mainBranch}...HEAD`)) ?? "")
|
|
69
|
+
: await aheadOfMainDiff(wt, mainBranch);
|
|
63
70
|
const dirtyFiles = await changedFiles(wt);
|
|
64
|
-
const uncommittedDiff = dirtyFiles.length === 0
|
|
65
|
-
|
|
71
|
+
const uncommittedDiff = dirtyFiles.length === 0
|
|
72
|
+
? ""
|
|
73
|
+
: opts.stat
|
|
74
|
+
? ((await gitTry(wt, "diff", "--stat", "HEAD")) ?? "")
|
|
75
|
+
: await cappedWorkingDiff(wt);
|
|
76
|
+
return { ...empty, state: "ready", ahead, behind, commits, diff, dirtyFiles, uncommittedDiff };
|
|
66
77
|
}
|
|
67
78
|
/** Parse `git log --oneline` output into {sha, subject} pairs — sha up to the first space,
|
|
68
79
|
* the rest of the line the subject. Blank trailing lines (runGit's trimEnd makes them
|
|
@@ -1,28 +1,29 @@
|
|
|
1
1
|
/** The renderer behind `tumwater diff` — the operator-facing text for the change views the
|
|
2
2
|
* collector (change-data.ts, beside src/report/report-data.ts and history-data.ts) gathers: one
|
|
3
|
-
* role's pending work (`--role <id>`) and the
|
|
3
|
+
* role's pending work (`--role <id>`) and the team-wide roster (no --role). Pure function of the
|
|
4
4
|
* collected view — no I/O, no clock reads. */
|
|
5
5
|
import { plural } from "../text/phrases.js";
|
|
6
6
|
/** The work-summary phrase both change views open their work lines with — `N commits ahead
|
|
7
|
-
* of <main>` plus `,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* renderRoleChange's header line. */
|
|
11
|
-
function workSummary(ahead, mainBranch, dirtyFiles) {
|
|
7
|
+
* of <main>` plus `, K commits behind` when the branch lags main, plus `, M uncommitted
|
|
8
|
+
* file(s)` when the worktree is dirty (each count through plural, so the singular/plural
|
|
9
|
+
* wording the tests pin cannot drift between the team roster and the per-role header).
|
|
10
|
+
* Exactly two call sites: renderTeamChange's per-role line and renderRoleChange's header line. */
|
|
11
|
+
function workSummary(ahead, behind, mainBranch, dirtyFiles) {
|
|
12
12
|
return (`${plural(ahead, "commit")} ahead of ${mainBranch}` +
|
|
13
|
+
(behind > 0 ? `, ${plural(behind, "commit")} behind` : "") +
|
|
13
14
|
(dirtyFiles.length > 0 ? `, ${plural(dirtyFiles.length, "uncommitted file")}` : ""));
|
|
14
15
|
}
|
|
15
16
|
/** Render the roster as the operator-facing text `tumwater diff` (no --role) prints: one
|
|
16
17
|
* line per role that holds pending work, in roster order; roles with no worktree and roles
|
|
17
18
|
* holding nothing are skipped. Every entry no-base means the baseline itself is gone — the
|
|
18
|
-
* per-role view's line says so,
|
|
19
|
-
export function
|
|
19
|
+
* per-role view's line says so, team-wide, exit 0 like every other degraded case. */
|
|
20
|
+
export function renderTeamChange(view) {
|
|
20
21
|
if (view.roles.length > 0 && view.roles.every((r) => r.state === "no-base")) {
|
|
21
22
|
return `main branch ${view.mainBranch} does not exist`;
|
|
22
23
|
}
|
|
23
24
|
const lines = view.roles
|
|
24
25
|
.filter((r) => r.state !== "no-base" && (r.ahead > 0 || r.dirtyFiles.length > 0))
|
|
25
|
-
.map((r) => `${r.role}: ${workSummary(r.ahead, view.mainBranch, r.dirtyFiles)}`);
|
|
26
|
+
.map((r) => `${r.role}: ${workSummary(r.ahead, r.behind, view.mainBranch, r.dirtyFiles)}`);
|
|
26
27
|
return lines.length > 0 ? lines.join("\n") : "no pending changes";
|
|
27
28
|
}
|
|
28
29
|
/** Render the view as the operator-facing text `tumwater diff --role <id>` prints. */
|
|
@@ -34,7 +35,7 @@ export function renderRoleChange(view) {
|
|
|
34
35
|
if (view.ahead === 0 && view.dirtyFiles.length === 0)
|
|
35
36
|
return `no pending change for ${view.role}`;
|
|
36
37
|
const lines = [
|
|
37
|
-
`${view.role}: ${view.branch}, ${workSummary(view.ahead, view.mainBranch, view.dirtyFiles)}`,
|
|
38
|
+
`${view.role}: ${view.branch}, ${workSummary(view.ahead, view.behind, view.mainBranch, view.dirtyFiles)}`,
|
|
38
39
|
];
|
|
39
40
|
for (const c of view.commits)
|
|
40
41
|
lines.push(`${c.sha} ${c.subject}`);
|