tumwater 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +25 -12
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +1 -8
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +9 -0
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +50 -27
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +1 -1
- package/dist/src/agent/subscription-account.js +3 -3
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +58 -102
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +1 -1
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-scoped.js +40 -16
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +34 -12
- package/dist/src/cli/cli-query-commands.js +1 -1
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-start.js +91 -58
- package/dist/src/cli/completion.js +82 -46
- package/dist/src/cli/config-commands.js +82 -71
- package/dist/src/cli/help.js +4 -6
- package/dist/src/cli/log-commands.js +43 -23
- package/dist/src/cli/notify-command.js +2 -2
- package/dist/src/cli/question-commands.js +86 -60
- package/dist/src/cli.js +181 -135
- package/dist/src/collections.js +31 -16
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +22 -0
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +5 -20
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +153 -102
- package/dist/src/config/config-schema.js +9 -11
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +4 -52
- package/dist/src/config/config-validation.js +4 -38
- package/dist/src/config/config-views.js +9 -34
- package/dist/src/config/config-write.js +6 -23
- package/dist/src/config/config.js +4 -5
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +24 -24
- package/dist/src/doctor/doctor-backlog.js +7 -5
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +5 -5
- package/dist/src/doctor/doctor-claude-checks.js +44 -28
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +44 -112
- package/dist/src/doctor/doctor-fallback-checks.js +2 -2
- package/dist/src/doctor/doctor-holds.js +6 -45
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +120 -20
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +3 -3
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +30 -54
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +1 -3
- package/dist/src/events/{notify.js → event-notify.js} +2 -2
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/failure/failure-cluster.js +6 -6
- package/dist/src/failure/failure-data.js +8 -4
- package/dist/src/failure/failure-render.js +44 -34
- package/dist/src/failure/failure-state-change.js +4 -34
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +27 -68
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +7 -7
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +22 -23
- package/dist/src/gui/gui-prompt-commands.js +23 -23
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +51 -33
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +8 -11
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +10 -13
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-verify.js +8 -18
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +76 -44
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +27 -32
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +1 -1
- package/dist/src/orchestrator/scheduling-skip.js +1 -1
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +8 -7
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +1 -1
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +32 -6
- package/dist/src/pi/pi-watchdogs.js +12 -5
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +49 -5
- package/dist/src/process/process.js +8 -8
- package/dist/src/process/run-marker.js +31 -13
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +1 -1
- package/dist/src/review/review-precheck.js +11 -1
- package/dist/src/review/review.js +40 -153
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +1 -14
- package/dist/src/roles/role-view.js +9 -21
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/run-apply.js +12 -6
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +135 -84
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +119 -50
- package/dist/src/run/stage-check.js +49 -32
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/runner-outcome.js +6 -1
- package/dist/src/runner/runner-pi.js +3 -10
- package/dist/src/runner/runner-recovery.js +1 -1
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -1
- package/dist/src/runner/runner-run.js +97 -35
- package/dist/src/runner/runner-state-load.js +12 -9
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-data.js +2 -2
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -17
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +98 -65
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +2 -15
- package/dist/src/team/team-polls.js +28 -126
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +20 -15
- package/dist/src/text/text.js +3 -3
- package/dist/src/ui/alert-feeds.js +49 -73
- package/dist/src/ui/badges.js +3 -13
- package/dist/src/ui/gui/gui-client-boot.js +2 -2
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +7 -17
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +4 -4
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +53 -40
- package/dist/src/ui/run-progress-model.js +9 -2
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +3 -7
- package/dist/src/ui/status-render.js +5 -9
- package/dist/src/ui/team-alert-backlog.js +6 -6
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +7 -8
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +1 -7
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +159 -92
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +3 -3
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -228
- package/dist/src/ui/tui/tui-pane-history.js +61 -70
- package/dist/src/ui/tui/tui-pane-pending.js +284 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +11 -7
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +29 -32
- package/dist/src/ui/tui/tui.js +3 -4
- package/dist/src/verdict/fix-claim.js +76 -54
- package/dist/src/verdict/refusal.js +1 -1
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
- /package/dist/src/runner/{leftover.js → runner-leftover.js} +0 -0
package/dist/src/cli.js
CHANGED
|
@@ -125,77 +125,88 @@ async function cmdReportCase(root, args) {
|
|
|
125
125
|
// trailing window vs a series over whole days), not a modifier of either.
|
|
126
126
|
await (await import("./report/report.js")).cmdReport(root, args);
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (args.includes(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
128
|
+
/** Run one doctor side-effect (`--kill-orphans`, `--commit-backlog`, `--resync-install`,
|
|
129
|
+
* `--drop-broken-pins`, `--retire-leftovers`) and return its report payload for the optional
|
|
130
|
+
* `--json` run — the shape every doctor*Case side-effect wrapper follows: import the effect's
|
|
131
|
+
* module and run it only when its flag is present, then (text form only) print the effect's
|
|
132
|
+
* own lines before the report with a separating blank line when there is anything to print;
|
|
133
|
+
* `--json` keeps stdout one document, carrying the result on the payload instead. The lazy
|
|
134
|
+
* per-effect `load` keeps each module imported only when its flag is asked for. */
|
|
135
|
+
async function doctorSideEffect(args, flag, load) {
|
|
136
|
+
if (!args.includes(flag))
|
|
137
|
+
return undefined;
|
|
138
|
+
const { run, lines } = await load();
|
|
139
|
+
const payload = await run();
|
|
140
|
+
// The text form prints the effect before the report; --json keeps stdout one document.
|
|
141
|
+
if (!args.includes("--json")) {
|
|
142
|
+
const out = lines(payload);
|
|
143
|
+
for (const line of out)
|
|
144
|
+
say(line);
|
|
145
|
+
if (out.length > 0)
|
|
143
146
|
say("");
|
|
144
|
-
}
|
|
145
147
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
+
return payload;
|
|
149
|
+
}
|
|
150
|
+
async function doctorKillOrphans(root, args) {
|
|
151
|
+
return doctorSideEffect(args, "--kill-orphans", async () => {
|
|
152
|
+
const { killOrphans, orphanKillLines } = await import("./doctor/doctor-orphans.js");
|
|
153
|
+
return { run: () => killOrphans(root), lines: orphanKillLines };
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function doctorCommitBacklog(root, args) {
|
|
157
|
+
return doctorSideEffect(args, "--commit-backlog", async () => {
|
|
148
158
|
const { commitUncommittedBacklog, backlogCommitLines } = await import("./doctor/doctor-backlog.js");
|
|
149
159
|
const { config } = loadConfigSafe(root); // the report's own config, so base resolution and the off-base skip match the `uncommitted backlog` row's
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
say("");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
let resyncInstall;
|
|
159
|
-
if (args.includes("--resync-install")) {
|
|
160
|
+
return { run: () => commitUncommittedBacklog(root, config), lines: backlogCommitLines };
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
async function doctorResyncInstall(root, args) {
|
|
164
|
+
return doctorSideEffect(args, "--resync-install", async () => {
|
|
160
165
|
const { resyncRootInstall, rootInstallResyncLines } = await import("./build/dep-install.js");
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
say(line);
|
|
167
|
-
say("");
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
let brokenPinDrop;
|
|
171
|
-
if (args.includes("--drop-broken-pins")) {
|
|
166
|
+
return { run: () => resyncRootInstall(root), lines: rootInstallResyncLines };
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async function doctorDropBrokenPins(root, args) {
|
|
170
|
+
return doctorSideEffect(args, "--drop-broken-pins", async () => {
|
|
172
171
|
const { dropBrokenPins, brokenPinDropLines } = await import("./doctor/doctor-contention-checks.js");
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
for (const line of lines)
|
|
179
|
-
say(line);
|
|
180
|
-
if (lines.length > 0)
|
|
181
|
-
say("");
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
let leftoverRetire;
|
|
185
|
-
if (args.includes("--retire-leftovers")) {
|
|
172
|
+
return { run: () => dropBrokenPins(root), lines: brokenPinDropLines };
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
async function doctorRetireLeftovers(root, args) {
|
|
176
|
+
return doctorSideEffect(args, "--retire-leftovers", async () => {
|
|
186
177
|
const { retireLeftovers, leftoverRetireLines } = await import("./doctor/doctor-contention-checks.js");
|
|
187
178
|
const { config } = loadConfigSafe(root); // the row's own config, so the reap and the report agree on what is retirable
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
179
|
+
return { run: () => retireLeftovers(root, config), lines: leftoverRetireLines };
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function doctorPayload(payload, parts) {
|
|
183
|
+
const out = { ...payload };
|
|
184
|
+
if (parts.leftoverRetire !== undefined)
|
|
185
|
+
out.leftoverRetire = parts.leftoverRetire;
|
|
186
|
+
if (parts.brokenPinDrop !== undefined)
|
|
187
|
+
out.brokenPinDrop = parts.brokenPinDrop;
|
|
188
|
+
if (parts.resyncInstall !== undefined)
|
|
189
|
+
out.resyncInstall = parts.resyncInstall;
|
|
190
|
+
if (parts.backlogCommit !== undefined)
|
|
191
|
+
out.backlogCommit = parts.backlogCommit;
|
|
192
|
+
if (parts.orphanKill !== undefined)
|
|
193
|
+
out.orphanKill = parts.orphanKill;
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
async function cmdDoctorCase(root, args) {
|
|
197
|
+
// No requireReadyRepo gate: doctor's job is to report WHY the environment isn't ready,
|
|
198
|
+
// so it must run outside a git repo and print fail lines rather than throwing.
|
|
199
|
+
const { positionals: checkNames, rest: doctorFlags } = peelPositionals(args);
|
|
200
|
+
rejectUnknownArgs("doctor", doctorFlags, [JSON_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, COMMIT_BACKLOG_FLAG, RESYNC_INSTALL_FLAG, DROP_BROKEN_PINS_FLAG, RETIRE_LEFTOVERS_FLAG]);
|
|
201
|
+
const only = parseDoctorChecks(checkNames);
|
|
202
|
+
const { runDoctor } = await import("./doctor/doctor.js");
|
|
203
|
+
const parts = {
|
|
204
|
+
orphanKill: await doctorKillOrphans(root, args),
|
|
205
|
+
backlogCommit: await doctorCommitBacklog(root, args),
|
|
206
|
+
resyncInstall: await doctorResyncInstall(root, args),
|
|
207
|
+
brokenPinDrop: await doctorDropBrokenPins(root, args),
|
|
208
|
+
leftoverRetire: await doctorRetireLeftovers(root, args),
|
|
209
|
+
};
|
|
199
210
|
const { renderDoctor, renderDoctorProblems, problemsOnly } = await import("./doctor/doctor-render.js");
|
|
200
211
|
const report = await runDoctor(root, undefined, undefined, only);
|
|
201
212
|
// --json prints the collector's own payload (the DoctorReport object), not a re-parse of
|
|
@@ -204,15 +215,10 @@ async function cmdDoctorCase(root, args) {
|
|
|
204
215
|
// --problems keeps the full report for the render (it counts the hidden ok rows) and
|
|
205
216
|
// hands --json the filtered one; the exit code below reads the full report either way.
|
|
206
217
|
// --kill-orphans adds the kill result to the JSON object as `orphanKill`.
|
|
207
|
-
const withKill = (payload) => (orphanKill === undefined ? payload : { ...payload, orphanKill });
|
|
208
|
-
const withBacklogCommit = (payload) => backlogCommit === undefined ? withKill(payload) : withKill({ ...payload, backlogCommit });
|
|
209
|
-
const withResyncInstall = (payload) => resyncInstall === undefined ? withBacklogCommit(payload) : withBacklogCommit({ ...payload, resyncInstall });
|
|
210
|
-
const withBrokenPinDrop = (payload) => brokenPinDrop === undefined ? withResyncInstall(payload) : withResyncInstall({ ...payload, brokenPinDrop });
|
|
211
|
-
const withLeftoverRetire = (payload) => leftoverRetire === undefined ? withBrokenPinDrop(payload) : withBrokenPinDrop({ ...payload, leftoverRetire });
|
|
212
218
|
if (args.includes("--problems"))
|
|
213
|
-
sayJsonOrRender(args, args.includes("--json") ?
|
|
219
|
+
sayJsonOrRender(args, args.includes("--json") ? doctorPayload(problemsOnly(report), parts) : report, renderDoctorProblems);
|
|
214
220
|
else
|
|
215
|
-
sayJsonOrRender(args, args.includes("--json") ?
|
|
221
|
+
sayJsonOrRender(args, args.includes("--json") ? doctorPayload(report, parts) : report, renderDoctor);
|
|
216
222
|
if (report.checks.some((c) => c.level === "fail"))
|
|
217
223
|
process.exitCode = 1; // Warnings never fail the exit.
|
|
218
224
|
}
|
|
@@ -267,69 +273,81 @@ async function cmdArchiveCase(root, args) {
|
|
|
267
273
|
await requireReadyRepo(root);
|
|
268
274
|
await (await import("./backlog/backlog-cli.js")).cmdArchive(root, args);
|
|
269
275
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
276
|
+
/** The one dispatch `bug` and `plan` both run through (see BacklogCommandSpec): route on the
|
|
277
|
+
* leading flag token and hand off to the matching shared AndAnnounce shell, or file a text
|
|
278
|
+
* entry by default. */
|
|
279
|
+
async function runBacklogCommand(root, args, s) {
|
|
280
|
+
const { cancelAndAnnounce, editAndAnnounce, fileAndAnnounce, releaseAndAnnounce, topAndAnnounce } = await import("./backlog/backlog-cli.js");
|
|
274
281
|
const { listAndAnnounce, showAndAnnounce } = await import("./backlog/backlog-cli-view.js");
|
|
275
|
-
const usage = 'tumwater bug "<symptom>"';
|
|
276
282
|
if (args.includes("--show")) {
|
|
277
|
-
showAndAnnounce(root, args,
|
|
283
|
+
showAndAnnounce(root, args, s.command);
|
|
278
284
|
return;
|
|
279
285
|
}
|
|
280
286
|
if (args.includes("--list")) {
|
|
281
|
-
listAndAnnounce(root, args,
|
|
287
|
+
listAndAnnounce(root, args, s.command, s.file, s.section);
|
|
282
288
|
return;
|
|
283
289
|
}
|
|
284
290
|
if (args.includes("--edit")) {
|
|
285
|
-
await editAndAnnounce(root, args,
|
|
291
|
+
await editAndAnnounce(root, args, s.command, s.edit, s.editText);
|
|
286
292
|
return;
|
|
287
293
|
}
|
|
288
294
|
if (args.includes("--cancel")) {
|
|
289
|
-
await cancelAndAnnounce(root, args,
|
|
295
|
+
await cancelAndAnnounce(root, args, s.command, s.cancel);
|
|
290
296
|
return;
|
|
291
297
|
}
|
|
292
298
|
if (args.includes("--top")) {
|
|
293
|
-
await topAndAnnounce(root, args,
|
|
299
|
+
await topAndAnnounce(root, args, s.command, s.top);
|
|
294
300
|
return;
|
|
295
301
|
}
|
|
296
302
|
if (args.includes("--release")) {
|
|
297
|
-
await releaseAndAnnounce(root, args,
|
|
303
|
+
await releaseAndAnnounce(root, args, s.command, s.release, s.releaseRole);
|
|
298
304
|
return;
|
|
299
305
|
}
|
|
300
|
-
await fileAndAnnounce(root, args,
|
|
306
|
+
await fileAndAnnounce(root, args, s.command, s.fileOp, s.releaseRole, s.wakeText, s.titleOf, s.fileText);
|
|
307
|
+
}
|
|
308
|
+
async function cmdBugCase(root, args) {
|
|
309
|
+
const { OPERATOR_FILE_KINDS, bugWakeText, cancelBug, editBug, editBugFromText, fileBug, fileBugFromText } = await import("./backlog/backlog-write.js");
|
|
310
|
+
const { moveEntryToTop, releaseEntry } = await import("./backlog/backlog-release.js");
|
|
311
|
+
const { bugTitleOf } = await import("./backlog/backlog-cli.js");
|
|
312
|
+
const usage = 'tumwater bug "<symptom>"';
|
|
313
|
+
await runBacklogCommand(root, args, {
|
|
314
|
+
command: "bug",
|
|
315
|
+
usage,
|
|
316
|
+
file: "BUGS.md",
|
|
317
|
+
section: "Open",
|
|
318
|
+
edit: (n, positionals) => editBug(root, n, positionals.join(" "), usage),
|
|
319
|
+
editText: (n, text) => editBugFromText(root, n, text, usage),
|
|
320
|
+
cancel: (n) => cancelBug(root, n),
|
|
321
|
+
top: (n) => moveEntryToTop(root, "bug", n),
|
|
322
|
+
release: (n) => releaseEntry(root, "bug", n),
|
|
323
|
+
releaseRole: OPERATOR_FILE_KINDS.bug.role,
|
|
324
|
+
fileOp: (positionals) => fileBug(root, positionals.join(" "), usage),
|
|
325
|
+
fileText: (text) => fileBugFromText(root, text, usage),
|
|
326
|
+
wakeText: bugWakeText,
|
|
327
|
+
titleOf: bugTitleOf,
|
|
328
|
+
});
|
|
301
329
|
}
|
|
302
330
|
async function cmdPlanCase(root, args) {
|
|
303
331
|
const { OPERATOR_FILE_KINDS, cancelPlan, editPlan, editPlanFromText, filePlan, filePlanFromText, planWakeText } = await import("./backlog/backlog-write.js");
|
|
304
332
|
const { moveEntryToTop, releaseEntry } = await import("./backlog/backlog-release.js");
|
|
305
|
-
const {
|
|
306
|
-
const { listAndAnnounce, showAndAnnounce } = await import("./backlog/backlog-cli-view.js");
|
|
333
|
+
const { planTitleOf } = await import("./backlog/backlog-cli.js");
|
|
307
334
|
const usage = 'tumwater plan "<title>" [body...]';
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
324
|
-
if (args.includes("--top")) {
|
|
325
|
-
await topAndAnnounce(root, args, "plan", (n) => moveEntryToTop(root, "plan", n));
|
|
326
|
-
return;
|
|
327
|
-
}
|
|
328
|
-
if (args.includes("--release")) {
|
|
329
|
-
await releaseAndAnnounce(root, args, "plan", (n) => releaseEntry(root, "plan", n), OPERATOR_FILE_KINDS.plan.role);
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
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));
|
|
335
|
+
await runBacklogCommand(root, args, {
|
|
336
|
+
command: "plan",
|
|
337
|
+
usage,
|
|
338
|
+
file: "PLANS.md",
|
|
339
|
+
section: "Planned",
|
|
340
|
+
edit: (n, positionals) => editPlan(root, n, positionals[0] ?? "", positionals.slice(1).join(" "), usage),
|
|
341
|
+
editText: (n, text) => editPlanFromText(root, n, text, usage),
|
|
342
|
+
cancel: (n) => cancelPlan(root, n),
|
|
343
|
+
top: (n) => moveEntryToTop(root, "plan", n),
|
|
344
|
+
release: (n) => releaseEntry(root, "plan", n),
|
|
345
|
+
releaseRole: OPERATOR_FILE_KINDS.plan.role,
|
|
346
|
+
fileOp: (positionals) => filePlan(root, positionals[0] ?? "", positionals.slice(1).join(" "), usage),
|
|
347
|
+
fileText: (text) => filePlanFromText(root, text, usage),
|
|
348
|
+
wakeText: planWakeText,
|
|
349
|
+
titleOf: planTitleOf,
|
|
350
|
+
});
|
|
333
351
|
}
|
|
334
352
|
async function cmdPromptCase(root, args) {
|
|
335
353
|
// Parse before the gate: prompt's positionals are free-form, so rejectUnknownArgs
|
|
@@ -364,31 +382,38 @@ async function cmdStopCase(root, args) {
|
|
|
364
382
|
await requireReadyRepo(root);
|
|
365
383
|
await (await import("./operator/operator-commands.js")).cmdStop(root, args);
|
|
366
384
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
385
|
+
function printCompletionRoles(root) {
|
|
386
|
+
for (const id of completionRoleIds(root))
|
|
387
|
+
say(id);
|
|
388
|
+
}
|
|
389
|
+
function printCompletionConfigKeys(root) {
|
|
390
|
+
for (const key of completionConfigKeys(root))
|
|
391
|
+
say(key);
|
|
392
|
+
}
|
|
393
|
+
function printCompletionConfigValue(key) {
|
|
394
|
+
// The vocabulary `config set <key> <value>` completes for a discrete-valued key
|
|
395
|
+
// (boolean/backend/model/thinking); free-form keys print nothing, so a `set` on one
|
|
396
|
+
// completes nothing instead of offering a wrong vocabulary.
|
|
397
|
+
for (const word of configValueWords(key))
|
|
398
|
+
say(word);
|
|
399
|
+
}
|
|
400
|
+
function printCompletionModels(root) {
|
|
401
|
+
for (const m of completionModels(root))
|
|
402
|
+
say(m);
|
|
403
|
+
}
|
|
404
|
+
const COMPLETION_LISTING_SHAPES = {
|
|
405
|
+
"--roles": "roles",
|
|
406
|
+
"--config-keys": "config-keys",
|
|
407
|
+
"--models": "models",
|
|
408
|
+
};
|
|
409
|
+
function completionListShape(args) {
|
|
410
|
+
if (args.length === 1 && args[0] in COMPLETION_LISTING_SHAPES)
|
|
411
|
+
return COMPLETION_LISTING_SHAPES[args[0]];
|
|
412
|
+
if (args.length === 2 && args[0] === "--config-value")
|
|
413
|
+
return "config-value";
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
function completionShell(args) {
|
|
392
417
|
const shell = args[0];
|
|
393
418
|
// A wrong word count names the count, not a bad shell. A single near-miss shell word gets
|
|
394
419
|
// the same typoSuffix `logs --type`/`history --result` give, so `completion zs` reads
|
|
@@ -399,7 +424,28 @@ async function cmdCompletionCase(root, args) {
|
|
|
399
424
|
fail(`completion takes one shell: bash, zsh, or fish (got ${args.length} words: ${args.join(" ")})`);
|
|
400
425
|
if (!SUPPORTED_SHELLS.includes(shell))
|
|
401
426
|
fail(`completion takes one shell: bash, zsh, or fish${gotSuffix(shell)}${typoSuffix(shell ?? "", SUPPORTED_SHELLS)}`);
|
|
402
|
-
|
|
427
|
+
return shell;
|
|
428
|
+
}
|
|
429
|
+
async function cmdCompletionCase(root, args) {
|
|
430
|
+
// Works outside a repo: no requireReadyRepo.
|
|
431
|
+
const shape = completionListShape(args);
|
|
432
|
+
if (shape === "roles") {
|
|
433
|
+
printCompletionRoles(root);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (shape === "config-keys") {
|
|
437
|
+
printCompletionConfigKeys(root);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (shape === "config-value") {
|
|
441
|
+
printCompletionConfigValue(args[1]);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
if (shape === "models") {
|
|
445
|
+
printCompletionModels(root);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
say(completionScript(completionShell(args)));
|
|
403
449
|
}
|
|
404
450
|
async function cmdVersionCase(args) {
|
|
405
451
|
// A stray flag fails like every other command's: `tumwater version --json` (a plausible
|
package/dist/src/collections.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
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-queued.ts's bucketMergeEvents
|
|
8
|
-
* the team-hold poll's observations-by-provider grouping (team-polls.ts's
|
|
9
|
-
* those sites cannot drift on how a missing key is seeded. Returns a fresh
|
|
10
|
-
* never mutated. */
|
|
7
|
+
* (history-queued.ts's bucketMergeEvents and failure/failure-time-spend.ts's rejected-by-role
|
|
8
|
+
* pass) and the team-hold poll's observations-by-provider grouping (team-polls.ts's
|
|
9
|
+
* pollTeamHold), so those sites cannot drift on how a missing key is seeded. Returns a fresh
|
|
10
|
+
* Map; the input is never mutated. */
|
|
11
11
|
export function groupBy(items, key) {
|
|
12
12
|
const groups = new Map();
|
|
13
13
|
for (const item of items) {
|
|
@@ -15,11 +15,26 @@ export function groupBy(items, key) {
|
|
|
15
15
|
}
|
|
16
16
|
return groups;
|
|
17
17
|
}
|
|
18
|
+
/** The distinct items of an iterable, in first-seen order — the single home of the
|
|
19
|
+
* `seen.includes -> push` dedupe, shared by the pending pane's three scope lists
|
|
20
|
+
* (mergeQueueRoles, postReviewRoles, overlapHolders in ui/tui/tui-pane-pending.ts), the
|
|
21
|
+
* session pane's scope cycle (ui/tui/tui-pane-session.ts's scopeCycle), and the report
|
|
22
|
+
* scope fold (report/report-fold.ts's scopeBases), so those sites cannot drift on how a
|
|
23
|
+
* repeat is skipped. Returns a fresh array; the input is never mutated. (The dashboard's
|
|
24
|
+
* role-scope builder in ui/gui/gui-client-report.ts keeps its own copy by design: it runs
|
|
25
|
+
* in the browser, where harness modules cannot be imported.) */
|
|
26
|
+
export function distinctOrdered(items) {
|
|
27
|
+
const seen = [];
|
|
28
|
+
for (const item of items)
|
|
29
|
+
if (!seen.includes(item))
|
|
30
|
+
seen.push(item);
|
|
31
|
+
return seen;
|
|
32
|
+
}
|
|
18
33
|
/** Return `map`'s value for `key`, creating and storing `make()`'s result first when the key is
|
|
19
34
|
* absent — the single home of the `map.get(key) ?? fresh; …; map.set(key, value)` step, shared
|
|
20
35
|
* 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 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
|
|
36
|
+
* its time-and-spend per-role row (failure-time-spend.ts), the team hold's provider+kind grouping
|
|
37
|
+
* (team-hold.ts), the error-storm reducer's roles-by-key set (src/team/team-error-storm.ts), and
|
|
23
38
|
* `groupBy` above, so those sites cannot drift on how a missing key is seeded. `make` runs only
|
|
24
39
|
* when the key is absent. A stored value of `undefined` reads as absent, matching the `??` idiom
|
|
25
40
|
* this replaces; the callers above never store one. */
|
|
@@ -50,7 +65,7 @@ export function filterKeepPositions(items, keep) {
|
|
|
50
65
|
}
|
|
51
66
|
/** Increment a Map counter, seeding a missing key at 1 — the single home of the
|
|
52
67
|
* `map.set(k, (map.get(k) ?? 0) + 1)` step, shared by the failure-spread kind tally
|
|
53
|
-
* (src/team/failure-spread.ts), the diff multiset's line tally (merge/merge-diff.ts), the
|
|
68
|
+
* (src/team/team-failure-spread.ts), the diff multiset's line tally (merge/merge-diff.ts), the
|
|
54
69
|
* section-title tally (backlog/backlog-structure.ts), the logs' per-type tally
|
|
55
70
|
* (cli/log-commands.ts), the flaky-check tally
|
|
56
71
|
* (doctor/doctor-project-checks.ts), and the multi-runner base tally
|
|
@@ -125,11 +140,11 @@ export function trimToLast(arr, max) {
|
|
|
125
140
|
* order the same way. These two helpers are the single home of that rule; every surface that
|
|
126
141
|
* ranks counters by count calls one of them rather than re-implementing the comparator —
|
|
127
142
|
* `rankCountEntries` serves the `[key, count]` entry shape (src/report/report-render.ts's
|
|
128
|
-
* rankedRoleMap, src/team/failure-spread.ts's strongest-kind pick) and `rankByCount` serves
|
|
129
|
-
* callers whose counts are fields or computations on other shapes
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
143
|
+
* rankedRoleMap, src/team/team-failure-spread.ts's strongest-kind pick) and `rankByCount` serves
|
|
144
|
+
* callers whose counts are fields or computations on other shapes (src/failure/failure-cluster.ts's
|
|
145
|
+
* clusters and src/team/team-error-storm.ts's strongest-cause pick; failure-data.ts's outcome
|
|
146
|
+
* table and failure-time-spend.ts's time-and-spend table). `sortByValueDesc` is the deliberate
|
|
147
|
+
* sibling for the same value-descending shape that wants a code-unit tiebreak
|
|
133
148
|
* instead of localeCompare, and unlike these two it sorts its input array in place. The one
|
|
134
149
|
* deliberate exception to both orderings is ui/gui/gui-client-report-chart.ts's reportRoleOrder,
|
|
135
150
|
* which keeps its own copy by design: it runs in the browser, where harness modules cannot be
|
|
@@ -143,9 +158,9 @@ export function rankByCount(items, count, key) {
|
|
|
143
158
|
* aliases `items` when it already fits; `hidden` is `max(0, items.length - top)`, which the
|
|
144
159
|
* slice-length arithmetic yields without the explicit clamp (a slice never returns more than
|
|
145
160
|
* `top` even when the list is shorter). Shared by the merge-intent and conflict-hunk caps
|
|
146
|
-
* (merge-git.ts), the failure digest's loss-cause and message-cluster tops
|
|
147
|
-
*
|
|
148
|
-
*
|
|
161
|
+
* (merge-git.ts), the failure digest's loss-cause and message-cluster tops (failure-time-spend.ts
|
|
162
|
+
* and failure-cluster.ts), and the answered-questions cap (backlog-question-answer.ts), so those
|
|
163
|
+
* cuts cannot drift on how the remainder is counted. */
|
|
149
164
|
export function topAndHidden(items, top) {
|
|
150
165
|
const visible = items.slice(0, top);
|
|
151
166
|
return { visible, hidden: items.length - visible.length };
|
|
@@ -200,7 +215,7 @@ export function sortEntriesByKey(entries) {
|
|
|
200
215
|
return [...entries].sort(([a], [b]) => (a ?? "").localeCompare(b ?? ""));
|
|
201
216
|
}
|
|
202
217
|
/** rankByCount for `[key, count]` entry pairs (count first): src/report/report-render.ts's
|
|
203
|
-
* rankedRoleMap, src/team/failure-spread.ts's strongest-kind pick, and
|
|
218
|
+
* rankedRoleMap, src/team/team-failure-spread.ts's strongest-kind pick, and
|
|
204
219
|
* src/doctor/doctor-contention-checks.ts's check-wait and provider-wait rankings. */
|
|
205
220
|
export function rankCountEntries(entries) {
|
|
206
221
|
return rankByCount(entries, (e) => e[1], (e) => e[0]);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { classifyLock, releaseOwnedLockDir, tryClaimLockDir } from "./lock.js";
|
|
3
|
+
import { ensureParentDir, isDirectory, removeTree, walkUp } from "../files/files.js";
|
|
4
|
+
/** The host-wide suite-slot ledger that bounds how many project check scripts (an agent's bare
|
|
5
|
+
* `npm run test:gate` and the harness's own spawned check alike) run their suite step at once,
|
|
6
|
+
* sized to the same `config.maxConcurrentChecks` the in-process check permit enforces. Where
|
|
7
|
+
* withCheckPermit is one process-wide semaphore (every suite spawned by THIS process shares it),
|
|
8
|
+
* the ledger is files on disk so suites started by DIFFERENT processes — the harness and the pi
|
|
9
|
+
* agent's bash tools, or two agents at once — still contend for the same cap. It exists to close
|
|
10
|
+
* the gap where an agent's own suite run takes no team check permit at all (BUGS.md 2026-10-28),
|
|
11
|
+
* so the two mechanisms coexist: withCheckPermit keeps its in-process tier queue, reentrancy and
|
|
12
|
+
* callers exactly as they are, and the npm check scripts additionally claim a slot here.
|
|
13
|
+
*
|
|
14
|
+
* A slot is a lock dir under `.tumwater/state/check-permits/slot-<n>` using lock.ts's directory-
|
|
15
|
+
* and-pid protocol unchanged (tryClaimLockDir / releaseOwnedLockDir / classifyLock), so a killed
|
|
16
|
+
* suite's slot is reclaimed by lock.ts's stale rules (dead pid, no-pid grace, then age) on a later
|
|
17
|
+
* claim — no separate sweep. A waiter gives up after a bounded wait and runs the suite anyway with
|
|
18
|
+
* a warning rather than hanging past the check deadline or the bash tool's 600 s cap: the ledger
|
|
19
|
+
* is a scheduling throttle, not a correctness mutex, so a full ledger must degrade to "run
|
|
20
|
+
* anyway", never to "hang forever". */
|
|
21
|
+
/** How long acquireCheckSlot keeps trying for a free slot before giving up and returning null (the
|
|
22
|
+
* callers then run the suite unpermitted with a warning). Bounded well under the bash tool's 600 s
|
|
23
|
+
* cap so a full ledger never prices a run's own timeout. */
|
|
24
|
+
const CHECK_SLOT_WAIT_TIMEOUT_MS = 60_000;
|
|
25
|
+
/** The retry cadence between full passes over a full ledger — a slot frees quickly (a suite's
|
|
26
|
+
* release is the moment its check process exits), so a 200 ms repoll finds it almost immediately
|
|
27
|
+
* without hammering the filesystem. */
|
|
28
|
+
const CHECK_SLOT_RETRY_MS = 200;
|
|
29
|
+
/** The ancestor of `startDir` that holds the team's `.tumwater/state`, or null when none within the
|
|
30
|
+
* walk's bound does. The slot ledger lives under that state dir, so `scripts/check-permit.mjs`
|
|
31
|
+
* locates the root the same way the harness's own root lookup does (shared walkUp climb), and a
|
|
32
|
+
* suite run outside any team root has no ledger to contend for. Exported for tests. */
|
|
33
|
+
export function findTeamRoot(startDir = process.cwd()) {
|
|
34
|
+
return walkUp(startDir, 63, (dir) => (isDirectory(path.join(dir, ".tumwater", "state")) ? dir : null));
|
|
35
|
+
}
|
|
36
|
+
/** The slot directory for `index` of a ledger rooted at `root` (under `root/.tumwater/state`). One
|
|
37
|
+
* function owns the path so the acquirer, the releaser and the reclaim all name the same dir. */
|
|
38
|
+
export function checkSlotDir(root, index) {
|
|
39
|
+
return path.join(root, ".tumwater", "state", "check-permits", `slot-${index}`);
|
|
40
|
+
}
|
|
41
|
+
/** Claim one of the `cap` slots of `root`'s ledger, waiting up to `waitMs` for one to free. Walks
|
|
42
|
+
* slots `0..cap-1`, breaking any it finds lock.ts's classifyLock calls "stale" (a dead holder, a
|
|
43
|
+
* leftover no-pid dir past its grace, or an old dir), then trying to claim each by atomic publish;
|
|
44
|
+
* when none is free it sleeps and retries until the budget is spent, returning null. A stale slot
|
|
45
|
+
* is broken as encountered because a dead holder's dir would otherwise block that index forever. */
|
|
46
|
+
export async function acquireCheckSlot(root, cap, waitMs = CHECK_SLOT_WAIT_TIMEOUT_MS) {
|
|
47
|
+
// The ledger's parent must exist before any slot's atomic publish can create it: lock.ts's
|
|
48
|
+
// publishLockDir never makes its own parent (withStateLock is where ensureParentDir runs), so a
|
|
49
|
+
// first claim on a root with no check-permits dir would fail its mkdirSync and never acquire.
|
|
50
|
+
ensureParentDir(checkSlotDir(root, 0));
|
|
51
|
+
const startedAt = Date.now();
|
|
52
|
+
for (;;) {
|
|
53
|
+
for (let index = 0; index < cap; index++) {
|
|
54
|
+
const dir = checkSlotDir(root, index);
|
|
55
|
+
if (classifyLock(dir) === "stale")
|
|
56
|
+
removeTree(dir);
|
|
57
|
+
if (tryClaimLockDir(dir))
|
|
58
|
+
return dir;
|
|
59
|
+
}
|
|
60
|
+
if (Date.now() - startedAt >= waitMs)
|
|
61
|
+
return null;
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, CHECK_SLOT_RETRY_MS));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Return a slot claimed by THIS process. Deleting a dir that another pid now owns must not
|
|
66
|
+
* happen (releaseOwnedLockDir checks the pid first); a slot stolen from under us stays for its
|
|
67
|
+
* new owner or for the stale rules. */
|
|
68
|
+
export function releaseCheckSlot(dir) {
|
|
69
|
+
releaseOwnedLockDir(dir);
|
|
70
|
+
}
|
|
@@ -93,6 +93,10 @@ function releaseOwnedLock(dir) {
|
|
|
93
93
|
return;
|
|
94
94
|
rmLockDir(dir);
|
|
95
95
|
}
|
|
96
|
+
/** The ledger-facing release primitive: the same releaseOwnedLock every mutex exits with, but
|
|
97
|
+
* exported so src/concurrency/check-slots.ts can hand a suite's slot back without re-implementing
|
|
98
|
+
* the owned-pid guard (a holder whose lock was stolen must not delete its successor's dir). */
|
|
99
|
+
export const releaseOwnedLockDir = releaseOwnedLock;
|
|
96
100
|
function acquireStep(dir, timeoutMs, waitedMs) {
|
|
97
101
|
// An existing path is a held lock to wait on (and maybe break); publishLockDir is only for
|
|
98
102
|
// an absent path, so a read-only parent that already holds a stale lock reports contention
|
|
@@ -149,6 +153,24 @@ function publishLockDir(dir) {
|
|
|
149
153
|
throw err;
|
|
150
154
|
}
|
|
151
155
|
}
|
|
156
|
+
/** The one-shot claim step of the lock protocol, exported for a caller that wants a single
|
|
157
|
+
* best-effort claim rather than withLock's retry loop: claim `dir` by atomic publish, returning
|
|
158
|
+
* true on success and false when `dir` already exists (a live holder) or contention — never
|
|
159
|
+
* throwing, unlike publishLockDir. The suite-slot ledger (check-slots.ts) calls it per index as
|
|
160
|
+
* it walks a ledger, so it must not throw on a held slot to inspect the others. A genuine
|
|
161
|
+
* filesystem failure (ENOSPC, a read-only parent) also returns false; the ledger degrades to
|
|
162
|
+
* "run unpermitted" on an unable-to-create ledger, which suits a best-effort scheduling cap. */
|
|
163
|
+
export function tryClaimLockDir(dir) {
|
|
164
|
+
try {
|
|
165
|
+
if (fs.existsSync(dir))
|
|
166
|
+
return false; // A held slot (live or stale) — the caller walks on.
|
|
167
|
+
publishLockDir(dir);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
152
174
|
/** Remove `.acquiring-*` temp dirs left beside a lock by a creator that crashed before its
|
|
153
175
|
* rename — the one remnant atomic publication can leave. Only dirs older than STALE_MS are
|
|
154
176
|
* touched, far beyond any scheduling stall, so a live creator's in-flight temp is never
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** The AsyncLocalStorage store that carries a run's lease across its work: a lease rides here
|
|
2
|
+
* for the length of a run, so an idle stretch inside it (a failed review's in-place retry
|
|
3
|
+
* pause, called via idleWithoutPermit) can hand the team permit back and re-take it, letting a
|
|
4
|
+
* parked run use the slot meanwhile. The check-permit.ts `holdingPermit` store is the pattern:
|
|
5
|
+
* nothing here is a separate permit, only the single lease the run path already holds. */
|
|
6
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
7
|
+
const runPermitStore = new AsyncLocalStorage();
|
|
8
|
+
/** Run `run` with `lease` in the run-permit store, so an idle wait inside it can pause and
|
|
9
|
+
* resume the same team permit. The same lease object is released by the caller after the store
|
|
10
|
+
* is unwrapped. */
|
|
11
|
+
export function withRunPermit(lease, run) {
|
|
12
|
+
return runPermitStore.run({ lease }, run);
|
|
13
|
+
}
|
|
14
|
+
/** Run `wait`, an idle stretch inside the calling lease's work, with the lease's team permit
|
|
15
|
+
* handed back (SemaphoreLease.pause), so a parked run can use it meanwhile, then take it back
|
|
16
|
+
* (resume) before returning. A failed review's in-place retry pause is the caller: with one
|
|
17
|
+
* team permit, a reviewer that kept failing held it through two 30 s pauses and starved every
|
|
18
|
+
* other role. Once `signal` has fired, nothing is taken back and the caller's release() settles
|
|
19
|
+
* a still-paused lease. No-op without a lease in context. */
|
|
20
|
+
export async function idleWithoutPermit(wait, signal) {
|
|
21
|
+
const ctx = runPermitStore.getStore();
|
|
22
|
+
if (!ctx)
|
|
23
|
+
return wait();
|
|
24
|
+
ctx.lease.pause();
|
|
25
|
+
try {
|
|
26
|
+
await wait();
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
if (!signal?.aborted)
|
|
30
|
+
await untilAborted(ctx.lease.resume(), signal);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** `p`, or an earlier return once `signal` fires: a resume parked on a full team must not keep an
|
|
34
|
+
* aborted verification waiting for a permit it no longer needs. */
|
|
35
|
+
async function untilAborted(p, signal) {
|
|
36
|
+
if (!signal)
|
|
37
|
+
return p;
|
|
38
|
+
let onAbort = () => { };
|
|
39
|
+
const aborted = new Promise((resolve) => {
|
|
40
|
+
onAbort = resolve;
|
|
41
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
42
|
+
});
|
|
43
|
+
try {
|
|
44
|
+
await Promise.race([p, aborted]);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
signal.removeEventListener("abort", onAbort);
|
|
48
|
+
}
|
|
49
|
+
}
|