tumwater 0.1.2 → 0.1.3
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 +26 -95
- package/dist/build-info.json +2 -2
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +41 -6
- package/dist/src/backlog/backlog-md.js +19 -1
- package/dist/src/backlog/backlog-write.js +152 -11
- package/dist/src/baseline/main-red.js +10 -4
- package/dist/src/brief-edit.js +42 -0
- package/dist/src/brief.js +33 -2
- package/dist/src/change/change-data.js +12 -3
- package/dist/src/cli/brief-command.js +56 -0
- package/dist/src/cli/cli-command-args.js +3 -4
- package/dist/src/cli/cli-editor.js +52 -0
- package/dist/src/cli/cli-flag-specs.js +23 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +32 -6
- package/dist/src/cli/cli-run.js +105 -12
- package/dist/src/cli/completion.js +149 -0
- package/dist/src/cli/help.js +71 -16
- package/dist/src/cli/question-commands.js +18 -2
- package/dist/src/cli.js +49 -10
- package/dist/src/config/config-views.js +12 -12
- package/dist/src/config/config-write.js +1 -1
- package/dist/src/doctor/doctor-backlog.js +75 -4
- package/dist/src/doctor/doctor-checks.js +31 -4
- package/dist/src/doctor/doctor-holds.js +83 -4
- package/dist/src/doctor/doctor-notify.js +28 -0
- package/dist/src/doctor/doctor.js +9 -2
- package/dist/src/events/event-format.js +12 -1
- package/dist/src/events/notify.js +75 -26
- package/dist/src/fleet/fleet-state.js +25 -1
- package/dist/src/fleet/orchestrator-info.js +2 -0
- package/dist/src/git/commit-message.js +6 -0
- package/dist/src/git/git.js +11 -0
- package/dist/src/gui/gui-endpoint-commands.js +52 -5
- package/dist/src/gui/gui-endpoints.js +12 -3
- package/dist/src/gui/gui-server.js +5 -2
- package/dist/src/history/history-data.js +21 -0
- package/dist/src/history/history.js +24 -4
- package/dist/src/inbox/inbox-attachments.js +9 -4
- package/dist/src/inbox/prompt-commands.js +61 -12
- package/dist/src/landing/landing-git.js +1 -2
- package/dist/src/landing/landing-queue-render.js +28 -3
- package/dist/src/landing/landing-slot.js +10 -0
- package/dist/src/loop/loop-pi.js +2 -2
- package/dist/src/loop/loop-state.js +9 -0
- package/dist/src/loop/loop-tick.js +9 -3
- package/dist/src/loop/loop.js +5 -1
- package/dist/src/operator/operator-commands.js +43 -4
- package/dist/src/operator/operator-intent.js +18 -1
- package/dist/src/operator/retire.js +17 -1
- package/dist/src/orchestrator/orchestrator-scheduling.js +60 -6
- package/dist/src/orchestrator/orchestrator.js +10 -5
- package/dist/src/paths.js +5 -0
- package/dist/src/pi/pi-watchdogs.js +44 -41
- package/dist/src/process/supervisor.js +15 -4
- package/dist/src/prompt/prompt-followup.js +7 -1
- package/dist/src/report/report-data.js +5 -1
- package/dist/src/report/report-render.js +46 -0
- package/dist/src/review/protected-edits.js +32 -0
- package/dist/src/review/review.js +28 -7
- package/dist/src/roles/role-catalog.js +17 -5
- package/dist/src/roles/roles.js +2 -1
- package/dist/src/scheduling/claims.js +10 -1
- package/dist/src/scheduling/scheduling.js +17 -1
- package/dist/src/status/status-data.js +10 -5
- package/dist/src/status/status-polls.js +25 -1
- package/dist/src/tick/stage-check.js +5 -1
- package/dist/src/tick/tick-detail-data.js +15 -2
- package/dist/src/tick/tick-detail.js +38 -10
- package/dist/src/tick/tick-stage.js +32 -2
- package/dist/src/tick/tick-verdict.js +1 -0
- package/dist/src/ui/badges.js +7 -0
- package/dist/src/ui/gui/gui-client-boot.js +10 -4
- package/dist/src/ui/gui/gui-client-fleet.js +9 -1
- package/dist/src/ui/gui/gui-client-history.js +2 -1
- package/dist/src/ui/gui/gui-client-loops.js +4 -5
- package/dist/src/ui/gui/gui-client-model.js +4 -1
- package/dist/src/ui/gui/gui-client-operator.js +61 -1
- package/dist/src/ui/gui/gui-client-pending.js +4 -3
- package/dist/src/ui/gui/gui-client-report.js +15 -5
- package/dist/src/ui/gui/gui-client-settings.js +27 -1
- package/dist/src/ui/gui/gui-page.js +1 -0
- package/dist/src/ui/status-model.js +24 -36
- package/dist/src/ui/status-payload.js +10 -1
- package/dist/src/ui/status-render.js +2 -1
- package/dist/src/ui/tone.js +3 -1
- package/dist/src/ui/tui/tui-frame.js +9 -1
- package/dist/src/ui/tui/tui-input.js +11 -0
- package/dist/src/ui/tui/tui-keys.js +129 -8
- package/dist/src/ui/tui/tui-pane.js +15 -1
- package/dist/src/ui/tui/tui.js +4 -2
- package/dist/src/verdict/fix-claim.js +98 -18
- package/dist/src/verdict/refusal.js +54 -2
- package/package.json +1 -1
package/dist/src/cli/cli-run.js
CHANGED
|
@@ -9,18 +9,22 @@ import { fail, say } from "./cli-output.js";
|
|
|
9
9
|
import { durationLabel, failOverDurationCap, failRivalShapes, flagValue, parseBranchFlag, parseDurationFlag, parseRoleFlag, } from "./cli-args.js";
|
|
10
10
|
import { PAUSE_FOR_MAX_MS } from "../operator/operator-intent.js";
|
|
11
11
|
import { parseInitArgs } from "./cli-command-args.js";
|
|
12
|
-
import { isFleetPaused, pausedRoles } from "../fleet/fleet-state.js";
|
|
12
|
+
import { cancelScheduledStop, isFleetPaused, pausedRoles, scheduledStopAt } from "../fleet/fleet-state.js";
|
|
13
13
|
import { orchestratorAlive, readOrchestratorInfo } from "../fleet/orchestrator-info.js";
|
|
14
14
|
import { runStartupCheck, runStartupProblem } from "../gates/startup-gate.js";
|
|
15
15
|
import { initProject } from "../init/init.js";
|
|
16
16
|
import { templateCatalog } from "../init/init-templates.js";
|
|
17
17
|
import { repairInterruptedSwap } from "../build/build-stage.js";
|
|
18
18
|
import { logEvent, subscribeEvents } from "../events/events.js";
|
|
19
|
+
import { notifyFleetDown } from "../events/notify.js";
|
|
19
20
|
import { formatEvent } from "../events/event-format.js";
|
|
20
21
|
import { RESTART_EXIT_CODE } from "../redeploy/redeploy-policy.js";
|
|
21
22
|
import { loadLoopState, stateSkipReason } from "../loop/loop-state.js";
|
|
22
23
|
import { plural } from "../text/phrases.js";
|
|
23
24
|
import { shortSha } from "../text/format.js";
|
|
25
|
+
import { readTickRowsSince, summarizeTickRows } from "../history/history-data.js";
|
|
26
|
+
import { renderSummary } from "../history/history.js";
|
|
27
|
+
import { SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
24
28
|
/** `tumwater init`: seed a project directory from the operator's brief (src/init/init.ts does the
|
|
25
29
|
* work; this prints the report). --dry-run prints the would-be actions without writing
|
|
26
30
|
* anything; an already-initialized repo is reported as a no-op, never an error. */
|
|
@@ -73,8 +77,9 @@ export async function cmdInit(root, args) {
|
|
|
73
77
|
* `--for <duration>` keeps the daemon shape (redeployer, LaunchServicesWatch, supervisor hand-off
|
|
74
78
|
* all stay) but arms a deadline: at the deadline the same graceful stop a Ctrl+C runs fires, and
|
|
75
79
|
* the process exits after printing the summary. A mid-run self-redeploy hands the same args
|
|
76
|
-
* (including `--for`) to the supervisor's next child,
|
|
77
|
-
*
|
|
80
|
+
* (including `--for`) to the supervisor's next child, and the supervisor hands every generation
|
|
81
|
+
* the same window start (RUN_WINDOW_START_ENV), so the deadline and the summary span the whole
|
|
82
|
+
* window; only the final exit prints the summary. */
|
|
78
83
|
export async function cmdRun(root, args) {
|
|
79
84
|
// The one function the self-redeploy asks before swapping onto a successor and the supervisor
|
|
80
85
|
// asks when a generation dies, so the three cannot disagree about what boots. The supervisor
|
|
@@ -114,11 +119,13 @@ export async function cmdRun(root, args) {
|
|
|
114
119
|
// compile, and the supervisor/redeploy/launch-services pieces ride beside it. Loading them
|
|
115
120
|
// dynamically, after the fail-fast gates above, keeps every other command's spawn — and
|
|
116
121
|
// `run`'s fail-fast paths — off that cost (see cli.ts's matching lazy dispatch).
|
|
117
|
-
const { SUPERVISED_ENV, startParentDeathWatch, expectedSupervisorPid } = await import("../process/supervisor.js");
|
|
122
|
+
const { SUPERVISED_ENV, RUN_WINDOW_START_ENV, ONCE_FAILED_EXIT_CODE, startParentDeathWatch, expectedSupervisorPid } = await import("../process/supervisor.js");
|
|
118
123
|
if (!process.env[SUPERVISED_ENV]) {
|
|
119
124
|
// `args` are the flags after the command token — forward them so the child generation
|
|
120
125
|
// targets the same branch (or whatever else the invocation named).
|
|
121
|
-
|
|
126
|
+
// A stop marker left by a fleet that died before its deadline must not stop this run.
|
|
127
|
+
cancelScheduledStop(root);
|
|
128
|
+
await superviseRunCommand(root, args, branchArg, forMs);
|
|
122
129
|
return;
|
|
123
130
|
}
|
|
124
131
|
const controller = new AbortController();
|
|
@@ -135,10 +142,15 @@ export async function cmdRun(root, args) {
|
|
|
135
142
|
// A windowed run's deadline arms the same graceful stop a Ctrl+C runs — the identical `stop`
|
|
136
143
|
// closure, so the drain path is one code path either way. clearTimeout in the finally below
|
|
137
144
|
// retracts it when the run ends first (an early Ctrl+C), so no stray timer outlives the run.
|
|
138
|
-
const
|
|
145
|
+
const windowStart = inheritedWindowStart(process.env[RUN_WINDOW_START_ENV], Date.now());
|
|
146
|
+
const deadlineTimer = forMs !== null ? armDeadlineTimer(Math.max(0, windowStart + forMs - Date.now()), () => {
|
|
139
147
|
say("deadline reached — stopping");
|
|
140
148
|
stop();
|
|
141
149
|
}) : null;
|
|
150
|
+
const scheduledStopWatch = armScheduledStopWatch(root, () => {
|
|
151
|
+
if (!stopping)
|
|
152
|
+
stop(); // A drain already under way must not be turned into a force-kill.
|
|
153
|
+
});
|
|
142
154
|
// A supervisor that dies without forwarding — SIGKILL (the OOM killer, `kill -9`) cannot be
|
|
143
155
|
// trapped or forwarded, and an uncaught supervisor crash forwards nothing either — leaves this
|
|
144
156
|
// generation reparented and ticking the fleet unattended. The parent-death watch polls for the
|
|
@@ -188,20 +200,56 @@ export async function cmdRun(root, args) {
|
|
|
188
200
|
launchServicesWatch,
|
|
189
201
|
once,
|
|
190
202
|
roleFilter: roleFilter ?? undefined,
|
|
203
|
+
stopsAt: forMs !== null ? windowStart + forMs : undefined,
|
|
191
204
|
});
|
|
192
205
|
}
|
|
193
206
|
finally {
|
|
194
207
|
if (deadlineTimer)
|
|
195
208
|
deadlineTimer.clear();
|
|
209
|
+
scheduledStopWatch.clear();
|
|
196
210
|
unsubscribe();
|
|
197
211
|
}
|
|
198
|
-
if (once
|
|
212
|
+
if (once)
|
|
199
213
|
say(onceSummary(root, roles, ticksBefore, exit.settled, exit.ticksRun));
|
|
214
|
+
else if (forMs !== null && !exit.restart) {
|
|
215
|
+
const { rows, covered } = readTickRowsSince(root, Date.now() - windowStart, null);
|
|
216
|
+
say(windowSummary(durationLabel(forMs), summarizeTickRows(rows), covered));
|
|
217
|
+
}
|
|
218
|
+
if (once) {
|
|
219
|
+
const failures = onceFailures(root, roles, ticksBefore, exit.ticksRun);
|
|
220
|
+
if (failures.length > 0) {
|
|
221
|
+
say(`once: failed — ${failures.map((f) => `${f.role} ${f.result}`).join(", ")}`);
|
|
222
|
+
process.exitCode = ONCE_FAILED_EXIT_CODE;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
200
225
|
// A self-redeploy swapped the new build into dist/: hand the terminal back to the supervisor,
|
|
201
226
|
// which respawns this same script — now the new code — as the next generation.
|
|
202
227
|
if (exit.restart)
|
|
203
228
|
process.exit(RESTART_EXIT_CODE);
|
|
204
229
|
}
|
|
230
|
+
/** The window start a generation adopts: the supervisor's RUN_WINDOW_START_ENV mark when it is a
|
|
231
|
+
* positive integer no later than `now`, else `now` (absent, malformed, or in the future). */
|
|
232
|
+
export function inheritedWindowStart(raw, now) {
|
|
233
|
+
const n = Number(raw);
|
|
234
|
+
return raw !== undefined && Number.isInteger(n) && n > 0 && n <= now ? n : now;
|
|
235
|
+
}
|
|
236
|
+
/** The `run --for` exit summary: a window line, then the `history --summary` table for the ticks
|
|
237
|
+
* that ended since the window opened — every tick counted once by its outcome class. Only the
|
|
238
|
+
* final exit prints it; a mid-window self-redeploy hands off silently and the next generation
|
|
239
|
+
* inherits the window start, so the table spans every generation. */
|
|
240
|
+
export function windowSummary(label, summary, covered) {
|
|
241
|
+
const { total } = summary;
|
|
242
|
+
const lines = [
|
|
243
|
+
total.ticks === 0
|
|
244
|
+
? `for ${label}: 0 ticks — nothing ran`
|
|
245
|
+
: `for ${label}: ${plural(total.ticks, "tick")} — ${total.changes} changes, ${total.none} none, ${total.problems} problems`,
|
|
246
|
+
];
|
|
247
|
+
if (total.ticks > 0)
|
|
248
|
+
lines.push(renderSummary(summary));
|
|
249
|
+
if (!covered)
|
|
250
|
+
lines.push(SPARSE_WINDOW_NOTE);
|
|
251
|
+
return lines.join("\n");
|
|
252
|
+
}
|
|
205
253
|
/** Arm the windowed run's deadline timer: the one home of the arming `cmdRun` does for
|
|
206
254
|
* `run --for <duration>`. A duration over setTimeout's own 32-bit signed ceiling
|
|
207
255
|
* (2_147_483_647 ms ≈ 24.8 days) is clamped by Node to 1ms with a TimeoutOverflowWarning, so
|
|
@@ -228,6 +276,23 @@ export function armDeadlineTimer(ms, onFired) {
|
|
|
228
276
|
arm(ms);
|
|
229
277
|
return { clear: () => clearTimeout(timer) };
|
|
230
278
|
}
|
|
279
|
+
const SCHEDULED_STOP_POLL_MS = 5000;
|
|
280
|
+
/** Poll for a `tumwater stop --in` marker: once its `until` has passed, consume the marker,
|
|
281
|
+
* stop polling, and call `onStop` (cmdRun's Ctrl+C closure) exactly once. The interval is
|
|
282
|
+
* unref'd; clear() retracts it. Exported for tests. */
|
|
283
|
+
export function armScheduledStopWatch(root, onStop) {
|
|
284
|
+
const timer = setInterval(() => {
|
|
285
|
+
const at = scheduledStopAt(root);
|
|
286
|
+
if (at === undefined || at > Date.now())
|
|
287
|
+
return;
|
|
288
|
+
clearInterval(timer);
|
|
289
|
+
cancelScheduledStop(root);
|
|
290
|
+
say("scheduled stop reached — stopping");
|
|
291
|
+
onStop();
|
|
292
|
+
}, SCHEDULED_STOP_POLL_MS);
|
|
293
|
+
timer.unref();
|
|
294
|
+
return { clear: () => clearInterval(timer) };
|
|
295
|
+
}
|
|
231
296
|
/** Parse `run --for <duration>` (the windowed run): the parsed duration in milliseconds, or null
|
|
232
297
|
* when the flag is absent. The two body-level rules fail fast, before any repo gate or boot: the
|
|
233
298
|
* value must parse as a duration (parseDurationFlag's own wording, the same messages the
|
|
@@ -245,6 +310,32 @@ export function parseRunWindow(args, once) {
|
|
|
245
310
|
failOverDurationCap("--for", ms, PAUSE_FOR_MAX_MS);
|
|
246
311
|
return ms;
|
|
247
312
|
}
|
|
313
|
+
/** Results that mean the harness or the project's check failed and the round's work did not
|
|
314
|
+
* land: a `--once` round with a role ending in one exits ONCE_FAILED_EXIT_CODE. `rejected` and
|
|
315
|
+
* `refused` are the review gate and the agent working as designed; `aborted`, `quiet_killed`, and
|
|
316
|
+
* `user_aborted` only reach a once round on a shutdown or operator abort, which exit their own way. */
|
|
317
|
+
export const ONCE_FAILURE_RESULTS = new Set([
|
|
318
|
+
"error",
|
|
319
|
+
"review_error",
|
|
320
|
+
"merge_conflict",
|
|
321
|
+
"merge_blocked",
|
|
322
|
+
"main_red",
|
|
323
|
+
]);
|
|
324
|
+
function ticksThisRound(s, role, ticksBefore, ticksRun) {
|
|
325
|
+
return ticksRun ? (ticksRun.get(role) ?? 0) : s.ticks - (ticksBefore.get(role) ?? 0);
|
|
326
|
+
}
|
|
327
|
+
/** The roles that ticked this round and whose last result is in ONCE_FAILURE_RESULTS. */
|
|
328
|
+
export function onceFailures(root, roles, ticksBefore, ticksRun) {
|
|
329
|
+
const out = [];
|
|
330
|
+
const extra = ticksRun ? [...ticksRun.keys()].filter((r) => !roles.includes(r)) : [];
|
|
331
|
+
for (const role of [...roles, ...extra]) {
|
|
332
|
+
const s = loadLoopState(root, role);
|
|
333
|
+
if (ticksThisRound(s, role, ticksBefore, ticksRun) > 0 && s.lastResult && ONCE_FAILURE_RESULTS.has(s.lastResult)) {
|
|
334
|
+
out.push({ role, result: s.lastResult });
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
248
339
|
/** The once round's one-line summary, read from the runners' persisted loop state plus the
|
|
249
340
|
* orchestrator's own settle reasons — a cron job's log shows what the round did without parsing
|
|
250
341
|
* events. Roles whose tick counter advanced bucket by their last completed result; the rest are
|
|
@@ -271,7 +362,7 @@ export function onceSummary(root, roles, ticksBefore, settled, ticksRun) {
|
|
|
271
362
|
const extra = ticksRun ? [...ticksRun.keys()].filter((r) => !roles.includes(r)) : [];
|
|
272
363
|
for (const role of [...roles, ...extra]) {
|
|
273
364
|
const s = loadLoopState(root, role);
|
|
274
|
-
const ran =
|
|
365
|
+
const ran = ticksThisRound(s, role, ticksBefore, ticksRun);
|
|
275
366
|
if (ran > 0) {
|
|
276
367
|
ticks += ran;
|
|
277
368
|
const key = s.lastResult ?? "no_change";
|
|
@@ -326,11 +417,12 @@ async function superviseDashboard(signal) {
|
|
|
326
417
|
* supervisor's own exit code is whatever the last generation's was. A fleet that goes down without
|
|
327
418
|
* the operator asking leaves a `supervisor_exit` event behind (BUGS.md 2026-09-23): the dead
|
|
328
419
|
* generation's stderr reached only this terminal, which nobody may be watching. */
|
|
329
|
-
async function superviseRunCommand(root, runArgs, branchArg) {
|
|
420
|
+
async function superviseRunCommand(root, runArgs, branchArg, forMs) {
|
|
330
421
|
// The supervisor machinery loads lazily, like cmdRun's orchestrator import above: a command
|
|
331
422
|
// that never boots the fleet never compiles it.
|
|
332
|
-
const { fleetDownEvent, spawnRunChild, superviseRun } = await import("../process/supervisor.js");
|
|
423
|
+
const { RUN_WINDOW_START_ENV, fleetDownEvent, spawnRunChild, superviseRun } = await import("../process/supervisor.js");
|
|
333
424
|
const plan = guiChildPlan(runArgs);
|
|
425
|
+
const windowEnv = forMs !== null ? { [RUN_WINDOW_START_ENV]: String(Date.now()) } : {};
|
|
334
426
|
const controller = new AbortController();
|
|
335
427
|
// The dashboard lives beside the orchestrator generations rather than inside one: it must
|
|
336
428
|
// outlive every generation a redeploy swaps out. Its own abort stops the current dashboard
|
|
@@ -347,7 +439,7 @@ async function superviseRunCommand(root, runArgs, branchArg) {
|
|
|
347
439
|
});
|
|
348
440
|
const gui = plan.spawnGui ? superviseDashboard(guiController.signal) : Promise.resolve();
|
|
349
441
|
const code = await superviseRun({
|
|
350
|
-
spawnChild: (signal) => spawnRunChild(signal, plan.orchestratorArgs),
|
|
442
|
+
spawnChild: (signal) => spawnRunChild(signal, plan.orchestratorArgs, windowEnv),
|
|
351
443
|
stopping: () => stopping,
|
|
352
444
|
onRespawn: (generation) => say(`\nrestarting on the new build (generation ${generation})\n`),
|
|
353
445
|
onCrashLoop: () => process.stderr.write("tumwater: the harness restarted itself too many times in a minute — giving up\n"),
|
|
@@ -356,7 +448,8 @@ async function superviseRunCommand(root, runArgs, branchArg) {
|
|
|
356
448
|
// tumwater.json gone). A crash loop's generations all asked for restarts, so the gate
|
|
357
449
|
// has nothing to say about it.
|
|
358
450
|
onFleetDown: async (down) => {
|
|
359
|
-
logEvent(root, fleetDownEvent(down, down.crashLoop ? null : await runStartupProblem(root, branchArg)));
|
|
451
|
+
const event = logEvent(root, fleetDownEvent(down, down.crashLoop ? null : await runStartupProblem(root, branchArg)));
|
|
452
|
+
notifyFleetDown(root, event);
|
|
360
453
|
},
|
|
361
454
|
// The end-of-supervision build repair (see build-stage.repairInterruptedSwap): a swap
|
|
362
455
|
// killed between its two renames leaves dist/ missing, and this process is the only one
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/** Shell completion derived from the help text. The spec is parsed from the same HELP stanzas
|
|
2
|
+
* `tumwater help <command>` and the unknown-command suggestion use, so a new command or flag
|
|
3
|
+
* completes the moment it gains a help stanza and can never drift from the listing. */
|
|
4
|
+
import { customLoopNames, knownRoleIds, loadConfig } from "../config/config.js";
|
|
5
|
+
import { AGENT_IDS, AGENT_KEYS, MODEL_TIERS, ROLE_ENTRY_KEYS, TOP_LEVEL_KEYS } from "../config/config-schema.js";
|
|
6
|
+
import { DOTTED_MAP_KEYS } from "../config/config-write.js";
|
|
7
|
+
import { HISTORY_RESULT_CLASSES } from "../history/history-data.js";
|
|
8
|
+
import { templateIds } from "../init/init-templates.js";
|
|
9
|
+
import { allRoleIds } from "../roles/roles.js";
|
|
10
|
+
import { HELP, helpStanzas } from "./help.js";
|
|
11
|
+
export const SUPPORTED_SHELLS = ["bash", "zsh"];
|
|
12
|
+
/** The completable words of a stanza's usage part: the text after `tumwater <command>` up to the
|
|
13
|
+
* description column (two or more spaces). Bare lowercase words count inside `[...]` and as the
|
|
14
|
+
* leading run directly after the command; outside brackets the first other token ends the run, so
|
|
15
|
+
* a single-space description (`role <id> --clear-notes Clear that notebook`) adds nothing. */
|
|
16
|
+
function usageWords(firstLine, command) {
|
|
17
|
+
const rest = firstLine.replace(new RegExp(`^ {2}tumwater ${command}`), "");
|
|
18
|
+
// Usage arguments follow the command after a single space; two or more spaces start the description.
|
|
19
|
+
const usage = /^ {2,}/.test(rest) ? "" : (rest.split(/ {2,}/)[0] ?? "");
|
|
20
|
+
const words = [];
|
|
21
|
+
let depth = 0;
|
|
22
|
+
let leading = true;
|
|
23
|
+
for (const token of usage.split(/\s+/).filter(Boolean)) {
|
|
24
|
+
const bare = token.replace(/[[\]|]/g, "");
|
|
25
|
+
const isWord = /^[a-z][a-z-]*$/.test(bare);
|
|
26
|
+
if (isWord && (depth > 0 || token.startsWith("[") || leading))
|
|
27
|
+
words.push(bare);
|
|
28
|
+
else if (depth === 0 && !token.startsWith("[") && bare !== "")
|
|
29
|
+
leading = false;
|
|
30
|
+
for (const ch of token)
|
|
31
|
+
depth += ch === "[" ? 1 : ch === "]" ? -1 : 0;
|
|
32
|
+
}
|
|
33
|
+
return words;
|
|
34
|
+
}
|
|
35
|
+
export function completionSpec(help = HELP) {
|
|
36
|
+
const stanzas = helpStanzas(help);
|
|
37
|
+
const commands = [...new Set(stanzas.map((s) => s.command))];
|
|
38
|
+
const subcommands = {};
|
|
39
|
+
const flags = {};
|
|
40
|
+
for (const command of commands) {
|
|
41
|
+
const own = stanzas.filter((s) => s.command === command);
|
|
42
|
+
const flagSet = new Set(["--help"]);
|
|
43
|
+
const subs = new Set();
|
|
44
|
+
for (const stanza of own) {
|
|
45
|
+
for (const match of stanza.text.matchAll(/--[a-z][a-z-]*/g))
|
|
46
|
+
flagSet.add(match[0]);
|
|
47
|
+
for (const word of usageWords(stanza.text.split("\n")[0] ?? "", command))
|
|
48
|
+
subs.add(word);
|
|
49
|
+
}
|
|
50
|
+
flags[command] = [...flagSet].sort();
|
|
51
|
+
subcommands[command] = command === "help" ? commands : [...subs];
|
|
52
|
+
}
|
|
53
|
+
return { commands, subcommands, flags };
|
|
54
|
+
}
|
|
55
|
+
/** The ids `--role` completes: this repo's loop ids, or the built-in catalog when tumwater.json is
|
|
56
|
+
* unreadable (a broken config must not write errors into the operator's prompt). */
|
|
57
|
+
export function completionRoleIds(root) {
|
|
58
|
+
try {
|
|
59
|
+
return knownRoleIds(loadConfig(root));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return allRoleIds();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** The keys `config get|set|unset` complete: every form parseConfigKey accepts, with the role-scoped
|
|
66
|
+
* keys of this repo's base roles and custom loops (not instance ids like `feature-2`). A broken
|
|
67
|
+
* tumwater.json falls back to the built-in catalog rather than writing errors into the prompt. */
|
|
68
|
+
export function completionConfigKeys(root) {
|
|
69
|
+
let roleIds;
|
|
70
|
+
try {
|
|
71
|
+
roleIds = [...allRoleIds(), ...customLoopNames(loadConfig(root))];
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
roleIds = allRoleIds();
|
|
75
|
+
}
|
|
76
|
+
const keys = [
|
|
77
|
+
...TOP_LEVEL_KEYS,
|
|
78
|
+
...MODEL_TIERS.map((tier) => `model.${tier}`),
|
|
79
|
+
...AGENT_IDS.flatMap((id) => AGENT_KEYS.map((field) => `agents.${id}.${field}`)),
|
|
80
|
+
...DOTTED_MAP_KEYS.flatMap((map) => roleIds.map((id) => `${map}.${id}`)),
|
|
81
|
+
...roleIds.flatMap((id) => ROLE_ENTRY_KEYS.map((field) => `roles.${id}.${field}`)),
|
|
82
|
+
];
|
|
83
|
+
return [...new Set(keys)];
|
|
84
|
+
}
|
|
85
|
+
/** Flags whose value is a file path: completion offers file names after them. */
|
|
86
|
+
const PATH_FLAGS = ["--file", "--attach", "--notes-file"];
|
|
87
|
+
function bashScript(help) {
|
|
88
|
+
const spec = completionSpec(help);
|
|
89
|
+
const arms = spec.commands.map((command) => {
|
|
90
|
+
const subs = spec.subcommands[command] ?? [];
|
|
91
|
+
const flags = spec.flags[command] ?? [];
|
|
92
|
+
return [
|
|
93
|
+
` ${command})`,
|
|
94
|
+
` if [[ "$cur" == -* ]]; then`,
|
|
95
|
+
` COMPREPLY=( $(compgen -W "${flags.join(" ")}" -- "$cur") )`,
|
|
96
|
+
...(subs.length > 0
|
|
97
|
+
? [
|
|
98
|
+
` elif [ "$COMP_CWORD" -eq 2 ]; then`,
|
|
99
|
+
` COMPREPLY=( $(compgen -W "${subs.join(" ")}" -- "$cur") )`,
|
|
100
|
+
]
|
|
101
|
+
: []),
|
|
102
|
+
` fi`,
|
|
103
|
+
` ;;`,
|
|
104
|
+
].join("\n");
|
|
105
|
+
});
|
|
106
|
+
return [
|
|
107
|
+
`# tumwater completion (generated from \`tumwater help\`)`,
|
|
108
|
+
`_tumwater() {`,
|
|
109
|
+
` local cur="\${COMP_WORDS[COMP_CWORD]}"`,
|
|
110
|
+
` local prev="\${COMP_WORDS[COMP_CWORD-1]}"`,
|
|
111
|
+
` COMPREPLY=()`,
|
|
112
|
+
` if [ "$prev" = "--role" ] || { [ "$COMP_CWORD" -eq 2 ] && [[ "\${COMP_WORDS[1]}" == role || "\${COMP_WORDS[1]}" == tick ]] && [[ "$cur" != -* ]]; }; then`,
|
|
113
|
+
` COMPREPLY=( $(compgen -W "$(tumwater completion --roles 2>/dev/null)" -- "$cur") )`,
|
|
114
|
+
` return 0`,
|
|
115
|
+
` fi`,
|
|
116
|
+
` case "$prev" in ${PATH_FLAGS.join("|")})`,
|
|
117
|
+
` COMPREPLY=( $(compgen -f -- "$cur") )`,
|
|
118
|
+
` return 0`,
|
|
119
|
+
` ;;`,
|
|
120
|
+
` esac`,
|
|
121
|
+
` if [ "$prev" = "--template" ]; then`,
|
|
122
|
+
` COMPREPLY=( $(compgen -W "${templateIds().join(" ")}" -- "$cur") )`,
|
|
123
|
+
` return 0`,
|
|
124
|
+
` fi`,
|
|
125
|
+
` if [ "$prev" = "--result" ]; then`,
|
|
126
|
+
` COMPREPLY=( $(compgen -W "${HISTORY_RESULT_CLASSES.join(" ")}" -- "$cur") )`,
|
|
127
|
+
` return 0`,
|
|
128
|
+
` fi`,
|
|
129
|
+
` if [ "$COMP_CWORD" -eq 3 ] && [ "\${COMP_WORDS[1]}" = config ] && [[ "\${COMP_WORDS[2]}" == get || "\${COMP_WORDS[2]}" == set || "\${COMP_WORDS[2]}" == unset ]] && [[ "$cur" != -* ]]; then`,
|
|
130
|
+
` COMPREPLY=( $(compgen -W "$(tumwater completion --config-keys 2>/dev/null)" -- "$cur") )`,
|
|
131
|
+
` return 0`,
|
|
132
|
+
` fi`,
|
|
133
|
+
` if [ "$COMP_CWORD" -eq 1 ]; then`,
|
|
134
|
+
` COMPREPLY=( $(compgen -W "${spec.commands.join(" ")}" -- "$cur") )`,
|
|
135
|
+
` return 0`,
|
|
136
|
+
` fi`,
|
|
137
|
+
` case "\${COMP_WORDS[1]}" in`,
|
|
138
|
+
...arms,
|
|
139
|
+
` esac`,
|
|
140
|
+
` return 0`,
|
|
141
|
+
`}`,
|
|
142
|
+
`complete -F _tumwater tumwater`,
|
|
143
|
+
``,
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
export function completionScript(shell, help = HELP) {
|
|
147
|
+
const bash = bashScript(help);
|
|
148
|
+
return shell === "zsh" ? `autoload -U +X bashcompinit && bashcompinit\n${bash}` : bash;
|
|
149
|
+
}
|
package/dist/src/cli/help.js
CHANGED
|
@@ -15,8 +15,11 @@ Usage:
|
|
|
15
15
|
Run all enabled loops (headless; Ctrl+C stops);
|
|
16
16
|
--once runs one full round of ticks, drains the
|
|
17
17
|
landings it produced, and exits (for cron/CI);
|
|
18
|
+
exits 3 when a tick ended in error, review_error,
|
|
19
|
+
merge_conflict, merge_blocked, or main_red;
|
|
18
20
|
--once --role <id> scopes that round to one loop;
|
|
19
|
-
--for <duration> runs for that window then drains
|
|
21
|
+
--for <duration> runs for that window then drains, prints a per-loop summary
|
|
22
|
+
(one window across self-redeploys),
|
|
20
23
|
and exits like Ctrl+C would (for an overnight
|
|
21
24
|
trial, a demo, a CI step);
|
|
22
25
|
--gui also serves the browser dashboard at
|
|
@@ -88,7 +91,7 @@ Usage:
|
|
|
88
91
|
tumwater notify --test Run the configured notify hook once with a synthetic event
|
|
89
92
|
so the command can be checked before a real page
|
|
90
93
|
tumwater history [--role <id>] [-n N] [--since <duration>] [--grep <text>]
|
|
91
|
-
[--result <changes|none|problems>] [--json]
|
|
94
|
+
[--result <changes|none|problems>] [--summary] [--json]
|
|
92
95
|
One row per completed tick, newest first — time, loop,
|
|
93
96
|
tick number, result, duration, tokens/cost, and the
|
|
94
97
|
summary (or error), each row one line; --since <duration>
|
|
@@ -100,30 +103,37 @@ Usage:
|
|
|
100
103
|
problems (everything else) — composed with --role, --since,
|
|
101
104
|
-n, and --grep; --json prints the rows as
|
|
102
105
|
machine-readable history data — ts, tokens, and costUsd
|
|
103
|
-
kept raw, the GUI's /api/history payload
|
|
104
|
-
|
|
106
|
+
kept raw, the GUI's /api/history payload; --summary prints one line per loop
|
|
107
|
+
counting its ticks by outcome class, with the time of its newest change (and a total line)
|
|
108
|
+
instead of the rows; with --json, { loops, total }
|
|
109
|
+
tumwater tick <role> [<n>] [--last] [--stat] [--json]
|
|
105
110
|
One completed tick's full event trail — a summary header
|
|
106
111
|
(result or in-flight, duration, tokens/cost, the pinned
|
|
107
112
|
commit sha) then every event of that tick's block in time
|
|
108
113
|
order; a still-running tick shows its events so far, marked
|
|
109
|
-
in flight;
|
|
110
|
-
|
|
111
|
-
|
|
114
|
+
in flight; a queued tick shows its landing's verdict and
|
|
115
|
+
outcome line once the landing finishes; --last shows the newest completed tick's trail
|
|
116
|
+
instead of numbering one; --stat appends the pinned
|
|
117
|
+
commit's per-file git show --stat summary; --json prints
|
|
118
|
+
the collector's payload as machine-readable data
|
|
112
119
|
tumwater diff [--json] One line per loop holding pending work — the ahead-of-main
|
|
113
120
|
commit count and uncommitted-file count, no patch; roles
|
|
114
121
|
with no worktree or nothing pending are skipped, and a
|
|
115
122
|
missing baseline prints the per-role view's degradation
|
|
116
123
|
line; --json prints the {mainBranch, roles} roster
|
|
117
|
-
tumwater diff --role <id> [--json]
|
|
124
|
+
tumwater diff --role <id> [--stat] [--json]
|
|
118
125
|
Show the change that loop holds: its branch's unlanded
|
|
119
126
|
commits (one line each, plus the ahead-of-main patch) and
|
|
120
127
|
its worktree's uncommitted edits (the file list, plus the
|
|
121
128
|
patch — staged and unstaged alike); a loop with no worktree
|
|
122
|
-
yet prints \`no worktree for <id>\`; --
|
|
129
|
+
yet prints \`no worktree for <id>\`; --stat prints each patch's per-file
|
|
130
|
+
\`git diff --stat\` summary instead; --json prints the
|
|
123
131
|
collector's payload as machine-readable data
|
|
124
132
|
tumwater queue [--json] List the durable land queue in execution order (oldest
|
|
125
133
|
first): one line per waiting landing — position, loop, tick
|
|
126
|
-
number, short pinned sha, age, and summary
|
|
134
|
+
number, short pinned sha, age, and summary — plus a bracketed
|
|
135
|
+
\`[landing 2m · reviewing]\` / \`[vetted, awaiting merge]\`
|
|
136
|
+
label while a running fleet works on it; an empty queue
|
|
127
137
|
prints \`land queue empty\`, and any torn/foreign queue file
|
|
128
138
|
the reader skipped is counted on a trailing line; --json
|
|
129
139
|
prints the collector's own {entries, unreadable} document
|
|
@@ -140,6 +150,10 @@ Usage:
|
|
|
140
150
|
File a bug from a markdown file ("-" reads stdin): the first
|
|
141
151
|
non-blank line is the symptom, the rest is the body kept
|
|
142
152
|
line for line; wakes the bugfix loop
|
|
153
|
+
tumwater bug --release <n> [--json]
|
|
154
|
+
Lift the Refused note from the Nth entry in BUGS.md ## Open
|
|
155
|
+
(as \`tumwater bug --list\` lists it), commit it on main,
|
|
156
|
+
and wake the bugfix loop; --json prints {file, title}
|
|
143
157
|
tumwater bug --cancel <n> [--json]
|
|
144
158
|
Remove the Nth entry in BUGS.md ## Open (as \`tumwater
|
|
145
159
|
bug --list\` lists it); no wake; --json prints {file, title}
|
|
@@ -152,10 +166,17 @@ Usage:
|
|
|
152
166
|
stdin): the first non-blank line is the title (symptom), the rest
|
|
153
167
|
the body kept line for line; position and original stamp
|
|
154
168
|
kept; no wake; --json prints {file, title}
|
|
169
|
+
tumwater bug --edit <n> [--json]
|
|
170
|
+
With neither text nor --file, open the Nth entry as --show
|
|
171
|
+
prints it in $VISUAL or $EDITOR and write it back on save;
|
|
172
|
+
an unchanged buffer writes nothing
|
|
155
173
|
tumwater bug --list [--json] List the entries in BUGS.md ## Open, numbered (the read
|
|
156
174
|
half of --edit/--cancel), each with its eligibility mark;
|
|
157
175
|
--json prints the {entries} array — position, title, body,
|
|
158
176
|
hold — as data
|
|
177
|
+
tumwater bug --show <n> [--json] Print the Nth entry as the markdown file --edit <n> --file
|
|
178
|
+
reads (title, blank line, body with indentation kept);
|
|
179
|
+
--json prints {position, title, body, hold}
|
|
159
180
|
tumwater plan "<title>" [body...] [--json]
|
|
160
181
|
File a plan request into PLANS.md ## Planned as the operator
|
|
161
182
|
(stamped with today's date; the feature loop fleshes the stub
|
|
@@ -165,6 +186,10 @@ Usage:
|
|
|
165
186
|
File a plan request from a markdown file ("-" reads stdin):
|
|
166
187
|
the first non-blank line is the title, the rest is the body
|
|
167
188
|
kept line for line; wakes the feature loop
|
|
189
|
+
tumwater plan --release <n> [--json]
|
|
190
|
+
Lift the Refused note from the Nth entry in PLANS.md ## Planned
|
|
191
|
+
(as \`tumwater plan --list\` lists it), commit it on main,
|
|
192
|
+
and wake the feature loop; --json prints {file, title}
|
|
168
193
|
tumwater plan --cancel <n> [--json]
|
|
169
194
|
Remove the Nth entry in PLANS.md ## Planned (as \`tumwater
|
|
170
195
|
plan --list\` lists it); no wake; --json prints {file, title}
|
|
@@ -177,10 +202,18 @@ Usage:
|
|
|
177
202
|
stdin): the first non-blank line is the title, the rest
|
|
178
203
|
the body kept line for line; position and original stamp
|
|
179
204
|
kept; no wake; --json prints {file, title}
|
|
205
|
+
tumwater plan --edit <n> [--json]
|
|
206
|
+
With neither text nor --file, open the Nth entry as --show
|
|
207
|
+
prints it in $VISUAL or $EDITOR and write it back on save;
|
|
208
|
+
an unchanged buffer writes nothing
|
|
180
209
|
tumwater plan --list [--json] List the entries in PLANS.md ## Planned, numbered (the
|
|
181
210
|
read half of --edit/--cancel), each with its eligibility
|
|
182
211
|
mark; --json prints the {entries} array — position, title,
|
|
183
212
|
body, hold — as data
|
|
213
|
+
tumwater plan --show <n> [--json]
|
|
214
|
+
Print the Nth entry as the markdown file --edit <n> --file
|
|
215
|
+
reads (title, blank line, body with indentation kept);
|
|
216
|
+
--json prints {position, title, body, hold}
|
|
184
217
|
tumwater questions [--json] List open questions numbered (the read half of the
|
|
185
218
|
question outbox); --json prints the {questions}
|
|
186
219
|
array — position, title, body — as data
|
|
@@ -194,6 +227,17 @@ Usage:
|
|
|
194
227
|
Read the decision from the file (- for stdin) with its
|
|
195
228
|
lines kept; a ## or ### heading line outside a code fence,
|
|
196
229
|
or an unclosed fence, is refused
|
|
230
|
+
tumwater questions answer [--json] <n>
|
|
231
|
+
With no decision, open the question in $VISUAL/$EDITOR;
|
|
232
|
+
only the text above the marker line is recorded
|
|
233
|
+
tumwater brief [--json] Show the project's initial prompt and the file that holds it
|
|
234
|
+
tumwater brief --file <path> [--json]
|
|
235
|
+
Replace the initial prompt with the file's text (- for
|
|
236
|
+
stdin), refused when empty, over the length cap, or
|
|
237
|
+
containing a tumwater:prompt marker; commits that one
|
|
238
|
+
file on main; no wake
|
|
239
|
+
tumwater brief --edit [--json] Open the initial prompt in $VISUAL/$EDITOR and save it as
|
|
240
|
+
--file would; an unchanged buffer writes nothing
|
|
197
241
|
tumwater role <id> [--json] Show one loop's standing prompt and resolved settings — its
|
|
198
242
|
find text, the roles.<id>.instructions override, the resolved
|
|
199
243
|
provider/model (naming the budget fallback pair when one is
|
|
@@ -236,6 +280,10 @@ Usage:
|
|
|
236
280
|
contents ("-" reads stdin), kept line for line; position,
|
|
237
281
|
age, and a pending --at deferral are kept, and --role <id>
|
|
238
282
|
disambiguates like --edit
|
|
283
|
+
tumwater prompt --edit <n> [--attach <path>]
|
|
284
|
+
Open the Nth queued prompt in $VISUAL/$EDITOR and save it
|
|
285
|
+
as the text form would; an unchanged buffer writes
|
|
286
|
+
nothing; --role <id> disambiguates like --edit
|
|
239
287
|
tumwater reset-counters [--role <id>]
|
|
240
288
|
Zero lifetime ticks/commits/tokens/cost (fresh
|
|
241
289
|
observation window; today's budget spend is kept — the
|
|
@@ -258,8 +306,15 @@ Usage:
|
|
|
258
306
|
tumwater resume [--role <id>] Lift a fleet or per-role pause
|
|
259
307
|
tumwater retire --role <id> Remove a disabled loop's worktree, branch, and per-role
|
|
260
308
|
state (landing ref, pause marker); --force overrides the
|
|
261
|
-
safety rails; --json prints what was removed
|
|
262
|
-
|
|
309
|
+
safety rails; --json prints what was removed;
|
|
310
|
+
--dry-run lists what would be removed and any objections,
|
|
311
|
+
changing nothing
|
|
312
|
+
tumwater stop [--in <duration> | --cancel]
|
|
313
|
+
Stop a running fleet (drains in-flight ticks, like Ctrl+C);
|
|
314
|
+
--in schedules the stop, --cancel drops it
|
|
315
|
+
tumwater completion <bash|zsh> Print a shell completion script (eval "$(tumwater completion zsh)" in your rc)
|
|
316
|
+
tumwater completion --roles Print the loop ids --role completes (this repo's; the catalog when tumwater.json is unreadable)
|
|
317
|
+
tumwater completion --config-keys Print the keys config get/set/unset complete (this repo's role keys; the catalog's when tumwater.json is unreadable)
|
|
263
318
|
tumwater help [<command>] Show all commands, or one command's usage
|
|
264
319
|
tumwater version Print the version
|
|
265
320
|
|
|
@@ -309,13 +364,13 @@ export function helpTopic(command, help = HELP) {
|
|
|
309
364
|
}
|
|
310
365
|
/** The free-form text commands and the flags that take a separate value token, for
|
|
311
366
|
* helpTopicForArgs: a valued flag's value is plumbing (`prompt --role qa --help`, `bug
|
|
312
|
-
* --cancel 2 --help`), never prose. bug/plan's valued flags are `--cancel`, `--edit
|
|
313
|
-
* and `--
|
|
367
|
+
* --cancel 2 --help`), never prose. bug/plan's valued flags are `--cancel`, `--edit`,
|
|
368
|
+
* `--file` and `--release`. */
|
|
314
369
|
const FREEFORM_VALUED_FLAGS = {
|
|
315
370
|
prompt: ["--role", "--file", "--at", "--cancel", "--edit", "--attach"],
|
|
316
371
|
init: ["--branch", "--template", "--file"],
|
|
317
|
-
bug: ["--cancel", "--edit", "--file"],
|
|
318
|
-
plan: ["--cancel", "--edit", "--file"],
|
|
372
|
+
bug: ["--cancel", "--edit", "--file", "--release"],
|
|
373
|
+
plan: ["--cancel", "--edit", "--file", "--release"],
|
|
319
374
|
};
|
|
320
375
|
/** The `--help`/`-h` request cli.ts answers before dispatch: the topic text for `command`
|
|
321
376
|
* when the invocation asks for help and names a topic, otherwise null — not a help request,
|
|
@@ -117,6 +117,8 @@ function answeredOrFail(n, outcome) {
|
|
|
117
117
|
fail(`no question at position ${n} (${outcome.open} open)`);
|
|
118
118
|
if (outcome.status === "fenced")
|
|
119
119
|
fail(UNCLOSED_FENCE_MESSAGE);
|
|
120
|
+
if (outcome.status === "moved")
|
|
121
|
+
fail(`question ${n} is now "${outcome.title}" — the list changed while you edited; nothing changed`);
|
|
120
122
|
if (outcome.status !== "answered")
|
|
121
123
|
fail(`no question at position ${n}`);
|
|
122
124
|
return { title: outcome.title, decision: outcome.decision };
|
|
@@ -124,14 +126,28 @@ function answeredOrFail(n, outcome) {
|
|
|
124
126
|
/** `tumwater questions answer <n> --file <path>`: the decision's lines kept (normalizeEntryBody),
|
|
125
127
|
* behind the same structural guard `plan --file` uses, so a stray `## ` line cannot grow a
|
|
126
128
|
* phantom section. Refusals exit 1 through `fail`. */
|
|
127
|
-
export function answerQuestionFromText(root, n, text) {
|
|
129
|
+
export function answerQuestionFromText(root, n, text, expectedTitle) {
|
|
128
130
|
const body = normalizeEntryBody(text);
|
|
129
131
|
if (body === "")
|
|
130
132
|
fail("questions answer needs a decision: the file is blank");
|
|
131
133
|
const problem = bodyStructureProblem(body);
|
|
132
134
|
if (problem !== null)
|
|
133
135
|
fail(`questions answer --file: ${problem}`);
|
|
134
|
-
return answeredOrFail(n, tryAnswerQuestion(root, n, body,
|
|
136
|
+
return answeredOrFail(n, tryAnswerQuestion(root, n, body, expectedTitle, true));
|
|
137
|
+
}
|
|
138
|
+
/** The line that ends the decision area of the `questions answer <n>` editor buffer. */
|
|
139
|
+
export const ANSWER_EDITOR_CUT = "<!-- tumwater: write your decision above this line; this line and everything below it are dropped -->";
|
|
140
|
+
/** The editor buffer for bare `questions answer <n>`: an empty decision area, the cut line, then
|
|
141
|
+
* the question's heading and body for reference. */
|
|
142
|
+
export function answerEditorBuffer(title, body) {
|
|
143
|
+
return ["", "", ANSWER_EDITOR_CUT, `### ${title}`, body].join("\n");
|
|
144
|
+
}
|
|
145
|
+
/** The decision an edited `answerEditorBuffer` holds: the text before the first cut line (all of
|
|
146
|
+
* it when the operator deleted the line). */
|
|
147
|
+
export function decisionFromEditorBuffer(text) {
|
|
148
|
+
const lines = text.split("\n");
|
|
149
|
+
const cut = lines.findIndex((l) => l.trim() === ANSWER_EDITOR_CUT);
|
|
150
|
+
return (cut === -1 ? lines : lines.slice(0, cut)).join("\n");
|
|
135
151
|
}
|
|
136
152
|
/** The non-exiting answer core, for callers (the dashboard server) that must not end the
|
|
137
153
|
* process: runs the whole read-modify-write under the cross-process primary-worktree lock
|