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.
- package/README.md +1 -1
- package/dist/build-info.json +2 -2
- package/dist/src/agent/backend-traits.js +13 -0
- package/dist/src/agent/claude/claude-backend.js +2 -2
- package/dist/src/agent/claude/claude-stream.js +3 -2
- package/dist/src/agent/codex/codex-backend.js +3 -3
- package/dist/src/agent/codex/codex-stream.js +3 -1
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/backlog/backlog-archive.js +60 -25
- package/dist/src/backlog/backlog-cli-view.js +3 -28
- package/dist/src/backlog/backlog-cli.js +9 -64
- package/dist/src/backlog/backlog-commit.js +17 -9
- package/dist/src/backlog/backlog-eligibility.js +44 -26
- package/dist/src/backlog/backlog-parts.js +23 -13
- package/dist/src/backlog/backlog-release.js +33 -16
- package/dist/src/baseline/main-guardrails.js +0 -73
- package/dist/src/baseline/main-red.js +31 -21
- package/dist/src/baseline/main-watch.js +82 -34
- package/dist/src/brief/brief-edit.js +3 -1
- package/dist/src/budget/budget-fallback-view.js +288 -0
- package/dist/src/budget/budget-wiring.js +158 -0
- package/dist/src/budget/budget.js +2 -1
- package/dist/src/build/dep-install.js +64 -1
- package/dist/src/cli/cli-args.js +63 -0
- package/dist/src/cli/cli-flag-specs.js +33 -0
- package/dist/src/cli/cli-query-commands.js +16 -41
- package/dist/src/cli/cli-review-commands.js +113 -0
- package/dist/src/cli/cli-role-command.js +118 -13
- package/dist/src/cli/cli-start-summary.js +10 -3
- package/dist/src/cli/cli-start.js +33 -5
- package/dist/src/cli/completion.js +21 -1
- package/dist/src/cli/config-commands.js +17 -8
- package/dist/src/cli/help.js +74 -14
- package/dist/src/cli/log-commands.js +26 -13
- package/dist/src/cli/question-commands.js +7 -1
- package/dist/src/cli.js +71 -6
- package/dist/src/collections.js +31 -4
- package/dist/src/config/config-editable-keys.js +3 -0
- package/dist/src/config/config-fallbacks.js +3 -2
- package/dist/src/config/config-validation.js +47 -42
- package/dist/src/config/config-views.js +10 -2
- package/dist/src/config/config-write.js +33 -1
- package/dist/src/config/config.js +7 -2
- package/dist/src/doctor/doctor-backlog-bugs.js +85 -42
- package/dist/src/doctor/doctor-backlog-plans.js +214 -79
- package/dist/src/doctor/doctor-backlog.js +155 -22
- package/dist/src/doctor/doctor-check-names.js +5 -1
- package/dist/src/doctor/doctor-checks.js +10 -15
- package/dist/src/doctor/doctor-claude-checks.js +6 -9
- package/dist/src/doctor/doctor-codex-checks.js +6 -9
- package/dist/src/doctor/doctor-contention-checks.js +244 -118
- package/dist/src/doctor/doctor-fallback-checks.js +130 -0
- package/dist/src/doctor/doctor-holds.js +4 -6
- package/dist/src/doctor/doctor-model-checks.js +45 -200
- package/dist/src/doctor/doctor-notify.js +7 -2
- package/dist/src/doctor/doctor-opencode-checks.js +7 -5
- package/dist/src/doctor/doctor-pi-checks.js +62 -0
- package/dist/src/doctor/doctor-project-checks.js +106 -12
- package/dist/src/doctor/doctor.js +10 -5
- package/dist/src/doctor/probe.js +14 -1
- package/dist/src/events/event-format.js +7 -6
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-read.js +13 -0
- package/dist/src/events/event-window.js +20 -2
- package/dist/src/failure/failure-data.js +9 -4
- package/dist/src/failure/failure-render.js +8 -8
- package/dist/src/failure/time-spend.js +0 -64
- package/dist/src/files/file-queue.js +17 -0
- package/dist/src/gates/budget-gates.js +27 -257
- package/dist/src/gates/gate-polls.js +49 -141
- package/dist/src/gates/maintenance-quota.js +27 -17
- package/dist/src/git/commit-message.js +12 -0
- package/dist/src/git/git.js +18 -0
- package/dist/src/git/slot-salvage.js +3 -3
- package/dist/src/gui/gui-backlog-commands.js +3 -3
- package/dist/src/gui/gui-backlog-endpoints.js +5 -4
- package/dist/src/gui/gui-command.js +19 -1
- package/dist/src/gui/gui-config-commands.js +75 -9
- package/dist/src/gui/gui-endpoint-commands.js +2 -2
- package/dist/src/gui/gui-operator-commands.js +134 -7
- package/dist/src/gui/gui-prompt-commands.js +15 -6
- package/dist/src/gui/gui-report-endpoints.js +10 -3
- package/dist/src/gui/gui-server.js +16 -2
- package/dist/src/history/history-queued.js +8 -0
- package/dist/src/history/history.js +2 -5
- package/dist/src/inbox/inbox-top.js +4 -9
- package/dist/src/merge/merge-core.js +2 -25
- package/dist/src/merge/merge-drain.js +22 -135
- package/dist/src/merge/merge-pipeline.js +1 -1
- package/dist/src/merge/merge-post-review.js +154 -30
- package/dist/src/merge/merge-queue-render.js +11 -12
- package/dist/src/merge/merge-queue.js +22 -1
- package/dist/src/merge/merge-requeue.js +1 -0
- package/dist/src/merge/merge-stack.js +1 -7
- package/dist/src/merge/merge-to-main.js +1 -9
- package/dist/src/merge/merge-top.js +33 -19
- package/dist/src/merge/merge-verify.js +64 -9
- package/dist/src/notes-cli.js +8 -3
- package/dist/src/orchestrator/orchestrator-poll.js +47 -15
- package/dist/src/orchestrator/orchestrator-scheduling.js +4 -2
- package/dist/src/orchestrator/orchestrator.js +5 -4
- package/dist/src/orchestrator/scheduling-scope.js +51 -7
- package/dist/src/orchestrator/scheduling-skip.js +15 -5
- package/dist/src/paths.js +16 -6
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/process/run-marker.js +8 -0
- package/dist/src/prompt/gate-notes.js +1 -1
- package/dist/src/prompt/gate-prompts.js +30 -7
- package/dist/src/prompt/notes-backlog.js +13 -10
- package/dist/src/prompt/prompt.js +3 -1
- package/dist/src/report/report-data.js +49 -69
- package/dist/src/report/report-fold.js +11 -12
- package/dist/src/report/report-render.js +6 -4
- package/dist/src/report/report.js +2 -5
- package/dist/src/review/diff-rules.js +53 -0
- package/dist/src/review/known-flakes.js +12 -0
- package/dist/src/review/post-merge-review-accept.js +51 -0
- package/dist/src/review/post-merge-review-render.js +112 -0
- package/dist/src/review/post-merge-review.js +157 -24
- package/dist/src/review/protected-edits.js +22 -7
- package/dist/src/review/quarantine-render.js +65 -0
- package/dist/src/review/review.js +33 -49
- package/dist/src/roles/role-catalog.js +60 -2
- package/dist/src/roles/role-guidance.js +10 -0
- package/dist/src/roles/role-labels.js +6 -3
- package/dist/src/roles/role-render.js +43 -8
- package/dist/src/roles/role-view.js +36 -9
- package/dist/src/roles/roles.js +3 -3
- package/dist/src/run/ci-parity.js +121 -0
- package/dist/src/run/qa-coverage.js +50 -15
- package/dist/src/run/run-detail-data.js +9 -13
- package/dist/src/run/run-detail-render.js +1 -8
- package/dist/src/run/run-finalize.js +1 -0
- package/dist/src/run/run-prompt.js +14 -0
- package/dist/src/run/run-stage.js +66 -37
- package/dist/src/run/run-verdict.js +14 -1
- package/dist/src/run/stage-check.js +17 -11
- package/dist/src/run/stage-checks-shared.js +65 -0
- package/dist/src/run/stage-name-checks.js +204 -0
- package/dist/src/run/stage-path-checks.js +16 -515
- package/dist/src/run/stage-prose-checks.js +261 -0
- package/dist/src/runner/leftover.js +50 -19
- package/dist/src/runner/revision.js +12 -23
- package/dist/src/runner/runner-outcome.js +4 -2
- package/dist/src/runner/runner-recovery.js +18 -3
- package/dist/src/runner/runner-run.js +21 -8
- package/dist/src/runner/runner-state.js +4 -3
- package/dist/src/scheduling/claim-entries.js +49 -0
- package/dist/src/status/status-data.js +6 -1
- package/dist/src/status/status-polls.js +15 -0
- package/dist/src/status/status-roles.js +11 -0
- package/dist/src/team/reclaim.js +2 -2
- package/dist/src/text/datetime.js +3 -2
- package/dist/src/text/phrases.js +6 -3
- package/dist/src/text/text.js +11 -0
- package/dist/src/ui/badges.js +34 -1
- package/dist/src/ui/gui/gui-client-backlog.js +25 -3
- package/dist/src/ui/gui/gui-client-boot.js +63 -41
- package/dist/src/ui/gui/gui-client-drawer-config.js +56 -14
- package/dist/src/ui/gui/gui-client-drawer.js +29 -4
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +74 -72
- package/dist/src/ui/gui/gui-client-labels.js +5 -1
- package/dist/src/ui/gui/gui-client-operator.js +6 -2
- package/dist/src/ui/gui/gui-client-pending.js +188 -11
- package/dist/src/ui/gui/gui-client-report-chart.js +36 -8
- package/dist/src/ui/gui/gui-client-report.js +20 -5
- package/dist/src/ui/gui/gui-client-settings.js +109 -3
- package/dist/src/ui/gui/gui-client-team.js +11 -0
- package/dist/src/ui/gui/gui-client.js +11 -1
- package/dist/src/ui/gui/gui-page.js +1 -0
- package/dist/src/ui/gui/gui-styles.js +4 -0
- package/dist/src/ui/run-progress-model.js +2 -1
- package/dist/src/ui/status-payload.js +237 -9
- package/dist/src/ui/status-render.js +12 -4
- package/dist/src/ui/team-alerts.js +512 -37
- package/dist/src/ui/tone.js +7 -0
- package/dist/src/ui/tui/tui-backlog.js +7 -5
- package/dist/src/ui/tui/tui-editor-attach.js +1 -7
- package/dist/src/ui/tui/tui-editor-backends.js +1 -6
- package/dist/src/ui/tui/tui-editor-brief.js +4 -24
- package/dist/src/ui/tui/tui-editor-budget.js +1 -8
- package/dist/src/ui/tui/tui-editor-check.js +29 -0
- package/dist/src/ui/tui/tui-editor-checks.js +28 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +1 -0
- package/dist/src/ui/tui/tui-editor-config.js +19 -6
- package/dist/src/ui/tui/tui-editor-entry.js +3 -17
- package/dist/src/ui/tui/tui-editor-instances.js +9 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +7 -26
- package/dist/src/ui/tui/tui-editor-interval.js +18 -7
- package/dist/src/ui/tui/tui-editor-notes.js +6 -24
- package/dist/src/ui/tui/tui-editor-quiet.js +2 -12
- package/dist/src/ui/tui/tui-editor-retire.js +1 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +126 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +1 -6
- package/dist/src/ui/tui/tui-editor-session.js +41 -0
- package/dist/src/ui/tui/tui-editor-slots.js +1 -0
- package/dist/src/ui/tui/tui-editors.js +10 -1
- package/dist/src/ui/tui/tui-frame.js +18 -6
- package/dist/src/ui/tui/tui-keys.js +9 -0
- package/dist/src/ui/tui/tui-pane-activity.js +21 -8
- package/dist/src/ui/tui/tui-pane-backlog.js +36 -9
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +113 -3
- package/dist/src/ui/tui/tui-pane-history.js +44 -0
- package/dist/src/ui/tui/tui-pane-pending.js +285 -56
- package/dist/src/ui/tui/tui-pane-queued.js +7 -2
- package/dist/src/ui/tui/tui-pane-report.js +9 -3
- package/dist/src/ui/tui/tui-pane-session.js +9 -0
- package/dist/src/ui/tui/tui-pane.js +16 -10
- package/dist/src/ui/tui/tui-panes.js +56 -7
- package/dist/src/ui/tui/tui.js +8 -5
- package/dist/src/verdict/fix-claim.js +47 -1
- package/dist/src/verdict/reply-contract.js +35 -0
- package/package.json +1 -1
- package/dist/src/baseline/main-culprit.js +0 -94
- package/dist/src/baseline/main-revert.js +0 -133
|
@@ -261,6 +261,27 @@ export const PROBLEMS_FLAG = { names: ["--problems"] };
|
|
|
261
261
|
/** The `--kill-orphans` flag spec, accepted by `doctor`: first kill the orphaned processes the
|
|
262
262
|
* orphans check lists, each with its descendants. Value-less, like PROBLEMS_FLAG. */
|
|
263
263
|
export const KILL_ORPHANS_FLAG = { names: ["--kill-orphans"] };
|
|
264
|
+
/** The `--commit-backlog` flag spec, accepted by `doctor`: first commit each dirty tracked backlog
|
|
265
|
+
* or brief file on the base branch before the report, under the primary-worktree lock, through the
|
|
266
|
+
* operator backlog commit path. The second mutating doctor flag, after `--kill-orphans`.
|
|
267
|
+
* Value-less, like KILL_ORPHANS_FLAG. */
|
|
268
|
+
export const COMMIT_BACKLOG_FLAG = { names: ["--commit-backlog"] };
|
|
269
|
+
/** The `--resync-install` flag spec, accepted by `doctor`: re-sync the root checkout's install
|
|
270
|
+
* on demand, under the merge lock, before the report (part 1/2; the Health surfaces that expose
|
|
271
|
+
* it are part 2/2). The third mutating doctor flag, after `--kill-orphans` and `--commit-backlog`.
|
|
272
|
+
* Value-less, like COMMIT_BACKLOG_FLAG. */
|
|
273
|
+
export const RESYNC_INSTALL_FLAG = { names: ["--resync-install"] };
|
|
274
|
+
/** The `--drop-broken-pins` flag spec, accepted by `doctor`: first drop each `refs/tumwater/`
|
|
275
|
+
* pin whose commit is gone — the manual `git update-ref -d` the `tumwater pins` row names. The
|
|
276
|
+
* fourth mutating doctor flag, after `--kill-orphans`, `--commit-backlog` and `--resync-install`.
|
|
277
|
+
* Value-less, like RESYNC_INSTALL_FLAG. */
|
|
278
|
+
export const DROP_BROKEN_PINS_FLAG = { names: ["--drop-broken-pins"] };
|
|
279
|
+
/** The `--retire-leftovers` flag spec, accepted by `doctor`: first retire each disabled role's
|
|
280
|
+
* leftover worktree, branch and merge ref in one pass — the reap for the `leftover role
|
|
281
|
+
* branches` row, through the same `retireRole` core `tumwater retire --role <id>` uses. The
|
|
282
|
+
* fifth mutating doctor flag, after `--kill-orphans`, `--commit-backlog`, `--resync-install` and
|
|
283
|
+
* `--drop-broken-pins`. Value-less, like DROP_BROKEN_PINS_FLAG. */
|
|
284
|
+
export const RETIRE_LEFTOVERS_FLAG = { names: ["--retire-leftovers"] };
|
|
264
285
|
/** The `--stat` flag spec, accepted by `diff --role`: print each patch as its `git diff --stat`
|
|
265
286
|
* summary. Value-less, like JSON_FLAG. */
|
|
266
287
|
export const STAT_FLAG = { names: ["--stat"] };
|
|
@@ -292,6 +313,10 @@ export const SUMMARY_FLAG = { names: ["--summary"] };
|
|
|
292
313
|
/** The `--count` flag spec, accepted by `logs`: tally the selected window's event types
|
|
293
314
|
* instead of printing the rows. Value-less, like SUMMARY_FLAG. */
|
|
294
315
|
export const COUNT_FLAG = { names: ["--count"] };
|
|
316
|
+
/** `logs --notable`: cull routine bookkeeping events the way the dashboard Activity feed's
|
|
317
|
+
* Notable choice does, keeping only non-routine rows. Value-less like JSON_FLAG and COUNT_FLAG,
|
|
318
|
+
* and a rival of the --transcript transcript view with the rest of the feed flags. */
|
|
319
|
+
export const NOTABLE_FLAG = { names: ["--notable"] };
|
|
295
320
|
/** The `--force` flag spec, accepted by `retire` (the only override-the-rails command): one
|
|
296
321
|
* definition of the flag's spelling beside ROLE_FLAG and JSON_FLAG, so the gate's accepted
|
|
297
322
|
* vocabulary cannot drift from the command body's `args.includes("--force")`. */
|
|
@@ -328,6 +353,14 @@ export function typeFlagSpec(missingValue) {
|
|
|
328
353
|
export function resultFlagSpec(missingValue) {
|
|
329
354
|
return { names: ["--result"], value: true, valueName: "<class>", missingValue };
|
|
330
355
|
}
|
|
356
|
+
/** The `--status <state>` flag spec of `reviews`' state filter, beside resultFlagSpec: one
|
|
357
|
+
* definition of the flag's spelling and value shape, so the gate's accepted vocabulary cannot
|
|
358
|
+
* drift. The missing-value wording names the command, so the spec is a factory taking it — the
|
|
359
|
+
* same string the body fails with; which states are accepted is the command body's rule
|
|
360
|
+
* (post-merge-review-render.ts's POST_REVIEW_STATES). */
|
|
361
|
+
export function statusFlagSpec(missingValue) {
|
|
362
|
+
return { names: ["--status"], value: true, valueName: "<state>", missingValue };
|
|
363
|
+
}
|
|
331
364
|
/** `history --model <selector>[,...]`: keeps the runs that started on the listed models;
|
|
332
365
|
* `missingValue` is the same string the command body fails with. */
|
|
333
366
|
export function modelFlagSpec(missingValue) {
|
|
@@ -7,7 +7,7 @@ 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,
|
|
10
|
+
import { flagValue, parseGrepFlag, parseRoleListScope, 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";
|
|
@@ -16,6 +16,8 @@ import { renderStatus } from "../ui/status-render.js";
|
|
|
16
16
|
import { backlogPayload } from "../backlog/backlog.js";
|
|
17
17
|
import { backlogHolds, backlogInProgress } from "../backlog/backlog-eligibility.js";
|
|
18
18
|
import { readyBacklog, grepBacklog, renderBacklogMarkdown } from "../backlog/backlog-render.js";
|
|
19
|
+
import { loadQuarantine } from "../review/known-flakes.js";
|
|
20
|
+
import { quarantineRows, renderQuarantine } from "../review/quarantine-render.js";
|
|
19
21
|
import { collectChangeOverlaps, scopeChangeOverlaps, scopeChangeOverlapsToPath, collectTeamChanges, collectRoleChange, collectRoleChanges, normalizeDiffPath } from "../change/change-data.js";
|
|
20
22
|
import { renderChangeOverlaps, renderTeamChange, renderRoleChange, renderRoleChanges } from "../change/change-render.js";
|
|
21
23
|
import { readMergeQueue } from "../merge/merge-queue.js";
|
|
@@ -198,26 +200,7 @@ export async function cmdQueue(root, args, seams = {}) {
|
|
|
198
200
|
* `tumwater queue`'s. An entry a live merge marker names for this sha is refused (wait for it
|
|
199
201
|
* to land, then revert); an out-of-range position fails naming the current depth. */
|
|
200
202
|
async function cmdQueueCancel(root, args) {
|
|
201
|
-
|
|
202
|
-
let json = false;
|
|
203
|
-
const rest = [];
|
|
204
|
-
for (let i = 0; i < args.length; i++) {
|
|
205
|
-
const arg = args[i];
|
|
206
|
-
if (arg === "--json") {
|
|
207
|
-
rejectRepeatFlag(json, "--json");
|
|
208
|
-
json = true;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (arg === "--cancel") {
|
|
212
|
-
rejectRepeatFlag(position !== null, "--cancel");
|
|
213
|
-
position = parsePositionFlag("--cancel", args[i + 1], "--cancel 2");
|
|
214
|
-
i++;
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
if (arg === "--help" || arg === "-h")
|
|
218
|
-
continue;
|
|
219
|
-
rest.push(arg);
|
|
220
|
-
}
|
|
203
|
+
const { json, position, rest } = scanPositionArgs(args, "--cancel", "--cancel 2");
|
|
221
204
|
// The vocabulary a cancel invocation accepts is exactly --cancel and --json, both consumed
|
|
222
205
|
// above; --role and -f/--follow are refused with the queue-specific wording the tests pin.
|
|
223
206
|
if (rest.includes("--role"))
|
|
@@ -259,26 +242,7 @@ async function cmdQueueCancel(root, args) {
|
|
|
259
242
|
* out-of-range position fails naming the current depth; position 1 prints the already-first
|
|
260
243
|
* line and exits 0; a concurrently-drained entry prints the gone line and exits 0. */
|
|
261
244
|
async function cmdQueueTop(root, args) {
|
|
262
|
-
|
|
263
|
-
let json = false;
|
|
264
|
-
const rest = [];
|
|
265
|
-
for (let i = 0; i < args.length; i++) {
|
|
266
|
-
const arg = args[i];
|
|
267
|
-
if (arg === "--json") {
|
|
268
|
-
rejectRepeatFlag(json, "--json");
|
|
269
|
-
json = true;
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (arg === "--top") {
|
|
273
|
-
rejectRepeatFlag(position !== null, "--top");
|
|
274
|
-
position = parsePositionFlag("--top", args[i + 1], "--top 2");
|
|
275
|
-
i++;
|
|
276
|
-
continue;
|
|
277
|
-
}
|
|
278
|
-
if (arg === "--help" || arg === "-h")
|
|
279
|
-
continue;
|
|
280
|
-
rest.push(arg);
|
|
281
|
-
}
|
|
245
|
+
const { json, position, rest } = scanPositionArgs(args, "--top", "--top 2");
|
|
282
246
|
// The vocabulary a top invocation accepts is exactly --top and --json, both consumed
|
|
283
247
|
// above; --role and -f/--follow are refused with the queue-specific wording the tests pin.
|
|
284
248
|
if (rest.includes("--role"))
|
|
@@ -374,3 +338,14 @@ export async function cmdBacklog(root, args, seams = {}) {
|
|
|
374
338
|
const result = readBacklog();
|
|
375
339
|
sayJsonOrRender(rest, result.view, () => renderBacklogText(result));
|
|
376
340
|
}
|
|
341
|
+
// `tumwater quarantine [--json]` — the read-only view of the review gate's flake
|
|
342
|
+
// quarantine. No requireReadyRepo gate: loadQuarantine degrades an absent or malformed
|
|
343
|
+
// file to the empty list (the cmdBacklog rationale), so the command prints the empty line
|
|
344
|
+
// in any directory, and it never writes quarantine.json (a test leaves the list only
|
|
345
|
+
// through releaseQuarantine). --json serves the same positioned rows to scripts.
|
|
346
|
+
export async function cmdQuarantine(root, args) {
|
|
347
|
+
rejectUnknownArgs("quarantine", args, [JSON_FLAG]);
|
|
348
|
+
refuseTeamWorkspace(root);
|
|
349
|
+
const rows = quarantineRows(loadQuarantine(root));
|
|
350
|
+
sayJsonOrRender(args, { tests: rows }, () => renderQuarantine(rows, Date.now()));
|
|
351
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/** `tumwater reviews` — the operator-facing twin of `tumwater queue` for the post-merge review
|
|
2
|
+
* queue (.tumwater/state/post-review.json): list every well-formed entry in FIFO order, or the
|
|
3
|
+
* same list as JSON (`--json`). It does not require a ready repo (the `cmdBacklog` rationale):
|
|
4
|
+
* `readPostReviews` reads a missing, torn or hand-edited file as the well-formed subset (or `[]`),
|
|
5
|
+
* so the command prints the empty line in any directory. Read-only except the one destructive
|
|
6
|
+
* `--accept <n>` mode, which removes the Nth entry the operator deliberately accepts unreviewed
|
|
7
|
+
* (the action the at-risk alert's advice names); that mode adds no new ready-repo requirement to
|
|
8
|
+
* the read path and gates the write with `requireReadyRepo`. Unlike `cmdQueue`'s `--cancel`, the
|
|
9
|
+
* read case still writes no file. */
|
|
10
|
+
import { JSON_FLAG, ROLE_FLAG, rejectUnknownArgs, statusFlagSpec } from "./cli-flag-specs.js";
|
|
11
|
+
import { sayJsonOrRender, say, sayJson, fail } from "./cli-output.js";
|
|
12
|
+
import { refuseTeamWorkspace, requireReadyRepo } from "./cli-repo-gate.js";
|
|
13
|
+
import { parseRoleListScope, parseStatusFlag, scanPositionArgs } from "./cli-args.js";
|
|
14
|
+
import { readPostReviews } from "../review/post-merge-review.js";
|
|
15
|
+
import { acceptPostReview } from "../review/post-merge-review-accept.js";
|
|
16
|
+
import { POST_REVIEW_STATES, postReviewState, renderPostReviews, scopePostReviews, } from "../review/post-merge-review-render.js";
|
|
17
|
+
import { roleInScope } from "../roles/runner-ids.js";
|
|
18
|
+
import { roleLabel } from "../roles/role-labels.js";
|
|
19
|
+
import { shortSha } from "../text/format.js";
|
|
20
|
+
import { gotSuffix, splitCommaList } from "../text/text.js";
|
|
21
|
+
import { typoSuffix } from "../text/suggest.js";
|
|
22
|
+
/** The missing-value wording of `reviews --status` (shared by the gate's statusFlagSpec and
|
|
23
|
+
* parseStatusFlag's own fail, so the two cannot drift). */
|
|
24
|
+
const REVIEWS_STATUS_VALUE_ERROR = "reviews --status needs a value";
|
|
25
|
+
/** Parse a `--status` value: a comma list of post-merge review states, deduplicated in
|
|
26
|
+
* first-seen order, accepting only `POST_REVIEW_STATES` members. The first unknown piece fails
|
|
27
|
+
* naming the accepted states, mirroring parseHistoryResultList's gotSuffix/typoSuffix shape. */
|
|
28
|
+
function parseReviewStatusList(raw) {
|
|
29
|
+
if (raw === "")
|
|
30
|
+
fail(REVIEWS_STATUS_VALUE_ERROR);
|
|
31
|
+
const pieces = splitCommaList(raw);
|
|
32
|
+
if (pieces.length === 0)
|
|
33
|
+
fail(REVIEWS_STATUS_VALUE_ERROR);
|
|
34
|
+
const spellings = [...POST_REVIEW_STATES];
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const piece of pieces) {
|
|
37
|
+
if (!POST_REVIEW_STATES.includes(piece))
|
|
38
|
+
fail(`reviews --status must be one of ${spellings.join(", ")}${gotSuffix(piece)}${typoSuffix(piece, spellings)}`);
|
|
39
|
+
if (!out.includes(piece))
|
|
40
|
+
out.push(piece);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** `tumwater reviews [--json]`: the post-merge review queue, oldest first.*/
|
|
45
|
+
export async function cmdReviews(root, args) {
|
|
46
|
+
// `tumwater reviews --accept <n>` is the read-only command's one destructive mode: route it
|
|
47
|
+
// to its own branch before the read gate, so --accept never leaks into the view parsing.
|
|
48
|
+
if (args.includes("--accept")) {
|
|
49
|
+
await cmdReviewsAccept(root, args);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
rejectUnknownArgs("reviews", args, [ROLE_FLAG, statusFlagSpec(REVIEWS_STATUS_VALUE_ERROR), JSON_FLAG]);
|
|
53
|
+
refuseTeamWorkspace(root);
|
|
54
|
+
// `--role` keeps only the listed roles' entries (a base id taking its instances too) under
|
|
55
|
+
// their whole-queue positions, so `--accept <n>` still addresses the position the unscoped
|
|
56
|
+
// read prints. Absent a ready config `parseRoleListScope` falls back to the built-in catalog.
|
|
57
|
+
const roles = parseRoleListScope(root, args);
|
|
58
|
+
const { raw: statusRaw } = parseStatusFlag(args, REVIEWS_STATUS_VALUE_ERROR);
|
|
59
|
+
const whole = readPostReviews(root);
|
|
60
|
+
let entries = whole;
|
|
61
|
+
let scope;
|
|
62
|
+
if (statusRaw === null) {
|
|
63
|
+
const scoped = roles ? scopePostReviews(whole, roles, (e) => roleInScope(e.role, roles)) : undefined;
|
|
64
|
+
entries = scoped ? scoped.entries : whole;
|
|
65
|
+
scope = scoped?.scope;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// `--status <state>[,...]` keeps only the listed states (the same precedence
|
|
69
|
+
// postReviewStatusLabel prints: a rejected-and-failing entry is rejected) under their
|
|
70
|
+
// whole-queue positions, composing with `--role`. roles=[] when unscoped.
|
|
71
|
+
const states = parseReviewStatusList(statusRaw);
|
|
72
|
+
const keep = (e) => (!roles || roleInScope(e.role, roles)) && states.includes(postReviewState(e));
|
|
73
|
+
const filterText = [roles ? `--role ${roles.join(",")}` : "", `--status ${states.join(",")}`]
|
|
74
|
+
.filter(Boolean)
|
|
75
|
+
.join(" ");
|
|
76
|
+
const scoped = scopePostReviews(whole, roles ?? [], keep, filterText);
|
|
77
|
+
entries = scoped.entries;
|
|
78
|
+
scope = scoped.scope;
|
|
79
|
+
}
|
|
80
|
+
sayJsonOrRender(args, { pending: entries }, () => renderPostReviews(entries, scope, Date.now()));
|
|
81
|
+
}
|
|
82
|
+
/** The `tumwater reviews --accept <n>` branch (PLANS.md): remove the Nth pending post-merge
|
|
83
|
+
* review `tumwater reviews` lists, accepting the commit unreviewed at the operator's request.
|
|
84
|
+
* Parsed the way `cmdQueueCancel` parses `--cancel`: `--accept <n>` the one valued flag,
|
|
85
|
+
* `--json` the one boolean (at most once), `--help`/`-h` tolerated, anything else refused. An
|
|
86
|
+
* out-of-range position fails naming the current depth; an entry whose repair already merged
|
|
87
|
+
* (`fixed`) is refused. A rejected entry waits on nothing and is accepted like any pending one. */
|
|
88
|
+
async function cmdReviewsAccept(root, args) {
|
|
89
|
+
const { json, position, rest } = scanPositionArgs(args, "--accept", "--accept 2");
|
|
90
|
+
// The vocabulary an accept invocation accepts is exactly --accept and --json, both consumed
|
|
91
|
+
// above; --role is refused by name (as cmdQueueCancel refuses it): accept addresses a
|
|
92
|
+
// whole-queue position `tumwater reviews` printed, and a scoped read must never renumber it.
|
|
93
|
+
if (rest.includes("--role") || rest.includes("--status"))
|
|
94
|
+
fail("reviews --accept accepts by whole-queue position from `tumwater reviews`; --role and --status scope a read only — drop them");
|
|
95
|
+
// Everything else is refused with the generic wording, so `--bogus` is named.
|
|
96
|
+
rejectUnknownArgs("reviews", rest, []);
|
|
97
|
+
if (position === null)
|
|
98
|
+
fail("--accept needs a position (usage: tumwater reviews --accept <n>)");
|
|
99
|
+
await requireReadyRepo(root);
|
|
100
|
+
const outcome = acceptPostReview(root, position);
|
|
101
|
+
if (outcome.status === "out-of-range") {
|
|
102
|
+
fail(`no post-merge review at position ${position} (post-merge review queue holds ${outcome.depth})`);
|
|
103
|
+
}
|
|
104
|
+
if (outcome.status === "settled") {
|
|
105
|
+
fail(`post-merge review ${position} already merged its fix on main — nothing to accept`);
|
|
106
|
+
}
|
|
107
|
+
if (json) {
|
|
108
|
+
sayJson({ accepted: { role: outcome.role, commit: outcome.commit, summary: outcome.summary } });
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
say(`accepted post-merge review entry ${position} unreviewed: ${roleLabel(outcome.role)} ${shortSha(outcome.commit)} — ${outcome.summary}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* keeps only the read-only query commands (status, diff, queue, backlog). cli.ts's dispatcher
|
|
3
3
|
* routes to cmdRole here exactly as it routes cmdStatus to cli-query-commands or cmdConfig to
|
|
4
4
|
* config-commands. cmdRole is the one query-named command that also writes: it renders a
|
|
5
|
-
* role's standing and next-run prompts and carries
|
|
5
|
+
* role's standing and next-run prompts and carries eleven per-role write flags (notes,
|
|
6
6
|
* instructions, quiet-hours, daily cap, min-run-interval, instances, enable/disable, and the
|
|
7
|
-
* provider/model/thinking
|
|
7
|
+
* provider/model/thinking/backend), each through the same setConfigKey/unsetConfigKey core
|
|
8
8
|
* `config set`/`config unset` use. It lives in its own file because its mixed
|
|
9
9
|
* positional/flag vocabulary and its write branches are its own, not the query commands'. */
|
|
10
10
|
import { fail, say, sayJsonOrRender } from "./cli-output.js";
|
|
@@ -44,6 +44,12 @@ const CAP_SPEC = { names: ["--cap"], value: true, valueName: "<usd>" };
|
|
|
44
44
|
function plainDecimalToJson(raw) {
|
|
45
45
|
return /^\d*\.?\d+$/.test(raw) ? String(Number(raw)) : null;
|
|
46
46
|
}
|
|
47
|
+
/** Detect exponent spellings that the cap, interval and instances writes must reject
|
|
48
|
+
* before JSON parsing can turn them into otherwise valid numeric config values. Other
|
|
49
|
+
* invalid spellings fall through to the config validator, which names the key. */
|
|
50
|
+
function isExponentSpelling(raw) {
|
|
51
|
+
return raw.trim() !== "" && plainDecimalToJson(raw) === null && /^\d+(\.\d+)?[eE][+-]?\d+$/.test(raw.trim());
|
|
52
|
+
}
|
|
47
53
|
/** `role`'s min-run-interval write flags: clear it, or set it to a second count (0 is
|
|
48
54
|
* back-to-back). */
|
|
49
55
|
const CLEAR_MIN_RUN_INTERVAL_SPEC = { names: ["--clear-min-run-interval"] };
|
|
@@ -56,6 +62,10 @@ const INSTANCES_SPEC = { names: ["--instances"], value: true, valueName: "<n>" }
|
|
|
56
62
|
* roles.<base>.enabled. */
|
|
57
63
|
const ENABLE_SPEC = { names: ["--enable"] };
|
|
58
64
|
const DISABLE_SPEC = { names: ["--disable"] };
|
|
65
|
+
/** `role`'s review-after-merge write flags: add the role to, or remove it from,
|
|
66
|
+
* review.afterMerge (written for the base id). */
|
|
67
|
+
const REVIEW_AFTER_MERGE_SPEC = { names: ["--review-after-merge"] };
|
|
68
|
+
const CLEAR_REVIEW_AFTER_MERGE_SPEC = { names: ["--clear-review-after-merge"] };
|
|
59
69
|
/** `role`'s model-triple write flags: clear the field, or set it, written to
|
|
60
70
|
* roles.<base>.provider/.model/.thinking. model and provider take an id; thinking takes a
|
|
61
71
|
* pi `--thinking` level (off|minimal|low|medium|high|xhigh|max). */
|
|
@@ -65,18 +75,21 @@ const CLEAR_MODEL_SPEC = { names: ["--clear-model"] };
|
|
|
65
75
|
const MODEL_SPEC = { names: ["--model"], value: true, valueName: "<id>" };
|
|
66
76
|
const CLEAR_THINKING_SPEC = { names: ["--clear-thinking"] };
|
|
67
77
|
const THINKING_SPEC = { names: ["--thinking"], value: true, valueName: "<level>" };
|
|
78
|
+
const CLEAR_BACKEND_SPEC = { names: ["--clear-backend"] };
|
|
79
|
+
const BACKEND_SPEC = { names: ["--backend"], value: true, valueName: "<id>" };
|
|
68
80
|
/** Peel `tumwater role <id>`'s positional id off the argument list: the first token that is
|
|
69
81
|
* neither a flag nor the value of a --role, --notes-file, --instructions-file,
|
|
70
|
-
* --quiet-hours, --cap, --min-run-interval, --instances, --provider, --model
|
|
71
|
-
* --thinking flag. Returns the id (null when absent) and the remaining
|
|
72
|
-
* only, ready for rejectUnknownArgs and sayJsonOrRender. The id may
|
|
82
|
+
* --quiet-hours, --cap, --min-run-interval, --instances, --provider, --model,
|
|
83
|
+
* --thinking or --backend flag. Returns the id (null when absent) and the remaining
|
|
84
|
+
* arguments — flags only, ready for rejectUnknownArgs and sayJsonOrRender. The id may
|
|
85
|
+
* also arrive as
|
|
73
86
|
* --role <id>, the flag spelling every other role-targeting command shares; the
|
|
74
87
|
* collector's caller rejects both spellings at once rather than silently picking one.
|
|
75
88
|
* Lives here because it exists only for this one
|
|
76
89
|
* command's mixed positional/flag vocabulary. */
|
|
77
90
|
function peelRolePositional(args) {
|
|
78
91
|
const valueIdx = new Set();
|
|
79
|
-
for (const flag of ["--role", "--notes-file", "--instructions-file", "--quiet-hours", "--cap", "--min-run-interval", "--instances", "--provider", "--model", "--thinking"]) {
|
|
92
|
+
for (const flag of ["--role", "--notes-file", "--instructions-file", "--quiet-hours", "--cap", "--min-run-interval", "--instances", "--provider", "--model", "--thinking", "--backend"]) {
|
|
80
93
|
const at = args.indexOf(flag);
|
|
81
94
|
if (at >= 0)
|
|
82
95
|
valueIdx.add(at + 1);
|
|
@@ -106,9 +119,10 @@ function requireKnownRole(root, id) {
|
|
|
106
119
|
if (!knownRoleIds(cfg).includes(id))
|
|
107
120
|
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
108
121
|
}
|
|
109
|
-
/** The config-key write the
|
|
110
|
-
* interval, provider, model and
|
|
111
|
-
* `key` under `base`; else set it. `serialize` shapes the value `setConfigKey`
|
|
122
|
+
/** The config-key write the seven per-role hold branches (quiet hours, daily cap, min run
|
|
123
|
+
* interval, provider, model, thinking and backend) share: with `clear`, or an empty typed
|
|
124
|
+
* value, unset `key` under `base`; else set it. `serialize` shapes the value `setConfigKey`
|
|
125
|
+
* stores (it
|
|
112
126
|
* JSON-encodes nothing itself) and `display` gives the human phrase for the confirmation
|
|
113
127
|
* line. The caller returns right after. The instructions branch keeps its own block: its
|
|
114
128
|
* value is a file's text, so an empty value is a legitimate write, not a clear. A no-op (a
|
|
@@ -148,13 +162,13 @@ export async function cmdRole(root, args) {
|
|
|
148
162
|
// Only a `--` (double-dash) token reads as a flag: a single-dash token like `-1` is a
|
|
149
163
|
// negative-number value that must reach the setters' own bound checks, and the file flags'
|
|
150
164
|
// `-` stdin value is not in this set at all.
|
|
151
|
-
for (const flag of ["--provider", "--model", "--thinking", "--cap", "--min-run-interval", "--instances", "--quiet-hours"]) {
|
|
165
|
+
for (const flag of ["--provider", "--model", "--thinking", "--backend", "--cap", "--min-run-interval", "--instances", "--quiet-hours"]) {
|
|
152
166
|
const at = rest.indexOf(flag);
|
|
153
167
|
const next = at >= 0 ? rest[at + 1] : undefined;
|
|
154
168
|
if (next?.startsWith("--"))
|
|
155
169
|
fail(`${flag} needs a value (the next token ${JSON.stringify(next)} is a flag, not a value)`);
|
|
156
170
|
}
|
|
157
|
-
rejectUnknownArgs("role", rest, [ROLE_FLAG, JSON_FLAG, CLEAR_NOTES_SPEC, NOTES_FILE_SPEC, CLEAR_INSTRUCTIONS_SPEC, INSTRUCTIONS_FILE_SPEC, CLEAR_QUIET_HOURS_SPEC, QUIET_HOURS_SPEC, ENABLE_SPEC, DISABLE_SPEC, CLEAR_CAP_SPEC, CAP_SPEC, CLEAR_MIN_RUN_INTERVAL_SPEC, MIN_RUN_INTERVAL_SPEC, CLEAR_INSTANCES_SPEC, INSTANCES_SPEC, CLEAR_PROVIDER_SPEC, PROVIDER_SPEC, CLEAR_MODEL_SPEC, MODEL_SPEC, CLEAR_THINKING_SPEC, THINKING_SPEC]);
|
|
171
|
+
rejectUnknownArgs("role", rest, [ROLE_FLAG, JSON_FLAG, CLEAR_NOTES_SPEC, NOTES_FILE_SPEC, CLEAR_INSTRUCTIONS_SPEC, INSTRUCTIONS_FILE_SPEC, CLEAR_QUIET_HOURS_SPEC, QUIET_HOURS_SPEC, ENABLE_SPEC, DISABLE_SPEC, CLEAR_CAP_SPEC, CAP_SPEC, CLEAR_MIN_RUN_INTERVAL_SPEC, MIN_RUN_INTERVAL_SPEC, CLEAR_INSTANCES_SPEC, INSTANCES_SPEC, CLEAR_PROVIDER_SPEC, PROVIDER_SPEC, CLEAR_MODEL_SPEC, MODEL_SPEC, CLEAR_THINKING_SPEC, THINKING_SPEC, CLEAR_BACKEND_SPEC, BACKEND_SPEC, REVIEW_AFTER_MERGE_SPEC, CLEAR_REVIEW_AFTER_MERGE_SPEC]);
|
|
158
172
|
refuseTeamWorkspace(root);
|
|
159
173
|
// The id is required: positional (`tumwater role <id>`) or --role <id> (the flag
|
|
160
174
|
// spelling every other role-targeting command shares). Both at once is a mistake, not
|
|
@@ -177,6 +191,7 @@ export async function cmdRole(root, args) {
|
|
|
177
191
|
const clearProvider = rest.includes("--clear-provider");
|
|
178
192
|
const clearModel = rest.includes("--clear-model");
|
|
179
193
|
const clearThinking = rest.includes("--clear-thinking");
|
|
194
|
+
const clearBackend = rest.includes("--clear-backend");
|
|
180
195
|
const notesWrite = clear || rest.includes("--notes-file");
|
|
181
196
|
const instructionsWrite = clearInst || rest.includes("--instructions-file");
|
|
182
197
|
const quietWrite = clearQuiet || rest.includes("--quiet-hours");
|
|
@@ -186,8 +201,12 @@ export async function cmdRole(root, args) {
|
|
|
186
201
|
const providerWrite = clearProvider || rest.includes("--provider");
|
|
187
202
|
const modelWrite = clearModel || rest.includes("--model");
|
|
188
203
|
const thinkingWrite = clearThinking || rest.includes("--thinking");
|
|
204
|
+
const backendWrite = clearBackend || rest.includes("--backend");
|
|
189
205
|
const enable = rest.includes("--enable");
|
|
190
206
|
const disable = rest.includes("--disable");
|
|
207
|
+
const reviewAfterMerge = rest.includes("--review-after-merge");
|
|
208
|
+
const clearReviewAfterMerge = rest.includes("--clear-review-after-merge");
|
|
209
|
+
const reviewAfterMergeWrite = reviewAfterMerge || clearReviewAfterMerge;
|
|
191
210
|
const writes = [];
|
|
192
211
|
if (notesWrite)
|
|
193
212
|
writes.push("notes");
|
|
@@ -207,10 +226,14 @@ export async function cmdRole(root, args) {
|
|
|
207
226
|
writes.push("model");
|
|
208
227
|
if (thinkingWrite)
|
|
209
228
|
writes.push("thinking");
|
|
229
|
+
if (backendWrite)
|
|
230
|
+
writes.push("backend");
|
|
210
231
|
if (enable)
|
|
211
232
|
writes.push("enable");
|
|
212
233
|
if (disable)
|
|
213
234
|
writes.push("disable");
|
|
235
|
+
if (reviewAfterMergeWrite)
|
|
236
|
+
writes.push("review-after-merge");
|
|
214
237
|
if (writes.length > 1)
|
|
215
238
|
fail(`${writes.join(" and ")} writes cannot share one invocation`);
|
|
216
239
|
if (notesWrite) {
|
|
@@ -292,8 +315,20 @@ export async function cmdRole(root, args) {
|
|
|
292
315
|
if (id === DIRECTOR_ROLE)
|
|
293
316
|
fail("the director has no daily cap");
|
|
294
317
|
const base = baseRoleOf(id);
|
|
318
|
+
const cfgKey = `maxDailyCostUsdPerRole.${base}`;
|
|
295
319
|
const cap = flagValue(rest, "--cap") ?? "";
|
|
296
|
-
|
|
320
|
+
// Shape, before the write: a spelling that is not a plain decimal but that JSON.parse
|
|
321
|
+
// would still read as a non-negative USD amount (an exponent like "1e2" → 100) would
|
|
322
|
+
// otherwise sail past both plainDecimalToJson (which only rewrites JSON-unparseable
|
|
323
|
+
// spellings like `05`) and checkDailyBudgetUsd's non-negative check and silently write
|
|
324
|
+
// the misread — the same silent misread the TUI Ctrl+B budget editor and the GUI cap
|
|
325
|
+
// drawer now reject. Every non-exponent spelling JSON reads as a non-negative USD is
|
|
326
|
+
// already a plain decimal, so the exponent form is exactly the silent-write set; hex,
|
|
327
|
+
// negative and non-numeric spellings fall through to the write path, whose
|
|
328
|
+
// validateConfig message names the key.
|
|
329
|
+
if (isExponentSpelling(cap))
|
|
330
|
+
fail(`${cfgKey} must be a plain dollar amount like 25, 12.50, or .5 — no +/-, exponent, or hex notation`);
|
|
331
|
+
writeRoleConfigKey(root, base, clearCap, cap, cfgKey, "daily cap", (v) => plainDecimalToJson(v) ?? v, (v) => v);
|
|
297
332
|
return;
|
|
298
333
|
}
|
|
299
334
|
// The min-run-interval write: the terminal twin of `config set
|
|
@@ -310,8 +345,20 @@ export async function cmdRole(root, args) {
|
|
|
310
345
|
fail("--json does not apply to a min-run-interval write");
|
|
311
346
|
requireKnownRole(root, id);
|
|
312
347
|
const base = baseRoleOf(id);
|
|
348
|
+
const cfgKey = `roles.${base}.minRunIntervalSeconds`;
|
|
313
349
|
const secs = flagValue(rest, "--min-run-interval") ?? "";
|
|
314
|
-
|
|
350
|
+
// Shape, before the write: a spelling that is not a plain decimal but that JSON.parse
|
|
351
|
+
// would still read as a non-negative second count (an exponent like "1e2" → 100) would
|
|
352
|
+
// otherwise sail past both plainDecimalToJson (which only rewrites JSON-unparseable
|
|
353
|
+
// spellings like `05`) and validateConfig's non-negative check and silently write the
|
|
354
|
+
// misread — the same silent misread the TUI Ctrl+S editor and the GUI interval drawer
|
|
355
|
+
// now reject. Every non-exponent spelling JSON reads as a non-negative count is already a
|
|
356
|
+
// plain decimal, so the exponent form is exactly the silent-write set; hex, negative and
|
|
357
|
+
// non-numeric spellings fall through to the write path, whose validateConfig message
|
|
358
|
+
// names the key.
|
|
359
|
+
if (isExponentSpelling(secs))
|
|
360
|
+
fail(`${cfgKey} must be a plain second count like 300, 0.5, or .5 — no +/-, exponent, or hex notation`);
|
|
361
|
+
writeRoleConfigKey(root, base, clearInterval, secs, cfgKey, "min run interval", (v) => plainDecimalToJson(v) ?? v, (v) => `${v}s`);
|
|
315
362
|
return;
|
|
316
363
|
}
|
|
317
364
|
// The instances write: the terminal twin of `config set roles.<base>.instances <n>`, the
|
|
@@ -331,7 +378,19 @@ export async function cmdRole(root, args) {
|
|
|
331
378
|
if (!knownRoleIds(cfg).includes(id))
|
|
332
379
|
throw new Error(unknownRoleMessage(id, knownRoleIds(cfg)));
|
|
333
380
|
const base = baseRoleOf(id);
|
|
381
|
+
const cfgKey = `roles.${base}.instances`;
|
|
334
382
|
const n = flagValue(rest, "--instances") ?? "";
|
|
383
|
+
// Shape, before the write: a spelling that is not a plain integer but that JSON.parse
|
|
384
|
+
// would still read as a 1–8 count (an exponent like "2e0" → 2) would otherwise sail past
|
|
385
|
+
// both plainDecimalToJson and validateConfig's INSTANCE_COUNT integer check and silently
|
|
386
|
+
// write the misread — the same silent misread the cap and min-run-interval writes refuse.
|
|
387
|
+
// Every non-exponent spelling that JSON reads as an integer in range is already a plain
|
|
388
|
+
// integer, so the exponent form is exactly the silent-write set; every exponent — in range
|
|
389
|
+
// or out ("9e0" → 9 too) — is refused here, while fractional, hex, negative and plain
|
|
390
|
+
// out-of-range integer spellings fall through to the write path, whose validateConfig
|
|
391
|
+
// message names the key.
|
|
392
|
+
if (isExponentSpelling(n))
|
|
393
|
+
fail(`${cfgKey} must be a plain integer from 1 to 8 — no exponent notation`);
|
|
335
394
|
// An absent count already means 1 (instanceCount's default), so asking for 1 is a no-op —
|
|
336
395
|
// but only for a role that may split; any other role falls through to validateConfig's refusal.
|
|
337
396
|
// A tumwater.json that fails to load is no basis for "already": fall through to the failure.
|
|
@@ -387,6 +446,22 @@ export async function cmdRole(root, args) {
|
|
|
387
446
|
writeRoleConfigKey(root, base, clearThinking, level, `roles.${base}.thinking`, "thinking", (v) => JSON.stringify(v), (v) => v);
|
|
388
447
|
return;
|
|
389
448
|
}
|
|
449
|
+
// The backend write: the twin of the provider/model/thinking writes, landing the role's
|
|
450
|
+
// backend (`pi`, `claude`, `opencode` or `codex`) in roles.<base>.backend through the same
|
|
451
|
+
// setConfigKey/unsetConfigKey core, so validateConfig's checkEntryBackend owns the id
|
|
452
|
+
// (rejecting an unknown one, and a `model` that cannot pair with the named backend) and
|
|
453
|
+
// the write stays atomic. An instance id writes its base role's key, like the others.
|
|
454
|
+
if (backendWrite) {
|
|
455
|
+
if (clearBackend && rest.includes("--backend"))
|
|
456
|
+
fail("give --clear-backend or --backend, not both");
|
|
457
|
+
if (rest.includes("--json"))
|
|
458
|
+
fail("--json does not apply to a backend write");
|
|
459
|
+
requireKnownRole(root, id);
|
|
460
|
+
const base = baseRoleOf(id);
|
|
461
|
+
const backend = flagValue(rest, "--backend") ?? "";
|
|
462
|
+
writeRoleConfigKey(root, base, clearBackend, backend, `roles.${base}.backend`, "backend", (v) => JSON.stringify(v), (v) => v);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
390
465
|
// The enable/disable write: the scoped write for the enabled state `tumwater role <id>`
|
|
391
466
|
// already renders, the terminal twin of `config set roles.<base>.enabled true|false`. The
|
|
392
467
|
// value lands in roles.<base>.enabled through the same setConfigKey core config set uses, so
|
|
@@ -414,6 +489,36 @@ export async function cmdRole(root, args) {
|
|
|
414
489
|
say(`${base}: ${state}`);
|
|
415
490
|
return;
|
|
416
491
|
}
|
|
492
|
+
// The review-after-merge write: the one command the post-merge churn alert's remedy
|
|
493
|
+
// needs — add the role to, or remove it from, review.afterMerge for the base id, the
|
|
494
|
+
// membership test reviewedAfterMerge shares. The whole-list value lands through the same
|
|
495
|
+
// setConfigKey core `config set review.afterMerge <json>` uses, so validateConfig owns the
|
|
496
|
+
// string-array shape, the known-role-id check and the `review.afterMerge must not be
|
|
497
|
+
// "director"` refusal, and the write is atomic. A role already in the asked-for state is
|
|
498
|
+
// reported as such and the file is left alone (the section branch compares the value
|
|
499
|
+
// against the resolved list, so a no-op reports unchanged and writes nothing).
|
|
500
|
+
if (reviewAfterMergeWrite) {
|
|
501
|
+
if (reviewAfterMerge && clearReviewAfterMerge)
|
|
502
|
+
fail("give --review-after-merge or --clear-review-after-merge, not both");
|
|
503
|
+
if (rest.includes("--json"))
|
|
504
|
+
fail("--json does not apply to a review-after-merge write");
|
|
505
|
+
requireKnownRole(root, id);
|
|
506
|
+
const base = baseRoleOf(id);
|
|
507
|
+
const list = loadConfigSafe(root).config?.review.afterMerge ?? [];
|
|
508
|
+
const has = list.includes(base);
|
|
509
|
+
const next = has === reviewAfterMerge ? list : reviewAfterMerge ? [...list, base] : list.filter((r) => r !== base);
|
|
510
|
+
const result = setConfigKey(root, "review.afterMerge", JSON.stringify(next));
|
|
511
|
+
if (!result.ok)
|
|
512
|
+
fail(result.error);
|
|
513
|
+
say(result.unchanged
|
|
514
|
+
? reviewAfterMerge
|
|
515
|
+
? `${base}: already in review.afterMerge, so tumwater.json was not written`
|
|
516
|
+
: `${base}: not in review.afterMerge, so nothing was cleared`
|
|
517
|
+
: reviewAfterMerge
|
|
518
|
+
? `${base}: added to review.afterMerge`
|
|
519
|
+
: `${base}: removed from review.afterMerge`);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
417
522
|
// The unknown-role answer (exit 1, unknownRoleMessage's wording) lives in the
|
|
418
523
|
// collector; here the id just has to be non-null for the thunk's types.
|
|
419
524
|
sayJsonOrRender(rest, () => rolePayload(root, id), renderRoleMarkdown);
|
|
@@ -11,16 +11,23 @@ import { plural } from "../text/phrases.js";
|
|
|
11
11
|
import { SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
12
12
|
import { renderSummary } from "../history/history-render.js";
|
|
13
13
|
import { failOverDurationCap, failRivalShapes, flagValue, parseDurationFlag, } from "./cli-args.js";
|
|
14
|
+
import { parsePositiveInt } from "../text/text.js";
|
|
14
15
|
import { PAUSE_FOR_MAX_MS } from "../operator/operator-intent.js";
|
|
15
16
|
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
16
17
|
import { stateSkipReason } from "../runner/runner-state.js";
|
|
17
18
|
import { isTeamPaused, pausedRoles } from "../team/team-state.js";
|
|
18
19
|
import { runnerPaused } from "../roles/runner-ids.js";
|
|
19
20
|
/** The window start a generation adopts: the supervisor's RUN_WINDOW_START_ENV mark when it is a
|
|
20
|
-
* positive integer no later than `now`, else `now` (absent, malformed, or in the future).
|
|
21
|
+
* positive integer no later than `now`, else `now` (absent, malformed, or in the future). The
|
|
22
|
+
* mark is read through the shared plain-decimal rule (parsePositiveInt) rather than bare
|
|
23
|
+
* Number(), which would silently coerce a hex/exponent/signed spelling ("0x10" → 16, "1e3" →
|
|
24
|
+
* 1000) into a window start the supervisor never wrote — the same plain-decimal discipline the
|
|
25
|
+
* CLI count/position flags apply to every other string boundary. */
|
|
21
26
|
export function inheritedWindowStart(raw, now) {
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
if (raw === undefined)
|
|
28
|
+
return now;
|
|
29
|
+
const n = parsePositiveInt(raw);
|
|
30
|
+
return n !== null && n <= now ? n : now;
|
|
24
31
|
}
|
|
25
32
|
/** The `run --for` exit summary: a window line, then the `history --summary` table for the runs
|
|
26
33
|
* that ended since the window opened — every run counted once by its outcome class. Only the
|
|
@@ -16,6 +16,13 @@ import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
|
16
16
|
import { shortSha } from "../text/format.js";
|
|
17
17
|
import { readRunRowsSince, summarizeRunRows } from "../history/history-data.js";
|
|
18
18
|
import { ONCE_FAILURE_RESULTS, inheritedWindowStart, onceFailures, onceReport, onceSummary, parseRunWindow, renderOnceSummary, windowReport, windowSummary, } from "./cli-start-summary.js";
|
|
19
|
+
/** The env var that carries the dashboard gate token between a supervisor and its dashboard
|
|
20
|
+
* child (and across a re-exec'd dashboard generation), for the `run --gui`/`start --gui`
|
|
21
|
+
* supervisor's own use. Defined here rather than imported from gui-command.js so this module
|
|
22
|
+
* (and every `start`/`run` invocation) never pulls the GUI server graph onto the page; it
|
|
23
|
+
* must stay the same literal as gui-command's GUI_TOKEN_ENV, and a merge that renames one
|
|
24
|
+
* renames the other. */
|
|
25
|
+
const GUI_TOKEN_ENV = "TUMWATER_GUI_TOKEN";
|
|
19
26
|
export { ONCE_FAILURE_RESULTS, inheritedWindowStart, onceFailures, onceReport, onceSummary, parseRunWindow, renderOnceSummary, windowReport, windowSummary, };
|
|
20
27
|
/** `tumwater start`: boot the team and stream its event feed to this terminal until it stops.
|
|
21
28
|
* Three layers cooperate here: without SUPERVISED_ENV this process becomes the supervisor
|
|
@@ -64,6 +71,17 @@ export async function cmdStart(root, args) {
|
|
|
64
71
|
const tokenProblem = tokenValueProblem(args, "tumwater start --gui");
|
|
65
72
|
if (tokenProblem)
|
|
66
73
|
fail(tokenProblem);
|
|
74
|
+
// The supervisor (this process, when run without SUPERVISED_ENV) is long-lived and the OS
|
|
75
|
+
// process table (ps, /proc/<pid>/cmdline) is world-readable, so an operator-typed secret must
|
|
76
|
+
// not sit in this process's argv for the whole run. guiChildPlan has already extracted the
|
|
77
|
+
// token into `plan.guiToken` for the dashboard child's env (see superviseDashboard), so take
|
|
78
|
+
// `--token <value>` off the command line here — the env rides every spawn and reexecSelf
|
|
79
|
+
// re-run, so no dashboard child or reloaded generation loses the gate — and say it is gone.
|
|
80
|
+
const argvToken = process.argv.indexOf("--token");
|
|
81
|
+
if (argvToken >= 0) {
|
|
82
|
+
process.argv.splice(argvToken, 2);
|
|
83
|
+
say(`removed --token from this process's command line; prefer the ${GUI_TOKEN_ENV} env var`);
|
|
84
|
+
}
|
|
67
85
|
const forMs = parseRunWindow(args, once);
|
|
68
86
|
const branchArg = parseBranchFlag(args);
|
|
69
87
|
const startup = await runStartupCheck(root, branchArg);
|
|
@@ -328,12 +346,16 @@ export function guiChildPlan(runArgs) {
|
|
|
328
346
|
guiArgs.push("--port", String(port));
|
|
329
347
|
if (runArgs.includes("--all-interfaces"))
|
|
330
348
|
guiArgs.push("--all-interfaces");
|
|
331
|
-
|
|
332
|
-
|
|
349
|
+
// The token is returned separately (guiToken) and reaches the child through its env, never
|
|
350
|
+
// guiArgs/argv: a secret on the command line is world-readable (/proc/<pid>/cmdline) and is
|
|
351
|
+
// re-run verbatim by a self-reload onto each new generation. The skip set still drops the
|
|
352
|
+
// operator's --token pair from the orchestrator generation (its cmdStart would refuse a
|
|
353
|
+
// dashboard flag without --gui, and a self-redeploy hand-off must never forward it).
|
|
333
354
|
return {
|
|
334
355
|
spawnGui: runArgs.includes("--gui"),
|
|
335
356
|
spawnTui: runArgs.includes("--tui"),
|
|
336
357
|
guiArgs,
|
|
358
|
+
guiToken: tokenRaw,
|
|
337
359
|
orchestratorArgs: runArgs.filter((arg, i) => arg !== "--gui" && arg !== "--tui" && arg !== "--all-interfaces" && !skip.has(i)),
|
|
338
360
|
childStdio: runArgs.includes("--tui") ? ["ignore", "ignore", "ignore"] : "inherit",
|
|
339
361
|
};
|
|
@@ -344,11 +366,17 @@ export function guiChildPlan(runArgs) {
|
|
|
344
366
|
* redeploy and exits on its own if this supervisor dies outright (SIGKILL, a crash) — a killed
|
|
345
367
|
* supervisor never leaves a dashboard holding its port. Any other dashboard exit (a failed bind
|
|
346
368
|
* prints its own message and exits) ends this loop and leaves the team running. */
|
|
347
|
-
async function superviseDashboard(signal, stdio = "inherit", guiArgs = []) {
|
|
369
|
+
async function superviseDashboard(signal, guiToken, stdio = "inherit", guiArgs = []) {
|
|
348
370
|
const { DASHBOARD_CHILD_ENV } = await import("../redeploy/self-reload.js");
|
|
349
371
|
const { spawnGuiChild } = await import("../process/supervisor.js");
|
|
372
|
+
const env = { [DASHBOARD_CHILD_ENV]: String(process.pid) };
|
|
373
|
+
// The gate token rides in the child's env, not its argv: /proc/<pid>/cmdline is world-readable,
|
|
374
|
+
// and the child's self-reload (reexecSelf, which propagates env via childEnv) would otherwise
|
|
375
|
+
// re-run the secret onto each fresh generation.
|
|
376
|
+
if (guiToken !== undefined)
|
|
377
|
+
env[GUI_TOKEN_ENV] = guiToken;
|
|
350
378
|
for (;;) {
|
|
351
|
-
const exit = await spawnGuiChild(signal,
|
|
379
|
+
const exit = await spawnGuiChild(signal, env, stdio, guiArgs);
|
|
352
380
|
if (signal.aborted || exit.code !== RESTART_EXIT_CODE)
|
|
353
381
|
return;
|
|
354
382
|
}
|
|
@@ -409,7 +437,7 @@ async function superviseStartCommand(root, runArgs, branchArg, forMs) {
|
|
|
409
437
|
guiController.abort();
|
|
410
438
|
})
|
|
411
439
|
: Promise.resolve();
|
|
412
|
-
const gui = plan.spawnGui ? superviseDashboard(guiController.signal, plan.childStdio, plan.guiArgs) : Promise.resolve();
|
|
440
|
+
const gui = plan.spawnGui ? superviseDashboard(guiController.signal, plan.guiToken, plan.childStdio, plan.guiArgs) : Promise.resolve();
|
|
413
441
|
const code = await superviseStart({
|
|
414
442
|
spawnChild: (signal) => spawnStartChild(signal, plan.orchestratorArgs, windowEnv, plan.childStdio),
|
|
415
443
|
stopping: () => stopping,
|