tumwater 0.4.0 → 0.4.2
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 +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- 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 +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -5,34 +5,11 @@ import { MERGE_SCOPES, SCOPE_WORDS, buildCheckEvent, buildCheckSkipWarning, runW
|
|
|
5
5
|
import { CHECK_TIER, withCheckPermit } from "../concurrency/check-permit.js";
|
|
6
6
|
import { sampleHostLoad, sampleSleepClock } from "./host-sleep.js";
|
|
7
7
|
import { npmInstall } from "./dep-install.js";
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
|
|
13
|
-
* outcome classification (runBuildCheck, BuildCheckOutcome) — because orchestration is its own
|
|
14
|
-
* concern: the permit, the event emission, the verdict-less retry, the merge-scope unverified
|
|
15
|
-
* remapping, and the skip warning. Every run is an event with its cost: the deterministic checks
|
|
16
|
-
* are where the team's compute goes after the authoring run, and "how long does npm test take per
|
|
17
|
-
* merge" must be answerable from the feed, not by timing it by hand. Returns null when no check is
|
|
18
|
-
* declared (nothing to run — the caller passes, exactly as before this split), otherwise the
|
|
19
|
-
* declared check plus its classified outcome. A "skipped" outcome also logs its standard warning
|
|
20
|
-
* here (wording keyed on the scope, the timeout as actually set — and, when its deadline fired
|
|
21
|
-
* late, as actually enforced); "failed" and "passed" are the caller's to decide (deterministic
|
|
22
|
-
* reject vs. verifiedHead / baseline seeding). A timeout or signal kill at a merge scope is
|
|
23
|
-
* remapped to a deterministic "failed" — the tree is unverified, so it must not merge. A check
|
|
24
|
-
* killed by a signal the harness did not send is retried once at any scope — the first run's death
|
|
25
|
-
* says nothing about the tree (it is another run's `pkill`), so one verdict from a clean attempt is
|
|
26
|
-
* owed before the skip is honored; a timeout whose deadline demonstrably fired late (the harness's
|
|
27
|
-
* own host-sleep evidence) earns that same one retry at any scope, and a failed run the host slept
|
|
28
|
-
* through does too; each attempt is priced as its own build_check event, carrying when the check
|
|
29
|
-
* itself ran (buildCheckRunFields). A configured `check.gateCommand` replaces the command at scope
|
|
30
|
-
* "gate" only (same cwd and timeout); the in-lock command (Fast merge "Defaults", mergeCommandOf) —
|
|
31
|
-
* `check.mergeCommand` when set, else `check.gateCommand`, else the full check — replaces it at the
|
|
32
|
-
* merge and batch scopes, and those events then carry `full: false`. The red-main baseline, which
|
|
33
|
-
* detects on its own, keeps running check.command. `permitWait` hears
|
|
34
|
-
* about a wait for the check permit (the merge cell's "waiting for a check slot"). Never throws. */
|
|
35
|
-
export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad) {
|
|
8
|
+
/** Resolve which command to detect for a scope and the effective deadline it runs under — the
|
|
9
|
+
* command-swap/detect rule (gateCommandOf/mergeCommandOf over detectBuildCheck), the
|
|
10
|
+
* merge-scope starved deadline, and the cheaper flag. Returns null when no check is declared
|
|
11
|
+
* (nothing to run). */
|
|
12
|
+
function resolveScopedCheck(wt, scope, config, timeoutMs, sampleLoad) {
|
|
36
13
|
const swapped = scope === "gate" ? gateCommandOf(config) : MERGE_SCOPES.has(scope) ? mergeCommandOf(config) : undefined;
|
|
37
14
|
const check = detectBuildCheck(wt, swapped === undefined ? config : { check: { ...config?.check, command: swapped } });
|
|
38
15
|
if (!check)
|
|
@@ -50,6 +27,12 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
|
|
|
50
27
|
? starvedDeadlineMs(checkTimeoutMs(check, timeoutMs), load)
|
|
51
28
|
: checkTimeoutMs(check, timeoutMs);
|
|
52
29
|
const runCheck = { ...check, timeoutMs: effectiveMs };
|
|
30
|
+
return { check, cheaper, runCheck, effectiveMs, load };
|
|
31
|
+
}
|
|
32
|
+
/** Run the resolved check under one permit, with the one clean retry for a verdict-less death —
|
|
33
|
+
* the one-permit-two-attempt rule. Prices the first attempt as its own build_check event and
|
|
34
|
+
* returns the final outcome and its run duration. */
|
|
35
|
+
async function runScopedCheckWithRetry(root, role, scope, wt, runCheck, timeoutMs, config, sampleSleep, install, permitWait, cheaper) {
|
|
53
36
|
// One permit covers both attempts (the killed-check retry re-runs under the permit it
|
|
54
37
|
// holds); durationMs prices the run itself, not the wait for a permit.
|
|
55
38
|
let durationMs = 0;
|
|
@@ -70,6 +53,12 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
|
|
|
70
53
|
granted: () => permitWait?.granted?.(),
|
|
71
54
|
waited: (waitedMs, parked) => logCheckWaitEvent(root, role, scope, waitedMs, parked),
|
|
72
55
|
}, `${role} ${scope}`);
|
|
56
|
+
return { raw, durationMs };
|
|
57
|
+
}
|
|
58
|
+
/** Classify the raw outcome: the merge-scope skip and slept-through rules become a deterministic
|
|
59
|
+
* unverified reject; anything else passes through. Never logs — the caller warns (see
|
|
60
|
+
* warnScopedCheck). Returns the unverified reason only when a rule fired. */
|
|
61
|
+
function unverifiedOutcomeFor(scope, check, raw, effectiveMs, load, durationMs) {
|
|
73
62
|
// A signal kill at a merge scope, or a timeout whose deadline fired on time (real slowness, a
|
|
74
63
|
// genuinely hung or oversized suite), made no verdict about the tree — and this is the check
|
|
75
64
|
// whose whole job is to catch a semantic conflict before it merges, so it rejects
|
|
@@ -89,6 +78,8 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
|
|
|
89
78
|
(((raw.status === "skipped" &&
|
|
90
79
|
(raw.skipReason === "timeout" || raw.skipReason === "killed" || raw.skipReason === "install")) ||
|
|
91
80
|
sleepFailed));
|
|
81
|
+
if (!unverifiedSkip)
|
|
82
|
+
return { outcome: raw, unverifiedReason: null };
|
|
92
83
|
const unverifiedReason = raw.skipReason === "install"
|
|
93
84
|
? installFailedPhrase(SCOPE_WORDS[scope].label, raw.install, UNVERIFIED_TREE)
|
|
94
85
|
: raw.skipReason === "killed"
|
|
@@ -99,17 +90,19 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
|
|
|
99
90
|
// An on-time kill (the late-deadline wording already names its own cause) names the
|
|
100
91
|
// starved host when the load average says the team was the check's competition.
|
|
101
92
|
(raw.run?.deadlineLateMs ?? 0) > DEADLINE_LATE_TOLERANCE_MS ? "" : loadedHostPhrase(load)}`;
|
|
102
|
-
const outcome =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
93
|
+
const outcome = {
|
|
94
|
+
status: "failed",
|
|
95
|
+
script: checkScriptName(check),
|
|
96
|
+
outputTail: [unverifiedReason],
|
|
97
|
+
run: raw.run,
|
|
98
|
+
unverified: true,
|
|
99
|
+
...(raw.install ? { install: raw.install } : {}),
|
|
100
|
+
};
|
|
101
|
+
return { outcome, unverifiedReason };
|
|
102
|
+
}
|
|
103
|
+
/** Warn for the two verdict-less roads: a skipped check (buildCheckSkipWarning) or an unverified
|
|
104
|
+
* merge reject. Reads the scope word table and the killed signal. */
|
|
105
|
+
function warnScopedCheck(root, role, scope, outcome, effectiveMs, durationMs, unverifiedReason) {
|
|
113
106
|
if (outcome.status === "skipped") {
|
|
114
107
|
// Environmental — deliberately NOT fail-closed, so a hung build script cannot wedge every
|
|
115
108
|
// code run into the 3-strike discard (gate) or a merge behind the merge lock.
|
|
@@ -118,8 +111,44 @@ export async function runScopedBuildCheck(root, role, scope, wt, config, timeout
|
|
|
118
111
|
? { signal: outcome.killedBy, durationMs }
|
|
119
112
|
: undefined, outcome.run, outcome.install));
|
|
120
113
|
}
|
|
121
|
-
else if (
|
|
114
|
+
else if (unverifiedReason) {
|
|
122
115
|
warnEventBestEffort(root, role, `${unverifiedReason}; rejecting the merge`);
|
|
123
116
|
}
|
|
124
|
-
|
|
117
|
+
}
|
|
118
|
+
/** Run the project's declared check for a named scope — the detect → run → build_check event →
|
|
119
|
+
* skip-warning sequence the review gate's pre-check (scope "gate"), the merge path's in-lock
|
|
120
|
+
* re-check (scope "merge"), and the batch merger's one check over the whole stacked tree (scope
|
|
121
|
+
* "batch") previously each ran inline, kept in one place so the event's shape and the skip warning
|
|
122
|
+
* cannot drift between them. Split out of build-check.ts — which keeps the check's execution and
|
|
123
|
+
* outcome classification (runBuildCheck, BuildCheckOutcome) — because orchestration is its own
|
|
124
|
+
* concern: the permit, the event emission, the verdict-less retry, the merge-scope unverified
|
|
125
|
+
* remapping, and the skip warning. Every run is an event with its cost: the deterministic checks
|
|
126
|
+
* are where the team's compute goes after the authoring run, and "how long does npm test take per
|
|
127
|
+
* merge" must be answerable from the feed, not by timing it by hand. Returns null when no check is
|
|
128
|
+
* declared (nothing to run — the caller passes, exactly as before this split), otherwise the
|
|
129
|
+
* declared check plus its classified outcome. A "skipped" outcome also logs its standard warning
|
|
130
|
+
* here (wording keyed on the scope, the timeout as actually set — and, when its deadline fired
|
|
131
|
+
* late, as actually enforced); "failed" and "passed" are the caller's to decide (deterministic
|
|
132
|
+
* reject vs. verifiedHead / baseline seeding). A timeout or signal kill at a merge scope is
|
|
133
|
+
* remapped to a deterministic "failed" — the tree is unverified, so it must not merge. A check
|
|
134
|
+
* killed by a signal the harness did not send is retried once at any scope — the first run's death
|
|
135
|
+
* says nothing about the tree (it is another run's `pkill`), so one verdict from a clean attempt is
|
|
136
|
+
* owed before the skip is honored; a timeout whose deadline demonstrably fired late (the harness's
|
|
137
|
+
* own host-sleep evidence) earns that same one retry at any scope, and a failed run the host slept
|
|
138
|
+
* through does too; each attempt is priced as its own build_check event, carrying when the check
|
|
139
|
+
* itself ran (buildCheckRunFields). A configured `check.gateCommand` replaces the command at scope
|
|
140
|
+
* "gate" only (same cwd and timeout); the in-lock command (Fast merge "Defaults", mergeCommandOf) —
|
|
141
|
+
* `check.mergeCommand` when set, else `check.gateCommand`, else the full check — replaces it at the
|
|
142
|
+
* merge and batch scopes, and those events then carry `full: false`. The red-main baseline, which
|
|
143
|
+
* detects on its own, keeps running check.command. `permitWait` hears
|
|
144
|
+
* about a wait for the check permit (the merge cell's "waiting for a check slot"). Never throws. */
|
|
145
|
+
export async function runScopedBuildCheck(root, role, scope, wt, config, timeoutMs = BUILD_CHECK_TIMEOUT_MS, sampleSleep = sampleSleepClock, install = npmInstall, permitWait, sampleLoad = sampleHostLoad) {
|
|
146
|
+
const r = resolveScopedCheck(wt, scope, config, timeoutMs, sampleLoad);
|
|
147
|
+
if (!r)
|
|
148
|
+
return null;
|
|
149
|
+
const { raw, durationMs } = await runScopedCheckWithRetry(root, role, scope, wt, r.runCheck, timeoutMs, config, sampleSleep, install, permitWait, r.cheaper);
|
|
150
|
+
const { outcome, unverifiedReason } = unverifiedOutcomeFor(scope, r.check, raw, r.effectiveMs, r.load, durationMs);
|
|
151
|
+
logEventBestEffort(root, buildCheckEvent(role, scope, outcome, durationMs, r.cheaper));
|
|
152
|
+
warnScopedCheck(root, role, scope, outcome, r.effectiveMs, durationMs, unverifiedReason);
|
|
153
|
+
return { check: r.check, outcome };
|
|
125
154
|
}
|
|
@@ -10,6 +10,7 @@ import { knownRoleIdsCached, loadConfigSafe } from "../config/config.js";
|
|
|
10
10
|
import { aheadOfMain, branchExists, countCommitsInRange, currentBranch, targetBranch } from "../git/git.js";
|
|
11
11
|
import { gitTry } from "../git/git-run.js";
|
|
12
12
|
import { aheadOfMainDiff, aheadOfMainFiles, changedFiles } from "../git/git-diff.js";
|
|
13
|
+
import { compareCodeUnits } from "../collections.js";
|
|
13
14
|
import { isAbsolute } from "node:path";
|
|
14
15
|
import { branchName } from "../paths.js";
|
|
15
16
|
import { isUsableWorktree } from "../git/worktree.js";
|
|
@@ -152,7 +153,8 @@ async function cappedWorkingDiff(wt, spec = []) {
|
|
|
152
153
|
return (`[uncommitted diff truncated: the full diff is ${full.length} bytes; showing --stat]\n\n` + stat);
|
|
153
154
|
}
|
|
154
155
|
/** The paths two or more distinct roles hold: a role's duplicate path counts once, roles stay
|
|
155
|
-
* in holdings order, paths sort
|
|
156
|
+
* in holdings order, paths sort in code-unit order (`compareCodeUnits`, the locale-independent
|
|
157
|
+
* order a plain `<` comparison gives). */
|
|
156
158
|
export function fileOverlaps(holdings) {
|
|
157
159
|
const byPath = new Map();
|
|
158
160
|
for (const { role, files } of holdings) {
|
|
@@ -166,7 +168,7 @@ export function fileOverlaps(holdings) {
|
|
|
166
168
|
}
|
|
167
169
|
return [...byPath.entries()]
|
|
168
170
|
.filter(([, roles]) => roles.length >= 2)
|
|
169
|
-
.sort(([a], [b]) => (a
|
|
171
|
+
.sort(([a], [b]) => compareCodeUnits(a, b))
|
|
170
172
|
.map(([path, roles]) => ({ path, roles }));
|
|
171
173
|
}
|
|
172
174
|
/** Keep only the overlaps naming a listed role; each kept entry still names every holder. */
|
package/dist/src/cli/cli-args.js
CHANGED
|
@@ -341,6 +341,15 @@ export function durationLabel(ms) {
|
|
|
341
341
|
}
|
|
342
342
|
return rest === 0 ? parts.join("") : `${ms}ms`;
|
|
343
343
|
}
|
|
344
|
+
/** The human phrase for a two-ended time window — `between 2h and 1h ago` — the shape
|
|
345
|
+
* `--since/--until` windows share with the history TUI and `history`'s bounded empties.
|
|
346
|
+
* Unbounded windows keep their own "last …"/"in …" phrasing; only the two-ended form is
|
|
347
|
+
* folded here so the three sites cannot drift. The two bounds are labeled in caller order
|
|
348
|
+
* (older before newer at every current site), and each is run through durationLabel, so a
|
|
349
|
+
* phrase always echoes a form the parser accepts. Lives beside durationLabel for that reason. */
|
|
350
|
+
export function betweenWindow(older, newer) {
|
|
351
|
+
return `between ${durationLabel(older)} and ${durationLabel(newer)} ago`;
|
|
352
|
+
}
|
|
344
353
|
/** The shared over-cap check for duration-valued flags: fail with the one message shape —
|
|
345
354
|
* `<flag> is capped at <max label> (got <value label>)` — so every capped duration names
|
|
346
355
|
* both bounds in the same phrasing and the sites cannot drift apart (the same idiom the
|
|
@@ -247,19 +247,19 @@ const PROMPT_FLAG_SPECS = [
|
|
|
247
247
|
{ names: ["--attach"], value: true, valueName: "<path>" },
|
|
248
248
|
{ names: ["--grep"], value: true, valueName: "<text>" },
|
|
249
249
|
];
|
|
250
|
-
/**
|
|
251
|
-
*
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
*
|
|
262
|
-
|
|
250
|
+
/** The six `--list`/`--cancel`/`--edit`/`--top` mode pairs that may not combine, in today's
|
|
251
|
+
* exact order. */
|
|
252
|
+
const PROMPT_MODE_EXCLUSIONS = [
|
|
253
|
+
["--list", "--cancel"],
|
|
254
|
+
["--list", "--edit"],
|
|
255
|
+
["--cancel", "--edit"],
|
|
256
|
+
["--list", "--top"],
|
|
257
|
+
["--cancel", "--top"],
|
|
258
|
+
["--edit", "--top"],
|
|
259
|
+
];
|
|
260
|
+
/** The flag-vocabulary gate and the once-check: the unknown-double-dash gate, the ten index
|
|
261
|
+
* collections and rejectDuplicateFlags's once-check all ride here, one scan, one home. */
|
|
262
|
+
function scanPromptFlags(args) {
|
|
263
263
|
// The unknown-double-dash gate reads PROMPT_FLAG_SPECS, so the list its error names admits
|
|
264
264
|
// exactly what the duplicate-flag check below accepts — one vocabulary, one message renderer.
|
|
265
265
|
rejectUnknownDoubleDash("prompt", args, PROMPT_FLAG_SPECS);
|
|
@@ -276,21 +276,30 @@ export function parsePromptArgs(args) {
|
|
|
276
276
|
// --attach is repeatable: every occurrence claims itself and its value token, like --role's
|
|
277
277
|
// single pair, so the pairs ride out of the prompt text in every mode.
|
|
278
278
|
const attachFlags = args.flatMap((a, i) => (a === "--attach" ? [i] : []));
|
|
279
|
+
const roleFlag = args.indexOf("--role");
|
|
279
280
|
const known = PROMPT_FLAG_SPECS.flatMap((s) => s.names).filter((f) => f !== "--attach"); // repeatable: the once-check skips it
|
|
280
281
|
rejectDuplicateFlags(args, known);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
282
|
+
return { jsonFlags, listFlag, cancelFlag, editFlag, topFlag, fileFlag, atFlag, grepFlag, attachFlags, roleFlag };
|
|
283
|
+
}
|
|
284
|
+
/** The six mode mutual exclusions: the mutually-exclusive `--list`/`--cancel`/`--edit`/
|
|
285
|
+
* `--top` pairs, in the table's order, so the first failing pair reports as it did before. */
|
|
286
|
+
function assertPromptModesDisjoint(scan) {
|
|
287
|
+
const index = {
|
|
288
|
+
"--list": scan.listFlag,
|
|
289
|
+
"--cancel": scan.cancelFlag,
|
|
290
|
+
"--edit": scan.editFlag,
|
|
291
|
+
"--top": scan.topFlag,
|
|
292
|
+
};
|
|
293
|
+
for (const [a, b] of PROMPT_MODE_EXCLUSIONS) {
|
|
294
|
+
// All four keys are always present in the object; ?? -1 just satisfies the strict
|
|
295
|
+
// indexed-access typing without changing the (never-undefined) compare.
|
|
296
|
+
if ((index[a] ?? -1) >= 0 && (index[b] ?? -1) >= 0)
|
|
297
|
+
fail(`${a} and ${b} are mutually exclusive`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** The `--role <id>` value pair: its raw value, its missing-value refusal and the pair it
|
|
301
|
+
* claims, verbatim. */
|
|
302
|
+
function parsePromptRole(args, roleFlag) {
|
|
294
303
|
const roleRaw = roleFlag >= 0 ? args[roleFlag + 1] : undefined;
|
|
295
304
|
if (roleFlag >= 0 && (!roleRaw || roleRaw.startsWith("--")))
|
|
296
305
|
fail(ROLE_VALUE_ERROR);
|
|
@@ -298,17 +307,30 @@ export function parsePromptArgs(args) {
|
|
|
298
307
|
// The flag and its value are never prompt content: --role is a scope, not text. Only claimed
|
|
299
308
|
// when actually present — negative indexes would over-claim real tokens.
|
|
300
309
|
const roleClaim = roleFlag >= 0 ? [roleFlag, roleFlag + 1] : [];
|
|
310
|
+
return { role, roleClaim };
|
|
311
|
+
}
|
|
312
|
+
/** The `--json` list-only refusal: in enqueue or cancel mode it must never silently ride
|
|
313
|
+
* along as prompt text (or beside a state change). */
|
|
314
|
+
function assertPromptJsonListOnly(scan) {
|
|
301
315
|
// --json is list-only: in enqueue or cancel mode it must never silently ride along as
|
|
302
316
|
// prompt text (or beside a state change), so it is refused before either branch runs.
|
|
303
|
-
if (jsonFlags.length > 0 && listFlag < 0)
|
|
317
|
+
if (scan.jsonFlags.length > 0 && scan.listFlag < 0)
|
|
304
318
|
fail("--json only applies to --list");
|
|
305
|
-
|
|
319
|
+
}
|
|
320
|
+
/** The `--grep <text>` value pair and its list-only refusal, verbatim. */
|
|
321
|
+
function parsePromptGrep(args, scan) {
|
|
322
|
+
if (scan.grepFlag >= 0 && scan.listFlag < 0)
|
|
306
323
|
fail("--grep only applies to --list");
|
|
307
|
-
const grepRaw = grepFlag >= 0 ? args[grepFlag + 1] : undefined;
|
|
308
|
-
if (grepFlag >= 0 && (!grepRaw || grepRaw.startsWith("--"))) {
|
|
324
|
+
const grepRaw = scan.grepFlag >= 0 ? args[scan.grepFlag + 1] : undefined;
|
|
325
|
+
if (scan.grepFlag >= 0 && (!grepRaw || grepRaw.startsWith("--"))) {
|
|
309
326
|
fail("--grep needs text (usage: tumwater prompt --list --grep <text>)");
|
|
310
327
|
}
|
|
311
|
-
const grepClaim = grepFlag >= 0 ? [grepFlag, grepFlag + 1] : [];
|
|
328
|
+
const grepClaim = scan.grepFlag >= 0 ? [scan.grepFlag, scan.grepFlag + 1] : [];
|
|
329
|
+
return { grepRaw, grepClaim };
|
|
330
|
+
}
|
|
331
|
+
/** The queue-only-mode refusals: --file and --at have nothing to mean except in the enqueue
|
|
332
|
+
* mode, and --attach is refused by --list and --cancel (--edit accepts it). */
|
|
333
|
+
function assertPromptQueueOnlyFlags(scan) {
|
|
312
334
|
// The queue-only flags --file and --at have nothing to mean in the read-only and destructive
|
|
313
335
|
// modes (--list and --cancel): --file is the file-shaped twin of free-form text (--edit
|
|
314
336
|
// accepts it as its replacement text, so the editFlag test below exempts that one mode), and
|
|
@@ -318,16 +340,16 @@ export function parsePromptArgs(args) {
|
|
|
318
340
|
// predicate and the `<flag> only queues a prompt` wording, so a new mode or queue-only flag
|
|
319
341
|
// is added here.
|
|
320
342
|
const refuseQueueOnlyFlag = (flag, present) => {
|
|
321
|
-
if (present && (listFlag >= 0 || cancelFlag >= 0 || editFlag >= 0 || topFlag >= 0))
|
|
343
|
+
if (present && (scan.listFlag >= 0 || scan.cancelFlag >= 0 || scan.editFlag >= 0 || scan.topFlag >= 0))
|
|
322
344
|
fail(`${flag} only queues a prompt`);
|
|
323
345
|
};
|
|
324
|
-
refuseQueueOnlyFlag("--file", fileFlag >= 0 && editFlag < 0);
|
|
325
|
-
refuseQueueOnlyFlag("--at", atFlag >= 0);
|
|
326
|
-
refuseQueueOnlyFlag("--attach", attachFlags.length > 0 && editFlag < 0);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
346
|
+
refuseQueueOnlyFlag("--file", scan.fileFlag >= 0 && scan.editFlag < 0);
|
|
347
|
+
refuseQueueOnlyFlag("--at", scan.atFlag >= 0);
|
|
348
|
+
refuseQueueOnlyFlag("--attach", scan.attachFlags.length > 0 && scan.editFlag < 0);
|
|
349
|
+
}
|
|
350
|
+
/** The `--attach <path>` value pair(s): every occurrence claims itself and its value token,
|
|
351
|
+
* with the missing-value refusal, verbatim. */
|
|
352
|
+
function parsePromptAttachments(args, attachFlags) {
|
|
331
353
|
// Each --attach pair claims the flag and its value; a trailing flag with no value names the
|
|
332
354
|
// flag here, like --cancel's missing-value wording, before any queue write.
|
|
333
355
|
const attachPaths = attachFlags.map((i) => args[i + 1]);
|
|
@@ -336,6 +358,14 @@ export function parsePromptArgs(args) {
|
|
|
336
358
|
if (args[i + 1] === undefined)
|
|
337
359
|
fail(`--attach needs a path (e.g. \`--attach shot.png\`)`);
|
|
338
360
|
}
|
|
361
|
+
return { attachPaths, attachClaims };
|
|
362
|
+
}
|
|
363
|
+
/** The `--at <duration>` parse and its 90-day cap, verbatim. */
|
|
364
|
+
function parsePromptDeferral(args, atFlag) {
|
|
365
|
+
// An edit keeps the target prompt's existing deferral exactly as it was (the marker is
|
|
366
|
+
// plumbing the edit carries over), so a --at alongside it has nothing to mean.
|
|
367
|
+
// The pair is claimed like --role's: the flag and its value are never prompt content.
|
|
368
|
+
const atClaim = atFlag >= 0 ? [atFlag, atFlag + 1] : [];
|
|
339
369
|
let atDelayMs = null;
|
|
340
370
|
if (atFlag >= 0) {
|
|
341
371
|
atDelayMs = parseDurationFlag("--at", args[atFlag + 1]);
|
|
@@ -344,75 +374,146 @@ export function parsePromptArgs(args) {
|
|
|
344
374
|
// (about 106751991d from now) made notBeforeMarker's toISOString throw a raw RangeError.
|
|
345
375
|
failOverDurationCap("--at", atDelayMs, PAUSE_FOR_MAX_MS);
|
|
346
376
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
377
|
+
return { atDelayMs, atClaim };
|
|
378
|
+
}
|
|
379
|
+
/** `tumwater prompt`'s flag scan and the checks it owns — everything shared by two or more
|
|
380
|
+
* modes, so a new mode or queue-only flag is added here: the unknown-double-dash gate, the
|
|
381
|
+
* index collection, the once-check, the six mutual exclusions, the --role pair, the
|
|
382
|
+
* --json/--grep list-only refusals, the queue-only-flag refusals, the --attach pairs and the
|
|
383
|
+
* --at parse and cap. Each job lives in one helper — scanPromptFlags, assertPromptModesDisjoint,
|
|
384
|
+
* parsePromptRole, assertPromptJsonListOnly, parsePromptGrep, assertPromptQueueOnlyFlags,
|
|
385
|
+
* parsePromptAttachments and parsePromptDeferral — so a check is found by name. Returns a
|
|
386
|
+
* struct the mode branches read. */
|
|
387
|
+
function parsePromptFlags(args) {
|
|
388
|
+
const scan = scanPromptFlags(args);
|
|
389
|
+
assertPromptModesDisjoint(scan);
|
|
390
|
+
const { role, roleClaim } = parsePromptRole(args, scan.roleFlag);
|
|
391
|
+
assertPromptJsonListOnly(scan);
|
|
392
|
+
const { grepRaw, grepClaim } = parsePromptGrep(args, scan);
|
|
393
|
+
assertPromptQueueOnlyFlags(scan);
|
|
394
|
+
const { attachPaths, attachClaims } = parsePromptAttachments(args, scan.attachFlags);
|
|
395
|
+
const { atDelayMs, atClaim } = parsePromptDeferral(args, scan.atFlag);
|
|
396
|
+
return {
|
|
397
|
+
role,
|
|
398
|
+
jsonFlags: scan.jsonFlags,
|
|
399
|
+
listFlag: scan.listFlag,
|
|
400
|
+
cancelFlag: scan.cancelFlag,
|
|
401
|
+
editFlag: scan.editFlag,
|
|
402
|
+
topFlag: scan.topFlag,
|
|
403
|
+
fileFlag: scan.fileFlag,
|
|
404
|
+
grepRaw,
|
|
405
|
+
grepClaim,
|
|
406
|
+
roleClaim,
|
|
407
|
+
attachPaths,
|
|
408
|
+
attachClaims,
|
|
409
|
+
atDelayMs,
|
|
410
|
+
atClaim,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** `--list`: the queue listing, optionally machine-readable and filtered by --grep. */
|
|
414
|
+
function listPromptArgs(args, f) {
|
|
415
|
+
failStrayArg(args, "with --list there is no prompt text", f.listFlag, ...f.jsonFlags, ...f.roleClaim, ...f.grepClaim);
|
|
416
|
+
return { mode: "list", role: f.role, json: f.jsonFlags.length > 0, grep: f.grepRaw ?? null };
|
|
417
|
+
}
|
|
418
|
+
/** `--cancel <n>`: remove one entry by its 1-based queue position. */
|
|
419
|
+
function cancelPromptArgs(args, f) {
|
|
420
|
+
const n = parsePositionFlag("--cancel", args[f.cancelFlag + 1], "--cancel 2");
|
|
421
|
+
// The position is the only token --cancel may carry; anything else alongside it would be
|
|
422
|
+
// prompt text, and this mode has none.
|
|
423
|
+
failStrayArg(args, "with --cancel there is no prompt text", f.cancelFlag, f.cancelFlag + 1, ...f.roleClaim);
|
|
424
|
+
return { mode: "cancel", role: f.role, position: n };
|
|
425
|
+
}
|
|
426
|
+
/** `--edit <n>`: rewrite one entry's text in place — the replacement is the --file contents,
|
|
427
|
+
* the tokens after the position, or (with neither) an editor open in the caller. */
|
|
428
|
+
function editPromptArgs(args, f) {
|
|
429
|
+
const n = parsePositionFlag("--edit", args[f.editFlag + 1], '--edit 2 "new text"');
|
|
430
|
+
// The --edit pair, the --role pair, and any --attach pairs are the only flags this mode
|
|
431
|
+
// claims. Tokens preceding --edit (other than a claimed flag) would be silently dropped —
|
|
432
|
+
// a mistyped command would edit prompt N with only the trailing fragment — so they are
|
|
433
|
+
// refused here by the same stray-argument wording the sibling modes use; tokens after the
|
|
434
|
+
// position are the replacement text, minus any claimed attach pair (none: edit in $EDITOR).
|
|
435
|
+
const fileClaim = f.fileFlag >= 0 ? [f.fileFlag, f.fileFlag + 1] : [];
|
|
436
|
+
for (let i = 0; i < f.editFlag; i++) {
|
|
437
|
+
if (f.roleClaim.includes(i) || f.attachClaims.includes(i) || fileClaim.includes(i))
|
|
438
|
+
continue;
|
|
439
|
+
fail(`unexpected argument ${JSON.stringify(args[i])} — with --edit the replacement text comes after the position`);
|
|
387
440
|
}
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
failStrayArg(args, "with --
|
|
393
|
-
return { mode: "
|
|
441
|
+
if (f.fileFlag >= 0) {
|
|
442
|
+
// The replacement comes from the file (`-` reads stdin), kept line for line; editPromptAtFile
|
|
443
|
+
// trims it. Typed text beside it would be ambiguous, so it is refused.
|
|
444
|
+
const file = promptFileValue(args[f.fileFlag + 1]);
|
|
445
|
+
failStrayArg(args, "with --file the replacement text comes from the file", f.editFlag, f.editFlag + 1, ...fileClaim, ...f.roleClaim, ...f.attachClaims);
|
|
446
|
+
return { mode: "edit", role: f.role, position: n, text: readPromptFileArg(file), attachPaths: f.attachPaths };
|
|
394
447
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// drained pipe would see an empty prompt (readFileSync(0) fails with EAGAIN on a TTY's
|
|
401
|
-
// stdin; errorMessage names it, and the operator passes a real pipe instead).
|
|
402
|
-
const file = promptFileValue(args[fileFlag + 1]);
|
|
403
|
-
failStrayArg(args, "with --file the prompt comes from the file", fileFlag, fileFlag + 1, ...roleClaim, ...atClaim, ...attachClaims);
|
|
404
|
-
const contents = readPromptFileArg(file);
|
|
405
|
-
return { mode: "enqueue", role, text: contents, atDelayMs, attachPaths: attachPaths };
|
|
448
|
+
const textTokens = [];
|
|
449
|
+
for (let i = f.editFlag + 2; i < args.length; i++) {
|
|
450
|
+
if (f.roleClaim.includes(i) || f.attachClaims.includes(i))
|
|
451
|
+
continue;
|
|
452
|
+
textTokens.push(args[i]);
|
|
406
453
|
}
|
|
454
|
+
const text = textTokens.join(" ").trim();
|
|
455
|
+
// No text and no --file: the caller opens the prompt's current body in the editor.
|
|
456
|
+
return { mode: "edit", role: f.role, position: n, text: text === "" ? null : text, attachPaths: f.attachPaths };
|
|
457
|
+
}
|
|
458
|
+
/** `--top <n>`: move one entry to the head of its role's queue. */
|
|
459
|
+
function topPromptArgs(args, f) {
|
|
460
|
+
const n = parsePositionFlag("--top", args[f.topFlag + 1], "--top 2");
|
|
461
|
+
// The position is the only token --top may carry; anything else alongside it would be
|
|
462
|
+
// prompt text, and this mode has none.
|
|
463
|
+
failStrayArg(args, "with --top there is no prompt text", f.topFlag, f.topFlag + 1, ...f.roleClaim);
|
|
464
|
+
return { mode: "top", role: f.role, position: n };
|
|
465
|
+
}
|
|
466
|
+
/** `--file <path>`: enqueue the file's (or stdin's) whole contents as the prompt. */
|
|
467
|
+
function filePromptArgs(args, f) {
|
|
468
|
+
// The file is the prompt: read it whole (a path of `-` reads stdin, so a pipe or heredoc
|
|
469
|
+
// works), following parseInitArgs' --file branch — the same claim/stray/read/empty shape,
|
|
470
|
+
// so the two commands speak one idiom. The stdin read is memoized at module level: cli.ts
|
|
471
|
+
// pre-parses prompt's args and cmdPrompt re-parses them, and a second readFileSync(0) on a
|
|
472
|
+
// drained pipe would see an empty prompt (readFileSync(0) fails with EAGAIN on a TTY's
|
|
473
|
+
// stdin; errorMessage names it, and the operator passes a real pipe instead).
|
|
474
|
+
const file = promptFileValue(args[f.fileFlag + 1]);
|
|
475
|
+
failStrayArg(args, "with --file the prompt comes from the file", f.fileFlag, f.fileFlag + 1, ...f.roleClaim, ...f.atClaim, ...f.attachClaims);
|
|
476
|
+
const contents = readPromptFileArg(file);
|
|
477
|
+
return { mode: "enqueue", role: f.role, text: contents, atDelayMs: f.atDelayMs, attachPaths: f.attachPaths };
|
|
478
|
+
}
|
|
479
|
+
/** The default mode: free-form text (minus the claimed --role/--at/--attach tokens) is joined
|
|
480
|
+
* and enqueued. */
|
|
481
|
+
function enqueuePromptArgs(args, f) {
|
|
407
482
|
// Everything except the --role pair and the --at pair is prompt text; the join keeps
|
|
408
483
|
// multi-word requests as one string exactly like the pre-1/2 behavior.
|
|
409
484
|
const text = args
|
|
410
|
-
.filter((_, i) => !roleClaim.includes(i) && !atClaim.includes(i) && !attachClaims.includes(i))
|
|
485
|
+
.filter((_, i) => !f.roleClaim.includes(i) && !f.atClaim.includes(i) && !f.attachClaims.includes(i))
|
|
411
486
|
.join(" ")
|
|
412
487
|
.trim();
|
|
413
488
|
// Name the fix, like the sibling operator commands' usage errors (bug/plan carry their
|
|
414
489
|
// BUG_USAGE/PLAN_USAGE lines): "prompt text required" alone never said what to type.
|
|
415
490
|
if (!text)
|
|
416
491
|
fail('prompt text required — usage: tumwater prompt "<text>" (add --role <id> to aim it at one role)');
|
|
417
|
-
return { mode: "enqueue", role, text, atDelayMs, attachPaths: attachPaths };
|
|
492
|
+
return { mode: "enqueue", role: f.role, text, atDelayMs: f.atDelayMs, attachPaths: f.attachPaths };
|
|
493
|
+
}
|
|
494
|
+
/** `tumwater prompt` argument handling, following parseInitArgs' pattern. Like init's,
|
|
495
|
+
* positionals are free-form prompt content — but a double-dash token must be a real flag
|
|
496
|
+
* (`--role <id>`, `--list`, `--json`, `--grep <text>`, `--cancel <n>`, `--edit <n>`,
|
|
497
|
+
* `--top <n>`, `--file <path>`, `--at <duration>`), or
|
|
498
|
+
* it would be baked into the queued prompt (the same class of bug parseInitArgs fixed: today
|
|
499
|
+
* `tumwater prompt --foo text` enqueues "--foo text"). Single-dash positionals remain prompt
|
|
500
|
+
* content. `--list`, `--cancel`, `--edit` and `--top` are mutually exclusive and may
|
|
501
|
+
* not combine with positional text; `--role` is accepted in every mode and is never
|
|
502
|
+
* prompt content. `--at <duration>` (PLANS.md "tumwater prompt --at <duration>") only queues
|
|
503
|
+
* a prompt: it defers delivery — parsed here with parseDurationFlag, riding the enqueue as
|
|
504
|
+
* the queue file's not-before marker — so the read-only and destructive modes refuse it
|
|
505
|
+
* rather than silently ignore it. */
|
|
506
|
+
export function parsePromptArgs(args) {
|
|
507
|
+
const f = parsePromptFlags(args);
|
|
508
|
+
if (f.listFlag >= 0)
|
|
509
|
+
return listPromptArgs(args, f);
|
|
510
|
+
if (f.cancelFlag >= 0)
|
|
511
|
+
return cancelPromptArgs(args, f);
|
|
512
|
+
if (f.editFlag >= 0)
|
|
513
|
+
return editPromptArgs(args, f);
|
|
514
|
+
if (f.topFlag >= 0)
|
|
515
|
+
return topPromptArgs(args, f);
|
|
516
|
+
if (f.fileFlag >= 0)
|
|
517
|
+
return filePromptArgs(args, f);
|
|
518
|
+
return enqueuePromptArgs(args, f);
|
|
418
519
|
}
|
|
@@ -98,6 +98,11 @@ export const ROLE_FLAG = {
|
|
|
98
98
|
valueName: "<id>",
|
|
99
99
|
missingValue: ROLE_VALUE_ERROR,
|
|
100
100
|
};
|
|
101
|
+
/** The `--all` flag spec, taken only by `tumwater abort` (`abort --all` aborts every enabled
|
|
102
|
+
* role in one request). A valueless targeting flag, so its spec carries only its spelling; the
|
|
103
|
+
* abort command body keeps the --role-vs--all rivalry rule. Every other marker command keeps
|
|
104
|
+
* its own flag set, so `pause --all` and the rest still fail as unknown flags. */
|
|
105
|
+
export const ABORT_ALL_FLAG = { names: ["--all"] };
|
|
101
106
|
/** The missing-value error for `logs --transcript` (LOGS_TRANSCRIPT_FLAG's trailing gate). */
|
|
102
107
|
export const LOGS_TRANSCRIPT_VALUE_ERROR = "--transcript needs a role id (e.g. `--transcript feature`)";
|
|
103
108
|
/** The `--transcript <id>` flag spec of `logs`: the role whose agent transcript to print. A
|
|
@@ -252,11 +257,8 @@ export function hasFollowFlag(args) {
|
|
|
252
257
|
/** The `--ready` flag spec, accepted by `backlog`: keep only the entries no hold keeps from
|
|
253
258
|
* being picked. Value-less, like STAT_FLAG. */
|
|
254
259
|
export const READY_FLAG = { names: ["--ready"] };
|
|
255
|
-
/** Force re-probing of every claimed image-input model in `tumwater doctor` (part 2/5),
|
|
256
|
-
* bypassing the 7-day cache: `--reprobe` (a value-less flag like JSON_FLAG). */
|
|
257
|
-
export const REPROBE_FLAG = { names: ["--reprobe"] };
|
|
258
260
|
/** The `--problems` flag spec, accepted by `doctor`: print only the warn and fail rows. Value-less,
|
|
259
|
-
* like
|
|
261
|
+
* like KILL_ORPHANS_FLAG. */
|
|
260
262
|
export const PROBLEMS_FLAG = { names: ["--problems"] };
|
|
261
263
|
/** The `--kill-orphans` flag spec, accepted by `doctor`: first kill the orphaned processes the
|
|
262
264
|
* orphans check lists, each with its descendants. Value-less, like PROBLEMS_FLAG. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DRY_RUN_FLAG, JSON_FLAG, durationFlagSpec, PAUSE_FOR_OVERFLOW_HINT, REASON_FLAG, WAKE_CANCEL_FLAG, WAKE_LIST_FLAG, WAKE_IN_FLAG, rejectUnknownArgs, ROLE_FLAG, } from "./cli-flag-specs.js";
|
|
1
|
+
import { ABORT_ALL_FLAG, DRY_RUN_FLAG, JSON_FLAG, durationFlagSpec, PAUSE_FOR_OVERFLOW_HINT, REASON_FLAG, WAKE_CANCEL_FLAG, WAKE_LIST_FLAG, WAKE_IN_FLAG, rejectUnknownArgs, ROLE_FLAG, } from "./cli-flag-specs.js";
|
|
2
2
|
import { requireReadyRepo } from "./cli-repo-gate.js";
|
|
3
3
|
import { cmdAbort, cmdPause, cmdReclaim, cmdResetCounters, cmdRestart, cmdResume, cmdWake } from "../operator/operator-commands.js";
|
|
4
4
|
/** The CLI command layer of each marker command, keyed by its CLI name (the command bodies
|
|
@@ -31,7 +31,7 @@ export async function runMarkerCommand(root, command, args) {
|
|
|
31
31
|
const perCommandFlags = {
|
|
32
32
|
"reset-counters": [ROLE_FLAG],
|
|
33
33
|
wake: [ROLE_FLAG, WAKE_IN_FLAG, WAKE_CANCEL_FLAG, WAKE_LIST_FLAG, JSON_FLAG],
|
|
34
|
-
abort: [ROLE_FLAG],
|
|
34
|
+
abort: [ROLE_FLAG, ABORT_ALL_FLAG],
|
|
35
35
|
pause: [
|
|
36
36
|
ROLE_FLAG,
|
|
37
37
|
durationFlagSpec("pause --for", PAUSE_FOR_OVERFLOW_HINT),
|