tumwater 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/README.md +1 -1
  2. package/dist/build-info.json +2 -2
  3. package/dist/src/agent/backend-traits.js +13 -0
  4. package/dist/src/agent/claude/claude-backend.js +2 -2
  5. package/dist/src/agent/claude/claude-stream.js +3 -2
  6. package/dist/src/agent/codex/codex-backend.js +3 -3
  7. package/dist/src/agent/codex/codex-stream.js +3 -1
  8. package/dist/src/agent/opencode/opencode-backend.js +2 -2
  9. package/dist/src/backlog/backlog-archive.js +60 -25
  10. package/dist/src/backlog/backlog-cli-view.js +3 -28
  11. package/dist/src/backlog/backlog-cli.js +9 -64
  12. package/dist/src/backlog/backlog-commit.js +17 -9
  13. package/dist/src/backlog/backlog-eligibility.js +44 -26
  14. package/dist/src/backlog/backlog-parts.js +23 -13
  15. package/dist/src/backlog/backlog-release.js +33 -16
  16. package/dist/src/baseline/main-guardrails.js +0 -73
  17. package/dist/src/baseline/main-red.js +31 -21
  18. package/dist/src/baseline/main-watch.js +82 -34
  19. package/dist/src/brief/brief-edit.js +3 -1
  20. package/dist/src/budget/budget-fallback-view.js +288 -0
  21. package/dist/src/budget/budget-wiring.js +158 -0
  22. package/dist/src/budget/budget.js +2 -1
  23. package/dist/src/build/dep-install.js +64 -1
  24. package/dist/src/cli/cli-args.js +63 -0
  25. package/dist/src/cli/cli-flag-specs.js +33 -0
  26. package/dist/src/cli/cli-query-commands.js +16 -41
  27. package/dist/src/cli/cli-review-commands.js +113 -0
  28. package/dist/src/cli/cli-role-command.js +118 -13
  29. package/dist/src/cli/cli-start-summary.js +10 -3
  30. package/dist/src/cli/cli-start.js +33 -5
  31. package/dist/src/cli/completion.js +21 -1
  32. package/dist/src/cli/config-commands.js +17 -8
  33. package/dist/src/cli/help.js +74 -14
  34. package/dist/src/cli/log-commands.js +26 -13
  35. package/dist/src/cli/question-commands.js +7 -1
  36. package/dist/src/cli.js +71 -6
  37. package/dist/src/collections.js +31 -4
  38. package/dist/src/config/config-editable-keys.js +3 -0
  39. package/dist/src/config/config-fallbacks.js +3 -2
  40. package/dist/src/config/config-validation.js +47 -42
  41. package/dist/src/config/config-views.js +10 -2
  42. package/dist/src/config/config-write.js +33 -1
  43. package/dist/src/config/config.js +7 -2
  44. package/dist/src/doctor/doctor-backlog-bugs.js +85 -42
  45. package/dist/src/doctor/doctor-backlog-plans.js +214 -79
  46. package/dist/src/doctor/doctor-backlog.js +155 -22
  47. package/dist/src/doctor/doctor-check-names.js +5 -1
  48. package/dist/src/doctor/doctor-checks.js +10 -15
  49. package/dist/src/doctor/doctor-claude-checks.js +6 -9
  50. package/dist/src/doctor/doctor-codex-checks.js +6 -9
  51. package/dist/src/doctor/doctor-contention-checks.js +244 -118
  52. package/dist/src/doctor/doctor-fallback-checks.js +130 -0
  53. package/dist/src/doctor/doctor-holds.js +4 -6
  54. package/dist/src/doctor/doctor-model-checks.js +45 -200
  55. package/dist/src/doctor/doctor-notify.js +7 -2
  56. package/dist/src/doctor/doctor-opencode-checks.js +7 -5
  57. package/dist/src/doctor/doctor-pi-checks.js +62 -0
  58. package/dist/src/doctor/doctor-project-checks.js +106 -12
  59. package/dist/src/doctor/doctor.js +10 -5
  60. package/dist/src/doctor/probe.js +14 -1
  61. package/dist/src/events/event-format.js +7 -6
  62. package/dist/src/events/event-labels.js +0 -1
  63. package/dist/src/events/event-read.js +13 -0
  64. package/dist/src/events/event-window.js +20 -2
  65. package/dist/src/failure/failure-data.js +9 -4
  66. package/dist/src/failure/failure-render.js +8 -8
  67. package/dist/src/failure/time-spend.js +0 -64
  68. package/dist/src/files/file-queue.js +17 -0
  69. package/dist/src/gates/budget-gates.js +27 -257
  70. package/dist/src/gates/gate-polls.js +49 -141
  71. package/dist/src/gates/maintenance-quota.js +27 -17
  72. package/dist/src/git/commit-message.js +12 -0
  73. package/dist/src/git/git.js +18 -0
  74. package/dist/src/git/slot-salvage.js +3 -3
  75. package/dist/src/gui/gui-backlog-commands.js +3 -3
  76. package/dist/src/gui/gui-backlog-endpoints.js +5 -4
  77. package/dist/src/gui/gui-command.js +19 -1
  78. package/dist/src/gui/gui-config-commands.js +75 -9
  79. package/dist/src/gui/gui-endpoint-commands.js +2 -2
  80. package/dist/src/gui/gui-operator-commands.js +134 -7
  81. package/dist/src/gui/gui-prompt-commands.js +15 -6
  82. package/dist/src/gui/gui-report-endpoints.js +10 -3
  83. package/dist/src/gui/gui-server.js +16 -2
  84. package/dist/src/history/history-queued.js +8 -0
  85. package/dist/src/history/history.js +2 -5
  86. package/dist/src/inbox/inbox-top.js +4 -9
  87. package/dist/src/merge/merge-core.js +2 -25
  88. package/dist/src/merge/merge-drain.js +22 -135
  89. package/dist/src/merge/merge-pipeline.js +1 -1
  90. package/dist/src/merge/merge-post-review.js +154 -30
  91. package/dist/src/merge/merge-queue-render.js +11 -12
  92. package/dist/src/merge/merge-queue.js +22 -1
  93. package/dist/src/merge/merge-requeue.js +1 -0
  94. package/dist/src/merge/merge-stack.js +1 -7
  95. package/dist/src/merge/merge-to-main.js +1 -9
  96. package/dist/src/merge/merge-top.js +33 -19
  97. package/dist/src/merge/merge-verify.js +64 -9
  98. package/dist/src/notes-cli.js +8 -3
  99. package/dist/src/orchestrator/orchestrator-poll.js +47 -15
  100. package/dist/src/orchestrator/orchestrator-scheduling.js +4 -2
  101. package/dist/src/orchestrator/orchestrator.js +5 -4
  102. package/dist/src/orchestrator/scheduling-scope.js +51 -7
  103. package/dist/src/orchestrator/scheduling-skip.js +15 -5
  104. package/dist/src/paths.js +16 -6
  105. package/dist/src/pi/pi-backend.js +2 -2
  106. package/dist/src/process/run-marker.js +8 -0
  107. package/dist/src/prompt/gate-notes.js +1 -1
  108. package/dist/src/prompt/gate-prompts.js +30 -7
  109. package/dist/src/prompt/notes-backlog.js +13 -10
  110. package/dist/src/prompt/prompt.js +3 -1
  111. package/dist/src/report/report-data.js +49 -69
  112. package/dist/src/report/report-fold.js +11 -12
  113. package/dist/src/report/report-render.js +6 -4
  114. package/dist/src/report/report.js +2 -5
  115. package/dist/src/review/diff-rules.js +53 -0
  116. package/dist/src/review/known-flakes.js +12 -0
  117. package/dist/src/review/post-merge-review-accept.js +51 -0
  118. package/dist/src/review/post-merge-review-render.js +112 -0
  119. package/dist/src/review/post-merge-review.js +157 -24
  120. package/dist/src/review/protected-edits.js +22 -7
  121. package/dist/src/review/quarantine-render.js +65 -0
  122. package/dist/src/review/review.js +33 -49
  123. package/dist/src/roles/role-catalog.js +60 -2
  124. package/dist/src/roles/role-guidance.js +10 -0
  125. package/dist/src/roles/role-labels.js +6 -3
  126. package/dist/src/roles/role-render.js +43 -8
  127. package/dist/src/roles/role-view.js +36 -9
  128. package/dist/src/roles/roles.js +3 -3
  129. package/dist/src/run/ci-parity.js +121 -0
  130. package/dist/src/run/qa-coverage.js +50 -15
  131. package/dist/src/run/run-detail-data.js +9 -13
  132. package/dist/src/run/run-detail-render.js +1 -8
  133. package/dist/src/run/run-finalize.js +1 -0
  134. package/dist/src/run/run-prompt.js +14 -0
  135. package/dist/src/run/run-stage.js +66 -37
  136. package/dist/src/run/run-verdict.js +14 -1
  137. package/dist/src/run/stage-check.js +17 -11
  138. package/dist/src/run/stage-checks-shared.js +65 -0
  139. package/dist/src/run/stage-name-checks.js +204 -0
  140. package/dist/src/run/stage-path-checks.js +16 -515
  141. package/dist/src/run/stage-prose-checks.js +261 -0
  142. package/dist/src/runner/leftover.js +50 -19
  143. package/dist/src/runner/revision.js +12 -23
  144. package/dist/src/runner/runner-outcome.js +4 -2
  145. package/dist/src/runner/runner-recovery.js +18 -3
  146. package/dist/src/runner/runner-run.js +21 -8
  147. package/dist/src/runner/runner-state.js +4 -3
  148. package/dist/src/scheduling/claim-entries.js +49 -0
  149. package/dist/src/status/status-data.js +6 -1
  150. package/dist/src/status/status-polls.js +15 -0
  151. package/dist/src/status/status-roles.js +11 -0
  152. package/dist/src/team/reclaim.js +2 -2
  153. package/dist/src/text/datetime.js +3 -2
  154. package/dist/src/text/phrases.js +6 -3
  155. package/dist/src/text/text.js +11 -0
  156. package/dist/src/ui/badges.js +34 -1
  157. package/dist/src/ui/gui/gui-client-backlog.js +25 -3
  158. package/dist/src/ui/gui/gui-client-boot.js +63 -41
  159. package/dist/src/ui/gui/gui-client-drawer-config.js +56 -14
  160. package/dist/src/ui/gui/gui-client-drawer.js +29 -4
  161. package/dist/src/ui/gui/gui-client-feed.js +1 -1
  162. package/dist/src/ui/gui/gui-client-history.js +74 -72
  163. package/dist/src/ui/gui/gui-client-labels.js +5 -1
  164. package/dist/src/ui/gui/gui-client-operator.js +6 -2
  165. package/dist/src/ui/gui/gui-client-pending.js +188 -11
  166. package/dist/src/ui/gui/gui-client-report-chart.js +36 -8
  167. package/dist/src/ui/gui/gui-client-report.js +20 -5
  168. package/dist/src/ui/gui/gui-client-settings.js +109 -3
  169. package/dist/src/ui/gui/gui-client-team.js +11 -0
  170. package/dist/src/ui/gui/gui-client.js +11 -1
  171. package/dist/src/ui/gui/gui-page.js +1 -0
  172. package/dist/src/ui/gui/gui-styles.js +4 -0
  173. package/dist/src/ui/run-progress-model.js +2 -1
  174. package/dist/src/ui/status-payload.js +237 -9
  175. package/dist/src/ui/status-render.js +12 -4
  176. package/dist/src/ui/team-alerts.js +512 -37
  177. package/dist/src/ui/tone.js +7 -0
  178. package/dist/src/ui/tui/tui-backlog.js +7 -5
  179. package/dist/src/ui/tui/tui-editor-attach.js +1 -7
  180. package/dist/src/ui/tui/tui-editor-backends.js +1 -6
  181. package/dist/src/ui/tui/tui-editor-brief.js +4 -24
  182. package/dist/src/ui/tui/tui-editor-budget.js +1 -8
  183. package/dist/src/ui/tui/tui-editor-check.js +29 -0
  184. package/dist/src/ui/tui/tui-editor-checks.js +28 -0
  185. package/dist/src/ui/tui/tui-editor-concurrency.js +1 -0
  186. package/dist/src/ui/tui/tui-editor-config.js +19 -6
  187. package/dist/src/ui/tui/tui-editor-entry.js +3 -17
  188. package/dist/src/ui/tui/tui-editor-instances.js +9 -0
  189. package/dist/src/ui/tui/tui-editor-instructions.js +7 -26
  190. package/dist/src/ui/tui/tui-editor-interval.js +18 -7
  191. package/dist/src/ui/tui/tui-editor-notes.js +6 -24
  192. package/dist/src/ui/tui/tui-editor-quiet.js +2 -12
  193. package/dist/src/ui/tui/tui-editor-retire.js +1 -7
  194. package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
  195. package/dist/src/ui/tui/tui-editor-role-key.js +1 -6
  196. package/dist/src/ui/tui/tui-editor-session.js +41 -0
  197. package/dist/src/ui/tui/tui-editor-slots.js +1 -0
  198. package/dist/src/ui/tui/tui-editors.js +10 -1
  199. package/dist/src/ui/tui/tui-frame.js +18 -6
  200. package/dist/src/ui/tui/tui-keys.js +9 -0
  201. package/dist/src/ui/tui/tui-pane-activity.js +21 -8
  202. package/dist/src/ui/tui/tui-pane-backlog.js +36 -9
  203. package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
  204. package/dist/src/ui/tui/tui-pane-health.js +113 -3
  205. package/dist/src/ui/tui/tui-pane-history.js +44 -0
  206. package/dist/src/ui/tui/tui-pane-pending.js +285 -56
  207. package/dist/src/ui/tui/tui-pane-queued.js +7 -2
  208. package/dist/src/ui/tui/tui-pane-report.js +9 -3
  209. package/dist/src/ui/tui/tui-pane-session.js +9 -0
  210. package/dist/src/ui/tui/tui-pane.js +16 -10
  211. package/dist/src/ui/tui/tui-panes.js +56 -7
  212. package/dist/src/ui/tui/tui.js +8 -5
  213. package/dist/src/verdict/fix-claim.js +47 -1
  214. package/dist/src/verdict/reply-contract.js +35 -0
  215. package/package.json +1 -1
  216. package/dist/src/baseline/main-culprit.js +0 -94
  217. package/dist/src/baseline/main-revert.js +0 -133
@@ -10,6 +10,7 @@ import { templateIds } from "../init/init-templates.js";
10
10
  import { DOCTOR_CHECK_NAMES } from "../doctor/doctor-check-names.js";
11
11
  import { allRoleIds } from "../roles/roles.js";
12
12
  import { EVENT_TYPE_LABELS } from "../events/event-labels.js";
13
+ import { POST_REVIEW_STATES } from "../review/post-merge-review-render.js";
13
14
  import { HELP, helpStanzas } from "./help.js";
14
15
  export const SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
15
16
  /** The completable words of a stanza's usage part: the text after `tumwater <command>` up to the
@@ -162,6 +163,11 @@ const EVENT_TYPE_CODES = Object.keys(EVENT_TYPE_LABELS).sort();
162
163
  * vocabulary the way `--type` and `--result` complete theirs. One source, so a new level
163
164
  * completes the moment THINKING_LEVELS grows. */
164
165
  const THINKING_LEVEL_WORDS = [...THINKING_LEVELS];
166
+ /** The review states `reviews --status` filters by: the canonical POST_REVIEW_STATES set,
167
+ * spread preserving its insertion order, so `--status` completes the same vocabulary
168
+ * `parseReviewStatusList` accepts. One source, so a new state completes the moment
169
+ * POST_REVIEW_STATES grows. */
170
+ const REVIEW_STATE_WORDS = [...POST_REVIEW_STATES];
165
171
  /** The doctor check names, each in plain double quotes, for the scripts to loop over. The names
166
172
  * hold no `"`, `$`, backtick or `\`, so plain double quotes are exact; a test pins this. */
167
173
  const DOCTOR_CHECK_WORDS = DOCTOR_CHECK_NAMES.map((n) => `"${n}"`).join(" ");
@@ -231,10 +237,18 @@ function bashScript(help) {
231
237
  ` _tumwater_list "${EVENT_TYPE_CODES.join(" ")}" "$cur"`,
232
238
  ` return 0`,
233
239
  ` fi`,
240
+ ` if [ "$prev" = "--status" ]; then`,
241
+ ` _tumwater_list "${REVIEW_STATE_WORDS.join(" ")}" "$cur"`,
242
+ ` return 0`,
243
+ ` fi`,
234
244
  ` if [ "$prev" = "--thinking" ]; then`,
235
245
  ` COMPREPLY=( $(compgen -W "${THINKING_LEVEL_WORDS.join(" ")}" -- "$cur") )`,
236
246
  ` return 0`,
237
247
  ` fi`,
248
+ ` if [ "$prev" = "--backend" ]; then`,
249
+ ` COMPREPLY=( $(compgen -W "${BACKEND_IDS.join(" ")}" -- "$cur") )`,
250
+ ` return 0`,
251
+ ` fi`,
238
252
  ` if [ "$prev" = "--by" ]; then`,
239
253
  ` COMPREPLY=( $(compgen -W "${HISTORY_SUMMARY_GROUPS.join(" ")}" -- "$cur") )`,
240
254
  ` return 0`,
@@ -276,7 +290,8 @@ function bashScript(help) {
276
290
  ].join("\n");
277
291
  }
278
292
  /** The value suffix a fish flag line carries: what completes after the flag. `--role`, `--result`
279
- * and `--type` take comma lists, so they complete per item through `__fish_complete_list`,
293
+ * `--type` and `--status` take comma lists, so they complete per item through
294
+ * `__fish_complete_list`,
280
295
  * skipping the items already listed. */
281
296
  function fishFlagValue(command, flag) {
282
297
  if (flag === "--role")
@@ -291,8 +306,12 @@ function fishFlagValue(command, flag) {
291
306
  return ` -x -a "(__fish_complete_list , __tumwater_results)"`;
292
307
  if (flag === "--type")
293
308
  return ` -x -a "(__fish_complete_list , __tumwater_types)"`;
309
+ if (flag === "--status")
310
+ return ` -x -a "(__fish_complete_list , __tumwater_review_states)"`;
294
311
  if (flag === "--thinking")
295
312
  return ` -x -a "${THINKING_LEVEL_WORDS.join(" ")}"`;
313
+ if (flag === "--backend")
314
+ return ` -x -a "${BACKEND_IDS.join(" ")}"`;
296
315
  if (flag === "--by")
297
316
  return ` -x -a "${HISTORY_SUMMARY_GROUPS.join(" ")}"`;
298
317
  // `role --model` completes the closed tier set; `history --model` completes the log's selectors.
@@ -314,6 +333,7 @@ function fishScript(help) {
314
333
  `function __tumwater_results; printf '%s\\n' ${Object.values(HISTORY_RESULT_FLAG_NAMES).join(" ")} | __tumwater_unlisted; end`,
315
334
  `function __tumwater_models; tumwater completion --models 2>/dev/null | __tumwater_unlisted; end`,
316
335
  `function __tumwater_types; printf '%s\\n' ${EVENT_TYPE_CODES.join(" ")} | __tumwater_unlisted; end`,
336
+ `function __tumwater_review_states; printf '%s\\n' ${REVIEW_STATE_WORDS.join(" ")} | __tumwater_unlisted; end`,
317
337
  `complete -c tumwater -n __fish_use_subcommand -a "${spec.commands.join(" ")}"`,
318
338
  ];
319
339
  for (const command of spec.commands) {
@@ -50,15 +50,24 @@ function screenOperableKey(root, key) {
50
50
  const { config, error } = loadConfigSafe(root);
51
51
  if (config === undefined)
52
52
  fail(error); // validateConfig's message, via the standard fail()
53
- if (parsed.kind === "map" || parsed.kind === "role") {
54
- const id = parsed.kind === "map" ? parsed.role : parsed.id;
53
+ if (parsed.kind === "role") {
55
54
  const ids = knownRoleIds(config);
56
- if (!ids.includes(id))
57
- fail(unknownRoleMessage(id, ids));
58
- if (parsed.kind === "role") {
59
- const fieldError = unknownRoleFieldError(parsed.id, parsed.field);
60
- if (fieldError)
61
- fail(fieldError);
55
+ if (!ids.includes(parsed.id))
56
+ fail(unknownRoleMessage(parsed.id, ids));
57
+ const fieldError = unknownRoleFieldError(parsed.id, parsed.field);
58
+ if (fieldError)
59
+ fail(fieldError);
60
+ }
61
+ else if (parsed.kind === "map") {
62
+ // Only role-keyed maps check the role catalog. `maxConcurrentPerProvider.<provider>` is
63
+ // keyed by provider, not a role id, and `config set` writes it without the role check —
64
+ // so get/unset must agree, or set would succeed while get/unset refused the provider as
65
+ // an "unknown role". A typo'd role id in the other two maps is refused the same way set's
66
+ // validator treats it.
67
+ if (parsed.map !== "maxConcurrentPerProvider") {
68
+ const ids = knownRoleIds(config);
69
+ if (!ids.includes(parsed.role))
70
+ fail(unknownRoleMessage(parsed.role, ids));
62
71
  }
63
72
  }
64
73
  return { parsed, config };
@@ -4,8 +4,9 @@
4
4
  * <command>` from it by splitting the usage block on its ` tumwater <command>` lines, so a
5
5
  * topic can never drift out of sync with the full listing. */
6
6
  import { suggestClosest } from "../text/suggest.js";
7
- /** Machine names HELP quotes verbatim; the scripting series renames them. */
8
- const SUMMARY_JSON_KEYS = "{ loops, total }"; // vocabulary-ok: --json keys until the scripting series
7
+ /** The role-grouped summary's JSON keys, quoted verbatim in `history --summary --json` help
8
+ * (its `--by model` twin is the hardcoded `{ models, total }` beside it). */
9
+ const SUMMARY_JSON_KEYS = "{ roles, total }";
9
10
  export const HELP = `tumwater — an autonomous development team, built on pi
10
11
 
11
12
  Usage:
@@ -84,12 +85,22 @@ Usage:
84
85
  (merge runs and backlog tallies omitted); --until <duration>
85
86
  ends that window the given duration ago (requires --since);
86
87
  not combinable with --days
87
- tumwater doctor [<check>...] [--json] [--reprobe] [--problems] [--kill-orphans]
88
+ tumwater doctor [<check>...] [--json] [--reprobe] [--problems] [--kill-orphans] [--commit-backlog] [--resync-install] [--drop-broken-pins] [--retire-leftovers]
88
89
  Pre-flight check: node, git, repo, config, fallback
89
90
  model, pi, locks, update, orphans, macOS port leak
90
- (read-only unless --kill-orphans, which first kills the
91
- orphaned processes the orphans check lists, each with
92
- its descendants; exit 0/1; --json prints the report object —
91
+ (read-only unless --kill-orphans, --commit-backlog
92
+ or --resync-install or --drop-broken-pins or --retire-leftovers:
93
+ --kill-orphans first kills the orphaned processes the orphans
94
+ check lists, each with its descendants; --commit-backlog first commits each uncommitted tracked
95
+ backlog or brief file (PLANS.md, BUGS.md, QUESTIONS.md, their archives, the
96
+ brief) on the base branch under the primary-worktree lock, so a merge the
97
+ dirty file blocks can proceed; --resync-install re-syncs the root checkout's
98
+ install on demand under the merge lock, so the always-on install alert's
99
+ advice is one command; --drop-broken-pins first drops each refs/tumwater/
100
+ pin whose commit is gone — the manual git update-ref -d the tumwater pins
101
+ row names; --retire-leftovers first retires each disabled role's leftover
102
+ worktree, branch and merge ref, like tumwater retire --role <id> per role,
103
+ never --force; exit 0/1; --json prints the report object —
93
104
  header, the checks array with level, name, and detail,
94
105
  and verdict; probes image-input models — --reprobe
95
106
  re-probes every one, bypassing the 7-day cache; --problems prints only the warn
@@ -112,7 +123,7 @@ Usage:
112
123
  roles still merges the built-in catalog per entry, so
113
124
  disable a role with roles.<id>.enabled false, not by
114
125
  omitting it
115
- tumwater logs [-f/--follow] [-n N] [--since <duration>] [--until <duration>] [--role <id>[,<id>...]] [--grep <text>] [--type <event type>[,<event type>...]] [--count] [--json]
126
+ tumwater logs [-f/--follow] [-n N] [--since <duration>] [--until <duration>] [--role <id>[,<id>...]] [--grep <text>] [--type <event type>[,<event type>...]] [--count] [--notable] [--json]
116
127
  Show (and follow) tumwater events; --since shows the
117
128
  events of the past window (capped at 7d); --until ends that
118
129
  window the given duration ago (requires --since); --grep shows
@@ -123,7 +134,10 @@ Usage:
123
134
  composes with --since); --count
124
135
  prints one \`N \u00d7 <type>\` line per present event type
125
136
  (most-frequent first) over the selected window instead
126
- of the rows; --json
137
+ of the rows; --notable
138
+ keeps only non-routine events, the dashboard feed's Notable
139
+ choice (it composes with --since, --grep, --type, --role,
140
+ -n, -f and --json); --json
127
141
  prints each event as one
128
142
  JSON object per line (NDJSON) — the raw event objects
129
143
  as stored in the log, instead of the rendered text
@@ -238,6 +252,26 @@ Usage:
238
252
  execution order (so it merges next when ready), preserving any
239
253
  notBefore deferral, refusing an entry a live merge is merging and
240
254
  --role/-f, and answering already-first for position 1
255
+ tumwater reviews [--accept <n>] [--role <id>[,<id>...]] [--status <state>[,<state>...]] [--json]
256
+ List the post-merge review queue (\.tumwater/state
257
+ /post-review.json) in FIFO order: one line per merged commit
258
+ waiting for its review — position, role, short sha, summary,
259
+ and either its rejected reasons
260
+ (\`[rejected — <reasons>]\`) or its
261
+ failed-review-run count (\`[N failed review runs]\`); an empty
262
+ queue prints \`post-merge review queue empty\`; --json prints
263
+ the normalized { pending: [...] } list for scripts; --role
264
+ keeps only the listed roles' entries (a base role id taking
265
+ its instances too), each under its position in the whole
266
+ queue; --status keeps only the entries in the listed states
267
+ (waiting, failing, rejected; an entry turned back is rejected
268
+ first), each under its position in the whole queue, and
269
+ composes with --role; --accept <n>
270
+ removes the Nth queued entry (the position \`tumwater reviews\`
271
+ prints) as deliberately accepted unreviewed, logging a warning;
272
+ it refuses an entry whose repair already merged (\`fixed\`);
273
+ no ready-repo gate for the read, a missing or torn file degrades
274
+ to the empty or well-formed list
241
275
  tumwater backlog [--ready] [--grep <text>] [-f/--follow] [--json]
242
276
  Show planned features, open bugs, and open questions (the
243
277
  dashboards' backlog view); --json prints machine-readable
@@ -253,6 +287,14 @@ Usage:
253
287
  -f/--follow repaints the backlog every couple of seconds until
254
288
  Ctrl+C or the process that started it exits, so a backgrounded
255
289
  follow does not outlive its parent; it refuses --json
290
+ tumwater quarantine [--json] List the flake quarantine — the tests the review gate recorded
291
+ as failing then passing on the same tree
292
+ (.tumwater/state/quarantine.json), oldest first, each naming
293
+ its first-seen and last-seen age, flake count, and the BUGS.md
294
+ entry filed for it (or "not filed"); empty prints
295
+ "flake quarantine is empty"; --json prints the same rows as
296
+ { tests: [...] }; it is read-only — a test leaves the list only
297
+ when its filed bug leaves BUGS.md ## Open
256
298
  tumwater archive [--json] Move the finished entries still under PLANS.md ## Done and
257
299
  BUGS.md ## Fixed to the top of PLANS-DONE.md and BUGS-FIXED.md
258
300
  and commit them on the base branch, waiting for a merge in
@@ -268,10 +310,11 @@ Usage:
268
310
  non-blank line is the symptom, the rest is the body kept
269
311
  line for line; wakes the bugfix role
270
312
  tumwater bug --release <n> [--json]
271
- Lift the Refused or Waiting for the next failure note from the
272
- Nth entry in BUGS.md ## Open (as \`tumwater bug --list\`
273
- lists it), commit it on main, and wake the bugfix role; --json
274
- prints {file, title}
313
+ Lift the Refused, Waiting for the next failure, or a
314
+ Needs-review/Needs-replan note held past its threshold
315
+ (one per run) from the Nth entry in BUGS.md ## Open (as
316
+ \`tumwater bug --list\` lists it), commit it on main, and wake
317
+ the bugfix role; --json prints {file, title, lifted}
275
318
  tumwater bug --top <n> [--json]
276
319
  Move the Nth entry (as \`tumwater bug --list\` lists it) to
277
320
  the head of BUGS.md ## Open, so bugfix claims it next; commit
@@ -324,9 +367,11 @@ Usage:
324
367
  the first non-blank line is the title, the rest is the body
325
368
  kept line for line; wakes the feature role
326
369
  tumwater plan --release <n> [--json]
327
- Lift the Refused note from the Nth entry in PLANS.md ## Planned
370
+ Lift the Refused, Waiting for the next failure, or a
371
+ Needs-review/Needs-replan note held past its threshold
372
+ (one per run) from the Nth entry in PLANS.md ## Planned
328
373
  (as \`tumwater plan --list\` lists it), commit it on main,
329
- and wake the feature role; --json prints {file, title}
374
+ and wake the feature role; --json prints {file, title, lifted}
330
375
  tumwater plan --top <n> [--json]
331
376
  Move the Nth entry (as \`tumwater plan --list\` lists it) to
332
377
  the head of PLANS.md ## Planned, so feature claims it next;
@@ -461,6 +506,15 @@ Usage:
461
506
  for the base id
462
507
  tumwater role <id> --clear-thinking
463
508
  Clear that role's thinking level
509
+ tumwater role <id> --backend <id>
510
+ Set that role's backend (pi|claude|opencode|codex — the
511
+ agent that runs the model); a model that cannot pair with
512
+ the backend is refused (e.g. a claude backend with a
513
+ provider/model id, or a pi backend with a bare model id),
514
+ written to roles.<id>.backend for the base id
515
+ tumwater role <id> --clear-backend
516
+ Clear that role's backend (back to the team/tier
517
+ default backend)
464
518
  tumwater role <id> --enable Enable the role, written to roles.<id>.enabled
465
519
  (the base id for an instance)
466
520
  tumwater role <id> --disable Disable it (refused for the director); clean up
@@ -471,6 +525,12 @@ Usage:
471
525
  written to roles.<id>.instances for the base id
472
526
  tumwater role <id> --clear-instances
473
527
  Clear that role's instance count (back to 1)
528
+ tumwater role <id> --review-after-merge
529
+ Review that role's commits after they merge, written
530
+ to review.afterMerge (the base id for an instance)
531
+ tumwater role <id> --clear-review-after-merge
532
+ Review it before merge again, removing it from
533
+ review.afterMerge
474
534
  tumwater prompt <text...> Send a message to the director's inbox
475
535
  tumwater prompt --file <path> Send a message read from a file ("-" reads stdin)
476
536
  tumwater prompt --role <id> <text...>
@@ -1,11 +1,11 @@
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 { durationLabel, failRivalShapes, flagValue, parseCountFlag, parseGrepFlag, parseRoleListScope, parseRoleScope, parseSinceFlag, parseTypeFlag } from "./cli-args.js";
3
+ import { durationLabel, 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";
7
7
  import { eventRole, parseEventLine, readEventsTailWithEnd } from "../events/event-read.js";
8
- import { eventMatchesGrep, formatEvent } from "../events/event-format.js";
8
+ import { eventMatchesGrep, eventResult, formatEvent } from "../events/event-format.js";
9
9
  import { canonicalEventType } from "../events/event-legacy.js";
10
10
  import { EVENT_TYPE_LABELS, eventTypeLabel } from "../events/event-labels.js";
11
11
  import { followFile } from "../files/tail.js";
@@ -15,6 +15,7 @@ import { typoSuffix } from "../text/suggest.js";
15
15
  import { splitCommaList } from "../text/text.js";
16
16
  import { createTranscriptFilter, createTranscriptRenderer } from "../ui/transcript.js";
17
17
  import { readTranscriptTail } from "../ui/transcript-tail.js";
18
+ import { isNotableEvent } from "../ui/tone.js";
18
19
  import { eventsLogPath, piLogPath } from "../paths.js";
19
20
  /** The read-only observing half of the CLI's non-dispatch commands: `tumwater logs` and its
20
21
  * `--transcript` view, split out of cli.ts so the entry point stays a dispatch table. Unlike
@@ -147,9 +148,18 @@ export async function cmdLogs(root, args, seams = {}) {
147
148
  : (e) => roleInScope(eventRole(e), roles);
148
149
  if (roles !== null)
149
150
  rejectTranscriptViewRival(rest, "--role", true);
151
+ // `--notable` culls routine bookkeeping events the way the dashboard Activity feed's Notable
152
+ // choice does (src/ui/tone.ts eventKind): only non-routine rows pass the feed. One harness-side
153
+ // rule (isNotableEvent) shared with the TUI Activity pane, so the CLI feed and the pane cannot
154
+ // disagree about what a notable filter hides. It composes with --since, --grep, --type, --role,
155
+ // -n, -f, --count and --json, and — like --json/--count/--type/--since — is a rival only of the
156
+ // --transcript transcript view.
157
+ const notable = rest.includes("--notable");
158
+ rejectTranscriptViewRival(rest, "--notable", notable);
150
159
  const feedFilter = (e) => (grepLower === null || eventMatchesGrep(e, grepLower)) &&
151
160
  (typeFilter === null || typeFilter(e)) &&
152
- (roleFilter === null || roleFilter(e));
161
+ (roleFilter === null || roleFilter(e)) &&
162
+ (!notable || isNotableEvent(e.type, eventResult(e)));
153
163
  // Empty-match prose: grep/type keep their wording when given; a role-only filter names the roles.
154
164
  const roleEmpty = roles === null ? "" : `no events from ${roles.join(", ")}`;
155
165
  const filtersGrepOrType = grepPattern !== null || type !== null;
@@ -180,10 +190,7 @@ export async function cmdLogs(root, args, seams = {}) {
180
190
  // both flags. (--prompt requires --transcript, so it is excluded with it.)
181
191
  const ms = parseSinceFlag(rest, "logs --since", LOGS_SINCE_MAX_MS);
182
192
  const untilMs = parseSinceFlag(rest, "logs --until", LOGS_SINCE_MAX_MS, "--until");
183
- if (untilMs !== null && ms === null)
184
- fail("logs --until requires --since");
185
- if (untilMs !== null && ms !== null && untilMs >= ms)
186
- fail(`logs --until (${durationLabel(untilMs)}) must be shorter than --since (${durationLabel(ms)})`);
193
+ failUntilShorterThanSince("logs", untilMs, ms);
187
194
  if (ms !== null) {
188
195
  if (follow)
189
196
  failRivalShapes("logs --since", "-f/--follow", 'follow means "from now on"; --since shows a bounded past window');
@@ -204,7 +211,9 @@ export async function cmdLogs(root, args, seams = {}) {
204
211
  // In JSON mode an empty result answers silently — an empty output IS the machine-readable
205
212
  // answer, and prose would corrupt a consumer's NDJSON stream.
206
213
  if (!json) {
207
- if (grepPattern !== null && type !== null)
214
+ if (notable && grepPattern === null && type === null && roles === null)
215
+ say(`no notable events ${windowPhrase}`);
216
+ else if (grepPattern !== null && type !== null)
208
217
  say(`no events matching "${grepPattern}" or ${typeEmpty(type, typePieces)}`);
209
218
  else if (type !== null)
210
219
  say(typeEmpty(type, typePieces));
@@ -251,7 +260,7 @@ export async function cmdLogs(root, args, seams = {}) {
251
260
  // the tail-with-end shape (not the per-poll cached readEvents) so the follow below can seed
252
261
  // from the byte end this very read covered instead of a later stat of the file.
253
262
  const tail = readEventsTailWithEnd(root, limit);
254
- const shown = grepLower !== null || type !== null || roleFilter !== null ? tail.events.filter(feedFilter) : tail.events;
263
+ const shown = grepLower !== null || type !== null || roleFilter !== null || notable ? tail.events.filter(feedFilter) : tail.events;
255
264
  // `--count` over the -n view answers with the tally of exactly the filtered rows the plain
256
265
  // view would print, and carries over the plain view's empty-match prose below (a `--type`
257
266
  // or `--grep` that matches nothing names its value; an empty unfiltered -n view prints
@@ -259,8 +268,10 @@ export async function cmdLogs(root, args, seams = {}) {
259
268
  if (count) {
260
269
  if (shown.length !== 0)
261
270
  sayTally(shown, json);
262
- else if (!json && (filtersGrepOrType || roles !== null)) {
263
- if (grepPattern !== null && type !== null)
271
+ else if (!json && (filtersGrepOrType || roles !== null || notable)) {
272
+ if (notable && grepPattern === null && type === null && roles === null)
273
+ say("no notable events");
274
+ else if (grepPattern !== null && type !== null)
264
275
  say(`no events matching "${grepPattern}" or ${typeEmpty(type, typePieces)}`);
265
276
  // Type-empty gets the hint (only the type branch); a grep-only empty stays the bare
266
277
  // no-match line, because a free-text pattern is not an event type to suggest against.
@@ -277,8 +288,10 @@ export async function cmdLogs(root, args, seams = {}) {
277
288
  // The empty-match line is prose: in JSON mode the empty output is the answer. It fires
278
289
  // for either filter, naming the flag's value(s) whose match came up empty so the operator
279
290
  // knows what to loosen.
280
- if (shown.length === 0 && !json && (filtersGrepOrType || roles !== null)) {
281
- if (grepPattern !== null && type !== null)
291
+ if (shown.length === 0 && !json && (filtersGrepOrType || roles !== null || notable)) {
292
+ if (notable && grepPattern === null && type === null && roles === null)
293
+ say("no notable events");
294
+ else if (grepPattern !== null && type !== null)
282
295
  say(`no events matching "${grepPattern}" or ${typeEmpty(type, typePieces)}`);
283
296
  // Type-empty gets the hint (only the type branch); a grep-only empty stays the bare
284
297
  // no-match line, because a free-text pattern is not an event type to suggest against.
@@ -148,7 +148,13 @@ export async function cmdQuestions(root, args) {
148
148
  }
149
149
  if (arg.startsWith("--") && arg !== "--json") {
150
150
  rejectEqualsForm(arg, [JSON_FLAG, FILE_SPEC, { names: ["--show"], value: true, valueName: "<n>" }, { names: ["--grep"], value: true, valueName: "<text>" }]);
151
- fail(`unknown argument: ${arg} (valid flags for tumwater questions: --json, --file, --answered, --show, --grep)`);
151
+ // The complete flag set (this gate only sees `--`-flags, but `-n` is a real flag the
152
+ // list should not omit) plus the same did-you-mean hint the sibling commands'
153
+ // unknown-argument gate (cli-flag-specs's failUnknownArgument) attaches, so a mistyped
154
+ // `--ansered` names `--answered` instead of only printing the valid list — the missing
155
+ // treatment the questions gate was the one unknown-argument caller without.
156
+ const spellings = ["--json", "--file", "--answered", "--show", "--grep", "-n"];
157
+ fail(`unknown argument: ${arg} (valid flags for tumwater questions: ${spellings.join(", ")})${typoSuffix(arg, spellings)}`);
152
158
  }
153
159
  if (!numbered) {
154
160
  // The position is a plain-decimal positive integer (parsePositiveInt's rule, the same
package/dist/src/cli.js CHANGED
@@ -13,10 +13,11 @@
13
13
  * ready-repo gate. An error escaping main() funnels through its catch to fail(errorMessage(err)),
14
14
  * the CLI's one error path. */
15
15
  import { parseCountFlag, parseGrepFlag, parseModelFlag, parseResultFlag } from "./cli/cli-args.js";
16
- import { grepFlagSpec, GUI_FLAG_SPECS, JSON_FLAG, LAST_FLAG, N_FLAG, rejectUnknownArgs, resultFlagSpec, modelFlagSpec, byFlagSpec, LOGS_TRANSCRIPT_FLAG, ROLE_FLAG, START_FLAG_SPECS, sinceFlagSpec, untilFlagSpec, STAT_FLAG, PATCH_FLAG, PROMPT_FLAG, TRANSCRIPT_FLAG, SUMMARY_FLAG, COUNT_FLAG, typeFlagSpec, DRY_RUN_FLAG, FORCE_FLAG, FOLLOW_FLAG, STOP_CANCEL_FLAG, STOP_IN_FLAG, REPROBE_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, } from "./cli/cli-flag-specs.js";
16
+ import { grepFlagSpec, GUI_FLAG_SPECS, JSON_FLAG, LAST_FLAG, N_FLAG, rejectUnknownArgs, resultFlagSpec, modelFlagSpec, byFlagSpec, LOGS_TRANSCRIPT_FLAG, ROLE_FLAG, START_FLAG_SPECS, sinceFlagSpec, untilFlagSpec, STAT_FLAG, PATCH_FLAG, PROMPT_FLAG, TRANSCRIPT_FLAG, SUMMARY_FLAG, COUNT_FLAG, NOTABLE_FLAG, typeFlagSpec, DRY_RUN_FLAG, FORCE_FLAG, FOLLOW_FLAG, STOP_CANCEL_FLAG, STOP_IN_FLAG, REPROBE_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, COMMIT_BACKLOG_FLAG, RESYNC_INSTALL_FLAG, DROP_BROKEN_PINS_FLAG, RETIRE_LEFTOVERS_FLAG, } from "./cli/cli-flag-specs.js";
17
17
  import { fail, say, sayJsonOrRender } from "./cli/cli-output.js";
18
18
  import { historyTrailForm, parseDoctorChecks, parsePromptArgs, peelPositionals } from "./cli/cli-command-args.js";
19
19
  import { refuseTeamWorkspace, requireReadyRepo } from "./cli/cli-repo-gate.js";
20
+ import { loadConfigSafe } from "./config/config.js";
20
21
  import { repoToplevel } from "./git/git.js";
21
22
  import { didYouMean } from "./text/suggest.js";
22
23
  import { errorMessage, gotSuffix, stripTerminalControls } from "./text/text.js";
@@ -171,7 +172,7 @@ async function main() {
171
172
  // No requireReadyRepo gate: doctor's job is to report WHY the environment isn't ready,
172
173
  // so it must run outside a git repo and print fail lines rather than throwing.
173
174
  const { positionals: checkNames, rest: doctorFlags } = peelPositionals(args);
174
- rejectUnknownArgs("doctor", doctorFlags, [JSON_FLAG, REPROBE_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG]);
175
+ rejectUnknownArgs("doctor", doctorFlags, [JSON_FLAG, REPROBE_FLAG, PROBLEMS_FLAG, KILL_ORPHANS_FLAG, COMMIT_BACKLOG_FLAG, RESYNC_INSTALL_FLAG, DROP_BROKEN_PINS_FLAG, RETIRE_LEFTOVERS_FLAG]);
175
176
  const only = parseDoctorChecks(checkNames);
176
177
  const { runDoctor } = await import("./doctor/doctor.js");
177
178
  let orphanKill;
@@ -185,6 +186,59 @@ async function main() {
185
186
  say("");
186
187
  }
187
188
  }
189
+ let backlogCommit;
190
+ if (args.includes("--commit-backlog")) {
191
+ const { commitUncommittedBacklog, backlogCommitLines } = await import("./doctor/doctor-backlog.js");
192
+ const { config } = loadConfigSafe(root); // the report's own config, so base resolution and the off-base skip match the `uncommitted backlog` row's
193
+ backlogCommit = await commitUncommittedBacklog(root, config);
194
+ // The text form prints the commits before the report; --json keeps stdout one document.
195
+ if (!args.includes("--json")) {
196
+ for (const line of backlogCommitLines(backlogCommit))
197
+ say(line);
198
+ say("");
199
+ }
200
+ }
201
+ let resyncInstall;
202
+ if (args.includes("--resync-install")) {
203
+ const { resyncRootInstall, rootInstallResyncLines } = await import("./build/dep-install.js");
204
+ resyncInstall = await resyncRootInstall(root);
205
+ // The text form prints the re-sync outcome before the report; --json keeps stdout one
206
+ // document, carrying the result as `resyncInstall` on the payload.
207
+ if (!args.includes("--json")) {
208
+ for (const line of rootInstallResyncLines(resyncInstall))
209
+ say(line);
210
+ say("");
211
+ }
212
+ }
213
+ let brokenPinDrop;
214
+ if (args.includes("--drop-broken-pins")) {
215
+ const { dropBrokenPins, brokenPinDropLines } = await import("./doctor/doctor-contention-checks.js");
216
+ brokenPinDrop = await dropBrokenPins(root);
217
+ // The text form prints the drop before the report; --json keeps stdout one document,
218
+ // carrying the result as `brokenPinDrop` on the payload.
219
+ if (!args.includes("--json")) {
220
+ const lines = brokenPinDropLines(brokenPinDrop);
221
+ for (const line of lines)
222
+ say(line);
223
+ if (lines.length > 0)
224
+ say("");
225
+ }
226
+ }
227
+ let leftoverRetire;
228
+ if (args.includes("--retire-leftovers")) {
229
+ const { retireLeftovers, leftoverRetireLines } = await import("./doctor/doctor-contention-checks.js");
230
+ const { config } = loadConfigSafe(root); // the row's own config, so the reap and the report agree on what is retirable
231
+ leftoverRetire = await retireLeftovers(root, config);
232
+ // The text form prints the retire lines before the report; --json keeps stdout one
233
+ // document, carrying the result as `leftoverRetire` on the payload.
234
+ if (!args.includes("--json")) {
235
+ const lines = leftoverRetireLines(leftoverRetire);
236
+ for (const line of lines)
237
+ say(line);
238
+ if (lines.length > 0)
239
+ say("");
240
+ }
241
+ }
188
242
  const { renderDoctor, renderDoctorProblems, problemsOnly } = await import("./doctor/doctor-render.js");
189
243
  // The terminal probes unclaimed-verified models by default (7-day cache); --reprobe
190
244
  // forces every claimed model. The TUI/GUI call runDoctor with the default "never", so a
@@ -198,10 +252,14 @@ async function main() {
198
252
  // hands --json the filtered one; the exit code below reads the full report either way.
199
253
  // --kill-orphans adds the kill result to the JSON object as `orphanKill`.
200
254
  const withKill = (payload) => (orphanKill === undefined ? payload : { ...payload, orphanKill });
255
+ const withBacklogCommit = (payload) => backlogCommit === undefined ? withKill(payload) : withKill({ ...payload, backlogCommit });
256
+ const withResyncInstall = (payload) => resyncInstall === undefined ? withBacklogCommit(payload) : withBacklogCommit({ ...payload, resyncInstall });
257
+ const withBrokenPinDrop = (payload) => brokenPinDrop === undefined ? withResyncInstall(payload) : withResyncInstall({ ...payload, brokenPinDrop });
258
+ const withLeftoverRetire = (payload) => leftoverRetire === undefined ? withBrokenPinDrop(payload) : withBrokenPinDrop({ ...payload, leftoverRetire });
201
259
  if (args.includes("--problems"))
202
- sayJsonOrRender(args, args.includes("--json") ? withKill(problemsOnly(report)) : report, renderDoctorProblems);
260
+ sayJsonOrRender(args, args.includes("--json") ? withLeftoverRetire(problemsOnly(report)) : report, renderDoctorProblems);
203
261
  else
204
- sayJsonOrRender(args, args.includes("--json") ? withKill(report) : report, renderDoctor);
262
+ sayJsonOrRender(args, args.includes("--json") ? withLeftoverRetire(report) : report, renderDoctor);
205
263
  if (report.checks.some((c) => c.level === "fail"))
206
264
  process.exitCode = 1; // Warnings never fail the exit.
207
265
  break;
@@ -217,6 +275,7 @@ async function main() {
217
275
  typeFlagSpec(TYPE_VALUE_ERROR),
218
276
  JSON_FLAG,
219
277
  COUNT_FLAG,
278
+ NOTABLE_FLAG,
220
279
  ROLE_FLAG,
221
280
  LOGS_TRANSCRIPT_FLAG,
222
281
  PROMPT_FLAG,
@@ -262,9 +321,15 @@ async function main() {
262
321
  case "queue":
263
322
  await (await import("./cli/cli-query-commands.js")).cmdQueue(root, args);
264
323
  break;
324
+ case "reviews":
325
+ await (await import("./cli/cli-review-commands.js")).cmdReviews(root, args);
326
+ break;
265
327
  case "backlog":
266
328
  await (await import("./cli/cli-query-commands.js")).cmdBacklog(root, args);
267
329
  break;
330
+ case "quarantine":
331
+ await (await import("./cli/cli-query-commands.js")).cmdQuarantine(root, args);
332
+ break;
268
333
  case "archive":
269
334
  rejectUnknownArgs("archive", args, [JSON_FLAG]);
270
335
  await requireReadyRepo(root);
@@ -463,7 +528,7 @@ async function main() {
463
528
  const topic = helpTopic(name);
464
529
  if (topic === null) {
465
530
  const suggestion = suggestCommand(name);
466
- fail(`no help topic: ${name}${didYouMean(suggestion)} (try \`tumwater help\` for the full command list)`);
531
+ fail(`no help topic: ${JSON.stringify(name)}${didYouMean(suggestion)} (try \`tumwater help\` for the full command list)`);
467
532
  }
468
533
  say(topic);
469
534
  }
@@ -474,7 +539,7 @@ async function main() {
474
539
  }
475
540
  default: {
476
541
  const suggestion = suggestCommand(command);
477
- fail(`unknown command: ${command}${didYouMean(suggestion)} (try \`tumwater help\`)`);
542
+ fail(`unknown command: ${JSON.stringify(command)}${didYouMean(suggestion)} (try \`tumwater help\`)`);
478
543
  }
479
544
  }
480
545
  }
@@ -31,14 +31,32 @@ export function getOrCreate(map, key, make) {
31
31
  map.set(key, created);
32
32
  return created;
33
33
  }
34
+ /** Filter a list by a keep predicate, returning the kept items and each kept item's 1-based
35
+ * position in the original list — the single home of the "keep some, but number by the whole
36
+ * list" step, shared by the two queue-scope twins `scopeMergeQueue` (src/merge/
37
+ * merge-queue-render.ts) and `scopePostReviews` (src/review/post-merge-review-render.ts), so
38
+ * `tumwater queue --role` and `tumwater reviews --role` keep the same whole-queue positions
39
+ * a later position-addressed verb (`--cancel`, `--accept`) still addresses. The predicate runs
40
+ * once per item, in input order; positions run 1..length, not a kept-only renumbering. */
41
+ export function filterKeepPositions(items, keep) {
42
+ const positions = [];
43
+ const kept = items.filter((item, i) => {
44
+ const isKept = keep(item);
45
+ if (isKept)
46
+ positions.push(i + 1);
47
+ return isKept;
48
+ });
49
+ return { kept, positions };
50
+ }
34
51
  /** Increment a Map counter, seeding a missing key at 1 — the single home of the
35
52
  * `map.set(k, (map.get(k) ?? 0) + 1)` step, shared by the failure-spread kind tally
36
53
  * (src/team/failure-spread.ts), the diff multiset's line tally (merge/merge-diff.ts), the
37
- * section-title tally (backlog/backlog-structure.ts), the once-summary outcome tally
38
- * (cli/cli-start.ts), the logs' per-type tally (cli/log-commands.ts), the flaky-check tally
54
+ * section-title tally (backlog/backlog-structure.ts), the logs' per-type tally
55
+ * (cli/log-commands.ts), the flaky-check tally
39
56
  * (doctor/doctor-project-checks.ts), and the multi-runner base tally
40
- * (orchestrator/orchestrator-scheduling.ts), so those sites cannot drift on how a missing key is
41
- * seeded. */
57
+ * (orchestrator/orchestrator-scheduling.ts) — the exit-summary round's outcome tally
58
+ * (cli/cli-start-summary.ts) seeds its own copy of this step inline — so those sites cannot
59
+ * drift on how a missing key is seeded. */
42
60
  export function increment(map, key) {
43
61
  map.set(key, (map.get(key) ?? 0) + 1);
44
62
  }
@@ -118,6 +136,15 @@ export function lowerMedian(values) {
118
136
  const sorted = [...values].sort((a, b) => a - b);
119
137
  return sorted[Math.floor((sorted.length - 1) / 2)];
120
138
  }
139
+ /** Most-days-waited-first comparator, ties in ascending index (file order) — `b.days - a.days ||
140
+ * a.index - b.index`, the single home of that one-line ordering. Shared by the stale-bug
141
+ * alert (src/ui/status-payload.ts's staleOpenBugs) and the stale-waiting-bug doctor row
142
+ * (src/doctor/doctor-backlog-bugs.ts's staleWaitingBugRows), which both build `{index, days}`
143
+ * rows off the same backlog scan, so the two cannot drift on how ties order. The generic holds
144
+ * any row type carrying both fields; a row without a natural index passes its first-seen order. */
145
+ export function byDaysThenIndex(a, b) {
146
+ return b.days - a.days || a.index - b.index;
147
+ }
121
148
  /** Ascending string comparator by UTF-16 code unit — the locale-independent order a bare `.sort()`
122
149
  * uses, unlike `localeCompare` (which `rankByCount` uses for its tiebreak). Used directly by
123
150
  * src/cli/cli-start-summary.ts, src/cli/log-commands.ts and src/history/history-data.ts, and by
@@ -35,4 +35,7 @@ export const EDITABLE_CONFIG_KEYS = [
35
35
  // The per-backend section (plans/agent-backends.md, part 4a/7): the whole `agents` object is
36
36
  // edited here as JSON. The CLI's dotted `backends.<id>.bin`/`.args` merge one field at a time.
37
37
  "backends",
38
+ // The declared check (plans/portability.md §6/7, the no-check row): its Settings row is the
39
+ // surfaced remedy for the amber `no-check` alert — set `check.command` and the alert clears.
40
+ "check",
38
41
  ];
@@ -19,8 +19,9 @@ import { FALLBACK_REVIEW_TIMEOUT_S, REVIEW_TIMEOUT_S, configForRole, isTierName,
19
19
  * claude + pi fleets, part 2/4"): the backend is the step's own, else its tier primary's
20
20
  * resolved backend (the tier entry's, else the top level's, else pi). A pair routed to a
21
21
  * non-pi backend carries that backend as `backend` and its name as `provider` — the key the
22
- * budget gate's handback (gate-polls.ts's runOnPair against the run's seamHoldKey capture,
23
- * runner-run.ts's setRunPair) matches on — and the id passes to it as written (routeModel); a
22
+ * budget gate's handback (src/budget/budget-wiring.ts's runOnPair against the run's
23
+ * seamHoldKey capture, runner-run.ts's setRunPair) matches on — and the id passes to it as
24
+ * written (routeModel); a
24
25
  * pi pair keeps its own provider (a bare pi selector keeps undefined). */
25
26
  function seamKeyedPairFields(config, tier, provider, model, thinking, stepBackend) {
26
27
  const backend = stepBackend ?? topTierBackend(config, tier) ?? config.backend ?? "pi";