tumwater 0.4.0 → 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/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.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-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- 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 +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- 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/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- 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 +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- 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 +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- 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-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- 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 +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -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-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -12,16 +12,13 @@
|
|
|
12
12
|
* which re-decide; `signal` aborts a waiter. */
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { loadConfig, slotCount } from "../config/config.js";
|
|
15
|
-
import {
|
|
16
|
-
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
17
|
-
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
15
|
+
import { slotWorktreePath, isCanonicalSlotDir } from "../paths.js";
|
|
18
16
|
import { readSlotsState, slotForDir, updateSlotsState } from "./slots-state.js";
|
|
19
17
|
import { salvageSlotLeftovers } from "./slot-salvage.js";
|
|
20
18
|
import { beginWorktreeUse } from "./worktree-use.js";
|
|
21
19
|
import { ensureDetachedWorktree, removeWorktreeDir } from "./worktree.js";
|
|
22
20
|
import { logEventBestEffort, warnEventBestEffort, tryOrWarn, TUMWATER_SOURCE } from "../events/events.js";
|
|
23
21
|
import { errorMessage } from "../text/text.js";
|
|
24
|
-
import { subdirectoryNames } from "../files/files.js";
|
|
25
22
|
import { dropProviderLeaseKey } from "../concurrency/provider-lease.js";
|
|
26
23
|
/** In-process waiters per root, woken when any slot is released. Keyed by resolved root so two
|
|
27
24
|
* roots never wake each other. */
|
|
@@ -229,12 +226,6 @@ function liveCount(root) {
|
|
|
229
226
|
return Number.MAX_SAFE_INTEGER;
|
|
230
227
|
}
|
|
231
228
|
}
|
|
232
|
-
/** A slot dir is canonical when it is exactly a `_slot-<n>` checkout under the worktrees dir.
|
|
233
|
-
* A migrated legacy `<role>` dir is not, which marks it for retirement once its resume finishes
|
|
234
|
-
* (retireLegacyRoleWorktrees and releaseSlot). */
|
|
235
|
-
function isCanonicalSlotDir(dir) {
|
|
236
|
-
return /^_slot-\d+$/.test(path.basename(dir));
|
|
237
|
-
}
|
|
238
229
|
/** Slot dirs per root whose release write to slots.json failed in this process; the next claim
|
|
239
230
|
* clears their stale lease. */
|
|
240
231
|
const unsavedReleases = new Map();
|
|
@@ -316,116 +307,6 @@ export async function removeDroppedSlots(root, dirs, remove = removeWorktreeDir)
|
|
|
316
307
|
}
|
|
317
308
|
}
|
|
318
309
|
}
|
|
319
|
-
/** One-time startup migration (plans/worktree-pool.md "Legacy role worktrees", part 4c/5):
|
|
320
|
-
* retire the pre-pool `.tumwater/worktrees/<role>` checkouts now that runs lease pooled slots.
|
|
321
|
-
* A resumable enabled role keeps its legacy dir as a pinned slot so its pending resume runs in
|
|
322
|
-
* the same cwd; every other legacy dir is removed (its branch keeps any commit), and pins for
|
|
323
|
-
* roles outside `configured` are cleared. `configured` is every role id the config's roles
|
|
324
|
-
* enable (`runnerIds`), never a `--once --role` filter: a scoped run must leave the other roles'
|
|
325
|
-
* legacy dirs and pins alone, or it silently loses their pending resumes. It runs after
|
|
326
|
-
* `runOrchestrator` constructs every
|
|
327
|
-
* `RoleRunner` but before any run can call `leaseSlot`: the constructor infers `resumePending`
|
|
328
|
-
* from the on-disk `running` flag only in memory (it does not save it), so `loadRunnerState` here
|
|
329
|
-
* still reports the on-disk flags. *Removing* a dir is best-effort and logged: the slot records
|
|
330
|
-
* are already updated, so a leaked directory is only a leak the next prune collects. */
|
|
331
|
-
export async function retireLegacyRoleWorktrees(root, configured) {
|
|
332
|
-
const dir = worktreesDir(root);
|
|
333
|
-
// A missing worktrees dir (a team that has not started) yields no names to migrate; the pin
|
|
334
|
-
// retirement pass below still runs over whatever slots.json holds.
|
|
335
|
-
const names = subdirectoryNames(dir, (name) => name !== DIRECTOR_ROLE && !name.startsWith("_"));
|
|
336
|
-
const configuredSet = new Set(configured);
|
|
337
|
-
const toRemove = new Set();
|
|
338
|
-
updateSlotsState(root, (state) => {
|
|
339
|
-
for (const role of names) {
|
|
340
|
-
const legacyDir = path.join(dir, role);
|
|
341
|
-
const legacy = loadRunnerState(root, role);
|
|
342
|
-
const resumable = legacy.resumePending === true || legacy.running === true;
|
|
343
|
-
const ownRecord = state.slots.find((slot) => slot.dir === legacyDir);
|
|
344
|
-
if (resumable && configuredSet.has(role)) {
|
|
345
|
-
const owned = state.slots.find((slot) => slot.pinnedFor === role ||
|
|
346
|
-
(slot.lease?.role === role && slot.lease.purpose === "run"));
|
|
347
|
-
if (owned !== undefined) {
|
|
348
|
-
// The resume happens in the owned slot: drop the legacy record and remove its dir.
|
|
349
|
-
if (owned.dir !== legacyDir) {
|
|
350
|
-
state.slots = state.slots.filter((slot) => slot.dir !== legacyDir);
|
|
351
|
-
toRemove.add(legacyDir);
|
|
352
|
-
}
|
|
353
|
-
if (owned.pinnedFor !== role)
|
|
354
|
-
owned.pinnedAt = Date.now();
|
|
355
|
-
owned.pinnedFor = role;
|
|
356
|
-
}
|
|
357
|
-
else if (ownRecord !== undefined) {
|
|
358
|
-
// Already migrated (a restart before the resume ran): keep it pinned and inert,
|
|
359
|
-
// preserving the pin's original age for the doctor's stuck-pin warning.
|
|
360
|
-
ownRecord.lease = null;
|
|
361
|
-
if (ownRecord.pinnedFor !== role)
|
|
362
|
-
ownRecord.pinnedAt = Date.now();
|
|
363
|
-
ownRecord.pinnedFor = role;
|
|
364
|
-
ownRecord.lastRole = null;
|
|
365
|
-
ownRecord.lastReleasedAt = null;
|
|
366
|
-
}
|
|
367
|
-
else {
|
|
368
|
-
state.slots.push({
|
|
369
|
-
dir: legacyDir,
|
|
370
|
-
lease: null,
|
|
371
|
-
pinnedFor: role,
|
|
372
|
-
pinnedAt: Date.now(),
|
|
373
|
-
lastRole: null,
|
|
374
|
-
lastReleasedAt: null,
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
state.slots = state.slots.filter((slot) => slot.dir !== legacyDir);
|
|
380
|
-
toRemove.add(legacyDir);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
// A paused or removed role must not leak a permanently pinned checkout.
|
|
384
|
-
for (const slot of [...state.slots]) {
|
|
385
|
-
if (slot.pinnedFor === null || configuredSet.has(slot.pinnedFor))
|
|
386
|
-
continue;
|
|
387
|
-
slot.pinnedFor = null;
|
|
388
|
-
slot.pinnedAt = null;
|
|
389
|
-
if (!isCanonicalSlotDir(slot.dir)) {
|
|
390
|
-
state.slots = state.slots.filter((s) => s !== slot);
|
|
391
|
-
toRemove.add(slot.dir);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
});
|
|
395
|
-
for (const victim of toRemove) {
|
|
396
|
-
try {
|
|
397
|
-
await removeWorktreeDir(root, victim);
|
|
398
|
-
}
|
|
399
|
-
catch (err) {
|
|
400
|
-
warnEventBestEffort(root, TUMWATER_SOURCE, `could not remove legacy role workspace ${victim}: ${errorMessage(err)}`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
/** Startup pass for runs the previous process died in (crash, kill -9, power loss): such a run's
|
|
405
|
-
* lease is still in slots.json with the dead pid, and its RoleRunner reads the persisted
|
|
406
|
-
* `running` flag as a resume that must run in the same checkout, the cwd its session used. Pin
|
|
407
|
-
* each such slot for its role, as the graceful shutdown's abort pin would have, unless the role
|
|
408
|
-
* already holds a pin; the next claim then clears the dead lease (marking the run unsettled) and
|
|
409
|
-
* the resume leases the pinned slot with its edits in place. Only configured roles count, as in
|
|
410
|
-
* retireLegacyRoleWorktrees, so a removed role never leaks a pin. Like that pass, it runs before
|
|
411
|
-
* any run can lease, while loadRunnerState still reports the on-disk flags. */
|
|
412
|
-
export function pinCrashedRuns(root, configured) {
|
|
413
|
-
const configuredSet = new Set(configured);
|
|
414
|
-
updateSlotsState(root, (state) => {
|
|
415
|
-
for (const slot of state.slots) {
|
|
416
|
-
const lease = slot.lease;
|
|
417
|
-
if (lease === null || lease.pid === process.pid || lease.purpose !== "run")
|
|
418
|
-
continue;
|
|
419
|
-
if (!configuredSet.has(lease.role) || state.slots.some((s) => s.pinnedFor === lease.role))
|
|
420
|
-
continue;
|
|
421
|
-
const runner = loadRunnerState(root, lease.role);
|
|
422
|
-
if (runner.running !== true && runner.resumePending !== true)
|
|
423
|
-
continue;
|
|
424
|
-
slot.pinnedFor = lease.role;
|
|
425
|
-
slot.pinnedAt = Date.now();
|
|
426
|
-
}
|
|
427
|
-
});
|
|
428
|
-
}
|
|
429
310
|
/** Lease a pooled slot for `role`, prepared at `ref`. Resolves once a slot is held and reset to
|
|
430
311
|
* `ref`, any uncommitted edits its last holder left saved first (salvageSlotLeftovers), as work
|
|
431
312
|
* to hand back when that holder was a run that never settled them; rejects on a `signal` abort
|
|
@@ -14,7 +14,7 @@ import { readBrief } from "../brief/brief.js";
|
|
|
14
14
|
import { readQueuedPromptBody } from "../inbox/inbox-edit.js";
|
|
15
15
|
import { rejectBadQueueFileName, rejectBadRole } from "./gui-args.js";
|
|
16
16
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
17
|
-
import { okGone, sendJson } from "./http-body.js";
|
|
17
|
+
import { okGone, sendJson } from "./gui-http-body.js";
|
|
18
18
|
/** Handle GET /api/prompt-text?file=<name>[&role=<id>]: one queued prompt's editable body —
|
|
19
19
|
* the text an edit replaces, for the dashboard's per-row edit affordance (PLANS.md
|
|
20
20
|
* 2026-10-11). The prompt is addressed by its queue-file basename (the /api/prompt-cancel
|
|
@@ -44,9 +44,8 @@ export function handlePromptText(q, res, root) {
|
|
|
44
44
|
/** Handle GET /api/doctor: the same DoctorReport `tumwater doctor --json` prints, so the
|
|
45
45
|
* Settings view's Health check card and the CLI cannot disagree. Read-only — the checks write
|
|
46
46
|
* nothing (runDoctor's own contract) — and it works whether or not the team is running. `run`
|
|
47
|
-
* exists so tests can skip the host probes;
|
|
48
|
-
|
|
49
|
-
export async function handleDoctor(res, root, run = (r) => runDoctor(r, undefined, undefined, "never")) {
|
|
47
|
+
* exists so tests can skip the host probes; a throw reaches the server's outer 500. */
|
|
48
|
+
export async function handleDoctor(res, root, run = (r) => runDoctor(r)) {
|
|
50
49
|
sendJson(res, 200, await run(root));
|
|
51
50
|
}
|
|
52
51
|
/** Handle GET /api/config: the Settings view's resolved values for exactly
|
package/dist/src/gui/gui-args.js
CHANGED
|
@@ -16,7 +16,7 @@ import { queueFileNameProblem } from "../inbox/inbox-cancel.js";
|
|
|
16
16
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
17
17
|
import { gotSuffix, parseNonNegativeInt, parsePositiveInt, splitCommaListUnique } from "../text/text.js";
|
|
18
18
|
import { typoSuffix } from "../text/suggest.js";
|
|
19
|
-
import { badRequest, sendJson } from "./http-body.js";
|
|
19
|
+
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
20
20
|
/** The role ids a role-targeting endpoint accepts: when tumwater.json parses, catalog +
|
|
21
21
|
* customRoles (knownRoleIds); a transiently broken file falls back to the built-in catalog
|
|
22
22
|
* rather than refusing every id — the rule config.knownRoleIdsCached owns. Shared by
|
|
@@ -18,7 +18,7 @@ import { tryMoveEntryToTop, tryReleaseEntry } from "../backlog/backlog-release.j
|
|
|
18
18
|
import { tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE } from "../backlog/question-answer.js";
|
|
19
19
|
import { promptLengthProblem } from "../inbox/inbox-submit.js";
|
|
20
20
|
import { requestWake, submitRolePromptAndWake, wakeQuestionAsker } from "../operator/operator-intent.js";
|
|
21
|
-
import { badRequest, okGone, sendJson } from "./http-body.js";
|
|
21
|
+
import { badRequest, okGone, sendJson } from "./gui-http-body.js";
|
|
22
22
|
import { collapseWhitespace, errorMessage, gotSuffix } from "../text/text.js";
|
|
23
23
|
import { readPostBody } from "./gui-endpoint-common.js";
|
|
24
24
|
/** Shared 400-validation for an entry-target body's `kind` — "bug" or "plan" — the one field
|
|
@@ -183,11 +183,26 @@ function heldAt(root, kind, position) {
|
|
|
183
183
|
const holds = backlogHolds(root);
|
|
184
184
|
return (kind === "bug" ? holds.bugs : holds.plans)[position - 1] ?? null;
|
|
185
185
|
}
|
|
186
|
+
/** Commit a successful backlog write and send the drawer's success response — the same
|
|
187
|
+
* post-write tail the release, move-to-top, and edit handlers repeat: commit the file change on
|
|
188
|
+
* main, re-read the entry's hold (its position may have moved), and send the 200 with the
|
|
189
|
+
* commit and a `heldNote` naming the hold when one still applies (a held entry is not newly
|
|
190
|
+
* eligible, so the caller must not wake a role — the release handler reads the returned hold
|
|
191
|
+
* to decide). `verb` names the action in the commit subject (`Release`/`Prioritize`/`Edit`),
|
|
192
|
+
* `status` is the response's status, and `extra` carries the success body fields only one
|
|
193
|
+
* handler has (release's `lifted`), spread before `committed` so that body's key order stays
|
|
194
|
+
* intact. Returns the hold it read. */
|
|
195
|
+
async function respondBacklogWrite(res, root, kind, position, outcome, verb, status, extra = {}) {
|
|
196
|
+
const committed = await commitOperatorBacklogEdit(root, outcome.file, stampedSubject("operator", `${verb} ${kind}: ${outcome.title}`)); // vocabulary-ok: the commit prefix stays tumwater(operator):
|
|
197
|
+
const hold = heldAt(root, kind, position);
|
|
198
|
+
sendJson(res, 200, { ok: true, status, file: outcome.file, title: outcome.title, ...extra, committed, ...(hold === null ? {} : { heldNote: stillHeldNote(hold) }) });
|
|
199
|
+
return hold;
|
|
200
|
+
}
|
|
186
201
|
/** Handle POST /api/backlog-release: the entry drawer's release — the same lock and splice as
|
|
187
202
|
* `tumwater plan|bug --release <n>` through the non-exiting tryReleaseEntry. Body as
|
|
188
203
|
* backlog-cancel's. A released entry that is not still held is newly eligible, so the kind's
|
|
189
204
|
* role is woken (a held one wakes no role and carries `heldNote`); `gone` and `moved` answer
|
|
190
|
-
* okGone, and an entry with none of the Refused
|
|
205
|
+
* okGone, and an entry with none of the Refused or
|
|
191
206
|
* held-for-operator notes answers status "not_refused" with no write and no wake. */
|
|
192
207
|
export async function handleBacklogRelease(req, res, root) {
|
|
193
208
|
const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>"}');
|
|
@@ -204,11 +219,9 @@ export async function handleBacklogRelease(req, res, root) {
|
|
|
204
219
|
if (outcome === null)
|
|
205
220
|
return;
|
|
206
221
|
if (outcome.status === "released") {
|
|
207
|
-
const
|
|
208
|
-
const hold = heldAt(root, kind, position);
|
|
222
|
+
const hold = await respondBacklogWrite(res, root, kind, position, outcome, "Release", "released", { lifted: outcome.lifted });
|
|
209
223
|
if (hold === null)
|
|
210
224
|
requestWake(root, [OPERATOR_FILE_KINDS[kind].role]);
|
|
211
|
-
sendJson(res, 200, { ok: true, status: "released", file: outcome.file, title: outcome.title, lifted: outcome.lifted, committed, ...(hold === null ? {} : { heldNote: stillHeldNote(hold) }) });
|
|
212
225
|
}
|
|
213
226
|
else if (outcome.status === "not_refused") {
|
|
214
227
|
sendJson(res, 200, { ok: true, status: "not_refused", title: outcome.title });
|
|
@@ -237,9 +250,7 @@ export async function handleBacklogTop(req, res, root) {
|
|
|
237
250
|
if (outcome === null)
|
|
238
251
|
return;
|
|
239
252
|
if (outcome.status === "moved_to_top") {
|
|
240
|
-
|
|
241
|
-
const hold = heldAt(root, kind, 1);
|
|
242
|
-
sendJson(res, 200, { ok: true, status: "moved_to_top", file: outcome.file, title: outcome.title, committed, ...(hold === null ? {} : { heldNote: stillHeldNote(hold) }) });
|
|
253
|
+
await respondBacklogWrite(res, root, kind, 1, outcome, "Prioritize", "moved_to_top");
|
|
243
254
|
}
|
|
244
255
|
else if (outcome.status === "already_first") {
|
|
245
256
|
sendJson(res, 200, { ok: true, status: "already_first", title: outcome.title });
|
|
@@ -284,9 +295,7 @@ export async function handleBacklogEdit(req, res, root) {
|
|
|
284
295
|
if (outcome === null)
|
|
285
296
|
return;
|
|
286
297
|
if (outcome.status === "edited") {
|
|
287
|
-
|
|
288
|
-
const hold = heldAt(root, kind, position);
|
|
289
|
-
sendJson(res, 200, { ok: true, status: "edited", file: outcome.file, title: outcome.title, committed, ...(hold === null ? {} : { heldNote: stillHeldNote(hold) }) });
|
|
298
|
+
await respondBacklogWrite(res, root, kind, position, outcome, "Edit", "edited");
|
|
290
299
|
}
|
|
291
300
|
else if (outcome.status === "unchanged") {
|
|
292
301
|
sendJson(res, 200, { ok: true, status: "unchanged", file: outcome.file, title: outcome.title });
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* inbox/inbox.ts, change/change-data.ts), so this module only adapts HTTP onto it. Barrelled
|
|
6
6
|
* into gui/gui-endpoints.ts beside the other per-family GET endpoint modules.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { refusalNote } from "../backlog/backlog-eligibility.js";
|
|
9
9
|
import { answeredQuestionEntries, entryMatches, donePlanEntries, fixedBugEntries, openBugEntries, openQuestionEntries, plannedPlanEntries } from "../backlog/backlog.js";
|
|
10
10
|
import { collectChangeOverlaps, scopeChangeOverlaps, scopeChangeOverlapsToPath, collectTeamChanges, collectRoleChange, normalizeDiffPath } from "../change/change-data.js";
|
|
11
11
|
import { listWideScope, queuedListRows } from "../inbox/inbox.js";
|
|
12
12
|
import { intQuery, rejectBadRole, roleListQuery } from "./gui-args.js";
|
|
13
|
-
import { badRequest, sendJson } from "./http-body.js";
|
|
13
|
+
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
14
14
|
import { typoSuffix } from "../text/suggest.js";
|
|
15
15
|
/** The /api/backlog `file` values and the open-section reader each addresses — one table so
|
|
16
16
|
* the on-demand dispatch and the "valid values" list in both 400s cannot drift apart. A Map,
|
|
@@ -36,10 +36,6 @@ const BACKLOG_FILE_VALUES = BACKLOG_FILE_NAMES.join(", ");
|
|
|
36
36
|
* order, unlike done/fixed) — zero-based.
|
|
37
37
|
* Unknown/missing file, missing or bad index, and out-of-range index
|
|
38
38
|
* → 400 JSON error via sendJson.
|
|
39
|
-
* On a held-for-operator entry (the entry in
|
|
40
|
-
* operatorHeldEntries' selection, file and index both matching), the 200 payload adds
|
|
41
|
-
* `heldForOperator: true` so the drawer shows its Release button there too; every other
|
|
42
|
-
* entry's payload carries no such key.
|
|
43
39
|
* The query arrives pre-parsed (gui/gui-server.ts's parseRequestTarget), like every
|
|
44
40
|
* GET-data handler here. */
|
|
45
41
|
export function handleBacklog(q, res, root) {
|
|
@@ -66,8 +62,6 @@ export function handleBacklog(q, res, root) {
|
|
|
66
62
|
title: entry.title,
|
|
67
63
|
body: entry.body,
|
|
68
64
|
refused: refusalNote(entry.body) !== null,
|
|
69
|
-
waiting: waitingNote(entry.body) !== null,
|
|
70
|
-
...(operatorHeldEntries(root).some((e) => e.file === file && e.index === index) ? { heldForOperator: true } : {}),
|
|
71
65
|
});
|
|
72
66
|
}
|
|
73
67
|
/** Handle GET /api/backlog-match?grep=<text>: for each /api/backlog `file`, the ascending
|
|
@@ -15,7 +15,7 @@ import { testNotifyHook } from "../cli/notify-command.js";
|
|
|
15
15
|
import { rejectBadRole, requireConfigKey, requireRoleField, requireStringField } from "./gui-args.js";
|
|
16
16
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
17
17
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
18
|
-
import { badRequest, sendJson } from "./http-body.js";
|
|
18
|
+
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
19
19
|
import { readPostBody } from "./gui-endpoint-common.js";
|
|
20
20
|
/** Handle POST /api/budget: the dashboard's budget-badge editor saves the daily cost cap —
|
|
21
21
|
* same body discipline as /api/prompt (readJsonObject), and the same shared validation rule +
|
|
@@ -173,40 +173,28 @@ export async function handleRoleInterval(req, res, root) {
|
|
|
173
173
|
return;
|
|
174
174
|
}
|
|
175
175
|
const base = baseRoleOf(role);
|
|
176
|
-
// A null,
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
176
|
+
// A non-null, non-blank value must pass the shared per-key screen (the finite
|
|
177
|
+
// 0..MAX_DURATION_SECONDS rule, naming the per-role dotted key so a rejected write tells the
|
|
178
|
+
// operator which key the drawer would set) before the write; a null/blank value is the clear
|
|
179
|
+
// path (writeRoleJsonKey), so the role falls back to the team-wide interval.
|
|
180
|
+
if (value !== null && !(typeof value === "string" && value.trim() === "")) {
|
|
181
|
+
const problem = checkMinRunInterval(value, `roles.${base}.minRunIntervalSeconds`);
|
|
182
|
+
if (problem) {
|
|
183
|
+
badRequest(res, problem);
|
|
184
184
|
return;
|
|
185
|
-
|
|
186
|
-
if (!clearResult.ok)
|
|
187
|
-
return configKeyError(res, `roles.${base}.minRunIntervalSeconds`, clearResult);
|
|
188
|
-
const message = clearResult.oldValue === undefined
|
|
189
|
-
? `${base}: min run interval is not set in tumwater.json, so nothing was cleared`
|
|
190
|
-
: `${base}: min run interval cleared`;
|
|
191
|
-
sendJson(res, 200, { ok: true, role: base, message });
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
// The shared rule (finite 0..MAX_DURATION_SECONDS), naming the per-role dotted key so a
|
|
195
|
-
// rejected write tells the operator which key the drawer would set.
|
|
196
|
-
const problem = checkMinRunInterval(value, `roles.${base}.minRunIntervalSeconds`);
|
|
197
|
-
if (problem) {
|
|
198
|
-
badRequest(res, problem);
|
|
199
|
-
return;
|
|
185
|
+
}
|
|
200
186
|
}
|
|
201
|
-
//
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
: `${base}: min run interval
|
|
209
|
-
|
|
187
|
+
// The set-or-clear tail shared with the other role JSON-key handlers: clears through
|
|
188
|
+
// unsetConfigKey and sets through the CLI's own setConfigKey dotted path, so the shared
|
|
189
|
+
// per-key validator (above) and whole-config validateConfig (which rejects an unknown role
|
|
190
|
+
// id) own the final truth. The clear arm carries rejectBadRole because unsetConfigKey's
|
|
191
|
+
// no-op map path skips validateConfig — the guard the set branch gets free from
|
|
192
|
+
// setConfigKey's unknown-role refusal (mirrors the CLI's knownRoleIds guard).
|
|
193
|
+
writeRoleJsonKey(res, root, base, `roles.${base}.minRunIntervalSeconds`, "min run interval", value, (v) => `${base}: min run interval set to ${v}s`, {
|
|
194
|
+
unchangedMessage: (v) => `${base}: min run interval is already ${v}s, so tumwater.json was not written`,
|
|
195
|
+
guardClear: () => rejectBadRole(root, res, role),
|
|
196
|
+
extra: { minRunIntervalSeconds: value },
|
|
197
|
+
});
|
|
210
198
|
}
|
|
211
199
|
/** Handle POST /api/role-instances: the role drawer's instance-count editor sets or clears one
|
|
212
200
|
* role's parallel-runner count — the count that lets `feature-2`…`feature-N` run. Same body
|
|
@@ -233,34 +221,17 @@ export async function handleRoleInstances(req, res, root) {
|
|
|
233
221
|
return;
|
|
234
222
|
}
|
|
235
223
|
const base = baseRoleOf(role);
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
return configKeyError(res, `roles.${base}.instances`, clearResult);
|
|
248
|
-
const message = clearResult.oldValue === undefined
|
|
249
|
-
? `${base}: instances is not set in tumwater.json, so nothing was cleared`
|
|
250
|
-
: `${base}: instances cleared`;
|
|
251
|
-
sendJson(res, 200, { ok: true, role: base, message });
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
// setConfigKey takes the raw CLI text; a JSON round-trip of an already-parsed value is
|
|
255
|
-
// exact, so the shared per-key validator + whole-config validateConfig (which owns which
|
|
256
|
-
// roles may split and the 1–8 integer bound) apply unchanged.
|
|
257
|
-
const result = setConfigKey(root, `roles.${base}.instances`, JSON.stringify(value));
|
|
258
|
-
if (!result.ok)
|
|
259
|
-
return configKeyError(res, `roles.${base}.instances`, result);
|
|
260
|
-
const message = result.unchanged
|
|
261
|
-
? `${base}: instances is already ${value}, so tumwater.json was not written`
|
|
262
|
-
: `${base}: instances set to ${value}`;
|
|
263
|
-
sendJson(res, 200, { ok: true, role: base, instances: value, message });
|
|
224
|
+
// The set-or-clear tail shared with the other role JSON-key handlers: clears through
|
|
225
|
+
// unsetConfigKey and sets through the CLI's own setConfigKey dotted path, so whole-candidate
|
|
226
|
+
// validateConfig owns the final truth (the INSTANCE_COUNT bound and which roles may split;
|
|
227
|
+
// an unknown id is refused there). The clear arm carries rejectBadRole because
|
|
228
|
+
// unsetConfigKey's no-op map path skips validateConfig — the guard the set branch gets free
|
|
229
|
+
// from setConfigKey's unknown-role refusal (mirrors the CLI's knownRoleIds guard).
|
|
230
|
+
writeRoleJsonKey(res, root, base, `roles.${base}.instances`, "instances", value, (v) => `${base}: instances set to ${v}`, {
|
|
231
|
+
unchangedMessage: (v) => `${base}: instances is already ${v}, so tumwater.json was not written`,
|
|
232
|
+
guardClear: () => rejectBadRole(root, res, role),
|
|
233
|
+
extra: { instances: value },
|
|
234
|
+
});
|
|
264
235
|
}
|
|
265
236
|
/** Handle POST /api/role-model: the role drawer's Model overrides editor sets or clears one
|
|
266
237
|
* role's provider/model/thinking/backend set (roles.<base>.provider/.model/.thinking/
|
|
@@ -337,7 +308,7 @@ export async function handleRoleInstructions(req, res, root) {
|
|
|
337
308
|
// 4096-char cap and the unknown-role check in writeConfigMutation. Unlike quiet hours the
|
|
338
309
|
// clear arm carries no role guard (the director may clear, and the unknown-role clear gap is
|
|
339
310
|
// pre-existing).
|
|
340
|
-
writeRoleJsonKey(res, root, base, `roles.${base}.instructions`, "instructions", text, (v) =>
|
|
311
|
+
writeRoleJsonKey(res, root, base, `roles.${base}.instructions`, "instructions", text, (v) => `${base}: instructions saved (${String(v).length} chars)`);
|
|
341
312
|
}
|
|
342
313
|
/** The shared write behind POST /api/role-enable and POST /api/role-disable: the Settings
|
|
343
314
|
* card's Enable button and the role drawer's two-click Disable both write
|
|
@@ -416,7 +387,7 @@ export async function handleRoleQuietHours(req, res, root) {
|
|
|
416
387
|
// branch's unknown-role refusal would never fire on a clear and a bogus id would answer 200
|
|
417
388
|
// "cleared" (mirrors the CLI's knownRoleIds guard). The set arm runs setConfigKey raw, its
|
|
418
389
|
// map branch checking the dotted key as the label and validateConfig owning the unknown role.
|
|
419
|
-
writeRoleJsonKey(res, root, base, `quietHoursPerRole.${base}`, "quiet hours", window, (v) =>
|
|
390
|
+
writeRoleJsonKey(res, root, base, `quietHoursPerRole.${base}`, "quiet hours", window, (v) => `${base}: quiet hours set to ${v}`, { guardClear: () => rejectBadRole(root, res, role) });
|
|
420
391
|
}
|
|
421
392
|
/** The shared 400 for a failed config mutation — the set and unset write paths both nest the
|
|
422
393
|
* write's error under the key it names, so the Settings view can tell which field failed and a
|
|
@@ -431,16 +402,18 @@ function configKeyError(res, key, result) {
|
|
|
431
402
|
function unchangedFlag(result) {
|
|
432
403
|
return result.unchanged ? { unchanged: true } : {};
|
|
433
404
|
}
|
|
434
|
-
/** The set-or-clear write
|
|
435
|
-
* instructions)
|
|
436
|
-
* check the clear arm runs first —
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
405
|
+
/** The set-or-clear write every per-role JSON-config-key handler (quiet hours, standing
|
|
406
|
+
* instructions, min run interval, instance count) shares: an empty/whitespace/`null` `raw`
|
|
407
|
+
* unsets `key` (after `guardClear`, a per-call check the clear arm runs first — the numeric
|
|
408
|
+
* keys' and quiet hours' unknown-role guard; instructions has none), else sets it via
|
|
409
|
+
* setConfigKey (which JSON-encodes nothing, so the JSON.stringify below shapes the stored
|
|
410
|
+
* value). `noun` names the hold in the "cleared" 200, `setMessage` is the exact set-arm 200
|
|
411
|
+
* message, and `unchangedMessage` overrides the default unchanged wording — threaded so each
|
|
412
|
+
* handler keeps its own wording. Always writes the response, so the caller never falls
|
|
413
|
+
* through. */
|
|
414
|
+
function writeRoleJsonKey(res, root, base, key, noun, raw, setMessage, options = {}) {
|
|
415
|
+
if (raw === null || (typeof raw === "string" && raw.trim() === "")) {
|
|
416
|
+
if (options.guardClear?.())
|
|
444
417
|
return;
|
|
445
418
|
const result = unsetConfigKey(root, key);
|
|
446
419
|
if (!result.ok)
|
|
@@ -453,12 +426,14 @@ function writeRoleJsonKey(res, root, base, key, noun, raw, serialize, setMessage
|
|
|
453
426
|
}
|
|
454
427
|
return;
|
|
455
428
|
}
|
|
456
|
-
const result = setConfigKey(root, key,
|
|
429
|
+
const result = setConfigKey(root, key, JSON.stringify(raw));
|
|
457
430
|
if (!result.ok)
|
|
458
431
|
configKeyError(res, key, result);
|
|
459
432
|
else {
|
|
460
|
-
const message = result.unchanged
|
|
461
|
-
|
|
433
|
+
const message = result.unchanged
|
|
434
|
+
? (options.unchangedMessage ?? (() => `${base}: ${noun} unchanged, so tumwater.json was not written`))(raw)
|
|
435
|
+
: setMessage(raw);
|
|
436
|
+
sendJson(res, 200, { ok: true, role: base, message, ...options.extra });
|
|
462
437
|
}
|
|
463
438
|
}
|
|
464
439
|
/** Handle POST /api/config-set: the Settings view's per-key Save — one curated key set to
|
|
@@ -10,10 +10,12 @@
|
|
|
10
10
|
* question-answer (gui/gui-backlog-commands), config set/unset + budget + notify-test
|
|
11
11
|
* (gui/gui-config-commands), team pause/role-pause/wake (gui/gui-team-commands),
|
|
12
12
|
* and operator lifecycle restart/reclaim/abort/reset-counters/stop-schedule/role-notes/
|
|
13
|
-
* retire/brief-edit (gui/gui-operator-commands)
|
|
13
|
+
* retire/brief-edit (gui/gui-operator-commands), and flake-quarantine release
|
|
14
|
+
* (gui/gui-quarantine-commands).
|
|
14
15
|
*/
|
|
15
16
|
export { handlePrompt, handlePromptRole, handlePromptCancel, handlePromptEdit, handlePromptTop } from "./gui-prompt-commands.js";
|
|
16
17
|
export { handleQuestionAnswer, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit } from "./gui-backlog-commands.js";
|
|
17
18
|
export { handleConfigSet, handleConfigUnset, handleBudget, handleRoleCap, handleRoleReviewAfterMerge, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest } from "./gui-config-commands.js";
|
|
18
19
|
export { handlePause, handlePauseRole, handleWake, handleWakeCancel } from "./gui-team-commands.js";
|
|
19
|
-
export { handleResetCounters, handleRestart, handleReclaim, handleKillOrphans, handleDropBrokenPins, handleCommitBacklog, handleResyncInstall, handleRetireLeftovers,
|
|
20
|
+
export { handleResetCounters, handleRestart, handleReclaim, handleKillOrphans, handleDropBrokenPins, handleCommitBacklog, handleResyncInstall, handleRetireLeftovers, handleArchiveBacklog, handleAbort, handleAbortAll, handleStopSchedule, handleRoleNotes, handleRetire, handleRevert, handleQueueCancel, handleQueueTop, handlePostReviewAccept, handleBriefEdit } from "./gui-operator-commands.js";
|
|
21
|
+
export { handleQuarantineRelease } from "./gui-quarantine-commands.js";
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The request-body helpers shared by the dashboard's per-domain POST command modules
|
|
3
|
-
* (gui-prompt-commands, gui-backlog-commands, gui-config-commands, gui-team-commands,
|
|
4
|
-
* gui-operator-commands,
|
|
3
|
+
* (gui-prompt-commands, gui-backlog-commands, gui-config-commands, gui-team-commands,
|
|
4
|
+
* gui-operator-commands, and gui-quarantine-commands, all barrelled into
|
|
5
|
+
* gui/gui-endpoint-commands.ts): readPostBody /
|
|
5
6
|
* readRoleBody — the body-discipline-and-role-check pair each mutating handler guards its
|
|
6
7
|
* inputs with — and sendRequestResult, the shared boolean-claiming marker reply. The
|
|
7
8
|
* per-domain `checked*` validators live with their domain's module, not here.
|
|
8
9
|
*/
|
|
9
10
|
import { rejectBadRole } from "./gui-args.js";
|
|
10
|
-
import { readJsonObject, sendJson } from "./http-body.js";
|
|
11
|
+
import { readJsonObject, sendJson } from "./gui-http-body.js";
|
|
11
12
|
/** Every POST handler's shared opening: readJsonObject reads the body and — when it is
|
|
12
13
|
* oversized, malformed, or not a JSON object — has already written the 413/400 itself. Its
|
|
13
14
|
* null return therefore means the response is sent and the handler must stop without touching
|
|
@@ -17,7 +17,7 @@ import { renderRunDetail, runDetailPatch, runDetailStat, runTranscriptLines } fr
|
|
|
17
17
|
import { resolveRevertTarget } from "../operator/revert.js";
|
|
18
18
|
import { rolePayload } from "../roles/role-view.js";
|
|
19
19
|
import { clampCount, intQuery, rejectBadRole, sinceMsQuery, untilMsQuery } from "./gui-args.js";
|
|
20
|
-
import { badRequest, sendJson } from "./http-body.js";
|
|
20
|
+
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
21
21
|
import { typoSuffix } from "../text/suggest.js";
|
|
22
22
|
import { gotSuffix, splitCommaList, splitCommaListUnique } from "../text/text.js";
|
|
23
23
|
import { newestRunRow, runNotFoundLastMessage, runNotFoundMessage } from "../run/run-detail.js";
|
|
@@ -98,6 +98,31 @@ function resultClassesQuery(q, res) {
|
|
|
98
98
|
* (the `history --result` rule, parsed by resultClassesQuery like /api/run's), after `grep` and
|
|
99
99
|
* before `model`; an unknown class is a 400. Reads files directly, so it works whether
|
|
100
100
|
* or not the team is running. */
|
|
101
|
+
/** Parse the `sinceMs`-window prologue the two history endpoints share. A present `sinceMs`
|
|
102
|
+
* (with no rival `n`) yields `{ kind: "window", sinceMs, untilMs }`; an absent `sinceMs`
|
|
103
|
+
* yields `{ kind: "count" }`. Any 400 — the rival-shapes rejection when both `sinceMs` and
|
|
104
|
+
* `n` are present, or a `sinceMs`/`untilMs` that fails to parse or caps (sinceMsQuery,
|
|
105
|
+
* untilMsQuery) — is already sent and yields null, so the caller returns early. Shared by
|
|
106
|
+
* handleHistory and handleLogs so the rival-shapes message and the parse-and-cap discipline
|
|
107
|
+
* cannot drift. */
|
|
108
|
+
function windowOrCount(q, res) {
|
|
109
|
+
if (q.get("sinceMs") !== null) {
|
|
110
|
+
if (q.get("n") !== null) {
|
|
111
|
+
badRequest(res, "sinceMs cannot be combined with n (a count and a window are rival shapes)");
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const sinceMs = sinceMsQuery(q, res, LOGS_SINCE_MAX_MS, (ms) => `sinceMs is capped at ${humanSeconds(LOGS_SINCE_MAX_MS / 1000)} (got ${ms})`);
|
|
115
|
+
if (sinceMs === null)
|
|
116
|
+
return null;
|
|
117
|
+
const untilMs = untilMsQuery(q, res, sinceMs);
|
|
118
|
+
if (untilMs === null)
|
|
119
|
+
return null;
|
|
120
|
+
return { kind: "window", sinceMs, untilMs };
|
|
121
|
+
}
|
|
122
|
+
if (untilMsQuery(q, res, undefined) === null)
|
|
123
|
+
return null; // a count has no window end to move
|
|
124
|
+
return { kind: "count" };
|
|
125
|
+
}
|
|
101
126
|
export function handleHistory(q, res, root) {
|
|
102
127
|
const role = q.get("role") || null; // "" and absent both read all roles
|
|
103
128
|
const grep = q.get("grep") || null; // "" and absent both filter nothing
|
|
@@ -110,24 +135,16 @@ export function handleHistory(q, res, root) {
|
|
|
110
135
|
const modelPieces = splitCommaListUnique(q.get("model") ?? "");
|
|
111
136
|
const onModels = (rows) => (modelPieces.length ? runRowsOnModels(rows, modelPieces) : rows);
|
|
112
137
|
const summary = q.get("summary") !== null; // the flag's presence, whatever its value
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
const sinceMs = sinceMsQuery(q, res, LOGS_SINCE_MAX_MS, (ms) => `sinceMs is capped at ${humanSeconds(LOGS_SINCE_MAX_MS / 1000)} (got ${ms})`);
|
|
119
|
-
if (sinceMs === null)
|
|
120
|
-
return;
|
|
121
|
-
const untilMs = untilMsQuery(q, res, sinceMs);
|
|
122
|
-
if (untilMs === null)
|
|
123
|
-
return;
|
|
138
|
+
const windowCtl = windowOrCount(q, res);
|
|
139
|
+
if (windowCtl === null)
|
|
140
|
+
return;
|
|
141
|
+
if (windowCtl.kind === "window") {
|
|
142
|
+
const { sinceMs, untilMs } = windowCtl;
|
|
124
143
|
const { rows, covered } = readRunRowsSince(root, sinceMs, role, untilMs ?? null);
|
|
125
144
|
const matched = onModels(inClasses(grep === null ? rows : runRowsMatchingGrep(rows, grep)));
|
|
126
145
|
sendJson(res, 200, { rows: matched, covered, note: covered ? "" : SPARSE_WINDOW_NOTE, ...(summary ? { summary: summarizeRunRows(matched), modelSummary: summarizeRunRowsByModel(matched) } : {}) });
|
|
127
146
|
return;
|
|
128
147
|
}
|
|
129
|
-
if (untilMsQuery(q, res, undefined) === null)
|
|
130
|
-
return; // a count has no window end to move
|
|
131
148
|
const parsed = intQuery(q, res, "n", "non-negative", HISTORY_DEFAULT_RUNS);
|
|
132
149
|
if (parsed === null)
|
|
133
150
|
return;
|
|
@@ -169,17 +186,11 @@ export function handleLogs(q, res, root) {
|
|
|
169
186
|
const grep = q.get("grep") || null; // "" and absent both filter nothing
|
|
170
187
|
const needle = grep === null ? null : grep.toLowerCase();
|
|
171
188
|
const keep = (e) => (role === null || eventRole(e) === role) && (needle === null || eventMatchesGrep(e, needle));
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
const sinceMs = sinceMsQuery(q, res, LOGS_SINCE_MAX_MS, (ms) => `sinceMs is capped at ${humanSeconds(LOGS_SINCE_MAX_MS / 1000)} (got ${ms})`);
|
|
178
|
-
if (sinceMs === null)
|
|
179
|
-
return;
|
|
180
|
-
const untilMs = untilMsQuery(q, res, sinceMs);
|
|
181
|
-
if (untilMs === null)
|
|
182
|
-
return;
|
|
189
|
+
const windowCtl = windowOrCount(q, res);
|
|
190
|
+
if (windowCtl === null)
|
|
191
|
+
return;
|
|
192
|
+
if (windowCtl.kind === "window") {
|
|
193
|
+
const { sinceMs, untilMs } = windowCtl;
|
|
183
194
|
const { cutoff, events: windowEvents, covered } = readEventsSince(root, sinceMs);
|
|
184
195
|
const events = untilMs === undefined ? windowEvents : windowEvents.filter((e) => e.ts < cutoff + sinceMs - untilMs);
|
|
185
196
|
sendJson(res, 200, {
|
|
@@ -189,8 +200,6 @@ export function handleLogs(q, res, root) {
|
|
|
189
200
|
});
|
|
190
201
|
return;
|
|
191
202
|
}
|
|
192
|
-
if (untilMsQuery(q, res, undefined) === null)
|
|
193
|
-
return; // a count has no window end to move
|
|
194
203
|
const parsed = intQuery(q, res, "n", "non-negative", LOGS_SEARCH_EVENTS);
|
|
195
204
|
if (parsed === null)
|
|
196
205
|
return;
|