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
package/dist/src/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// vocabulary: plain
|
|
2
3
|
/** The CLI's entry point and command dispatcher — the `bin` target package.json points at
|
|
3
4
|
* (dist/src/cli.js). main() runs the Node-floor gate from package.json's `engines.node`, answers a
|
|
4
5
|
* `--help` topic before any command parses arguments or gates on a ready repo, resolves the repo
|
|
@@ -7,23 +8,39 @@
|
|
|
7
8
|
* statically loads only the argument parsing, output, repo gate and help it shares, so
|
|
8
9
|
* `tumwater report` never pays for the orchestrator or ink, and `tumwater logs` never pays for
|
|
9
10
|
* the status collectors. A process that starts, runs one command and exits (the CLI as the
|
|
10
|
-
*
|
|
11
|
+
* team's agents and the test suite call it, ~700 spawns a suite run) otherwise compiled all
|
|
11
12
|
* 169 modules every time. The work itself lives in the command
|
|
12
13
|
* modules; this file owns the shared sequence, including each case's flag rejection and its
|
|
13
14
|
* ready-repo gate. An error escaping main() funnels through its catch to fail(errorMessage(err)),
|
|
14
15
|
* the CLI's one error path. */
|
|
15
16
|
import { parseCountFlag, } from "./cli/cli-args.js";
|
|
16
|
-
import { grepFlagSpec, GUI_FLAG_SPECS, JSON_FLAG, LAST_FLAG, N_FLAG, rejectUnknownArgs, resultFlagSpec, ROLE_FLAG, RUN_FLAG_SPECS, sinceFlagSpec, FORCE_FLAG, } from "./cli/cli-flag-specs.js";
|
|
17
|
+
import { grepFlagSpec, GUI_FLAG_SPECS, JSON_FLAG, LAST_FLAG, N_FLAG, rejectUnknownArgs, resultFlagSpec, ROLE_FLAG, RUN_FLAG_SPECS, sinceFlagSpec, STAT_FLAG, SUMMARY_FLAG, COUNT_FLAG, typeFlagSpec, DRY_RUN_FLAG, FORCE_FLAG, STOP_CANCEL_FLAG, STOP_IN_FLAG, REPROBE_FLAG, PROBLEMS_FLAG, } from "./cli/cli-flag-specs.js";
|
|
17
18
|
import { fail, say, sayJsonOrRender } from "./cli/cli-output.js";
|
|
18
|
-
import { parsePromptArgs, peelPositionals } from "./cli/cli-command-args.js";
|
|
19
|
+
import { historyTrailForm, parsePromptArgs, peelPositionals } from "./cli/cli-command-args.js";
|
|
19
20
|
import { requireReadyRepo } from "./cli/cli-repo-gate.js";
|
|
20
21
|
import { repoToplevel } from "./git/git.js";
|
|
21
22
|
import { didYouMean } from "./text/suggest.js";
|
|
22
23
|
import { errorMessage, gotSuffix, stripTerminalControls } from "./text/text.js";
|
|
24
|
+
import { typoSuffix } from "./text/suggest.js";
|
|
25
|
+
import { completionConfigKeys, completionRoleIds, completionScript, SUPPORTED_SHELLS } from "./cli/completion.js";
|
|
23
26
|
import { HELP, helpTopic, helpTopicForArgs, suggestCommand } from "./cli/help.js";
|
|
24
27
|
import { PACKAGE_JSON, nodeFloorProblem, packageEnginesNode, packageVersion } from "./version.js";
|
|
25
28
|
// The CLI's help text and its per-command topic parser live in help.ts — importing cli.ts
|
|
26
29
|
// would run main(), so tests pin the topics against help.ts directly.
|
|
30
|
+
/** One run's trail, shared by `history <role> <n>` and its hidden alias `tick` so the two cannot
|
|
31
|
+
* drift. Positional-first: the <role> <n> tokens peel off and the remaining flags go through
|
|
32
|
+
* rejectUnknownArgs (only --json, --last, --stat), so a listing flag such as --role is refused.
|
|
33
|
+
* Arity is gated before the ready-repo check so a malformed call fails with its usage in any
|
|
34
|
+
* directory; cmdTick owns the value validation and the --last/numbered rivals. */
|
|
35
|
+
async function runTrailCommand(root, args) {
|
|
36
|
+
const { cmdTick, TICK_USAGE } = await import("./tick/tick-detail.js");
|
|
37
|
+
const { positionals, rest } = peelPositionals(args);
|
|
38
|
+
rejectUnknownArgs("history", rest, [JSON_FLAG, LAST_FLAG, STAT_FLAG]);
|
|
39
|
+
if (positionals.length < 1 || positionals.length > 2)
|
|
40
|
+
fail(`usage: ${TICK_USAGE}`);
|
|
41
|
+
await requireReadyRepo(root);
|
|
42
|
+
await cmdTick(root, positionals, rest.includes("--json"), rest.includes("--last"), rest.includes("--stat"));
|
|
43
|
+
}
|
|
27
44
|
async function main() {
|
|
28
45
|
const [, , command, ...args] = process.argv;
|
|
29
46
|
// The Node-floor gate, before any command does work: package.json's engines spec is the
|
|
@@ -55,14 +72,15 @@ async function main() {
|
|
|
55
72
|
const root = (await repoToplevel(process.cwd())) ?? process.cwd();
|
|
56
73
|
switch (command) {
|
|
57
74
|
case "init":
|
|
58
|
-
// Imported lazily: cli/cli-run.ts pulls the whole
|
|
75
|
+
// Imported lazily: cli/cli-run.ts pulls the whole team-booting subgraph (orchestrator,
|
|
59
76
|
// supervisor, redeploy, launch-services — the run path's machinery), and every other
|
|
60
77
|
// command's spawn pays module compilation for it unless the dispatch defers that load
|
|
61
|
-
// to the commands that actually boot or serve a
|
|
78
|
+
// to the commands that actually boot or serve a team.
|
|
62
79
|
await (await import("./cli/cli-run.js")).cmdInit(root, args);
|
|
63
80
|
break;
|
|
81
|
+
case "start":
|
|
64
82
|
case "run":
|
|
65
|
-
rejectUnknownArgs("
|
|
83
|
+
rejectUnknownArgs("start", args, RUN_FLAG_SPECS);
|
|
66
84
|
await (await import("./cli/cli-run.js")).cmdRun(root, args);
|
|
67
85
|
break;
|
|
68
86
|
case "tui":
|
|
@@ -131,26 +149,37 @@ async function main() {
|
|
|
131
149
|
case "doctor": {
|
|
132
150
|
// No requireReadyRepo gate: doctor's job is to report WHY the environment isn't ready,
|
|
133
151
|
// so it must run outside a git repo and print fail lines rather than throwing.
|
|
134
|
-
rejectUnknownArgs("doctor", args, [JSON_FLAG]);
|
|
152
|
+
rejectUnknownArgs("doctor", args, [JSON_FLAG, REPROBE_FLAG, PROBLEMS_FLAG]);
|
|
135
153
|
const { runDoctor } = await import("./doctor/doctor.js");
|
|
136
|
-
const { renderDoctor } = await import("./doctor/doctor-render.js");
|
|
137
|
-
|
|
154
|
+
const { renderDoctor, renderDoctorProblems, problemsOnly } = await import("./doctor/doctor-render.js");
|
|
155
|
+
// The terminal probes unclaimed-verified models by default (7-day cache); --reprobe
|
|
156
|
+
// forces every claimed model. The TUI/GUI call runDoctor with the default "never", so a
|
|
157
|
+
// refresh never spends a model call.
|
|
158
|
+
const probeImages = args.includes("--reprobe") ? "all" : "missing";
|
|
159
|
+
const report = await runDoctor(root, undefined, undefined, probeImages);
|
|
138
160
|
// --json prints the collector's own payload (the DoctorReport object), not a re-parse of
|
|
139
161
|
// the render — the `report --json` precedent. The exit-code contract below holds in both
|
|
140
162
|
// forms: 1 when any check fails, 0 otherwise; warnings never fail the exit.
|
|
141
|
-
|
|
163
|
+
// --problems keeps the full report for the render (it counts the hidden ok rows) and
|
|
164
|
+
// hands --json the filtered one; the exit code below reads the full report either way.
|
|
165
|
+
if (args.includes("--problems"))
|
|
166
|
+
sayJsonOrRender(args, args.includes("--json") ? problemsOnly(report) : report, renderDoctorProblems);
|
|
167
|
+
else
|
|
168
|
+
sayJsonOrRender(args, report, renderDoctor);
|
|
142
169
|
if (report.checks.some((c) => c.level === "fail"))
|
|
143
170
|
process.exitCode = 1; // Warnings never fail the exit.
|
|
144
171
|
break;
|
|
145
172
|
}
|
|
146
173
|
case "logs": {
|
|
147
|
-
const { cmdLogs, GREP_VALUE_ERROR } = await import("./cli/log-commands.js");
|
|
174
|
+
const { cmdLogs, GREP_VALUE_ERROR, TYPE_VALUE_ERROR } = await import("./cli/log-commands.js");
|
|
148
175
|
rejectUnknownArgs("logs", args, [
|
|
149
176
|
{ names: ["-f", "--follow"] },
|
|
150
177
|
N_FLAG,
|
|
151
178
|
sinceFlagSpec("logs --since"),
|
|
152
179
|
grepFlagSpec(GREP_VALUE_ERROR),
|
|
180
|
+
typeFlagSpec(TYPE_VALUE_ERROR),
|
|
153
181
|
JSON_FLAG,
|
|
182
|
+
COUNT_FLAG,
|
|
154
183
|
ROLE_FLAG,
|
|
155
184
|
{ names: ["--prompt"] },
|
|
156
185
|
]);
|
|
@@ -165,6 +194,10 @@ async function main() {
|
|
|
165
194
|
break;
|
|
166
195
|
case "history": {
|
|
167
196
|
const { cmdHistory, HISTORY_GREP_VALUE_ERROR, HISTORY_RESULT_VALUE_ERROR } = await import("./history/history.js");
|
|
197
|
+
if (historyTrailForm(args)) {
|
|
198
|
+
await runTrailCommand(root, args);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
168
201
|
rejectUnknownArgs("history", args, [
|
|
169
202
|
N_FLAG,
|
|
170
203
|
sinceFlagSpec("history --since"),
|
|
@@ -172,30 +205,15 @@ async function main() {
|
|
|
172
205
|
resultFlagSpec(HISTORY_RESULT_VALUE_ERROR),
|
|
173
206
|
JSON_FLAG,
|
|
174
207
|
ROLE_FLAG,
|
|
208
|
+
SUMMARY_FLAG,
|
|
175
209
|
]);
|
|
176
210
|
await requireReadyRepo(root);
|
|
177
211
|
await cmdHistory(root, args);
|
|
178
212
|
break;
|
|
179
213
|
}
|
|
180
|
-
case "tick":
|
|
181
|
-
|
|
182
|
-
// Positional-first command (the role command's pattern): the <role> <n> tokens peel off,
|
|
183
|
-
// the flags that remain go through rejectUnknownArgs (only --json is admitted), and
|
|
184
|
-
// cmdTick owns the arity and value validation — a missing role, an unknown id, a
|
|
185
|
-
// non-positive or non-numeric n, or a stray extra positional fails there with the usage.
|
|
186
|
-
const { positionals, rest } = peelPositionals(args);
|
|
187
|
-
rejectUnknownArgs("tick", rest, [JSON_FLAG, LAST_FLAG]);
|
|
188
|
-
// Arity before the ready-repo gate (the config subcommands' precedent): a malformed
|
|
189
|
-
// invocation fails with its usage no matter the directory. cmdTick keeps the guard too —
|
|
190
|
-
// in-process callers reach it without this dispatcher. One positional is the --last
|
|
191
|
-
// form (<role>), two is the numbered pair (<role> <n>); cmdTick picks the arity by the
|
|
192
|
-
// flag, so the gate stays loose here and the rivals check lives there.
|
|
193
|
-
if (positionals.length < 1 || positionals.length > 2)
|
|
194
|
-
fail(`usage: ${TICK_USAGE}`);
|
|
195
|
-
await requireReadyRepo(root);
|
|
196
|
-
await cmdTick(root, positionals, rest.includes("--json"), rest.includes("--last"));
|
|
214
|
+
case "tick": // vocabulary-ok: command name stays
|
|
215
|
+
await runTrailCommand(root, args);
|
|
197
216
|
break;
|
|
198
|
-
}
|
|
199
217
|
case "diff":
|
|
200
218
|
await (await import("./cli/cli-query-commands.js")).cmdDiff(root, args);
|
|
201
219
|
break;
|
|
@@ -205,9 +223,19 @@ async function main() {
|
|
|
205
223
|
case "backlog":
|
|
206
224
|
await (await import("./cli/cli-query-commands.js")).cmdBacklog(root, args);
|
|
207
225
|
break;
|
|
226
|
+
case "archive":
|
|
227
|
+
rejectUnknownArgs("archive", args, []);
|
|
228
|
+
await requireReadyRepo(root);
|
|
229
|
+
await (await import("./backlog/backlog-cli.js")).cmdArchive(root);
|
|
230
|
+
break;
|
|
208
231
|
case "bug": {
|
|
209
|
-
const { OPERATOR_FILE_KINDS,
|
|
232
|
+
const { OPERATOR_FILE_KINDS, bugWakeText, cancelBug, moveEntryToTop, releaseEntry, editBug, editBugFromText, fileBug, fileBugFromText } = await import("./backlog/backlog-write.js");
|
|
233
|
+
const { bugTitleOf, cancelAndAnnounce, releaseAndAnnounce, topAndAnnounce, editAndAnnounce, fileAndAnnounce, listAndAnnounce, showAndAnnounce } = await import("./backlog/backlog-cli.js");
|
|
210
234
|
const usage = 'tumwater bug "<symptom>"';
|
|
235
|
+
if (args.includes("--show")) {
|
|
236
|
+
showAndAnnounce(root, args, "bug");
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
211
239
|
if (args.includes("--list")) {
|
|
212
240
|
listAndAnnounce(root, args, "bug", "BUGS.md", "Open");
|
|
213
241
|
break;
|
|
@@ -220,12 +248,25 @@ async function main() {
|
|
|
220
248
|
await cancelAndAnnounce(root, args, "bug", (n) => cancelBug(root, n));
|
|
221
249
|
break;
|
|
222
250
|
}
|
|
251
|
+
if (args.includes("--top")) {
|
|
252
|
+
await topAndAnnounce(root, args, "bug", (n) => moveEntryToTop(root, "bug", n));
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
if (args.includes("--release")) {
|
|
256
|
+
await releaseAndAnnounce(root, args, "bug", (n) => releaseEntry(root, "bug", n), OPERATOR_FILE_KINDS.bug.role);
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
223
259
|
await fileAndAnnounce(root, args, "bug", (positionals) => fileBug(root, positionals.join(" "), usage), OPERATOR_FILE_KINDS.bug.role, bugWakeText, bugTitleOf, (text) => fileBugFromText(root, text, usage));
|
|
224
260
|
break;
|
|
225
261
|
}
|
|
226
262
|
case "plan": {
|
|
227
|
-
const { OPERATOR_FILE_KINDS,
|
|
263
|
+
const { OPERATOR_FILE_KINDS, cancelPlan, moveEntryToTop, releaseEntry, editPlan, editPlanFromText, filePlan, filePlanFromText, planWakeText } = await import("./backlog/backlog-write.js");
|
|
264
|
+
const { cancelAndAnnounce, releaseAndAnnounce, topAndAnnounce, editAndAnnounce, fileAndAnnounce, listAndAnnounce, showAndAnnounce, planTitleOf } = await import("./backlog/backlog-cli.js");
|
|
228
265
|
const usage = 'tumwater plan "<title>" [body...]';
|
|
266
|
+
if (args.includes("--show")) {
|
|
267
|
+
showAndAnnounce(root, args, "plan");
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
229
270
|
if (args.includes("--list")) {
|
|
230
271
|
listAndAnnounce(root, args, "plan", "PLANS.md", "Planned");
|
|
231
272
|
break;
|
|
@@ -238,12 +279,23 @@ async function main() {
|
|
|
238
279
|
await cancelAndAnnounce(root, args, "plan", (n) => cancelPlan(root, n));
|
|
239
280
|
break;
|
|
240
281
|
}
|
|
282
|
+
if (args.includes("--top")) {
|
|
283
|
+
await topAndAnnounce(root, args, "plan", (n) => moveEntryToTop(root, "plan", n));
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
if (args.includes("--release")) {
|
|
287
|
+
await releaseAndAnnounce(root, args, "plan", (n) => releaseEntry(root, "plan", n), OPERATOR_FILE_KINDS.plan.role);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
241
290
|
await fileAndAnnounce(root, args, "plan", (positionals) => filePlan(root, positionals[0] ?? "", positionals.slice(1).join(" "), usage), OPERATOR_FILE_KINDS.plan.role, planWakeText, planTitleOf, (text) => filePlanFromText(root, text, usage));
|
|
242
291
|
break;
|
|
243
292
|
}
|
|
244
293
|
case "questions":
|
|
245
294
|
await (await import("./cli/cli-query-commands.js")).cmdQuestions(root, args);
|
|
246
295
|
break;
|
|
296
|
+
case "brief":
|
|
297
|
+
await (await import("./cli/brief-command.js")).cmdBrief(root, args);
|
|
298
|
+
break;
|
|
247
299
|
case "role":
|
|
248
300
|
await (await import("./cli/cli-query-commands.js")).cmdRole(root, args);
|
|
249
301
|
break;
|
|
@@ -261,33 +313,62 @@ async function main() {
|
|
|
261
313
|
await (await import("./inbox/prompt-commands.js")).cmdPrompt(root, args);
|
|
262
314
|
break;
|
|
263
315
|
case "reset-counters":
|
|
264
|
-
// Fall through: the
|
|
316
|
+
// Fall through: the seven marker commands share one guard+dispatch shape — unknown-args
|
|
265
317
|
// rejection against the command's flag set, the ready-repo gate, then the
|
|
266
|
-
// operator-commands core — so runMarkerCommand holds it once instead of
|
|
318
|
+
// operator-commands core — so runMarkerCommand holds it once instead of seven copies
|
|
267
319
|
// drifting. The case labels here are exactly MarkerCommand's members, which is what
|
|
268
320
|
// makes the cast below exhaustive.
|
|
269
321
|
case "wake":
|
|
270
322
|
case "abort":
|
|
271
323
|
case "pause":
|
|
272
324
|
case "resume":
|
|
273
|
-
case "reclaim":
|
|
325
|
+
case "reclaim":
|
|
326
|
+
case "restart": {
|
|
274
327
|
await (await import("./cli/cli-marker-commands.js")).runMarkerCommand(root, command, args);
|
|
275
328
|
break;
|
|
276
329
|
}
|
|
277
330
|
case "retire": {
|
|
278
331
|
// Not a marker command (it acts immediately, not via a request file), but it shares the
|
|
279
332
|
// marker commands' gate shape: unknown-args rejection, then the ready-repo gate.
|
|
280
|
-
rejectUnknownArgs("retire", args, [ROLE_FLAG, FORCE_FLAG, JSON_FLAG]);
|
|
333
|
+
rejectUnknownArgs("retire", args, [ROLE_FLAG, FORCE_FLAG, DRY_RUN_FLAG, JSON_FLAG]);
|
|
281
334
|
await requireReadyRepo(root);
|
|
282
335
|
await (await import("./operator/operator-commands.js")).cmdRetire(root, args);
|
|
283
336
|
break;
|
|
284
337
|
}
|
|
338
|
+
case "revert": {
|
|
339
|
+
// Acts immediately on the primary checkout under the primary-worktree lock; the single
|
|
340
|
+
// positional (the commit to undo) is validated inside cmdRevert.
|
|
341
|
+
await requireReadyRepo(root);
|
|
342
|
+
await (await import("./operator/operator-commands.js")).cmdRevert(root, args);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
285
345
|
case "stop": {
|
|
286
|
-
//
|
|
287
|
-
// a mistake and fails before a signal is ever sent.
|
|
288
|
-
rejectUnknownArgs("stop", args, []);
|
|
346
|
+
// Bare stop signals the recorded orchestrator pid; `--in`/`--cancel` schedule or drop a
|
|
347
|
+
// deferred stop. Anything else is a mistake and fails before a signal is ever sent.
|
|
348
|
+
rejectUnknownArgs("stop", args, [STOP_IN_FLAG, STOP_CANCEL_FLAG]);
|
|
289
349
|
await requireReadyRepo(root);
|
|
290
|
-
await (await import("./operator/operator-commands.js")).cmdStop(root);
|
|
350
|
+
await (await import("./operator/operator-commands.js")).cmdStop(root, args);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case "completion": {
|
|
354
|
+
// Works outside a repo: no requireReadyRepo.
|
|
355
|
+
if (args.length === 1 && args[0] === "--roles") {
|
|
356
|
+
for (const id of completionRoleIds(root))
|
|
357
|
+
say(id);
|
|
358
|
+
break;
|
|
359
|
+
}
|
|
360
|
+
if (args.length === 1 && args[0] === "--config-keys") {
|
|
361
|
+
for (const key of completionConfigKeys(root))
|
|
362
|
+
say(key);
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
const shell = args[0];
|
|
366
|
+
if (args.length !== 1 || !SUPPORTED_SHELLS.includes(shell))
|
|
367
|
+
// A single near-miss shell word gets the same typoSuffix `logs --type`/`history --result`
|
|
368
|
+
// give, so `completion zs` reads "did you mean `zsh`?" while a far-off value (`tcsh`) and
|
|
369
|
+
// a wrong arity (none or several words, typed shown by gotSuffix) stay hint-free.
|
|
370
|
+
fail(`completion takes one shell: bash, zsh, or fish${gotSuffix(args.length > 1 ? args[1] : shell)}${args.length === 1 ? typoSuffix(shell ?? "", SUPPORTED_SHELLS) : ""}`);
|
|
371
|
+
say(completionScript(shell));
|
|
291
372
|
break;
|
|
292
373
|
}
|
|
293
374
|
case "version":
|
|
@@ -301,7 +382,7 @@ async function main() {
|
|
|
301
382
|
// raw stack trace, and a non-string version fails instead of printing "undefined".
|
|
302
383
|
const { version, problem } = packageVersion(PACKAGE_JSON);
|
|
303
384
|
if (problem !== undefined || version === undefined)
|
|
304
|
-
fail(problem ?? "package.json carries no version field (the running
|
|
385
|
+
fail(problem ?? "package.json carries no version field (the running tumwater install looks broken)");
|
|
305
386
|
say(version);
|
|
306
387
|
break;
|
|
307
388
|
}
|
package/dist/src/collections.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* utilities module. */
|
|
5
5
|
/** Group items by a key, preserving first-seen key order and input order within each group.
|
|
6
6
|
* The single home of the "append to a Map of arrays" step, shared by the event bucketers
|
|
7
|
-
* (history-data.ts's
|
|
8
|
-
* the
|
|
7
|
+
* (history-data.ts's bucketMergeEvents, failure/time-spend.ts's rejected-by-loop pass) and
|
|
8
|
+
* the team-hold poll's observations-by-provider grouping (team-polls.ts's pollTeamHold), so
|
|
9
9
|
* those sites cannot drift on how a missing key is seeded. Returns a fresh Map; the input is
|
|
10
10
|
* never mutated. */
|
|
11
11
|
export function groupBy(items, key) {
|
|
@@ -18,8 +18,8 @@ export function groupBy(items, key) {
|
|
|
18
18
|
/** Return `map`'s value for `key`, creating and storing `make()`'s result first when the key is
|
|
19
19
|
* absent — the single home of the `map.get(key) ?? fresh; …; map.set(key, value)` step, shared
|
|
20
20
|
* by the failure digest's per-role stats, outcome, and prompt-token accumulators (failure-data.ts),
|
|
21
|
-
* its time-and-spend per-role row (time-spend.ts), the
|
|
22
|
-
* (
|
|
21
|
+
* its time-and-spend per-role row (time-spend.ts), the team hold's provider+kind grouping
|
|
22
|
+
* (team-hold.ts), the error-storm reducer's roles-by-key set (src/team/error-storm.ts), and
|
|
23
23
|
* `groupBy` above, so those sites cannot drift on how a missing key is seeded. `make` runs only
|
|
24
24
|
* when the key is absent. A stored value of `undefined` reads as absent, matching the `??` idiom
|
|
25
25
|
* this replaces; the callers above never store one. */
|
|
@@ -33,9 +33,12 @@ export function getOrCreate(map, key, make) {
|
|
|
33
33
|
}
|
|
34
34
|
/** Increment a Map counter, seeding a missing key at 1 — the single home of the
|
|
35
35
|
* `map.set(k, (map.get(k) ?? 0) + 1)` step, shared by the failure-spread kind tally
|
|
36
|
-
* (src/
|
|
37
|
-
* section-title tally (backlog/backlog-structure.ts),
|
|
38
|
-
* (cli/cli-run.ts),
|
|
36
|
+
* (src/team/failure-spread.ts), the diff multiset's line tally (merge/merge-diff.ts), the
|
|
37
|
+
* section-title tally (backlog/backlog-structure.ts), the once-summary outcome tally
|
|
38
|
+
* (cli/cli-run.ts), the logs' per-type tally (cli/log-commands.ts), the flaky-check tally
|
|
39
|
+
* (doctor/doctor-checks.ts), and the multi-runner base tally
|
|
40
|
+
* (orchestrator/orchestrator-scheduling.ts), so those sites cannot drift on how a missing key is
|
|
41
|
+
* seeded. */
|
|
39
42
|
export function increment(map, key) {
|
|
40
43
|
map.set(key, (map.get(key) ?? 0) + 1);
|
|
41
44
|
}
|
|
@@ -44,7 +47,7 @@ export function increment(map, key) {
|
|
|
44
47
|
* step (the seed matters because `noUncheckedIndexedAccess` reads every entry as possibly
|
|
45
48
|
* undefined), shared by the failure digest's outcome tally per role (failure-data.ts), the
|
|
46
49
|
* usage report's window role totals (report-render.ts), and its per-day fold's tick and cost
|
|
47
|
-
* tallies (report-
|
|
50
|
+
* tallies (report-fold.ts), so those sites cannot drift on how a missing key is seeded. The
|
|
48
51
|
* test duration reporter (test/test-durations-reporter.ts) keeps its own copy: it imports no
|
|
49
52
|
* tumwater module by design, since node --test loads it into its own parent process.
|
|
50
53
|
*
|
|
@@ -56,22 +59,69 @@ export function addTo(record, key, amount) {
|
|
|
56
59
|
const current = Object.hasOwn(record, key) ? record[key] : 0;
|
|
57
60
|
record[key] = (current ?? 0) + amount;
|
|
58
61
|
}
|
|
62
|
+
/** Set `key` to `value` in a bounded memo `cache`, moving an existing key to the LRU tail
|
|
63
|
+
* (delete + re-set, so a re-read at the cap does not evict itself) and, when the map would
|
|
64
|
+
* exceed `max` entries, dropping the oldest (first-inserted) one. The single home of the
|
|
65
|
+
* bounded-memo discipline — the `delete(key); set(key, …)` refresh and the
|
|
66
|
+
* `keys().next().value` oldest-eviction — shared by the report fold (report-fold.ts's
|
|
67
|
+
* reportFoldCache), the failure digest (failure-data.ts's failureDigestCache), and the
|
|
68
|
+
* baseline verdict cache (main-baseline.ts's baselineCache), so those sites cannot drift on
|
|
69
|
+
* how a hit is refreshed or an over-cap cache drained. After any call the map holds at most
|
|
70
|
+
* `max` entries. */
|
|
71
|
+
export function memoSetLru(cache, key, value, max) {
|
|
72
|
+
if (cache.has(key))
|
|
73
|
+
cache.delete(key);
|
|
74
|
+
if (cache.size >= max) {
|
|
75
|
+
const oldest = cache.keys().next().value;
|
|
76
|
+
if (oldest !== undefined)
|
|
77
|
+
cache.delete(oldest);
|
|
78
|
+
}
|
|
79
|
+
cache.set(key, value);
|
|
80
|
+
}
|
|
59
81
|
/** One deterministic ranking rule: count descending, then key ascending, so equal counts always
|
|
60
82
|
* order the same way. These two helpers are the single home of that rule; every surface that
|
|
61
|
-
* ranks counters calls one of them rather than re-implementing the comparator —
|
|
83
|
+
* ranks counters by count calls one of them rather than re-implementing the comparator —
|
|
62
84
|
* `rankCountEntries` serves the `[key, count]` entry shape (src/report/report-render.ts's
|
|
63
|
-
* rankedRoleMap, src/
|
|
85
|
+
* rankedRoleMap, src/team/failure-spread.ts's strongest-kind pick) and `rankByCount` serves
|
|
64
86
|
* callers whose counts are fields or computations on other shapes
|
|
65
|
-
* (src/failure/failure-cluster.ts's clusters, src/
|
|
66
|
-
* failure-data.ts's outcome table, and time-spend.ts's time-and-spend table).
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
87
|
+
* (src/failure/failure-cluster.ts's clusters, src/team/error-storm.ts's strongest-cause pick,
|
|
88
|
+
* failure-data.ts's outcome table, and time-spend.ts's time-and-spend table). `sortByValueDesc`
|
|
89
|
+
* is the deliberate sibling for the same value-descending shape that wants a code-unit tiebreak
|
|
90
|
+
* instead of localeCompare, and unlike these two it sorts its input array in place. The one
|
|
91
|
+
* deliberate exception to both orderings is ui/gui/gui-client-report.ts's reportRoleOrder, which
|
|
92
|
+
* keeps its own copy by design: it runs in the browser, where harness modules cannot be imported.
|
|
93
|
+
* These two helpers return a fresh sorted array; their input is never mutated. */
|
|
70
94
|
export function rankByCount(items, count, key) {
|
|
71
95
|
return [...items].sort((a, b) => count(b) - count(a) || key(a).localeCompare(key(b)));
|
|
72
96
|
}
|
|
97
|
+
/** The lower median of a non-empty numeric list: the middle value, or the smaller of the
|
|
98
|
+
* middle two for an even count. Returns a fresh sorted copy; the input is never mutated.
|
|
99
|
+
* Shared by report-data.ts's per-day medians, doctor-checks.ts's and doctor-backlog.ts's
|
|
100
|
+
* 24 h phrase medians. (failure-data.ts's `median` is deliberately different: the mean of
|
|
101
|
+
* the middle two for an even count.) */
|
|
102
|
+
export function lowerMedian(values) {
|
|
103
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
104
|
+
return sorted[Math.floor((sorted.length - 1) / 2)];
|
|
105
|
+
}
|
|
106
|
+
/** Ascending string comparator by UTF-16 code unit — the locale-independent order a bare `.sort()`
|
|
107
|
+
* uses, unlike `localeCompare` (which `rankByCount` uses for its tiebreak). Used directly by
|
|
108
|
+
* src/cli/cli-run.ts, src/cli/log-commands.ts and src/history/history-data.ts, and by
|
|
109
|
+
* `sortByValueDesc` for its tiebreak. */
|
|
110
|
+
export function compareCodeUnits(a, b) {
|
|
111
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
112
|
+
}
|
|
113
|
+
/** Rank `[key, value, …]` pairs by value descending, breaking ties in code-unit order
|
|
114
|
+
* (compareCodeUnits — the locale-independent order, unlike rankByCount's localeCompare), so
|
|
115
|
+
* equal values always order the same way. The single home of that comparator, which
|
|
116
|
+
* src/cli/log-commands.ts's per-type event-count lines and src/report/report-render.ts's
|
|
117
|
+
* mergeDone sort hand-rolled before; `…rest` lets a site carry extra trailing fields
|
|
118
|
+
* (report-render's third column) through the sort untouched. Mutates and returns the input
|
|
119
|
+
* array, matching the `.sort()` the call sites replaced. */
|
|
120
|
+
export function sortByValueDesc(entries) {
|
|
121
|
+
return entries.sort((a, b) => b[1] - a[1] || compareCodeUnits(a[0], b[0]));
|
|
122
|
+
}
|
|
73
123
|
/** rankByCount for `[key, count]` entry pairs (count first): src/report/report-render.ts's
|
|
74
|
-
* rankedRoleMap and src/
|
|
124
|
+
* rankedRoleMap and src/team/failure-spread.ts's strongest-kind pick. */
|
|
75
125
|
export function rankCountEntries(entries) {
|
|
76
126
|
return rankByCount(entries, (e) => e[1], (e) => e[0]);
|
|
77
127
|
}
|
|
@@ -2,28 +2,27 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
2
2
|
import { defaultConfig } from "../config/config.js";
|
|
3
3
|
import { Semaphore } from "./semaphore.js";
|
|
4
4
|
/** One process-wide bound on concurrent runs of the declared check (config.maxConcurrentChecks):
|
|
5
|
-
* every full suite the harness runs — runScopedBuildCheck's
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
5
|
+
* every full suite the harness runs — runScopedBuildCheck's gate/merge/batch scopes and
|
|
6
|
+
* main-baseline.ts's checkMainBaseline, which runs the suite directly — takes a permit here first,
|
|
7
|
+
* so a burst of merges cannot stack suites on the host beside the authors' own test runs (the suite
|
|
8
|
+
* has load-sensitive tests). Split out of build/build-check.ts — which keeps the deterministic
|
|
9
|
+
* execution and classification (detect → spawn → classify) — because permit accounting is
|
|
10
|
+
* scheduling policy, not check semantics: it changes when the concurrency or fairness policy
|
|
11
|
+
* changes, not when the check's execution does. Separate from the orchestrator's maxConcurrent
|
|
12
|
+
* semaphore: a role tick holds one of those while it waits here, but a check permit is held only
|
|
13
|
+
* around the check process itself — never across a pi run, the merge lock, or another check — so no
|
|
14
|
+
* permit holder waits on anything a waiter holds. Sized at each acquire from the caller's live
|
|
15
|
+
* config (checkCap), so a tumwater.json edit applies to the next check; Semaphore.setCapacity never
|
|
16
|
+
* preempts a running check on a shrink. */
|
|
17
17
|
const checkPermits = new Semaphore(defaultConfig().maxConcurrentChecks);
|
|
18
18
|
/** Set while the current async context holds a check permit: a nested withCheckPermit runs
|
|
19
19
|
* inside the permit it already has instead of queueing for a second one — at a cap of 1 that
|
|
20
20
|
* second wait could never be granted (its own holder is the one blocking it). No call path
|
|
21
|
-
* nests today; this keeps a future one from deadlocking the
|
|
21
|
+
* nests today; this keeps a future one from deadlocking the team's checks. */
|
|
22
22
|
const holdingPermit = new AsyncLocalStorage();
|
|
23
|
-
/** Waiting-queue tiers (Semaphore.acquire): a merge-scope check runs inside the merge lock, so
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* preempted. */
|
|
23
|
+
/** Waiting-queue tiers (Semaphore.acquire): a merge-scope check runs inside the merge lock, so it
|
|
24
|
+
* is granted the next free permit ahead of queued gate and baseline checks — every check it waited
|
|
25
|
+
* behind would be lock-hold time for every other merge. A running check is never preempted. */
|
|
27
26
|
export const CHECK_TIER = { merge: 0, other: 1 };
|
|
28
27
|
/** The live cap: config.maxConcurrentChecks when it is a positive integer (validateConfig
|
|
29
28
|
* enforces that for tumwater.json), the default otherwise — a caller passing a partial config
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
1
2
|
import fs from "node:fs";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { pidAlive } from "../process/process.js";
|
|
@@ -225,11 +226,11 @@ export function withStateLock(lock, fn, timeoutMs = 10_000) {
|
|
|
225
226
|
ensureParentDir(lock);
|
|
226
227
|
return withSyncLock(lock, fn, timeoutMs);
|
|
227
228
|
}
|
|
228
|
-
/** The async twin of withStateLock, for a critical section that must await (the
|
|
229
|
-
* working-tree fast-forward runs git). Same lock directory and protocol as withStateLock, so a
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
229
|
+
/** The async twin of withStateLock, for a critical section that must await (the merge's
|
|
230
|
+
* working-tree fast-forward runs git). Same lock directory and protocol as withStateLock, so a sync
|
|
231
|
+
* writer and an async one on the same path exclude each other; ensureParentDir runs first for the
|
|
232
|
+
* same reason. The default timeout is withStateLock's, not withLock's longer merge-lock budget:
|
|
233
|
+
* these sections are one file read-modify-write or one fast-forward. */
|
|
233
234
|
export async function withStateLockAsync(lock, fn, timeoutMs = 10_000) {
|
|
234
235
|
ensureParentDir(lock);
|
|
235
236
|
return withLock(lock, fn, timeoutMs);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** The AsyncLocalStorage store that lets a tick's lease follow its runs across providers
|
|
2
|
+
* (plans/provider-concurrency.md §5): a lease rides here for the length of a tick, a verify task,
|
|
3
|
+
* or a resolver run, and `LoopPi.runWithTransientRetry` re-keys it to the provider each run
|
|
4
|
+
* actually uses. The one choke point every run a loop makes (authoring, gate, merge and resolver)
|
|
5
|
+
* passes through, so a budget handback or a model-failure switch that moves a mid-tick run to
|
|
6
|
+
* another provider moves the lease with it instead of leaving it parked on the key it was admitted
|
|
7
|
+
* on. The check-permit.ts `holdingPermit` store is the pattern: nothing here is a separate permit,
|
|
8
|
+
* only the single lease the run path already holds.
|
|
9
|
+
*
|
|
10
|
+
* The store carries the `piDefault` (the caller's cached readPiDefaultModel result) beside the
|
|
11
|
+
* lease so `ensureProviderLease` can resolve a bare no-model pi seam's key exactly as the
|
|
12
|
+
* admission/review seam did — a run whose key were computed without it would re-key a default
|
|
13
|
+
* team's tick off pi's own provider. It also carries the semaphore and, optionally, wait
|
|
14
|
+
* hooks, so a re-key that has to park on a full target key reports its wait the way a parked
|
|
15
|
+
* resolver does (merge-verify.ts's providerWaitStage). */
|
|
16
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
|
+
import { seamConcurrencyKey } from "../config/config-views.js";
|
|
18
|
+
const providerLeaseStore = new AsyncLocalStorage();
|
|
19
|
+
/** Run `run` with `lease` in the provider-lease store, so any run it makes re-keys that lease to
|
|
20
|
+
* the provider it actually uses (ensureProviderLease). `opts.piDefault` is the caller's cached pi
|
|
21
|
+
* default model, so a bare no-model seam resolves to the same key the admission or review seam
|
|
22
|
+
* used; `opts.hooks` (the verify: providerWaitStage) hears a re-key wait, if there is one. The same
|
|
23
|
+
* lease object is released by the caller after the store is unwrapped — a re-key mutates it in
|
|
24
|
+
* place, so release() frees whatever key the run path ended on. */
|
|
25
|
+
export function withProviderLease(lease, semaphore, run, opts = {}) {
|
|
26
|
+
return providerLeaseStore.run({ lease, semaphore, piDefault: opts.piDefault, hooks: opts.hooks }, run);
|
|
27
|
+
}
|
|
28
|
+
/** Re-key the calling lease to the provider `cfg` runs on, if it is not there yet. No-op when
|
|
29
|
+
* the current async context carries no lease (a direct LoopPi unit test, `tumwater tick`) or
|
|
30
|
+
* when the run is already on the lease's key. When it does re-key and `hooks` are present and
|
|
31
|
+
* the target key is at its cap, the hooks hear `waiting` before the lease parks and `granted`
|
|
32
|
+
* once the key frees — a re-key never waits on the team total (it already holds a team
|
|
33
|
+
* permit), only on the target key's headroom. */
|
|
34
|
+
export async function ensureProviderLease(cfg) {
|
|
35
|
+
const ctx = providerLeaseStore.getStore();
|
|
36
|
+
if (!ctx)
|
|
37
|
+
return;
|
|
38
|
+
const key = seamConcurrencyKey(cfg, ctx.piDefault);
|
|
39
|
+
if (ctx.lease.key === key)
|
|
40
|
+
return;
|
|
41
|
+
// The leased currently holds `old` (or nothing); the target key's in-use does not include it,
|
|
42
|
+
// so the key being at its cap is exactly whether rekey() will park.
|
|
43
|
+
const parks = ctx.hooks !== undefined && keyAtCap(ctx, key);
|
|
44
|
+
if (parks)
|
|
45
|
+
ctx.hooks.waiting();
|
|
46
|
+
await ctx.lease.rekey(key);
|
|
47
|
+
if (parks)
|
|
48
|
+
ctx.hooks.granted();
|
|
49
|
+
}
|
|
50
|
+
/** Whether `key` is at its configured cap right now (an absent or uncapped key is not) — the
|
|
51
|
+
* same test semaphore.drive uses to withhold a re-key grant, decided from the live snapshot so
|
|
52
|
+
* a re-key wait can be announced before the lease parks (providerWaitStage). */
|
|
53
|
+
function keyAtCap(ctx, key) {
|
|
54
|
+
if (key === undefined)
|
|
55
|
+
return false;
|
|
56
|
+
const entry = ctx.semaphore.snapshot().byKey[key];
|
|
57
|
+
return entry?.limit !== undefined && entry.inUse >= entry.limit;
|
|
58
|
+
}
|
|
59
|
+
/** The pool-slot wait rule (plans/provider-concurrency.md §5 "No deadlock"): a lease never holds a
|
|
60
|
+
* provider key while it waits for a worktree pool slot. Called by leaseSlot just before it parks on
|
|
61
|
+
* a busy pool; it re-keys the calling lease to no key (which never waits), so a tick or verify
|
|
62
|
+
* waiting for a slot does not hold — and cannot deadlock — a provider slot whose in-use a re-key
|
|
63
|
+
* waiter is waiting to drain. No-op without a lease in context. */
|
|
64
|
+
export async function dropProviderLeaseKey() {
|
|
65
|
+
const ctx = providerLeaseStore.getStore();
|
|
66
|
+
if (!ctx)
|
|
67
|
+
return;
|
|
68
|
+
await ctx.lease.rekey(undefined);
|
|
69
|
+
}
|