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
|
@@ -2,21 +2,21 @@
|
|
|
2
2
|
* The dashboard's POST operator-lifecycle command handlers (barrelled into
|
|
3
3
|
* gui/gui-endpoint-commands.ts, which routes to them from src/gui/gui-server.ts): reset-counters,
|
|
4
4
|
* restart, reclaim, abort, stop-schedule, role-notes, retire, revert, brief-edit, queue-cancel,
|
|
5
|
-
* queue-top, post-review-accept, and
|
|
6
|
-
* prompt/backlog/config/team. Each handler
|
|
5
|
+
* queue-top, post-review-accept, commit-backlog, and archive-backlog — every POST handler
|
|
6
|
+
* that is not prompt/backlog/config/team. Each handler
|
|
7
7
|
* adapts HTTP onto the domain work
|
|
8
8
|
* one layer down (operator-intent, role-notes-write, brief-edit) and touches no socket beyond
|
|
9
9
|
* its own `res`.
|
|
10
10
|
*/
|
|
11
11
|
import { resyncRootInstall, rootInstallResyncLines } from "../build/dep-install.js";
|
|
12
|
-
import { cancelScheduledStopMessage, requestAbort, requestResetCounters, requestRestart, requestScheduledStop, runManualReclaim } from "../operator/operator-intent.js";
|
|
13
|
-
import { runDoctor } from "../doctor/doctor.js";
|
|
12
|
+
import { cancelScheduledStopMessage, requestAbort, requestAbortAll, requestResetCounters, requestRestart, requestScheduledStop, runManualReclaim } from "../operator/operator-intent.js";
|
|
14
13
|
import { killOrphans, orphanKillLines } from "../doctor/doctor-orphans.js";
|
|
15
14
|
import { dropBrokenPins, brokenPinDropLines, retireLeftovers, leftoverRetireLines } from "../doctor/doctor-contention-checks.js";
|
|
16
15
|
import { commitUncommittedBacklog, backlogCommitMessage } from "../doctor/doctor-backlog.js";
|
|
16
|
+
import { archiveFinishedOnMain, describeArchivedFile } from "../backlog/backlog-archive.js";
|
|
17
17
|
import { loadConfigSafe } from "../config/config.js";
|
|
18
18
|
import { saveRoleNote } from "../agent/role-notes-write.js";
|
|
19
|
-
import { roleNotesPath } from "../paths.js";
|
|
19
|
+
import { roleNotesPath, MERGE_LOCK_WAIT_MS } from "../paths.js";
|
|
20
20
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
21
21
|
import { artifactPhrase, retireRole } from "../operator/retire.js";
|
|
22
22
|
import { previewRevert, revertFollowUp, revertMergedCommit } from "../operator/revert.js";
|
|
@@ -30,7 +30,7 @@ import { orchestratorAlive } from "../team/orchestrator-info.js";
|
|
|
30
30
|
import { shortSha } from "../text/format.js";
|
|
31
31
|
import { editBriefText } from "../brief/brief-edit.js";
|
|
32
32
|
import { requireStringField, validRoleIds } from "./gui-args.js";
|
|
33
|
-
import { badRequest, sendJson } from "./http-body.js";
|
|
33
|
+
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
34
34
|
import { errorMessage, gotSuffix } from "../text/text.js";
|
|
35
35
|
import { readPostBody, readRoleBody, sendRequestResult } from "./gui-endpoint-common.js";
|
|
36
36
|
import { wakeInSecondsMs } from "./gui-team-commands.js";
|
|
@@ -140,6 +140,35 @@ export async function handleCommitBacklog(req, res, root, commit = (r) => commit
|
|
|
140
140
|
const report = await commit(root);
|
|
141
141
|
sendJson(res, 200, { ok: report.failed.length === 0, message: backlogCommitMessage(report), committed: report.committed, failed: report.failed, skipped: report.skipped, base: report.base });
|
|
142
142
|
}
|
|
143
|
+
/** Handle POST /api/doctor-archive-backlog: the dashboard Health card's Archive backlog
|
|
144
|
+
* button — the same archiveFinishedOnMain `tumwater archive` runs (holding the merge lock),
|
|
145
|
+
* answering its message verbatim. `ok` is true when the finished entries were archived or
|
|
146
|
+
* there was nothing to archive; `skipped` (an off-base checkout, uncommitted backlog/archive
|
|
147
|
+
* files, or an unreadable file) answers `ok: false` with the skip reason and writes nothing.
|
|
148
|
+
* The archived body also carries the full commit sha and the moved files so the card can
|
|
149
|
+
* repaint precisely. Same body discipline as /api/kill-orphans; `archive` is injectable so a
|
|
150
|
+
* test need not touch git or hold the merge lock. */
|
|
151
|
+
export async function handleArchiveBacklog(req, res, root, archive = (r) => archiveFinishedOnMain(r, { lockTimeoutMs: MERGE_LOCK_WAIT_MS })) {
|
|
152
|
+
const body = await readPostBody(req, res, "{}");
|
|
153
|
+
if (!body)
|
|
154
|
+
return;
|
|
155
|
+
const outcome = await archive(root);
|
|
156
|
+
if (outcome.status === "nothing") {
|
|
157
|
+
sendJson(res, 200, { ok: true, message: "nothing to archive: PLANS.md ## Done and BUGS.md ## Fixed hold no finished entries" });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (outcome.status === "skipped") {
|
|
161
|
+
sendJson(res, 200, { ok: false, message: `nothing archived: ${outcome.reason}` });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
sendJson(res, 200, {
|
|
165
|
+
ok: true,
|
|
166
|
+
message: [...outcome.moved.map(describeArchivedFile), `committed ${shortSha(outcome.commit)}`].join("; "),
|
|
167
|
+
archived: true,
|
|
168
|
+
commit: outcome.commit,
|
|
169
|
+
moved: outcome.moved,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
143
172
|
/** Handle POST /api/doctor-resync-install: the dashboard Health card's Re-sync install button —
|
|
144
173
|
* the terminal twin of `tumwater doctor --resync-install` and the TUI's Ctrl+U, all reached
|
|
145
174
|
* through the same resyncRootInstall. It answers the same lines the CLI prints
|
|
@@ -154,17 +183,6 @@ export async function handleResyncInstall(req, res, root, resync = resyncRootIns
|
|
|
154
183
|
const r = await resync(root);
|
|
155
184
|
sendJson(res, 200, { ok: r.ok, message: rootInstallResyncLines(r).join("; ") });
|
|
156
185
|
}
|
|
157
|
-
/** Handle POST /api/doctor-reprobe: the dashboard Health card's Re-probe models button, the
|
|
158
|
-
* twin of `tumwater doctor --reprobe`. A POST because it spends one model call per claimed pi
|
|
159
|
-
* image-input model and rewrites `.tumwater/state/image-probes.json`; GET /api/doctor stays
|
|
160
|
-
* probe-free. Same body discipline as /api/kill-orphans; `run` takes the probe mode so a test
|
|
161
|
-
* can pin that the handler asks for "all" without spawning a probe. */
|
|
162
|
-
export async function handleDoctorReprobe(req, res, root, run = (r, p) => runDoctor(r, undefined, undefined, p)) {
|
|
163
|
-
const body = await readPostBody(req, res, "{}");
|
|
164
|
-
if (!body)
|
|
165
|
-
return;
|
|
166
|
-
sendJson(res, 200, await run(root, "all"));
|
|
167
|
-
}
|
|
168
186
|
/** Handle POST /api/abort: the dashboard's per-row abort control — the same marker-writing
|
|
169
187
|
* core `tumwater abort` calls (requestAbort). The role is required and validated like
|
|
170
188
|
* /api/transcript; requestAbort's not-live error comes back 409 — the marker is valid but
|
|
@@ -176,6 +194,14 @@ export async function handleAbort(req, res, root) {
|
|
|
176
194
|
return;
|
|
177
195
|
sendRequestResult(res, requestAbort(root, body.role));
|
|
178
196
|
}
|
|
197
|
+
/** Handle POST /api/abort-all: the dashboard Roles card's Abort all button — the same
|
|
198
|
+
* marker-writing core `tumwater abort --all` calls (requestAbortAll). It reads no body (the
|
|
199
|
+
* request targets every enabled runner), so a stale `role` field from another request changes
|
|
200
|
+
* nothing; requestAbortAll's not-live error comes back 409 like /api/abort's, through
|
|
201
|
+
* sendRequestResult. */
|
|
202
|
+
export async function handleAbortAll(_req, res, root) {
|
|
203
|
+
sendRequestResult(res, requestAbortAll(root));
|
|
204
|
+
}
|
|
179
205
|
/** Handle POST /api/role-notes: the role drawer's notes Edit/Clear — the same saveRoleNote
|
|
180
206
|
* `tumwater role <id> --notes-file` / `--clear-notes` call, so the 4 KB cap and its wording
|
|
181
207
|
* cannot drift. `"text": ""` clears. The director has no notes (400, the CLI's wording); a
|
|
@@ -17,7 +17,7 @@ import { parseDurationMs } from "../cli/cli-args.js";
|
|
|
17
17
|
import { PAUSE_FOR_MAX_MS, submitRolePromptAndWake } from "../operator/operator-intent.js";
|
|
18
18
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
19
19
|
import { rejectBadQueueFileName, rejectBadRole, requirePromptText } from "./gui-args.js";
|
|
20
|
-
import { badRequest, OK_GONE, sendJson } from "./http-body.js";
|
|
20
|
+
import { badRequest, OK_GONE, sendJson } from "./gui-http-body.js";
|
|
21
21
|
import { errorMessage, gotSuffix, joinParts } from "../text/text.js";
|
|
22
22
|
import { readPostBody, readRoleBody } from "./gui-endpoint-common.js";
|
|
23
23
|
/** Handle POST /api/prompt: queue a director prompt. An over-long prompt
|
|
@@ -92,6 +92,23 @@ function checkedPromptFields(res, body, role = DIRECTOR_ROLE) {
|
|
|
92
92
|
return null;
|
|
93
93
|
return { text, images, notBeforeMs };
|
|
94
94
|
}
|
|
95
|
+
/** The GUI prompt handlers' shared image-input gate (plans/image-input.md "Attachments",
|
|
96
|
+
* part 5/5): when the body carries images, refuse (400) an attachment the target role
|
|
97
|
+
* cannot read and that no tier reroutes — the same gate submitRolePrompt enforces, surfaced
|
|
98
|
+
* as a user-input error rather than the server's 500 — and return the notice a blinder's
|
|
99
|
+
* reroute should flash, or "" when there is nothing to say (image-less, or a rerouted role).
|
|
100
|
+
* Callers answer 400 and stop when it returns null. The role-queue submit and file-edit
|
|
101
|
+
* handlers share this one gate so their refusal text cannot drift from each other's. */
|
|
102
|
+
function promptImageGate(res, root, role, hasImages) {
|
|
103
|
+
if (!hasImages)
|
|
104
|
+
return "";
|
|
105
|
+
const guidance = imagePromptGuidance(root, role);
|
|
106
|
+
if (guidance.refusal) {
|
|
107
|
+
badRequest(res, guidance.refusal);
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return guidance.notice ?? "";
|
|
111
|
+
}
|
|
95
112
|
/** Handle POST /api/prompt-role: queue a prompt for one role — the dashboard's per-row
|
|
96
113
|
* prompt affordance submits here, calling the same path `tumwater prompt --role <id>` uses
|
|
97
114
|
* (submitRolePrompt enqueues into that role's own queue and logs under it, then a single-role
|
|
@@ -113,15 +130,9 @@ export async function handlePromptRole(req, res, root) {
|
|
|
113
130
|
// anything is queued — the same gate submitRolePrompt enforces, surfaced as a user-input
|
|
114
131
|
// error rather than the server's 500. A role that reroutes a blind read is not refused; its
|
|
115
132
|
// notice rides in the confirmation the composer flashes.
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
|
|
119
|
-
if (guidance.refusal) {
|
|
120
|
-
badRequest(res, guidance.refusal);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
notice = guidance.notice ?? "";
|
|
124
|
-
}
|
|
133
|
+
const notice = promptImageGate(res, root, role, (fields.images?.length ?? 0) > 0);
|
|
134
|
+
if (notice === null)
|
|
135
|
+
return;
|
|
125
136
|
const message = joinParts([
|
|
126
137
|
submitRolePromptAndWake(root, role, fields.text, fields.images, fields.notBeforeMs),
|
|
127
138
|
notice,
|
|
@@ -183,17 +194,12 @@ export async function handlePromptEdit(req, res, root) {
|
|
|
183
194
|
const fields = checkedPromptFields(res, body, role);
|
|
184
195
|
if (!fields)
|
|
185
196
|
return;
|
|
186
|
-
// Image-input gate (plans/image-input.md "Attachments", part 5/5)
|
|
187
|
-
//
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
if (
|
|
191
|
-
|
|
192
|
-
if (guidance.refusal) {
|
|
193
|
-
badRequest(res, guidance.refusal);
|
|
194
|
-
return;
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
+
// Image-input gate (plans/image-input.md "Attachments", part 5/5): an image edited into
|
|
198
|
+
// the queued prompt answers 400 naming the fix when the target role cannot read it (and no
|
|
199
|
+
// tier reroutes it) — the same gate submitRolePrompt enforces — before anything is written.
|
|
200
|
+
// The edit handler does not flash a reroute notice, so it ignores the helper's return.
|
|
201
|
+
if (promptImageGate(res, root, role, (fields.images?.length ?? 0) > 0) === null)
|
|
202
|
+
return;
|
|
197
203
|
let outcome;
|
|
198
204
|
try {
|
|
199
205
|
outcome = editQueuedFile(root, role, body.file, fields.text, fields.images);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dashboard's POST flake-quarantine command handler (barrelled into
|
|
3
|
+
* gui/gui-endpoint-commands.ts, which routes to it from src/gui/gui-server.ts):
|
|
4
|
+
* quarantine-release — the twin of `tumwater quarantine --release <n>`. The dashboard Failures
|
|
5
|
+
* view's per-entry "Release" button and the CLI predicate run the same releaseQuarantinedTest
|
|
6
|
+
* core, so the operator who decides a quarantined test's filed bug is fixed can retire it from
|
|
7
|
+
* the card without opening a terminal. The handler adapts HTTP onto that domain work and
|
|
8
|
+
* touches no socket beyond its own `res`.
|
|
9
|
+
*/
|
|
10
|
+
import { releaseQuarantinedTest } from "../review/known-flakes.js";
|
|
11
|
+
import { readPostBody } from "./gui-endpoint-common.js";
|
|
12
|
+
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
13
|
+
/** Handle POST /api/quarantine-release: release one quarantined test (the body's `test`), so
|
|
14
|
+
* the review gate no longer excuses its failures. A missing/empty `test` 400s (nothing
|
|
15
|
+
* written); a name not currently quarantined answers ok:false (nothing written); a hit removes
|
|
16
|
+
* it from .tumwater/state/quarantine.json and answers ok:true naming the test. */
|
|
17
|
+
export async function handleQuarantineRelease(req, res, root) {
|
|
18
|
+
const body = await readPostBody(req, res, "{}");
|
|
19
|
+
if (!body)
|
|
20
|
+
return;
|
|
21
|
+
const test = typeof body.test === "string" ? body.test : "";
|
|
22
|
+
if (test === "") {
|
|
23
|
+
badRequest(res, "a quarantined test name is required");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const ok = releaseQuarantinedTest(root, test);
|
|
27
|
+
sendJson(res, 200, ok
|
|
28
|
+
? { ok: true, message: `released quarantined test: ${test} — the gate no longer excuses it` }
|
|
29
|
+
: { ok: false, message: `not in the flake quarantine: ${test}` });
|
|
30
|
+
}
|
|
@@ -5,15 +5,17 @@
|
|
|
5
5
|
* this module only adapts HTTP onto it. Barrelled into gui/gui-endpoints.ts beside the other
|
|
6
6
|
* per-family GET endpoint modules.
|
|
7
7
|
*/
|
|
8
|
-
import { collectReport
|
|
8
|
+
import { collectReport } from "../report/report-data.js";
|
|
9
|
+
import { collectReportSince } from "../report/report-since-data.js";
|
|
9
10
|
import { collectFailureReport, collectFailureReportSince } from "../failure/failure-data.js";
|
|
10
11
|
import { renderFailureMarkdown } from "../failure/failure-render.js";
|
|
11
12
|
import { renderSinceReportMarkdown } from "../report/report-render.js";
|
|
12
13
|
import { loadQuarantine } from "../review/known-flakes.js";
|
|
13
|
-
import { quarantineRows,
|
|
14
|
+
import { quarantineRows, quarantineViewRows } from "../review/quarantine-render.js";
|
|
15
|
+
import { quarantineWarning } from "../baseline/main-guardrails.js";
|
|
14
16
|
import { REPORT_SINCE_MAX_MS, reportSinceMsg } from "../events/event-window.js";
|
|
15
17
|
import { roleListQuery, sinceMsQuery, untilMsQuery, windowDays } from "./gui-args.js";
|
|
16
|
-
import { badRequest, sendJson } from "./http-body.js";
|
|
18
|
+
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
17
19
|
/** Parse and validate the role-scoped, sinceMs-or-days query the report and failures endpoints
|
|
18
20
|
* both take: the `?role=` filter, a comma-separated list (each id trimmed, deduplicated, and
|
|
19
21
|
* validated by rejectBadRole — an unknown id or an empty list → 400 naming it) and, when
|
|
@@ -73,11 +75,12 @@ export function handleReport(q, res, root) {
|
|
|
73
75
|
* same bounded Markdown failure digest the selfcheck role feeds on and `tumwater report
|
|
74
76
|
* --failures` prints, as JSON ({ markdown }) — the dashboard's failures tab renders it; with
|
|
75
77
|
* sinceMs it is the trailing-window digest (collectFailureReportSince) through the same render,
|
|
76
|
-
* the CLI's `report --failures --since` shape. The response also carries `quarantine
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
78
|
+
* the CLI's `report --failures --since` shape. The response also carries `quarantine` —
|
|
79
|
+
* `{ warning, rows }` (quarantineViewRows over the current loadQuarantine list, an empty
|
|
80
|
+
* rows list when it is empty) the Failures view's card paints below the digest — the interactive
|
|
81
|
+
* twin of `tumwater quarantine --release <n>` (each row's Release button posts to
|
|
82
|
+
* POST /api/quarantine-release), current rather than window- or role-scoped. The digest's
|
|
83
|
+
* `markdown` is unchanged. The days window follows windowDays (so it can
|
|
81
84
|
* never drift from /api/report's). role rides the same rule as
|
|
82
85
|
* handleReport: absent → the team digest, present → the validated listed roles, summed. Reads files
|
|
83
86
|
* directly, so it works with no team running. */
|
|
@@ -88,7 +91,8 @@ export function handleFailures(q, res, root) {
|
|
|
88
91
|
const untilMs = untilMsQuery(q, res, scope.sinceMs);
|
|
89
92
|
if (untilMs === null)
|
|
90
93
|
return;
|
|
91
|
-
const
|
|
94
|
+
const rows = quarantineRows(loadQuarantine(root));
|
|
95
|
+
const quarantine = { warning: quarantineWarning(rows, Date.now()), rows: quarantineViewRows(rows, Date.now()) };
|
|
92
96
|
if (scope.sinceMs !== undefined) {
|
|
93
97
|
sendJson(res, 200, { markdown: renderFailureMarkdown(collectFailureReportSince(root, scope.sinceMs, scope.roles, untilMs)), quarantine });
|
|
94
98
|
return;
|
|
@@ -13,8 +13,8 @@ import { captureStartupBuild, createReloadWatch, reexecSelf, watchReloadSupervis
|
|
|
13
13
|
import { warnEvent, TUMWATER_SOURCE } from "../events/events.js";
|
|
14
14
|
import { errorMessage } from "../text/text.js";
|
|
15
15
|
import { handleBacklog, handleBacklogMatch, handleBrief, handleConfig, handleDiff, handleFailures, handleReport, handleHistory, handleLogs, handlePromptText, handleRun, handleRole, handleTranscript, handleDoctor, handleRetireList, } from "./gui-endpoints.js";
|
|
16
|
-
import { handleAbort, handleBriefEdit, handleBudget, handleConfigSet, handleConfigUnset, handleRoleCap, handleRoleReviewAfterMerge, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest, handlePause, handlePauseRole, handlePrompt, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit, handlePromptCancel, handlePromptEdit, handlePromptRole, handlePromptTop, handleQuestionAnswer, handleReclaim, handleKillOrphans, handleDropBrokenPins, handleCommitBacklog, handleResyncInstall, handleRetireLeftovers,
|
|
17
|
-
import { sendJson } from "./http-body.js";
|
|
16
|
+
import { handleAbort, handleAbortAll, handleBriefEdit, handleBudget, handleConfigSet, handleConfigUnset, handleRoleCap, handleRoleReviewAfterMerge, handleRoleEnable, handleRoleDisable, handleRoleInstances, handleRoleInterval, handleRoleInstructions, handleRoleModel, handleRoleQuietHours, handleNotifyTest, handlePause, handlePauseRole, handlePrompt, handleBacklogFile, handleBacklogCancel, handleBacklogRelease, handleBacklogTop, handleBacklogEdit, handlePromptCancel, handlePromptEdit, handlePromptRole, handlePromptTop, handleQuestionAnswer, handleReclaim, handleKillOrphans, handleDropBrokenPins, handleCommitBacklog, handleResyncInstall, handleRetireLeftovers, handleArchiveBacklog, handleRestart, handleStopSchedule, handleResetCounters, handleRoleNotes, handleRetire, handleRevert, handleQueueCancel, handleQueueTop, handlePostReviewAccept, handleQuarantineRelease, handleWake, handleWakeCancel, } from "./gui-endpoint-commands.js";
|
|
17
|
+
import { sendJson } from "./gui-http-body.js";
|
|
18
18
|
/** Constant-time credential comparison for the shared-token gate: `timingSafeEqual` throws
|
|
19
19
|
* on unequal lengths, so the length equality is the guard. A naive `===` string compare
|
|
20
20
|
* would leak the token's length and prefix byte by byte. */
|
|
@@ -194,6 +194,7 @@ export function startGui(root, port, allInterfaces = false, token = "", watch =
|
|
|
194
194
|
"POST /api/queue-cancel": (req, res) => handleQueueCancel(req, res, root),
|
|
195
195
|
"POST /api/queue-top": (req, res) => handleQueueTop(req, res, root),
|
|
196
196
|
"POST /api/post-review-accept": (req, res) => handlePostReviewAccept(req, res, root),
|
|
197
|
+
"POST /api/quarantine-release": (req, res) => handleQuarantineRelease(req, res, root),
|
|
197
198
|
"POST /api/restart": (req, res) => handleRestart(req, res, root),
|
|
198
199
|
"POST /api/stop-schedule": (req, res) => handleStopSchedule(req, res, root),
|
|
199
200
|
"POST /api/reclaim": (req, res) => handleReclaim(req, res, root),
|
|
@@ -202,8 +203,9 @@ export function startGui(root, port, allInterfaces = false, token = "", watch =
|
|
|
202
203
|
"POST /api/doctor-commit-backlog": (req, res) => handleCommitBacklog(req, res, root),
|
|
203
204
|
"POST /api/doctor-resync-install": (req, res) => handleResyncInstall(req, res, root),
|
|
204
205
|
"POST /api/doctor-retire-leftovers": (req, res) => handleRetireLeftovers(req, res, root),
|
|
205
|
-
"POST /api/doctor-
|
|
206
|
+
"POST /api/doctor-archive-backlog": (req, res) => handleArchiveBacklog(req, res, root),
|
|
206
207
|
"POST /api/abort": (req, res) => handleAbort(req, res, root),
|
|
208
|
+
"POST /api/abort-all": (req, res) => handleAbortAll(req, res, root),
|
|
207
209
|
"POST /api/pause-role": (req, res) => handlePauseRole(req, res, root),
|
|
208
210
|
};
|
|
209
211
|
const server = http.createServer(async (req, res) => {
|
|
@@ -9,7 +9,7 @@ import { baseRoleOf, INSTANCE_ROLES } from "../roles/runner-ids.js";
|
|
|
9
9
|
import { normalizePauseReason, pauseTeam, pauseRole, resumeTeam, resumeRole } from "../team/team-state.js";
|
|
10
10
|
import { cancelWakes, PAUSE_FOR_MAX_MS, requestWake } from "../operator/operator-intent.js";
|
|
11
11
|
import { requirePausedFlag, validRoleIds } from "./gui-args.js";
|
|
12
|
-
import { badRequest, sendJson } from "./http-body.js";
|
|
12
|
+
import { badRequest, sendJson } from "./gui-http-body.js";
|
|
13
13
|
import { gotSuffix } from "../text/text.js";
|
|
14
14
|
import { readPostBody, readRoleBody } from "./gui-endpoint-common.js";
|
|
15
15
|
/** The shared optional-seconds rule behind the timed pause's `forSeconds` and the scheduled
|
|
@@ -17,6 +17,7 @@ import { shortSpanPhrase } from "../text/phrases.js";
|
|
|
17
17
|
import { outcomeText, usageText } from "../events/event-format.js";
|
|
18
18
|
import { compareCodeUnits } from "../collections.js";
|
|
19
19
|
import { bucketMergeEvents, resolveQueuedResult } from "./history-queued.js";
|
|
20
|
+
import { nonNegativeNumber } from "../files/json-object.js";
|
|
20
21
|
/** `history`'s default row count and ceiling. The default shows a working hour of a quiet
|
|
21
22
|
* team; the ceiling bounds how much log one ask re-reads — more rows only re-read more log
|
|
22
23
|
* with no added signal, so an explicit flag above it fails fast (the report's --days pattern). */
|
|
@@ -68,6 +69,8 @@ function tallyRows(rows, keyOf) {
|
|
|
68
69
|
t.costUsd += row.costUsd;
|
|
69
70
|
if (row.durationMs !== null)
|
|
70
71
|
t.durationMs = (t.durationMs ?? 0) + row.durationMs;
|
|
72
|
+
if (row.peakContextTokens !== undefined)
|
|
73
|
+
t.peakContextTokens = Math.max(t.peakContextTokens ?? 0, row.peakContextTokens);
|
|
71
74
|
if (cls === "changes")
|
|
72
75
|
t.lastChangeTs = Math.max(t.lastChangeTs ?? row.ts, row.ts);
|
|
73
76
|
}
|
|
@@ -118,7 +121,7 @@ export function runRowCells(row) {
|
|
|
118
121
|
return {
|
|
119
122
|
role: roleLabel(row.role),
|
|
120
123
|
run: String(row.run),
|
|
121
|
-
result: eventTypeLabel(row.result),
|
|
124
|
+
result: row.cutOff === true ? `${eventTypeLabel(row.result)} (cut off)` : eventTypeLabel(row.result),
|
|
122
125
|
model: row.model ?? "",
|
|
123
126
|
duration: row.durationMs === null ? "—" : shortSpanPhrase(row.durationMs),
|
|
124
127
|
usage: row.usage,
|
|
@@ -209,11 +212,14 @@ export function runRows(events, limit, role, join) {
|
|
|
209
212
|
?.result ?? rawResult)
|
|
210
213
|
: rawResult;
|
|
211
214
|
const usage = eventUsage(e);
|
|
215
|
+
const peak = nonNegativeNumber(e.peakContextTokens, 0);
|
|
212
216
|
rows.push({
|
|
213
217
|
ts: e.ts,
|
|
214
218
|
time: formatTimestamp(e.ts),
|
|
215
219
|
tokens: usage.tokens,
|
|
216
220
|
costUsd: usage.costUsd,
|
|
221
|
+
...(peak > 0 ? { peakContextTokens: peak } : {}),
|
|
222
|
+
...(e.cutOff === true ? { cutOff: true } : {}),
|
|
217
223
|
role: eventRole(e),
|
|
218
224
|
run: eventRun(e),
|
|
219
225
|
result,
|
|
@@ -77,20 +77,25 @@ export function renderModelSummary(summary) {
|
|
|
77
77
|
}
|
|
78
78
|
function renderSummaryTable(header, all) {
|
|
79
79
|
const roleWidth = Math.max(...[header, ...all.map((r) => r.label)].map(displayWidth));
|
|
80
|
-
const cols = ["runs", "changes", "none", "refused", "problems", "tokens", "cost", "time"];
|
|
80
|
+
const cols = ["runs", "changes", "none", "refused", "problems", "tokens", "peak", "cost", "time"];
|
|
81
|
+
const colLabel = { runs: "runs", changes: "changes", none: "none", refused: "refused", problems: "problems", tokens: "tokens", peak: "peak ctx", cost: "cost", time: "time" };
|
|
81
82
|
const cell = (r, c) => c === "tokens"
|
|
82
83
|
? compactTokens(r.tokens)
|
|
83
|
-
: c === "
|
|
84
|
-
?
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
:
|
|
90
|
-
|
|
84
|
+
: c === "peak"
|
|
85
|
+
? r.peakContextTokens === undefined
|
|
86
|
+
? "—"
|
|
87
|
+
: compactTokens(r.peakContextTokens)
|
|
88
|
+
: c === "cost"
|
|
89
|
+
? usd(r.costUsd)
|
|
90
|
+
: c === "time"
|
|
91
|
+
? r.durationMs === undefined
|
|
92
|
+
? "—"
|
|
93
|
+
: humanSeconds(r.durationMs / 1000)
|
|
94
|
+
: String(c === "runs" ? r.runs : r[c]);
|
|
95
|
+
const widths = cols.map((c) => Math.max(colLabel[c].length, ...all.map((r) => cell(r.row, c).length)));
|
|
91
96
|
const line = (role, cells, lastChange) => [padToWidth(role, roleWidth), ...cells.map((c, i) => c.padStart(widths[i] ?? 0)), lastChange].join(" ").trimEnd();
|
|
92
97
|
return [
|
|
93
|
-
line(header, [
|
|
98
|
+
line(header, cols.map((c) => colLabel[c]), "last change"),
|
|
94
99
|
...all.map(({ label, row }) => line(label, cols.map((c) => cell(row, c)), row.lastChangeTs === null ? "—" : formatTimestamp(row.lastChangeTs))),
|
|
95
100
|
].join("\n");
|
|
96
101
|
}
|