tumwater 0.4.4 → 0.4.6

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.
Files changed (227) hide show
  1. package/README.md +1 -1
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/agent-run.js +1 -0
  4. package/dist/src/agent/opencode/write-guard-plugin/index.js +11 -8
  5. package/dist/src/agent/subscription-account.js +68 -3
  6. package/dist/src/agent/write-guard.js +50 -13
  7. package/dist/src/backlog/backlog-eligibility.js +8 -4
  8. package/dist/src/backlog/backlog-index.js +2 -2
  9. package/dist/src/backlog/backlog-md.js +45 -19
  10. package/dist/src/backlog/backlog-release.js +3 -11
  11. package/dist/src/backlog/backlog-write.js +19 -11
  12. package/dist/src/baseline/main-baseline.js +47 -72
  13. package/dist/src/baseline/main-guardrails.js +11 -7
  14. package/dist/src/baseline/main-red.js +171 -19
  15. package/dist/src/baseline/main-watch.js +71 -24
  16. package/dist/src/budget/budget-fallback-view.js +96 -28
  17. package/dist/src/build/build-check-detect.js +22 -3
  18. package/dist/src/build/build-check-events.js +25 -5
  19. package/dist/src/build/build-check-report.js +40 -4
  20. package/dist/src/build/build-check-rerun.js +62 -0
  21. package/dist/src/build/build-check-scoped.js +13 -2
  22. package/dist/src/build/build-check.js +2 -1
  23. package/dist/src/build/build-info.js +4 -0
  24. package/dist/src/build/dep-install.js +1 -2
  25. package/dist/src/build/host-sleep.js +9 -0
  26. package/dist/src/change/change-render.js +2 -1
  27. package/dist/src/cli/cli-args.js +48 -3
  28. package/dist/src/cli/cli-command-args.js +12 -4
  29. package/dist/src/cli/cli-flag-specs.js +5 -3
  30. package/dist/src/cli/cli-init.js +5 -1
  31. package/dist/src/cli/cli-log-commands.js +2 -2
  32. package/dist/src/cli/cli-query-commands.js +31 -15
  33. package/dist/src/cli/cli-review-commands.js +3 -3
  34. package/dist/src/cli/cli-role-command.js +338 -244
  35. package/dist/src/cli/cli-start.js +10 -2
  36. package/dist/src/cli/completion.js +6 -2
  37. package/dist/src/cli/help.js +6 -3
  38. package/dist/src/cli.js +112 -140
  39. package/dist/src/collections.js +21 -0
  40. package/dist/src/concurrency/check-permit.js +30 -3
  41. package/dist/src/concurrency/lock.js +73 -14
  42. package/dist/src/config/config-editable-keys.js +9 -0
  43. package/dist/src/config/config-fallbacks.js +16 -5
  44. package/dist/src/config/config-schema.js +3 -3
  45. package/dist/src/config/config-validation.js +2 -0
  46. package/dist/src/config/config-views.js +55 -33
  47. package/dist/src/config/config-write.js +76 -16
  48. package/dist/src/config/config.js +12 -5
  49. package/dist/src/doctor/doctor-backlog-bugs.js +33 -1
  50. package/dist/src/doctor/doctor-backlog.js +6 -8
  51. package/dist/src/doctor/doctor-check-names.js +2 -0
  52. package/dist/src/doctor/doctor-checks.js +13 -6
  53. package/dist/src/doctor/doctor-claude-checks.js +6 -13
  54. package/dist/src/doctor/doctor-codex-checks.js +2 -2
  55. package/dist/src/doctor/doctor-contention-checks.js +7 -6
  56. package/dist/src/doctor/doctor-fallback-checks.js +37 -2
  57. package/dist/src/doctor/doctor-holds.js +7 -10
  58. package/dist/src/doctor/doctor-model-checks.js +16 -1
  59. package/dist/src/doctor/doctor-opencode-checks.js +2 -2
  60. package/dist/src/doctor/doctor-orphans.js +103 -27
  61. package/dist/src/doctor/doctor-project-checks.js +43 -26
  62. package/dist/src/doctor/doctor.js +5 -3
  63. package/dist/src/events/event-format.js +113 -136
  64. package/dist/src/events/event-labels.js +3 -1
  65. package/dist/src/events/events.js +3 -3
  66. package/dist/src/failure/failure-data.js +6 -4
  67. package/dist/src/failure/failure-render.js +1 -0
  68. package/dist/src/failure/failure-state-change.js +14 -4
  69. package/dist/src/failure/failure-time-spend.js +1 -0
  70. package/dist/src/files/file-queue.js +32 -19
  71. package/dist/src/files/files.js +2 -2
  72. package/dist/src/files/json-object.js +3 -2
  73. package/dist/src/gates/budget-gates.js +37 -2
  74. package/dist/src/gates/gate-polls.js +8 -7
  75. package/dist/src/gates/{maintenance-quota.js → maintenance-quota-gates.js} +1 -1
  76. package/dist/src/gates/startup-gates.js +18 -1
  77. package/dist/src/git/git.js +8 -0
  78. package/dist/src/git/slot-salvage.js +6 -0
  79. package/dist/src/git/worktree-pool.js +18 -5
  80. package/dist/src/gui/gui-admin-endpoints.js +11 -1
  81. package/dist/src/gui/gui-args.js +2 -2
  82. package/dist/src/gui/gui-backlog-endpoints.js +2 -1
  83. package/dist/src/gui/gui-history-endpoints.js +7 -7
  84. package/dist/src/gui/gui-operator-commands.js +2 -3
  85. package/dist/src/history/history.js +12 -8
  86. package/dist/src/{run → history}/run-detail-data.js +2 -2
  87. package/dist/src/{run → history}/run-detail-render.js +2 -2
  88. package/dist/src/{run → history}/run-detail.js +2 -2
  89. package/dist/src/inbox/director-request.js +92 -0
  90. package/dist/src/inbox/inbox-top.js +2 -1
  91. package/dist/src/merge/merge-backlog-conflicts.js +33 -11
  92. package/dist/src/merge/merge-batch.js +22 -235
  93. package/dist/src/merge/merge-cancel.js +2 -1
  94. package/dist/src/merge/merge-core.js +26 -22
  95. package/dist/src/merge/merge-drain.js +116 -101
  96. package/dist/src/merge/merge-pipeline.js +11 -8
  97. package/dist/src/merge/merge-post-review-drain.js +8 -2
  98. package/dist/src/merge/merge-questions.js +3 -3
  99. package/dist/src/merge/merge-queue-render.js +2 -2
  100. package/dist/src/merge/merge-queue.js +9 -1
  101. package/dist/src/merge/merge-requeue.js +67 -26
  102. package/dist/src/merge/merge-slot.js +41 -10
  103. package/dist/src/merge/merge-to-main.js +20 -165
  104. package/dist/src/merge/merge-top.js +2 -1
  105. package/dist/src/merge/merge-verify.js +18 -5
  106. package/dist/src/merge/merge-withdrawn.js +71 -0
  107. package/dist/src/model-check/model-check-gate.js +206 -0
  108. package/dist/src/model-check/model-check-probe.js +109 -0
  109. package/dist/src/model-check/model-check.js +101 -0
  110. package/dist/src/{run → observer}/ci-parity.js +15 -2
  111. package/dist/src/operator/operator-commands.js +8 -25
  112. package/dist/src/operator/operator-revert.js +1 -1
  113. package/dist/src/orchestrator/orchestrator-launch.js +6 -1
  114. package/dist/src/orchestrator/orchestrator-poll.js +32 -5
  115. package/dist/src/orchestrator/orchestrator-scheduling.js +7 -7
  116. package/dist/src/orchestrator/orchestrator.js +74 -13
  117. package/dist/src/paths.js +41 -3
  118. package/dist/src/pi/pi-run-result.js +16 -0
  119. package/dist/src/pi/pi-watchdogs.js +15 -4
  120. package/dist/src/process/process.js +24 -0
  121. package/dist/src/prompt/gate-notes.js +10 -4
  122. package/dist/src/prompt/gate-prompts.js +19 -15
  123. package/dist/src/prompt/principles.js +13 -3
  124. package/dist/src/prompt/prompt-followup.js +14 -5
  125. package/dist/src/prompt/prompt.js +11 -0
  126. package/dist/src/redeploy/redeploy-probes.js +53 -18
  127. package/dist/src/redeploy/redeploy-target.js +100 -0
  128. package/dist/src/redeploy/redeployer.js +135 -70
  129. package/dist/src/report/report-main-red.js +2 -2
  130. package/dist/src/report/report-render.js +2 -2
  131. package/dist/src/report/report.js +2 -2
  132. package/dist/src/review/known-flakes.js +105 -42
  133. package/dist/src/review/post-merge-review-accept.js +2 -1
  134. package/dist/src/review/post-merge-review.js +46 -34
  135. package/dist/src/review/review-precheck.js +72 -28
  136. package/dist/src/review/review-verdict.js +25 -17
  137. package/dist/src/roles/role-catalog.js +23 -7
  138. package/dist/src/roles/role-render.js +158 -83
  139. package/dist/src/roles/role-view.js +297 -75
  140. package/dist/src/roles/roles.js +1 -1
  141. package/dist/src/run/run-apply.js +3 -8
  142. package/dist/src/run/run-dismiss.js +2 -2
  143. package/dist/src/run/run-finalize.js +4 -0
  144. package/dist/src/run/run-prompt.js +4 -4
  145. package/dist/src/run/run-stage.js +7 -1
  146. package/dist/src/run/run-timing.js +2 -2
  147. package/dist/src/run/run-usage.js +9 -1
  148. package/dist/src/run/run-verdict.js +80 -52
  149. package/dist/src/runner/runner-leftover.js +48 -1
  150. package/dist/src/runner/runner-outcome.js +1 -2
  151. package/dist/src/runner/runner-pi.js +6 -0
  152. package/dist/src/runner/runner-recovery.js +5 -4
  153. package/dist/src/runner/runner-revision.js +2 -1
  154. package/dist/src/runner/runner-run.js +165 -110
  155. package/dist/src/runner/runner-state-load.js +115 -53
  156. package/dist/src/runner/runner-state.js +3 -2
  157. package/dist/src/runner/runner.js +39 -20
  158. package/dist/src/{orchestrator/scheduling-admit.js → scheduling/admit.js} +2 -2
  159. package/dist/src/scheduling/backoff.js +6 -6
  160. package/dist/src/scheduling/claims.js +103 -26
  161. package/dist/src/{orchestrator/scheduling-scope.js → scheduling/scope.js} +24 -60
  162. package/dist/src/{orchestrator/scheduling-skip.js → scheduling/skip.js} +112 -78
  163. package/dist/src/{run → stage}/stage-path-checks.js +3 -3
  164. package/dist/src/status/status-budget.js +38 -4
  165. package/dist/src/status/status-data.js +23 -14
  166. package/dist/src/status/status-polls.js +83 -26
  167. package/dist/src/status/status-role-notes.js +25 -7
  168. package/dist/src/status/status-roles.js +56 -25
  169. package/dist/src/team/team-orchestrator-info.js +51 -0
  170. package/dist/src/team/team-state.js +25 -9
  171. package/dist/src/text/format.js +20 -2
  172. package/dist/src/text/phrases.js +59 -12
  173. package/dist/src/text/suggest.js +14 -1
  174. package/dist/src/text/text.js +31 -2
  175. package/dist/src/ui/badges.js +102 -17
  176. package/dist/src/ui/gui/gui-client-drawer-config.js +12 -6
  177. package/dist/src/ui/gui/gui-client-model.js +5 -0
  178. package/dist/src/ui/gui/gui-client-operator.js +11 -14
  179. package/dist/src/ui/gui/gui-client-pending.js +10 -3
  180. package/dist/src/ui/gui/gui-client-report-chart.js +22 -15
  181. package/dist/src/ui/gui/gui-client-settings.js +25 -11
  182. package/dist/src/ui/gui/gui-client-team.js +7 -0
  183. package/dist/src/ui/status-model.js +21 -6
  184. package/dist/src/ui/status-payload.js +19 -5
  185. package/dist/src/ui/status-render.js +14 -8
  186. package/dist/src/ui/team-alert-shared.js +2 -2
  187. package/dist/src/ui/team-alert-standings.js +96 -0
  188. package/dist/src/ui/team-alert-status.js +39 -12
  189. package/dist/src/ui/team-alerts.js +8 -4
  190. package/dist/src/ui/tone.js +2 -1
  191. package/dist/src/ui/tui/tui-backlog.js +12 -0
  192. package/dist/src/ui/tui/tui-editor-attach.js +2 -6
  193. package/dist/src/ui/tui/tui-editor-brief.js +2 -6
  194. package/dist/src/ui/tui/tui-editor-interval.js +2 -2
  195. package/dist/src/ui/tui/tui-editor-release.js +2 -6
  196. package/dist/src/ui/tui/tui-editor-retire.js +2 -6
  197. package/dist/src/ui/tui/tui-editor-role-enabled.js +2 -6
  198. package/dist/src/ui/tui/tui-editor-role-model.js +5 -1
  199. package/dist/src/ui/tui/tui-editor-session.js +14 -0
  200. package/dist/src/ui/tui/tui-guard.js +16 -0
  201. package/dist/src/ui/tui/tui-input.js +2 -2
  202. package/dist/src/ui/tui/tui-keys.js +1 -1
  203. package/dist/src/ui/tui/tui-pane-backlog.js +2 -2
  204. package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
  205. package/dist/src/ui/tui/tui-pane-health.js +29 -35
  206. package/dist/src/ui/tui/tui-pane-history.js +158 -479
  207. package/dist/src/ui/tui/tui-pane-pending-selection.js +346 -0
  208. package/dist/src/ui/tui/tui-pane-pending.js +54 -344
  209. package/dist/src/ui/tui/tui-pane-queued.js +2 -2
  210. package/dist/src/ui/tui/tui-pane-trail.js +415 -0
  211. package/dist/src/ui/tui/tui-pane-transcript.js +2 -7
  212. package/dist/src/ui/tui/tui-pane.js +3 -3
  213. package/dist/src/ui/tui/tui.js +2 -1
  214. package/dist/src/verdict/fix-claim.js +10 -4
  215. package/dist/src/verdict/reply-contract.js +14 -5
  216. package/package.json +9 -7
  217. package/dist/src/merge/merge-check-failures.js +0 -212
  218. package/dist/src/merge/merge-stack.js +0 -235
  219. /package/dist/src/{run → observer}/feedback-filings.js +0 -0
  220. /package/dist/src/{run → observer}/qa-coverage.js +0 -0
  221. /package/dist/src/{run → observer}/selfcheck-digest.js +0 -0
  222. /package/dist/src/{run → stage}/acceptance-greps.js +0 -0
  223. /package/dist/src/{run → stage}/stage-backlog-checks.js +0 -0
  224. /package/dist/src/{run → stage}/stage-check.js +0 -0
  225. /package/dist/src/{run → stage}/stage-checks-shared.js +0 -0
  226. /package/dist/src/{run → stage}/stage-name-checks.js +0 -0
  227. /package/dist/src/{run → stage}/stage-prose-checks.js +0 -0
@@ -1,7 +1,7 @@
1
1
  import { BUILD_CHECK_TIMEOUT_MS } from "./build-check-detect.js";
2
2
  import { EXEC_MAX_BUFFER, execFileAsync } from "../process/process.js";
3
3
  import { KILL_GRACE_MS, runScriptGroup } from "../process/process-group.js";
4
- import { clipBuildTail, parseFailingTests } from "./build-check-report.js";
4
+ import { clipBuildTail, parseFailingTestFiles, parseFailingTests } from "./build-check-report.js";
5
5
  import { parseTestCounts } from "./build-check-counts.js";
6
6
  import { sampleSleepClock, sleptMsBetween } from "./host-sleep.js";
7
7
  import { npmInstall, syncInstall } from "./dep-install.js";
@@ -177,6 +177,7 @@ async function runDeclaredCheck(wt, check, script, effectiveMs, killGraceMs, sam
177
177
  script,
178
178
  outputTail: clipBuildTail(output),
179
179
  failingTests: parseFailingTests(output),
180
+ failingTestFiles: parseFailingTestFiles(output),
180
181
  run,
181
182
  counts: parseTestCounts(output),
182
183
  };
@@ -54,6 +54,10 @@ export async function isSelfHosted(root, info) {
54
54
  return false;
55
55
  return commitExists(root, info.sha);
56
56
  }
57
+ /** The steps a self-redeploy holds new runs through, in order: main's check of the target head
58
+ * has not answered yet, the staged build of it is compiling, then the built update waits while
59
+ * the runs already in flight finish. */
60
+ export const RESTART_HOLD_STAGES = ["checking", "compiling", "finishing"];
57
61
  /** Compare a build's commit against main's head over the build inputs only: commits touching
58
62
  * just tests or markdown leave the running code unchanged and never count as stale. Null when
59
63
  * `buildSha` is not a commit of this repo (the build came from somewhere else — see
@@ -106,8 +106,7 @@ export async function syncRootInstall(root, role, install = npmInstall) {
106
106
  if (!r)
107
107
  return;
108
108
  // Best-effort: the root install already ran and the ff that prompted it already moved main, so an
109
- // unwritable feed must not reject the merge that awaited this (merge-to-main.ts's tryMerge,
110
- // merge-stack.ts's ffStackToMain).
109
+ // unwritable feed must not reject the merge that awaited this (merge-to-main.ts's tryMerge).
111
110
  logEventBestEffort(root, {
112
111
  source: role,
113
112
  type: "dep_install",
@@ -85,4 +85,13 @@ export function sleptMsBetween(a, b) {
85
85
  }
86
86
  return undefined;
87
87
  }
88
+ /** Is the host starved — its one-minute load average above the core count? The one bar for
89
+ * "the team's own load, not the work": the merge-scope deadline stretch and its on-time-kill
90
+ * note (starvedDeadlineMs / loadedHostPhrase in build-check-events.ts) and the doctor's
91
+ * check-slot contention advice (doctor-contention-checks.ts) all read the host through this,
92
+ * so the surfaces cannot drift on what "starved" means. A core count of zero or below is not
93
+ * starved: there is no denominator for the ratio to exceed. */
94
+ export function hostLoadStarved(load) {
95
+ return load.cpus > 0 && load.load1 > load.cpus;
96
+ }
88
97
  export const sampleHostLoad = () => ({ load1: os.loadavg()[0] ?? 0, cpus: os.cpus().length });
@@ -2,6 +2,7 @@
2
2
  * collector (change-data.ts, beside src/report/report-data.ts and history-data.ts) gathers: one
3
3
  * role's pending work (`--role <id>`) and the team-wide roster (no --role). Pure function of the
4
4
  * collected view — no I/O, no clock reads. */
5
+ import { joinOr } from "../text/format.js";
5
6
  import { plural } from "../text/phrases.js";
6
7
  /** The work-summary phrase both change views open their work lines with — `N commits ahead
7
8
  * of <main>` plus `, K commits behind` when the branch lags main, plus `, M uncommitted
@@ -35,7 +36,7 @@ export function renderTeamChange(view) {
35
36
  const lines = view.roles
36
37
  .filter((r) => r.state !== "no-base" && (r.ahead > 0 || r.dirtyFiles.length > 0))
37
38
  .map((r) => `${r.role}: ${workSummary(r.ahead, r.behind, view.mainBranch, r.dirtyFiles)}`);
38
- return lines.length > 0 ? lines.join("\n") : "no pending changes";
39
+ return joinOr(lines, "\n", "no pending changes");
39
40
  }
40
41
  /** Render the view as the operator-facing text `tumwater diff --role <id>` prints. */
41
42
  export function renderRoleChange(view) {
@@ -4,6 +4,7 @@ import { knownRoleIdsCached } from "../config/config.js";
4
4
  import { allRoleIds, unknownRoleMessage } from "../roles/roles.js";
5
5
  import { gotSuffix, parsePositiveInt, splitCommaList } from "../text/text.js";
6
6
  import { canonicalRoleId } from "../roles/legacy-role-ids.js";
7
+ import { HOUR_MS, DAY_MS } from "../text/datetime.js";
7
8
  /** CLI argument parsing and validation, shared by every command in cli.ts. The execution
8
9
  * layer calls these before running a command, so a bad flag fails fast with an actionable
9
10
  * message instead of the command silently running with default behavior. The plain-decimal
@@ -150,6 +151,12 @@ export function parseModelFlag(args, missingValueError) {
150
151
  export function parseCountFlag(flag, raw, max) {
151
152
  if (raw === undefined)
152
153
  fail(`${flag} needs a value`);
154
+ // A `--`-token in the count's slot is the operator's slipped value (`logs -n --json`, a
155
+ // `--cancel --json`), never a count — counts never start with `-`, so refuse it up front
156
+ // with rejectFlagLookingValue's shared wording instead of reporting the dropped flag as a
157
+ // malformed count ("got \"--json\""). A single-dash token like `-5` is a malformed count
158
+ // and still reads as one. The sibling parsePositionFlag applies the same guard.
159
+ rejectFlagLookingValue(flag, raw);
153
160
  const n = parsePositiveInt(raw);
154
161
  if (n === null)
155
162
  fail(`${flag} needs a positive integer${gotSuffix(raw)}`);
@@ -168,6 +175,10 @@ export function parseCountFlag(flag, raw, max) {
168
175
  export function parsePositionFlag(flag, raw, example) {
169
176
  if (raw === undefined)
170
177
  fail(`${flag} needs a position number (e.g. \`${example}\`)`);
178
+ // The same slipped-`--`-flag guard parseCountFlag applies: a position never starts with
179
+ // `-`, so `--cancel --json` (or any `--`-token) names its own slipped flag rather than
180
+ // reporting "(got \"--json\")" as a malformed position number.
181
+ rejectFlagLookingValue(flag, raw);
171
182
  const n = parsePositiveInt(raw);
172
183
  if (n === null)
173
184
  fail(`${flag} needs a positive integer${gotSuffix(raw)}`);
@@ -240,6 +251,7 @@ export function parseRoleListFlag(args, validIds) {
240
251
  const typed = flagValue(args, "--role");
241
252
  if (typed === null)
242
253
  return null;
254
+ rejectFlagLookingValue("--role", typed);
243
255
  const pieces = splitCommaList(typed ?? "");
244
256
  if (pieces.length === 0)
245
257
  fail(ROLE_VALUE_ERROR);
@@ -260,13 +272,22 @@ export function parseRoleListFlag(args, validIds) {
260
272
  export function parsePortFlag(raw) {
261
273
  if (raw === undefined)
262
274
  fail("--port needs a value");
275
+ // A `--`-token in the slot is the operator's slipped next flag (`gui --port --json`),
276
+ // never a port number — a port never starts with `-` — so refuse it up front with
277
+ // rejectFlagLookingValue's shared wording, the same guard history's --result/--model/--by
278
+ // and the count/position/duration/role flag homes apply, instead of reporting the next
279
+ // flag as a malformed number.
280
+ rejectFlagLookingValue("--port", raw);
263
281
  const n = parsePositiveInt(raw);
264
282
  if (n === null || n > 65535)
265
283
  fail(`--port must be an integer between 1 and 65535${gotSuffix(raw)}`);
266
284
  return n;
267
285
  }
268
- /** Milliseconds per unit, the multiplier parseDurationFlag's regex group is applied to. */
269
- const UNIT_MS = { s: 1_000, m: 60_000, h: 3_600_000, d: 86_400_000 };
286
+ /** Milliseconds per unit, the multiplier parseDurationFlag's regex group is applied to.
287
+ * The hour and day multipliers are HOUR_MS / DAY_MS (text/datetime.ts's shared units, the
288
+ * convention that records why no two sites may spell them differently); second and minute
289
+ * keep their own literals as there is no shared MINUTE_MS/SECOND_MS constant to fold onto. */
290
+ const UNIT_MS = { s: 1_000, m: 60_000, h: HOUR_MS, d: DAY_MS };
270
291
  /** The position of each unit in the descending order a composite duration must follow
271
292
  * (d → h → m → s), so `30m1h` and `1h30m1h` fail instead of parsing to a surprising total. */
272
293
  const UNIT_ORDER = { d: 0, h: 1, m: 2, s: 3 };
@@ -310,6 +331,12 @@ export function parseDurationMs(raw) {
310
331
  export function parseDurationFlag(flag, raw) {
311
332
  if (raw === undefined)
312
333
  fail(`${flag} needs a value`);
334
+ // A `--`-token in the value slot is the operator's slipped value (`start --for --json`),
335
+ // never a duration — durations never start with `-` — so refuse it up front with
336
+ // rejectFlagLookingValue's shared wording, the same guard history's --result/--model/--by,
337
+ // reviews --status and role-command's write flags apply, instead of misreading the next
338
+ // flag as a duration and reporting it as malformed ("got \"--json\"").
339
+ rejectFlagLookingValue(flag, raw);
313
340
  const ms = parseDurationMs(raw);
314
341
  if (ms === null)
315
342
  fail(`${flag} needs a duration like 45s, 90m, 1h30m, or 2d${gotSuffix(raw)}`);
@@ -371,6 +398,14 @@ export function failOverDurationCap(flag, ms, maxMs, hint) {
371
398
  export function failRivalShapes(subject, rival, why) {
372
399
  fail(`${subject} cannot be combined with ${rival} (${why})`);
373
400
  }
401
+ /** The shared why-clause for a trailing-window view's `-f/--follow` rival: follow is an
402
+ * unbounded "from now on" read, while `--since` opens a bounded past window, so the two are
403
+ * mutually exclusive on the same grounds in all three windowed views (logs, history, report).
404
+ * One clause so the three sites cannot drift on what follow means and why it excludes
405
+ * `--since`; the subject keeps the command's own name. */
406
+ export function failFollowSinceRival(command) {
407
+ return failRivalShapes(`${command} --since`, "-f/--follow", 'follow means "from now on"; --since shows a bounded past window');
408
+ }
374
409
  /** Guard a trailing-window command's `--until <window>` against its `--since <window>` pair —
375
410
  * the shared fail sentences the three windowed views (logs, history, report) print, so the
376
411
  * wording cannot drift per command. When `--until` is present but `--since` is not, it fails
@@ -417,6 +452,11 @@ export function parseRoleFlag(args, validIds, flag = "--role", missingValue = RO
417
452
  const typed = flagValue(args, flag);
418
453
  if (typed === null)
419
454
  return null;
455
+ // A `--`-token in the id's slot is the operator's slipped value (`--role --json`), never a
456
+ // role id — role ids are bare words that never start with `-` — so refuse it up front with
457
+ // rejectFlagLookingValue's shared wording, the same guard history's --result/--model/--by
458
+ // and reviews --status apply, instead of reporting the next flag as an "unknown role".
459
+ rejectFlagLookingValue(flag, typed);
420
460
  if (!typed)
421
461
  fail(missingValue);
422
462
  const role = canonicalRoleId(typed);
@@ -427,11 +467,16 @@ export function parseRoleFlag(args, validIds, flag = "--role", missingValue = RO
427
467
  }
428
468
  /** Parse an optional `--branch <name>` flag: the named branch, or null when absent — the same
429
469
  * shape as parseRoleFlag, so every command that scopes to one branch validates identically.
430
- * A missing value fails fast instead of silently falling back to the checked-out branch. */
470
+ * A missing value fails fast instead of silently falling back to the checked-out branch. A
471
+ * `--`-token in the slot is the operator's slipped next flag (`run --branch --json`), never a
472
+ * branch name — a branch name never starts with `-` — so refuse it up front with
473
+ * rejectFlagLookingValue's shared wording, the same guard every other value slot applies,
474
+ * instead of reporting the next flag as a missing name. */
431
475
  export function parseBranchFlag(args) {
432
476
  const branch = flagValue(args, "--branch");
433
477
  if (branch === null)
434
478
  return null;
479
+ rejectFlagLookingValue("--branch", branch);
435
480
  if (!branch || branch.startsWith("-"))
436
481
  fail(BRANCH_VALUE_ERROR);
437
482
  return branch;
@@ -8,7 +8,7 @@
8
8
  * apart from cli/cli-args.ts's generic machinery — which also keeps that module, minus these
9
9
  * two, free of I/O: --file's readFileSync is the one read in the CLI's arg layer. */
10
10
  import fs from "node:fs";
11
- import { failOverDurationCap, parseBranchFlag, parseDurationFlag, parsePositionFlag, rejectRepeatFlag } from "./cli-args.js";
11
+ import { failOverDurationCap, parseBranchFlag, parseDurationFlag, parsePositionFlag, rejectFlagLookingValue, rejectRepeatFlag } from "./cli-args.js";
12
12
  import { PAUSE_FOR_MAX_MS } from "../operator/operator-intent.js";
13
13
  import { JSON_FLAG, ROLE_FLAG, ROLE_VALUE_ERROR, rejectUnknownDoubleDash, } from "./cli-flag-specs.js";
14
14
  import { fail } from "./cli-output.js";
@@ -99,8 +99,11 @@ function readPromptFile(file) {
99
99
  * shared by every `--file`-taking parser — init, prompt's edit and enqueue branches, plan/bug
100
100
  * add and edit (fileAndAnnounce / editAndAnnounce), and the questions command — so the wording
101
101
  * cannot drift between them. The flag token itself is claimed by each parser's stray-argument
102
- * accounting; this helper only owns the value. */
102
+ * accounting; this helper only owns the value. A flag-shaped token (`--file --json`) is
103
+ * refused up front via rejectFlagLookingValue instead of being read as a path that fails deep
104
+ * with a misleading "cannot read prompt file" about a flag nobody intended as a file. */
103
105
  export function promptFileValue(file) {
106
+ rejectFlagLookingValue("--file", file);
104
107
  if (!file)
105
108
  fail("--file needs a path");
106
109
  return file;
@@ -386,13 +389,18 @@ function assertPromptQueueOnlyFlags(scan) {
386
389
  * with the missing-value refusal, verbatim. */
387
390
  function parsePromptAttachments(args, attachFlags) {
388
391
  // Each --attach pair claims the flag and its value; a trailing flag with no value names the
389
- // flag here, like --cancel's missing-value wording, before any queue write.
390
- const attachPaths = attachFlags.map((i) => args[i + 1]);
392
+ // flag here, like --cancel's missing-value wording, before any queue write. A `--`-shaped
393
+ // token in the value slot is the operator's slip (`--attach --role qa`), so it is refused up
394
+ // front with rejectFlagLookingValue's shared wording — the same guard history's
395
+ // --result/--model and role <id>'s write flags apply — instead of failing deep in image
396
+ // reading with a misleading message about a path nobody intended.
391
397
  const attachClaims = attachFlags.flatMap((i) => [i, i + 1]);
392
398
  for (const i of attachFlags) {
393
399
  if (args[i + 1] === undefined)
394
400
  fail(`--attach needs a path (e.g. \`--attach shot.png\`)`);
401
+ rejectFlagLookingValue("--attach", args[i + 1]);
395
402
  }
403
+ const attachPaths = attachFlags.map((i) => args[i + 1]);
396
404
  return { attachPaths, attachClaims };
397
405
  }
398
406
  /** The `--at <duration>` parse and its 90-day cap, verbatim. */
@@ -240,7 +240,8 @@ export const N_FLAG = {
240
240
  * whole spec, and the render-vs-JSON switch stays in each command body beside its render. */
241
241
  export const JSON_FLAG = { names: ["--json"] };
242
242
  /** The `-f/--follow` flag spec, shared by every command with a follow view (logs, history,
243
- * status, queue, report, backlog and diff — each snapshot command's `-f/--follow` twin): one
243
+ * status, queue, report, backlog, diff and quarantine — each snapshot command's `-f/--follow`
244
+ * twin): one
244
245
  * definition of the flag's spelling, beside ROLE_FLAG and JSON_FLAG, so the gate's accepted
245
246
  * vocabulary cannot drift apart. It takes no value, like JSON_FLAG; each body renders its
246
247
  * own live view, and detects the flag through hasFollowFlag, so the spelling lives in exactly
@@ -248,8 +249,9 @@ export const JSON_FLAG = { names: ["--json"] };
248
249
  export const FOLLOW_FLAG = { names: ["-f", "--follow"] };
249
250
  /** Whether an arg array carries the `-f/--follow` flag — the detection every follow-capable
250
251
  * command body spelled out as an `includes("-f") || includes("--follow")` one-liner over
251
- * its own arg array (`args` for status, queue, report and diff; `rest` for logs, history and
252
- * backlog). Now derived from FOLLOW_FLAG's own spelling, so the detection cannot drift from
252
+ * its own arg array (`args` for status, queue, report, diff and quarantine; `rest` for logs,
253
+ * history and backlog). Now derived from FOLLOW_FLAG's own spelling, so the detection cannot
254
+ * drift from
253
255
  * the gate's accepted vocabulary. */
254
256
  export function hasFollowFlag(args) {
255
257
  return FOLLOW_FLAG.names.some((name) => args.includes(name));
@@ -37,7 +37,11 @@ function printInitIntro(result) {
37
37
  if (result.template !== "blank")
38
38
  say(`template: ${result.template}`);
39
39
  if (result.adopted) {
40
- say(`${result.dryRun ? "would adopt" : "adopting"} an existing repo: the project brief goes in TUMWATER.md and README.md is left untouched`);
40
+ // Adoption preserves the project's own README.md; the "existing repo" claim holds only when
41
+ // the directory already was a repo — when init seeds one itself, naming it "existing" would
42
+ // contradict the "initialized a new git repository" line the report prints right after.
43
+ const target = result.repoInitialized ? "the existing README.md" : "an existing repo";
44
+ say(`${result.dryRun ? "would adopt" : "adopting"} ${target}: the project brief goes in TUMWATER.md and README.md is left untouched`);
41
45
  }
42
46
  }
43
47
  /** Print the dry-run preview: repo line, the would-create/would-leave-alone lists, and the
@@ -1,6 +1,6 @@
1
1
  import { fail, say, sayJsonLine } from "./cli-output.js";
2
2
  import { hasFollowFlag, LOGS_TRANSCRIPT_VALUE_ERROR } from "./cli-flag-specs.js";
3
- import { betweenWindow, durationLabel, failRivalShapes, failUntilShorterThanSince, flagValue, parseCountFlag, parseGrepFlag, parseRoleListScope, parseRoleScope, parseSinceFlag, parseTypeFlag } from "./cli-args.js";
3
+ import { betweenWindow, durationLabel, failFollowSinceRival, failRivalShapes, failUntilShorterThanSince, flagValue, parseCountFlag, parseGrepFlag, parseRoleListScope, parseRoleScope, parseSinceFlag, parseTypeFlag } from "./cli-args.js";
4
4
  import { LOGS_SINCE_MAX_MS, readEventsSince, SPARSE_WINDOW_NOTE, } from "../events/event-window.js";
5
5
  import { compareCodeUnits, increment } from "../collections.js";
6
6
  import { eventTypeTally } from "../events/event-tally.js";
@@ -229,7 +229,7 @@ function parseLogsFeed(root, args) {
229
229
  * prose, the `--count` tally, the per-event print and the sparse-window note. */
230
230
  function sayLogsWindow(root, feed, ms, untilMs) {
231
231
  if (feed.follow)
232
- failRivalShapes("logs --since", "-f/--follow", 'follow means "from now on"; --since shows a bounded past window');
232
+ failFollowSinceRival("logs");
233
233
  if (feed.rest.includes("-n"))
234
234
  failRivalShapes("logs --since", "-n", "a count and a window are rival shapes");
235
235
  rejectTranscriptViewRival(feed.rest, "--since", true);
@@ -7,12 +7,13 @@ import { fail, say, sayJson, sayJsonOrRender } from "./cli-output.js";
7
7
  import { cancelQueuedMerge } from "../merge/merge-cancel.js";
8
8
  import { moveQueuedMergeToTop } from "../merge/merge-top.js";
9
9
  import { followView } from "./follow-view.js";
10
- import { flagValue, parseGrepFlag, parseRoleListScope, rejectRepeatFlag, scanPositionArgs } from "./cli-args.js";
10
+ import { flagValue, parseGrepFlag, parseRoleListScope, rejectFlagLookingValue, rejectRepeatFlag, scanPositionArgs } from "./cli-args.js";
11
11
  import { DIFF_PATH_FLAG, FOLLOW_FLAG, hasFollowFlag, JSON_FLAG, READY_FLAG, grepFlagSpec, rejectUnknownArgs, OVERLAPS_FLAG, ROLE_FLAG, STAT_FLAG } from "./cli-flag-specs.js";
12
12
  import { refuseTeamWorkspace, requireReadyRepo } from "./cli-repo-gate.js";
13
13
  import { scopeSnapshotToRoles, snapshot } from "../status/status-data.js";
14
14
  import { statusPayload } from "../ui/status-payload.js";
15
15
  import { renderStatus } from "../ui/status-render.js";
16
+ import { gotSuffix } from "../text/text.js";
16
17
  import { backlogPayload } from "../backlog/backlog.js";
17
18
  import { backlogHolds, backlogInProgress } from "../backlog/backlog-eligibility.js";
18
19
  import { readyBacklog, grepBacklog, renderBacklogMarkdown } from "../backlog/backlog-render.js";
@@ -22,7 +23,7 @@ import { collectChangeOverlaps, scopeChangeOverlaps, scopeChangeOverlapsToPath,
22
23
  import { renderChangeOverlaps, renderTeamChange, renderRoleChange, renderRoleChanges } from "../change/change-render.js";
23
24
  import { readMergeQueue } from "../merge/merge-queue.js";
24
25
  import { renderMergeQueue, scopeMergeQueue } from "../merge/merge-queue-render.js";
25
- import { mergeChanges } from "../merge/merge-slot.js";
26
+ import { mergeHoldsSha } from "../merge/merge-slot.js";
26
27
  import { shortSha } from "../text/format.js";
27
28
  import { roleLabel } from "../roles/role-labels.js";
28
29
  import { orchestratorAlive } from "../team/team-orchestrator-info.js";
@@ -135,14 +136,17 @@ export async function cmdDiff(root, args, seams = {}) {
135
136
  sayJsonOrRender(args, view, () => render());
136
137
  }
137
138
  /** The normalised `--path` value of `diff --role` (undefined when absent): leading `./` pieces
138
- * and trailing `/` characters stripped, failing on an empty, absolute or `..`-bearing result. */
139
+ * and trailing `/` characters stripped, failing on an empty, absolute or `..`-bearing result.
140
+ * A `--` token in the value slot is refused via rejectFlagLookingValue first, so `diff --role
141
+ * feature --path --json` names the flag instead of failing downstream as a path nobody meant. */
139
142
  function diffPathArg(args) {
140
143
  const raw = flagValue(args, "--path");
141
144
  if (raw === null || raw === undefined)
142
145
  return undefined;
146
+ rejectFlagLookingValue("--path", raw);
143
147
  const norm = normalizeDiffPath(raw);
144
148
  if (norm === null)
145
- fail("diff --path takes a repo-relative path such as src/cli.ts (as diff --overlaps prints it)");
149
+ fail(`diff --path takes a repo-relative path such as src/cli.ts (as diff --overlaps prints it)${gotSuffix(raw)}`);
146
150
  return norm;
147
151
  }
148
152
  /** `tumwater queue [-f/--follow]`: the durable merge queue in execution order, as text or as
@@ -216,7 +220,7 @@ async function cmdQueueCancel(root, args) {
216
220
  const view = readMergeQueue(root);
217
221
  const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
218
222
  const target = view.entries[position - 1];
219
- if (inFlight && target && (target.sha === inFlight.sha || mergeChanges(inFlight).some((c) => c.sha === target.sha))) {
223
+ if (inFlight && target && mergeHoldsSha(inFlight, target.sha)) {
220
224
  fail(`queue entry ${position} is being merged right now — wait for it to land, then \`tumwater revert ${shortSha(target.sha)}\``);
221
225
  }
222
226
  const outcome = await cancelQueuedMerge(root, position);
@@ -258,7 +262,7 @@ async function cmdQueueTop(root, args) {
258
262
  const view = readMergeQueue(root);
259
263
  const inFlight = queueInFlight(root, view.entries, orchestratorAlive(root));
260
264
  const target = view.entries[position - 1];
261
- if (inFlight && target && (target.sha === inFlight.sha || mergeChanges(inFlight).some((c) => c.sha === target.sha))) {
265
+ if (inFlight && target && mergeHoldsSha(inFlight, target.sha)) {
262
266
  fail(`queue entry ${position} is being merged right now — it's already heading into the merge, so there's nothing to top`);
263
267
  }
264
268
  const outcome = moveQueuedMergeToTop(root, position);
@@ -338,22 +342,34 @@ export async function cmdBacklog(root, args, seams = {}) {
338
342
  const result = readBacklog();
339
343
  sayJsonOrRender(rest, result.view, () => renderBacklogText(result));
340
344
  }
341
- // `tumwater quarantine [--release <n>] [--json]` — the view of the review gate's flake
342
- // quarantine, read-only except the one destructive `--release <n>` mode that removes the Nth
343
- // entry the list prints at the operator's request. The read path keeps its no-gate
344
- // refuseTeamWorkspace stance: loadQuarantine degrades an absent or malformed file to the empty
345
- // list, so the command prints the empty line in any directory and the read never writes
346
- // quarantine.json (a test otherwise leaves the list through releaseQuarantine). --json serves
347
- // the same positioned rows to scripts.
348
- export async function cmdQuarantine(root, args) {
345
+ // `tumwater quarantine [-f/--follow] [--release <n>] [--json]` — the view of the review
346
+ // gate's flake quarantine, read-only except the one destructive `--release <n>` mode that
347
+ // removes the Nth entry the list prints at the operator's request. `-f/--follow` repaints the
348
+ // list every couple of seconds until Ctrl+C or the process that started it exits, so a
349
+ // backgrounded follow does not outlive its parent (the same rule `queue -f` uses). The read
350
+ // path keeps its no-gate refuseTeamWorkspace stance: loadQuarantine degrades an absent or
351
+ // malformed file to the empty list, so the command prints the empty line in any directory and
352
+ // the read never writes quarantine.json (a test otherwise leaves the list through
353
+ // releaseQuarantine). --json serves the same positioned rows to scripts.
354
+ export async function cmdQuarantine(root, args, seams = {}) {
349
355
  // `--release <n>` is the read-only command's one destructive mode: route it to its own
350
356
  // branch before the read gate, exactly as cmdReviews routes `--accept`.
351
357
  if (args.includes("--release")) {
352
358
  await cmdQuarantineRelease(root, args);
353
359
  return;
354
360
  }
355
- rejectUnknownArgs("quarantine", args, [JSON_FLAG]);
361
+ rejectUnknownArgs("quarantine", args, [JSON_FLAG, FOLLOW_FLAG]);
356
362
  refuseTeamWorkspace(root);
363
+ const follow = hasFollowFlag(args);
364
+ if (follow && args.includes("--json"))
365
+ fail("quarantine --json prints one document; -f/--follow repaints a live view — drop one of them");
366
+ // Each frame re-reads the quarantine file and reads the clock once, so an entry that
367
+ // appears, ages toward the ceiling, or leaves when its bug closes shows up in the next
368
+ // repaint — the same per-frame read `queue -f` uses.
369
+ if (follow) {
370
+ await followView(() => renderQuarantine(quarantineRows(loadQuarantine(root)), Date.now()), seams);
371
+ return;
372
+ }
357
373
  const rows = quarantineRows(loadQuarantine(root));
358
374
  sayJsonOrRender(args, { tests: rows }, () => renderQuarantine(rows, Date.now()));
359
375
  }
@@ -18,8 +18,8 @@ import { POST_REVIEW_STATES, postReviewState, renderPostReviews, scopePostReview
18
18
  import { roleInScope } from "../roles/runner-ids.js";
19
19
  import { roleLabel } from "../roles/role-labels.js";
20
20
  import { shortSha } from "../text/format.js";
21
- import { gotSuffix, splitCommaList } from "../text/text.js";
22
- import { typoSuffix } from "../text/suggest.js";
21
+ import { splitCommaList } from "../text/text.js";
22
+ import { oneOfMessage } from "../text/suggest.js";
23
23
  /** The missing-value wording of `reviews --status` (shared by the gate's statusFlagSpec and
24
24
  * parseStatusFlag's own fail, so the two cannot drift). */
25
25
  const REVIEWS_STATUS_VALUE_ERROR = "reviews --status needs a value";
@@ -36,7 +36,7 @@ function parseReviewStatusList(raw) {
36
36
  const out = [];
37
37
  for (const piece of pieces) {
38
38
  if (!POST_REVIEW_STATES.includes(piece))
39
- fail(`reviews --status must be one of ${spellings.join(", ")}${gotSuffix(piece)}${typoSuffix(piece, spellings)}`);
39
+ fail(`reviews --status ${oneOfMessage(spellings)(piece)}`);
40
40
  if (!out.includes(piece))
41
41
  out.push(piece);
42
42
  }