tumwater 0.4.1 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/backend-traits.js +14 -0
- package/dist/src/agent/claude/claude-stream.js +27 -31
- package/dist/src/agent/codex/codex-rollout.js +38 -24
- package/dist/src/agent/codex/codex-stream.js +71 -38
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +4 -4
- package/dist/src/agent/run-agent.js +11 -2
- package/dist/src/agent/run-settle.js +22 -13
- package/dist/src/agent/run-spawn.js +3 -12
- package/dist/src/agent/run-stream.js +2 -10
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/backlog/backlog-archive.js +64 -165
- package/dist/src/backlog/backlog-eligibility.js +50 -58
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +2 -2
- package/dist/src/backlog/backlog-md.js +38 -27
- package/dist/src/backlog/backlog-release.js +11 -19
- package/dist/src/backlog/backlog-structure.js +14 -1
- package/dist/src/backlog/backlog-write.js +1 -1
- package/dist/src/backlog/backlog.js +5 -4
- package/dist/src/backlog/question-answer.js +30 -14
- package/dist/src/baseline/main-baseline.js +30 -20
- package/dist/src/baseline/main-guardrails.js +54 -17
- package/dist/src/baseline/main-watch.js +1 -1
- package/dist/src/budget/budget-wiring.js +0 -5
- package/dist/src/budget/budget.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +1 -4
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +27 -50
- package/dist/src/cli/cli-start.js +62 -12
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +6 -7
- package/dist/src/cli/log-commands.js +3 -1
- package/dist/src/cli.js +3 -7
- package/dist/src/collections.js +21 -7
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +19 -26
- package/dist/src/doctor/doctor-backlog-bugs.js +17 -115
- package/dist/src/doctor/doctor-backlog-plans.js +8 -8
- package/dist/src/doctor/doctor-backlog.js +22 -36
- package/dist/src/doctor/doctor-check-names.js +2 -2
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-contention-checks.js +1 -1
- package/dist/src/doctor/doctor-holds.js +13 -11
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +85 -48
- package/dist/src/doctor/doctor-project-checks.js +66 -17
- package/dist/src/doctor/doctor-role-notes.js +6 -24
- package/dist/src/doctor/doctor.js +6 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +5 -15
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/failure/failure-state-change.js +183 -68
- package/dist/src/failure/time-spend.js +1 -1
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/git/slot-salvage.js +5 -4
- package/dist/src/gui/gui-admin-endpoints.js +2 -3
- package/dist/src/gui/gui-backlog-commands.js +1 -1
- package/dist/src/gui/gui-backlog-endpoints.js +1 -7
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-operator-commands.js +0 -12
- package/dist/src/gui/gui-prompt-commands.js +26 -20
- package/dist/src/gui/gui-report-endpoints.js +2 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/history/history.js +102 -67
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/prompt-commands.js +24 -19
- package/dist/src/init/init.js +68 -15
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-post-review-drain.js +1 -1
- package/dist/src/operator/operator-commands.js +68 -47
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +0 -6
- package/dist/src/pi/pi-watchdogs.js +20 -1
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/process/process.js +23 -0
- package/dist/src/process/supervisor.js +17 -1
- package/dist/src/prompt/gate-notes.js +9 -9
- package/dist/src/prompt/notes-backlog.js +11 -24
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +4 -5
- package/dist/src/report/report-data.js +10 -225
- package/dist/src/report/report-fold.js +20 -295
- package/dist/src/report/report-main-red.js +45 -42
- package/dist/src/report/report-render.js +66 -39
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +3 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +9 -9
- package/dist/src/review/quarantine-render.js +2 -1
- package/dist/src/roles/role-catalog.js +18 -30
- package/dist/src/roles/role-guidance.js +18 -43
- package/dist/src/roles/role-view.js +1 -2
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-detail-data.js +23 -7
- package/dist/src/run/run-detail-render.js +6 -0
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-prompt.js +100 -56
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +4 -21
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +8 -73
- package/dist/src/run/stage-backlog-checks.js +2 -29
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-run.js +2 -79
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +3 -10
- package/dist/src/status/status-data.js +5 -9
- package/dist/src/status/status-role-notes.js +18 -14
- package/dist/src/status/status-roles.js +2 -35
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +7 -212
- package/dist/src/ui/badges.js +2 -3
- package/dist/src/ui/gui/gui-client-boot.js +19 -4
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-report-chart.js +10 -8
- package/dist/src/ui/gui/gui-client-report.js +1 -2
- package/dist/src/ui/gui/gui-client-settings.js +0 -23
- package/dist/src/ui/gui/gui-client-team.js +26 -0
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/status-model.js +2 -2
- package/dist/src/ui/status-payload.js +3 -3
- package/dist/src/ui/status-render.js +3 -3
- package/dist/src/ui/team-alert-backlog.js +23 -54
- package/dist/src/ui/team-alert-shared.js +23 -10
- package/dist/src/ui/team-alert-status.js +8 -3
- package/dist/src/ui/team-alert-tail.js +1 -14
- package/dist/src/ui/team-alerts.js +6 -17
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +105 -93
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-pause.js +209 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +44 -34
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +121 -108
- package/dist/src/ui/tui/tui-editor-release.js +50 -42
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +58 -46
- package/dist/src/ui/tui/tui-editor-role-key.js +49 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-frame.js +1 -1
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-health.js +3 -24
- package/dist/src/ui/tui/tui-pane-history.js +20 -24
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +3 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +151 -128
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +2 -3
- package/dist/src/ui/tui/tui.js +3 -3
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +30 -6
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- package/dist/src/ui/team-alert-backlog-feed.js +0 -181
- package/dist/src/ui/team-alert-merge.js +0 -183
|
@@ -247,19 +247,19 @@ const PROMPT_FLAG_SPECS = [
|
|
|
247
247
|
{ names: ["--attach"], value: true, valueName: "<path>" },
|
|
248
248
|
{ names: ["--grep"], value: true, valueName: "<text>" },
|
|
249
249
|
];
|
|
250
|
-
/**
|
|
251
|
-
*
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
*
|
|
262
|
-
|
|
250
|
+
/** The six `--list`/`--cancel`/`--edit`/`--top` mode pairs that may not combine, in today's
|
|
251
|
+
* exact order. */
|
|
252
|
+
const PROMPT_MODE_EXCLUSIONS = [
|
|
253
|
+
["--list", "--cancel"],
|
|
254
|
+
["--list", "--edit"],
|
|
255
|
+
["--cancel", "--edit"],
|
|
256
|
+
["--list", "--top"],
|
|
257
|
+
["--cancel", "--top"],
|
|
258
|
+
["--edit", "--top"],
|
|
259
|
+
];
|
|
260
|
+
/** The flag-vocabulary gate and the once-check: the unknown-double-dash gate, the ten index
|
|
261
|
+
* collections and rejectDuplicateFlags's once-check all ride here, one scan, one home. */
|
|
262
|
+
function scanPromptFlags(args) {
|
|
263
263
|
// The unknown-double-dash gate reads PROMPT_FLAG_SPECS, so the list its error names admits
|
|
264
264
|
// exactly what the duplicate-flag check below accepts — one vocabulary, one message renderer.
|
|
265
265
|
rejectUnknownDoubleDash("prompt", args, PROMPT_FLAG_SPECS);
|
|
@@ -276,21 +276,30 @@ export function parsePromptArgs(args) {
|
|
|
276
276
|
// --attach is repeatable: every occurrence claims itself and its value token, like --role's
|
|
277
277
|
// single pair, so the pairs ride out of the prompt text in every mode.
|
|
278
278
|
const attachFlags = args.flatMap((a, i) => (a === "--attach" ? [i] : []));
|
|
279
|
+
const roleFlag = args.indexOf("--role");
|
|
279
280
|
const known = PROMPT_FLAG_SPECS.flatMap((s) => s.names).filter((f) => f !== "--attach"); // repeatable: the once-check skips it
|
|
280
281
|
rejectDuplicateFlags(args, known);
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
282
|
+
return { jsonFlags, listFlag, cancelFlag, editFlag, topFlag, fileFlag, atFlag, grepFlag, attachFlags, roleFlag };
|
|
283
|
+
}
|
|
284
|
+
/** The six mode mutual exclusions: the mutually-exclusive `--list`/`--cancel`/`--edit`/
|
|
285
|
+
* `--top` pairs, in the table's order, so the first failing pair reports as it did before. */
|
|
286
|
+
function assertPromptModesDisjoint(scan) {
|
|
287
|
+
const index = {
|
|
288
|
+
"--list": scan.listFlag,
|
|
289
|
+
"--cancel": scan.cancelFlag,
|
|
290
|
+
"--edit": scan.editFlag,
|
|
291
|
+
"--top": scan.topFlag,
|
|
292
|
+
};
|
|
293
|
+
for (const [a, b] of PROMPT_MODE_EXCLUSIONS) {
|
|
294
|
+
// All four keys are always present in the object; ?? -1 just satisfies the strict
|
|
295
|
+
// indexed-access typing without changing the (never-undefined) compare.
|
|
296
|
+
if ((index[a] ?? -1) >= 0 && (index[b] ?? -1) >= 0)
|
|
297
|
+
fail(`${a} and ${b} are mutually exclusive`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** The `--role <id>` value pair: its raw value, its missing-value refusal and the pair it
|
|
301
|
+
* claims, verbatim. */
|
|
302
|
+
function parsePromptRole(args, roleFlag) {
|
|
294
303
|
const roleRaw = roleFlag >= 0 ? args[roleFlag + 1] : undefined;
|
|
295
304
|
if (roleFlag >= 0 && (!roleRaw || roleRaw.startsWith("--")))
|
|
296
305
|
fail(ROLE_VALUE_ERROR);
|
|
@@ -298,17 +307,30 @@ export function parsePromptArgs(args) {
|
|
|
298
307
|
// The flag and its value are never prompt content: --role is a scope, not text. Only claimed
|
|
299
308
|
// when actually present — negative indexes would over-claim real tokens.
|
|
300
309
|
const roleClaim = roleFlag >= 0 ? [roleFlag, roleFlag + 1] : [];
|
|
310
|
+
return { role, roleClaim };
|
|
311
|
+
}
|
|
312
|
+
/** The `--json` list-only refusal: in enqueue or cancel mode it must never silently ride
|
|
313
|
+
* along as prompt text (or beside a state change). */
|
|
314
|
+
function assertPromptJsonListOnly(scan) {
|
|
301
315
|
// --json is list-only: in enqueue or cancel mode it must never silently ride along as
|
|
302
316
|
// prompt text (or beside a state change), so it is refused before either branch runs.
|
|
303
|
-
if (jsonFlags.length > 0 && listFlag < 0)
|
|
317
|
+
if (scan.jsonFlags.length > 0 && scan.listFlag < 0)
|
|
304
318
|
fail("--json only applies to --list");
|
|
305
|
-
|
|
319
|
+
}
|
|
320
|
+
/** The `--grep <text>` value pair and its list-only refusal, verbatim. */
|
|
321
|
+
function parsePromptGrep(args, scan) {
|
|
322
|
+
if (scan.grepFlag >= 0 && scan.listFlag < 0)
|
|
306
323
|
fail("--grep only applies to --list");
|
|
307
|
-
const grepRaw = grepFlag >= 0 ? args[grepFlag + 1] : undefined;
|
|
308
|
-
if (grepFlag >= 0 && (!grepRaw || grepRaw.startsWith("--"))) {
|
|
324
|
+
const grepRaw = scan.grepFlag >= 0 ? args[scan.grepFlag + 1] : undefined;
|
|
325
|
+
if (scan.grepFlag >= 0 && (!grepRaw || grepRaw.startsWith("--"))) {
|
|
309
326
|
fail("--grep needs text (usage: tumwater prompt --list --grep <text>)");
|
|
310
327
|
}
|
|
311
|
-
const grepClaim = grepFlag >= 0 ? [grepFlag, grepFlag + 1] : [];
|
|
328
|
+
const grepClaim = scan.grepFlag >= 0 ? [scan.grepFlag, scan.grepFlag + 1] : [];
|
|
329
|
+
return { grepRaw, grepClaim };
|
|
330
|
+
}
|
|
331
|
+
/** The queue-only-mode refusals: --file and --at have nothing to mean except in the enqueue
|
|
332
|
+
* mode, and --attach is refused by --list and --cancel (--edit accepts it). */
|
|
333
|
+
function assertPromptQueueOnlyFlags(scan) {
|
|
312
334
|
// The queue-only flags --file and --at have nothing to mean in the read-only and destructive
|
|
313
335
|
// modes (--list and --cancel): --file is the file-shaped twin of free-form text (--edit
|
|
314
336
|
// accepts it as its replacement text, so the editFlag test below exempts that one mode), and
|
|
@@ -318,16 +340,16 @@ export function parsePromptArgs(args) {
|
|
|
318
340
|
// predicate and the `<flag> only queues a prompt` wording, so a new mode or queue-only flag
|
|
319
341
|
// is added here.
|
|
320
342
|
const refuseQueueOnlyFlag = (flag, present) => {
|
|
321
|
-
if (present && (listFlag >= 0 || cancelFlag >= 0 || editFlag >= 0 || topFlag >= 0))
|
|
343
|
+
if (present && (scan.listFlag >= 0 || scan.cancelFlag >= 0 || scan.editFlag >= 0 || scan.topFlag >= 0))
|
|
322
344
|
fail(`${flag} only queues a prompt`);
|
|
323
345
|
};
|
|
324
|
-
refuseQueueOnlyFlag("--file", fileFlag >= 0 && editFlag < 0);
|
|
325
|
-
refuseQueueOnlyFlag("--at", atFlag >= 0);
|
|
326
|
-
refuseQueueOnlyFlag("--attach", attachFlags.length > 0 && editFlag < 0);
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
346
|
+
refuseQueueOnlyFlag("--file", scan.fileFlag >= 0 && scan.editFlag < 0);
|
|
347
|
+
refuseQueueOnlyFlag("--at", scan.atFlag >= 0);
|
|
348
|
+
refuseQueueOnlyFlag("--attach", scan.attachFlags.length > 0 && scan.editFlag < 0);
|
|
349
|
+
}
|
|
350
|
+
/** The `--attach <path>` value pair(s): every occurrence claims itself and its value token,
|
|
351
|
+
* with the missing-value refusal, verbatim. */
|
|
352
|
+
function parsePromptAttachments(args, attachFlags) {
|
|
331
353
|
// Each --attach pair claims the flag and its value; a trailing flag with no value names the
|
|
332
354
|
// flag here, like --cancel's missing-value wording, before any queue write.
|
|
333
355
|
const attachPaths = attachFlags.map((i) => args[i + 1]);
|
|
@@ -336,6 +358,14 @@ export function parsePromptArgs(args) {
|
|
|
336
358
|
if (args[i + 1] === undefined)
|
|
337
359
|
fail(`--attach needs a path (e.g. \`--attach shot.png\`)`);
|
|
338
360
|
}
|
|
361
|
+
return { attachPaths, attachClaims };
|
|
362
|
+
}
|
|
363
|
+
/** The `--at <duration>` parse and its 90-day cap, verbatim. */
|
|
364
|
+
function parsePromptDeferral(args, atFlag) {
|
|
365
|
+
// An edit keeps the target prompt's existing deferral exactly as it was (the marker is
|
|
366
|
+
// plumbing the edit carries over), so a --at alongside it has nothing to mean.
|
|
367
|
+
// The pair is claimed like --role's: the flag and its value are never prompt content.
|
|
368
|
+
const atClaim = atFlag >= 0 ? [atFlag, atFlag + 1] : [];
|
|
339
369
|
let atDelayMs = null;
|
|
340
370
|
if (atFlag >= 0) {
|
|
341
371
|
atDelayMs = parseDurationFlag("--at", args[atFlag + 1]);
|
|
@@ -344,75 +374,146 @@ export function parsePromptArgs(args) {
|
|
|
344
374
|
// (about 106751991d from now) made notBeforeMarker's toISOString throw a raw RangeError.
|
|
345
375
|
failOverDurationCap("--at", atDelayMs, PAUSE_FOR_MAX_MS);
|
|
346
376
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
377
|
+
return { atDelayMs, atClaim };
|
|
378
|
+
}
|
|
379
|
+
/** `tumwater prompt`'s flag scan and the checks it owns — everything shared by two or more
|
|
380
|
+
* modes, so a new mode or queue-only flag is added here: the unknown-double-dash gate, the
|
|
381
|
+
* index collection, the once-check, the six mutual exclusions, the --role pair, the
|
|
382
|
+
* --json/--grep list-only refusals, the queue-only-flag refusals, the --attach pairs and the
|
|
383
|
+
* --at parse and cap. Each job lives in one helper — scanPromptFlags, assertPromptModesDisjoint,
|
|
384
|
+
* parsePromptRole, assertPromptJsonListOnly, parsePromptGrep, assertPromptQueueOnlyFlags,
|
|
385
|
+
* parsePromptAttachments and parsePromptDeferral — so a check is found by name. Returns a
|
|
386
|
+
* struct the mode branches read. */
|
|
387
|
+
function parsePromptFlags(args) {
|
|
388
|
+
const scan = scanPromptFlags(args);
|
|
389
|
+
assertPromptModesDisjoint(scan);
|
|
390
|
+
const { role, roleClaim } = parsePromptRole(args, scan.roleFlag);
|
|
391
|
+
assertPromptJsonListOnly(scan);
|
|
392
|
+
const { grepRaw, grepClaim } = parsePromptGrep(args, scan);
|
|
393
|
+
assertPromptQueueOnlyFlags(scan);
|
|
394
|
+
const { attachPaths, attachClaims } = parsePromptAttachments(args, scan.attachFlags);
|
|
395
|
+
const { atDelayMs, atClaim } = parsePromptDeferral(args, scan.atFlag);
|
|
396
|
+
return {
|
|
397
|
+
role,
|
|
398
|
+
jsonFlags: scan.jsonFlags,
|
|
399
|
+
listFlag: scan.listFlag,
|
|
400
|
+
cancelFlag: scan.cancelFlag,
|
|
401
|
+
editFlag: scan.editFlag,
|
|
402
|
+
topFlag: scan.topFlag,
|
|
403
|
+
fileFlag: scan.fileFlag,
|
|
404
|
+
grepRaw,
|
|
405
|
+
grepClaim,
|
|
406
|
+
roleClaim,
|
|
407
|
+
attachPaths,
|
|
408
|
+
attachClaims,
|
|
409
|
+
atDelayMs,
|
|
410
|
+
atClaim,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** `--list`: the queue listing, optionally machine-readable and filtered by --grep. */
|
|
414
|
+
function listPromptArgs(args, f) {
|
|
415
|
+
failStrayArg(args, "with --list there is no prompt text", f.listFlag, ...f.jsonFlags, ...f.roleClaim, ...f.grepClaim);
|
|
416
|
+
return { mode: "list", role: f.role, json: f.jsonFlags.length > 0, grep: f.grepRaw ?? null };
|
|
417
|
+
}
|
|
418
|
+
/** `--cancel <n>`: remove one entry by its 1-based queue position. */
|
|
419
|
+
function cancelPromptArgs(args, f) {
|
|
420
|
+
const n = parsePositionFlag("--cancel", args[f.cancelFlag + 1], "--cancel 2");
|
|
421
|
+
// The position is the only token --cancel may carry; anything else alongside it would be
|
|
422
|
+
// prompt text, and this mode has none.
|
|
423
|
+
failStrayArg(args, "with --cancel there is no prompt text", f.cancelFlag, f.cancelFlag + 1, ...f.roleClaim);
|
|
424
|
+
return { mode: "cancel", role: f.role, position: n };
|
|
425
|
+
}
|
|
426
|
+
/** `--edit <n>`: rewrite one entry's text in place — the replacement is the --file contents,
|
|
427
|
+
* the tokens after the position, or (with neither) an editor open in the caller. */
|
|
428
|
+
function editPromptArgs(args, f) {
|
|
429
|
+
const n = parsePositionFlag("--edit", args[f.editFlag + 1], '--edit 2 "new text"');
|
|
430
|
+
// The --edit pair, the --role pair, and any --attach pairs are the only flags this mode
|
|
431
|
+
// claims. Tokens preceding --edit (other than a claimed flag) would be silently dropped —
|
|
432
|
+
// a mistyped command would edit prompt N with only the trailing fragment — so they are
|
|
433
|
+
// refused here by the same stray-argument wording the sibling modes use; tokens after the
|
|
434
|
+
// position are the replacement text, minus any claimed attach pair (none: edit in $EDITOR).
|
|
435
|
+
const fileClaim = f.fileFlag >= 0 ? [f.fileFlag, f.fileFlag + 1] : [];
|
|
436
|
+
for (let i = 0; i < f.editFlag; i++) {
|
|
437
|
+
if (f.roleClaim.includes(i) || f.attachClaims.includes(i) || fileClaim.includes(i))
|
|
438
|
+
continue;
|
|
439
|
+
fail(`unexpected argument ${JSON.stringify(args[i])} — with --edit the replacement text comes after the position`);
|
|
387
440
|
}
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
//
|
|
391
|
-
|
|
392
|
-
failStrayArg(args, "with --
|
|
393
|
-
return { mode: "
|
|
441
|
+
if (f.fileFlag >= 0) {
|
|
442
|
+
// The replacement comes from the file (`-` reads stdin), kept line for line; editPromptAtFile
|
|
443
|
+
// trims it. Typed text beside it would be ambiguous, so it is refused.
|
|
444
|
+
const file = promptFileValue(args[f.fileFlag + 1]);
|
|
445
|
+
failStrayArg(args, "with --file the replacement text comes from the file", f.editFlag, f.editFlag + 1, ...fileClaim, ...f.roleClaim, ...f.attachClaims);
|
|
446
|
+
return { mode: "edit", role: f.role, position: n, text: readPromptFileArg(file), attachPaths: f.attachPaths };
|
|
394
447
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
// drained pipe would see an empty prompt (readFileSync(0) fails with EAGAIN on a TTY's
|
|
401
|
-
// stdin; errorMessage names it, and the operator passes a real pipe instead).
|
|
402
|
-
const file = promptFileValue(args[fileFlag + 1]);
|
|
403
|
-
failStrayArg(args, "with --file the prompt comes from the file", fileFlag, fileFlag + 1, ...roleClaim, ...atClaim, ...attachClaims);
|
|
404
|
-
const contents = readPromptFileArg(file);
|
|
405
|
-
return { mode: "enqueue", role, text: contents, atDelayMs, attachPaths: attachPaths };
|
|
448
|
+
const textTokens = [];
|
|
449
|
+
for (let i = f.editFlag + 2; i < args.length; i++) {
|
|
450
|
+
if (f.roleClaim.includes(i) || f.attachClaims.includes(i))
|
|
451
|
+
continue;
|
|
452
|
+
textTokens.push(args[i]);
|
|
406
453
|
}
|
|
454
|
+
const text = textTokens.join(" ").trim();
|
|
455
|
+
// No text and no --file: the caller opens the prompt's current body in the editor.
|
|
456
|
+
return { mode: "edit", role: f.role, position: n, text: text === "" ? null : text, attachPaths: f.attachPaths };
|
|
457
|
+
}
|
|
458
|
+
/** `--top <n>`: move one entry to the head of its role's queue. */
|
|
459
|
+
function topPromptArgs(args, f) {
|
|
460
|
+
const n = parsePositionFlag("--top", args[f.topFlag + 1], "--top 2");
|
|
461
|
+
// The position is the only token --top may carry; anything else alongside it would be
|
|
462
|
+
// prompt text, and this mode has none.
|
|
463
|
+
failStrayArg(args, "with --top there is no prompt text", f.topFlag, f.topFlag + 1, ...f.roleClaim);
|
|
464
|
+
return { mode: "top", role: f.role, position: n };
|
|
465
|
+
}
|
|
466
|
+
/** `--file <path>`: enqueue the file's (or stdin's) whole contents as the prompt. */
|
|
467
|
+
function filePromptArgs(args, f) {
|
|
468
|
+
// The file is the prompt: read it whole (a path of `-` reads stdin, so a pipe or heredoc
|
|
469
|
+
// works), following parseInitArgs' --file branch — the same claim/stray/read/empty shape,
|
|
470
|
+
// so the two commands speak one idiom. The stdin read is memoized at module level: cli.ts
|
|
471
|
+
// pre-parses prompt's args and cmdPrompt re-parses them, and a second readFileSync(0) on a
|
|
472
|
+
// drained pipe would see an empty prompt (readFileSync(0) fails with EAGAIN on a TTY's
|
|
473
|
+
// stdin; errorMessage names it, and the operator passes a real pipe instead).
|
|
474
|
+
const file = promptFileValue(args[f.fileFlag + 1]);
|
|
475
|
+
failStrayArg(args, "with --file the prompt comes from the file", f.fileFlag, f.fileFlag + 1, ...f.roleClaim, ...f.atClaim, ...f.attachClaims);
|
|
476
|
+
const contents = readPromptFileArg(file);
|
|
477
|
+
return { mode: "enqueue", role: f.role, text: contents, atDelayMs: f.atDelayMs, attachPaths: f.attachPaths };
|
|
478
|
+
}
|
|
479
|
+
/** The default mode: free-form text (minus the claimed --role/--at/--attach tokens) is joined
|
|
480
|
+
* and enqueued. */
|
|
481
|
+
function enqueuePromptArgs(args, f) {
|
|
407
482
|
// Everything except the --role pair and the --at pair is prompt text; the join keeps
|
|
408
483
|
// multi-word requests as one string exactly like the pre-1/2 behavior.
|
|
409
484
|
const text = args
|
|
410
|
-
.filter((_, i) => !roleClaim.includes(i) && !atClaim.includes(i) && !attachClaims.includes(i))
|
|
485
|
+
.filter((_, i) => !f.roleClaim.includes(i) && !f.atClaim.includes(i) && !f.attachClaims.includes(i))
|
|
411
486
|
.join(" ")
|
|
412
487
|
.trim();
|
|
413
488
|
// Name the fix, like the sibling operator commands' usage errors (bug/plan carry their
|
|
414
489
|
// BUG_USAGE/PLAN_USAGE lines): "prompt text required" alone never said what to type.
|
|
415
490
|
if (!text)
|
|
416
491
|
fail('prompt text required — usage: tumwater prompt "<text>" (add --role <id> to aim it at one role)');
|
|
417
|
-
return { mode: "enqueue", role, text, atDelayMs, attachPaths: attachPaths };
|
|
492
|
+
return { mode: "enqueue", role: f.role, text, atDelayMs: f.atDelayMs, attachPaths: f.attachPaths };
|
|
493
|
+
}
|
|
494
|
+
/** `tumwater prompt` argument handling, following parseInitArgs' pattern. Like init's,
|
|
495
|
+
* positionals are free-form prompt content — but a double-dash token must be a real flag
|
|
496
|
+
* (`--role <id>`, `--list`, `--json`, `--grep <text>`, `--cancel <n>`, `--edit <n>`,
|
|
497
|
+
* `--top <n>`, `--file <path>`, `--at <duration>`), or
|
|
498
|
+
* it would be baked into the queued prompt (the same class of bug parseInitArgs fixed: today
|
|
499
|
+
* `tumwater prompt --foo text` enqueues "--foo text"). Single-dash positionals remain prompt
|
|
500
|
+
* content. `--list`, `--cancel`, `--edit` and `--top` are mutually exclusive and may
|
|
501
|
+
* not combine with positional text; `--role` is accepted in every mode and is never
|
|
502
|
+
* prompt content. `--at <duration>` (PLANS.md "tumwater prompt --at <duration>") only queues
|
|
503
|
+
* a prompt: it defers delivery — parsed here with parseDurationFlag, riding the enqueue as
|
|
504
|
+
* the queue file's not-before marker — so the read-only and destructive modes refuse it
|
|
505
|
+
* rather than silently ignore it. */
|
|
506
|
+
export function parsePromptArgs(args) {
|
|
507
|
+
const f = parsePromptFlags(args);
|
|
508
|
+
if (f.listFlag >= 0)
|
|
509
|
+
return listPromptArgs(args, f);
|
|
510
|
+
if (f.cancelFlag >= 0)
|
|
511
|
+
return cancelPromptArgs(args, f);
|
|
512
|
+
if (f.editFlag >= 0)
|
|
513
|
+
return editPromptArgs(args, f);
|
|
514
|
+
if (f.topFlag >= 0)
|
|
515
|
+
return topPromptArgs(args, f);
|
|
516
|
+
if (f.fileFlag >= 0)
|
|
517
|
+
return filePromptArgs(args, f);
|
|
518
|
+
return enqueuePromptArgs(args, f);
|
|
418
519
|
}
|
|
@@ -257,11 +257,8 @@ export function hasFollowFlag(args) {
|
|
|
257
257
|
/** The `--ready` flag spec, accepted by `backlog`: keep only the entries no hold keeps from
|
|
258
258
|
* being picked. Value-less, like STAT_FLAG. */
|
|
259
259
|
export const READY_FLAG = { names: ["--ready"] };
|
|
260
|
-
/** Force re-probing of every claimed image-input model in `tumwater doctor` (part 2/5),
|
|
261
|
-
* bypassing the 7-day cache: `--reprobe` (a value-less flag like JSON_FLAG). */
|
|
262
|
-
export const REPROBE_FLAG = { names: ["--reprobe"] };
|
|
263
260
|
/** The `--problems` flag spec, accepted by `doctor`: print only the warn and fail rows. Value-less,
|
|
264
|
-
* like
|
|
261
|
+
* like KILL_ORPHANS_FLAG. */
|
|
265
262
|
export const PROBLEMS_FLAG = { names: ["--problems"] };
|
|
266
263
|
/** The `--kill-orphans` flag spec, accepted by `doctor`: first kill the orphaned processes the
|
|
267
264
|
* orphans check lists, each with its descendants. Value-less, like PROBLEMS_FLAG. */
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* `readPostReviews` reads a missing, torn or hand-edited file as the well-formed subset (or `[]`),
|
|
5
5
|
* so the command prints the empty line in any directory. Read-only except the one destructive
|
|
6
6
|
* `--accept <n>` mode, which removes the Nth entry the operator deliberately accepts unreviewed
|
|
7
|
-
* (the action the at-risk
|
|
7
|
+
* (the action the doctor's at-risk post-review row's advice names); that mode adds no new
|
|
8
|
+
* ready-repo requirement to
|
|
8
9
|
* the read path and gates the write with `requireReadyRepo`. Unlike `cmdQueue`'s `--cancel`, the
|
|
9
10
|
* read case still writes no file. */
|
|
10
11
|
import { JSON_FLAG, ROLE_FLAG, rejectUnknownArgs, statusFlagSpec } from "./cli-flag-specs.js";
|
|
@@ -17,7 +17,7 @@ import { renderRoleMarkdown } from "../roles/role-render.js";
|
|
|
17
17
|
import { DIRECTOR_ROLE, unknownRoleMessage } from "../roles/roles.js";
|
|
18
18
|
import { saveRoleNote } from "../agent/role-notes-write.js";
|
|
19
19
|
import { roleNotesPath } from "../paths.js";
|
|
20
|
-
import { errorMessage } from "../text/text.js";
|
|
20
|
+
import { errorMessage, isExponentSpelling } from "../text/text.js";
|
|
21
21
|
import { readPromptFileArg } from "./cli-command-args.js";
|
|
22
22
|
import { canonicalRoleId } from "../roles/legacy-role-ids.js";
|
|
23
23
|
import { INSTANCE_ROLES, baseRoleOf } from "../roles/runner-ids.js";
|
|
@@ -44,12 +44,6 @@ 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
|
-
}
|
|
53
47
|
/** `role`'s min-run-interval write flags: clear it, or set it to a second count (0 is
|
|
54
48
|
* back-to-back). */
|
|
55
49
|
const CLEAR_MIN_RUN_INTERVAL_SPEC = { names: ["--clear-min-run-interval"] };
|
|
@@ -327,7 +321,7 @@ export async function cmdRole(root, args) {
|
|
|
327
321
|
// already a plain decimal, so the exponent form is exactly the silent-write set; hex,
|
|
328
322
|
// negative and non-numeric spellings fall through to the write path, whose
|
|
329
323
|
// validateConfig message names the key.
|
|
330
|
-
if (isExponentSpelling(cap))
|
|
324
|
+
if (isExponentSpelling(cap.trim()))
|
|
331
325
|
fail(`${cfgKey} must be a plain dollar amount like 25, 12.50, or .5 — no +/-, exponent, or hex notation`);
|
|
332
326
|
writeRoleConfigKey(root, base, clearCap, cap, cfgKey, "daily cap", (v) => plainDecimalToJson(v) ?? v, (v) => v);
|
|
333
327
|
return;
|
|
@@ -354,7 +348,7 @@ export async function cmdRole(root, args) {
|
|
|
354
348
|
// plain decimal, so the exponent form is exactly the silent-write set; hex, negative and
|
|
355
349
|
// non-numeric spellings fall through to the write path, whose validateConfig message
|
|
356
350
|
// names the key.
|
|
357
|
-
if (isExponentSpelling(secs))
|
|
351
|
+
if (isExponentSpelling(secs.trim()))
|
|
358
352
|
fail(`${cfgKey} must be a plain second count like 300, 0.5, or .5 — no +/-, exponent, or hex notation`);
|
|
359
353
|
writeRoleConfigKey(root, base, clearInterval, secs, cfgKey, "min run interval", (v) => plainDecimalToJson(v) ?? v, (v) => `${v}s`);
|
|
360
354
|
return;
|
|
@@ -384,7 +378,7 @@ export async function cmdRole(root, args) {
|
|
|
384
378
|
// or out ("9e0" → 9 too) — is refused here, while fractional, hex, negative and plain
|
|
385
379
|
// out-of-range integer spellings fall through to the write path, whose validateConfig
|
|
386
380
|
// message names the key.
|
|
387
|
-
if (isExponentSpelling(n))
|
|
381
|
+
if (isExponentSpelling(n.trim()))
|
|
388
382
|
fail(`${cfgKey} must be a plain integer from 1 to 8 — no exponent notation`);
|
|
389
383
|
// An absent count already means 1 (instanceCount's default), so asking for 1 is a no-op —
|
|
390
384
|
// but only for a role that may split; any other role falls through to validateConfig's refusal.
|
|
@@ -396,47 +390,30 @@ export async function cmdRole(root, args) {
|
|
|
396
390
|
writeRoleConfigKey(root, base, clearInstances, n, `roles.${base}.instances`, "instances", (v) => v, (v) => v);
|
|
397
391
|
return;
|
|
398
392
|
}
|
|
399
|
-
// The model-
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
return;
|
|
420
|
-
}
|
|
421
|
-
if (thinkingWrite) {
|
|
422
|
-
rejectSetClearAndJson(rest, "--thinking", "--clear-thinking", "a thinking write");
|
|
423
|
-
requireKnownRole(root, id);
|
|
424
|
-
const base = baseRoleOf(id);
|
|
425
|
-
const level = flagValue(rest, "--thinking") ?? "";
|
|
426
|
-
writeRoleConfigKey(root, base, clearThinking, level, `roles.${base}.thinking`, "thinking", (v) => JSON.stringify(v), (v) => v);
|
|
427
|
-
return;
|
|
428
|
-
}
|
|
429
|
-
// The backend write: the twin of the provider/model/thinking writes, landing the role's
|
|
430
|
-
// backend (`pi`, `claude`, `opencode` or `codex`) in roles.<base>.backend through the same
|
|
431
|
-
// setConfigKey/unsetConfigKey core, so validateConfig's checkEntryBackend owns the id
|
|
432
|
-
// (rejecting an unknown one, and a `model` that cannot pair with the named backend) and
|
|
433
|
-
// the write stays atomic. An instance id writes its base role's key, like the others.
|
|
434
|
-
if (backendWrite) {
|
|
435
|
-
rejectSetClearAndJson(rest, "--backend", "--clear-backend", "a backend write");
|
|
393
|
+
// The model-writing branches — provider, model, thinking and backend — are one shape: each
|
|
394
|
+
// lands the role's field in roles.<base>.<field> (the terminal twin of `config set
|
|
395
|
+
// roles.<base>.<field> <value>` and `config unset`) through the same setConfigKey/unsetConfigKey
|
|
396
|
+
// core, so validateConfig owns the id — checkModelTripleField ("tier-name" shape: model
|
|
397
|
+
// accepts a tier name, thinking a THINKING_LEVELS level) for the provider/model/thinking
|
|
398
|
+
// fields that configForRole resolves through seamOverrides, checkEntryBackend for backend
|
|
399
|
+
// (`pi`, `claude`, `opencode` or `codex`, refusing an unknown one and a model that cannot
|
|
400
|
+
// pair with it) — and each write is atomic. No director refusal: each is accepted exactly as
|
|
401
|
+
// --min-run-interval is, and an instance id writes its base role's key. Every branch calls
|
|
402
|
+
// writeRoleConfigKey with the same JSON-encode/identity shaping, differing only in the flag
|
|
403
|
+
// spellings, the key field and the noun the confirmation line names — one table, one body.
|
|
404
|
+
for (const { flag, clearFlag, write, clear, field, noun } of [
|
|
405
|
+
{ flag: "--provider", clearFlag: "--clear-provider", write: providerWrite, clear: clearProvider, field: "provider", noun: "provider" },
|
|
406
|
+
{ flag: "--model", clearFlag: "--clear-model", write: modelWrite, clear: clearModel, field: "model", noun: "model" },
|
|
407
|
+
{ flag: "--thinking", clearFlag: "--clear-thinking", write: thinkingWrite, clear: clearThinking, field: "thinking", noun: "thinking" },
|
|
408
|
+
{ flag: "--backend", clearFlag: "--clear-backend", write: backendWrite, clear: clearBackend, field: "backend", noun: "backend" },
|
|
409
|
+
]) {
|
|
410
|
+
if (!write)
|
|
411
|
+
continue;
|
|
412
|
+
rejectSetClearAndJson(rest, flag, clearFlag, `a ${noun} write`);
|
|
436
413
|
requireKnownRole(root, id);
|
|
437
414
|
const base = baseRoleOf(id);
|
|
438
|
-
const
|
|
439
|
-
writeRoleConfigKey(root, base,
|
|
415
|
+
const value = flagValue(rest, flag) ?? "";
|
|
416
|
+
writeRoleConfigKey(root, base, clear, value, `roles.${base}.${field}`, noun, (v) => JSON.stringify(v), (v) => v);
|
|
440
417
|
return;
|
|
441
418
|
}
|
|
442
419
|
// The enable/disable write: the scoped write for the enabled state `tumwater role <id>`
|
|
@@ -466,7 +443,7 @@ export async function cmdRole(root, args) {
|
|
|
466
443
|
say(`${base}: ${state}`);
|
|
467
444
|
return;
|
|
468
445
|
}
|
|
469
|
-
// The review-after-merge write: the one command the post-merge churn
|
|
446
|
+
// The review-after-merge write: the one command the post-merge churn row's remedy
|
|
470
447
|
// needs — add the role to, or remove it from, review.afterMerge for the base id, the
|
|
471
448
|
// membership test reviewedAfterMerge shares. The whole-list value lands through the same
|
|
472
449
|
// setConfigKey core `config set review.afterMerge <json>` uses, so validateConfig owns the
|