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
|
@@ -84,7 +84,6 @@ export const EVENT_TYPE_LABELS = {
|
|
|
84
84
|
model_fallback_ended: "fallback model ended",
|
|
85
85
|
opencode_free_model_billed: "free model billed",
|
|
86
86
|
image_blind: "image not seen",
|
|
87
|
-
image_reroute: "image sent to another model",
|
|
88
87
|
build_stale: "update available",
|
|
89
88
|
restart_pending: "update waits for idle",
|
|
90
89
|
restart: "restarted onto update",
|
|
@@ -34,7 +34,7 @@ const LEGACY_EVENT_TYPES = {
|
|
|
34
34
|
/** The ordered field rewrites; later parts of the series append here, and they run in order. */
|
|
35
35
|
const LEGACY_EVENT_REWRITES = [
|
|
36
36
|
{
|
|
37
|
-
// The run number rode `tick` on run_start, run_end, prompt_taken, image_blind
|
|
37
|
+
// The run number rode `tick` on run_start, run_end, prompt_taken, image_blind.
|
|
38
38
|
name: "tick-to-run", // retired-name-ok: legacy name
|
|
39
39
|
apply(ev) {
|
|
40
40
|
const e = ev;
|
|
@@ -349,7 +349,7 @@ export function eventsSince(root, sinceMs, now, keep = () => true) {
|
|
|
349
349
|
return readEventsSince(root, sinceMs).events.filter((ev) => keep(ev) && ev.ts >= now - sinceMs);
|
|
350
350
|
}
|
|
351
351
|
/** The `--since` / `?sinceMs=` trailing window's bounds and identity slice, shared by the two
|
|
352
|
-
* trailing-window collectors (`collectReportSince` in report-data.ts and
|
|
352
|
+
* trailing-window collectors (`collectReportSince` in report-since-data.ts and
|
|
353
353
|
* `collectFailureReportSince` in failure-data.ts): an open-ended window (cutoff = now − sinceMs,
|
|
354
354
|
* end = now) when `untilMs` is absent, else the bounded [now − sinceMs, now − untilMs). Both
|
|
355
355
|
* collectors budget events with the same open/closed comparisons against `cutoff`/`end` (and
|
|
@@ -58,8 +58,21 @@ export function logEvent(root, event) {
|
|
|
58
58
|
ensureParentDir(file);
|
|
59
59
|
rotateIfLarge(file, EVENTS_MAX_BYTES, EVENTS_KEEP);
|
|
60
60
|
appendEventLine(file, JSON.stringify(full) + "\n");
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
// The event is committed to the log before dispatch, so a subscriber's own failure must not
|
|
62
|
+
// make the write look like it failed to the caller, starve the other subscribers of this or
|
|
63
|
+
// any later event, or crash an unwrapped writer (the orchestrator's poll-loop warn site)
|
|
64
|
+
// for one listener's bug. Each listener is isolated: its throw is surfaced loudly on stderr
|
|
65
|
+
// (a terminal boundary, so the text is sanitized) and the dispatch moves on. The set is
|
|
66
|
+
// snapshotted so a listener that unsubscribes itself mid-dispatch cannot skip or double-run
|
|
67
|
+
// a sibling.
|
|
68
|
+
for (const listener of [...listeners]) {
|
|
69
|
+
try {
|
|
70
|
+
listener(full);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
process.stderr.write(`tumwater: event subscriber failed: ${stripTerminalControls(errorMessage(err))}\n`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
63
76
|
return full;
|
|
64
77
|
}
|
|
65
78
|
/** Log a warning event: the harness's "something is off but the loop continues" signal. The
|
|
@@ -31,7 +31,7 @@ export const NOTIFY_EVENT_TYPES = [
|
|
|
31
31
|
"role_streak_paused", // the error-streak circuit breaker auto-paused a role
|
|
32
32
|
"role_cap_paused", // the role's own daily cost cap is reached — it stops starting runs
|
|
33
33
|
"disk_low", // free space on the worktrees volume dropped below diskHoldGB — new work is held
|
|
34
|
-
"main_red_hold", // main's own check is red —
|
|
34
|
+
"main_red_hold", // main's own check is red — roles keep authoring; the merge-side main_red pin queues a kept change until main moves past it, and the ci healer is handed the failing tests
|
|
35
35
|
"merge_failed", // the merge slot finished without merging a change
|
|
36
36
|
"restart_blocked", // the self-redeploy for main is blocked and latched — the team is stuck behind it
|
|
37
37
|
"question_posted", // a merge posted a new QUESTIONS.md ## Open entry — only a human can answer it
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* run-timeout shapes and their pooling, and the grouping engine live here rather than
|
|
5
5
|
* inside either consumer. Pure string
|
|
6
6
|
* and grouping logic — no event reads, no clock. */
|
|
7
|
-
import { rankByCount } from "../collections.js";
|
|
7
|
+
import { rankByCount, topAndHidden } from "../collections.js";
|
|
8
8
|
import { cutSplitsSurrogatePair } from "../text/text.js";
|
|
9
9
|
/** The verbatim example's trim bound, shared by the normalized key and each cluster's example.
|
|
10
10
|
* The digest's other caps (top-N counts, summary width) live beside their consumers in
|
|
@@ -146,8 +146,9 @@ export function clusterMessages(messages, top, urgent) {
|
|
|
146
146
|
// Clusters whose example is `urgent` rank ahead of every other, whatever their count, so a
|
|
147
147
|
// once-per-window singleton survives the cut past recurring noise.
|
|
148
148
|
const sorted = rankByCount(drafts.values(), (d) => d.count + (urgent?.(d.example) ? Number.MAX_SAFE_INTEGER / 2 : 0), (d) => d.key);
|
|
149
|
+
const { visible, hidden } = topAndHidden(sorted, top);
|
|
149
150
|
return {
|
|
150
|
-
clusters:
|
|
151
|
+
clusters: visible.map((d) => ({
|
|
151
152
|
key: d.key,
|
|
152
153
|
count: d.count,
|
|
153
154
|
roles: sortedRoles(d.roles),
|
|
@@ -155,6 +156,6 @@ export function clusterMessages(messages, top, urgent) {
|
|
|
155
156
|
lastSeen: d.lastSeen,
|
|
156
157
|
example: d.example,
|
|
157
158
|
})),
|
|
158
|
-
hiddenClusters:
|
|
159
|
+
hiddenClusters: hidden,
|
|
159
160
|
};
|
|
160
161
|
}
|
|
@@ -217,7 +217,66 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
217
217
|
const allEvents = bases === undefined ? allEventsRaw : scopeToRole(allEventsRaw, bases);
|
|
218
218
|
const oldestRetained = allEventsRaw.length > 0 ? eventDayKey(allEventsRaw[0]) : null;
|
|
219
219
|
const runEvents = current.filter((ev) => ev.type === "run_end");
|
|
220
|
-
|
|
220
|
+
const outcomes = outcomeRows(runEvents);
|
|
221
|
+
// Time and spend: the same runs priced by wall-clock span and cost, per role × outcome
|
|
222
|
+
// class, plus the loss ranking that weighs causes by agent-hours rather than run counts.
|
|
223
|
+
const { timeSpend, lossCauses, lossCausesHidden } = timeAndSpend(runEvents, allEvents);
|
|
224
|
+
// Prompt-token telemetry: per role, the median prompt tokens a run sent and how much of
|
|
225
|
+
// the prompt budget went out before the first edit. Only runs carrying the field count, so
|
|
226
|
+
// a pre-feature log leaves the section off entirely.
|
|
227
|
+
const promptStats = promptStatsFor(runEvents);
|
|
228
|
+
const deltas = deltaRows(current, prior);
|
|
229
|
+
const errors = errorSection(runEvents);
|
|
230
|
+
const warnings = warningSection(current);
|
|
231
|
+
const { rejections, postMergeRejections } = rejectionSection(current);
|
|
232
|
+
const mergeFailures = mergeFailureSection(current);
|
|
233
|
+
const reviewFailures = reviewFailureSection(current);
|
|
234
|
+
const { merged, mergedTotal } = mergedCommits(current);
|
|
235
|
+
const { stateChanges, stateChangesTotal } = stateChangeRows(current);
|
|
236
|
+
// Judged on the two partitions, not the whole read: the since collector reads from priorFrom's
|
|
237
|
+
// local day key, which can reach events earlier that day but before priorFrom — outside both
|
|
238
|
+
// windows. Counting those as "events" would suppress the empty-window line with a window that
|
|
239
|
+
// in fact held none. The day collector's read is exactly current ∪ prior (whole local days), so
|
|
240
|
+
// this changes nothing there.
|
|
241
|
+
const hasEvents = current.length > 0 || prior.length > 0;
|
|
242
|
+
const oldestEventDate = hasEvents ? oldestRetained : null;
|
|
243
|
+
// Complete only when the reader proves the whole read window (priorFrom..to) is covered:
|
|
244
|
+
// coversFullWindow fires when the oldest retained line predates priorFrom, so both the delta
|
|
245
|
+
// baseline and the current window reach their starts. Every retained event's day is >= priorFrom
|
|
246
|
+
// (the reader filters on it), so an event-side comparison can never strengthen that proof — the
|
|
247
|
+
// former `oldestEventDate < from` arm declared the digest complete while the prior half was still
|
|
248
|
+
// truncated, leaving the delta table to present a skewed baseline as a real trend. Only a proven
|
|
249
|
+
// window can be trusted as idle rather than truncated by rotation.
|
|
250
|
+
const windowComplete = coversFullWindow;
|
|
251
|
+
return {
|
|
252
|
+
...window,
|
|
253
|
+
...(bases === undefined ? {} : { role: bases.join(",") }),
|
|
254
|
+
runs: runEvents.length,
|
|
255
|
+
hasEvents,
|
|
256
|
+
emptyLog: !hasEvents && !coversFullWindow,
|
|
257
|
+
logState,
|
|
258
|
+
partial: hasEvents && !windowComplete,
|
|
259
|
+
oldestEventDate,
|
|
260
|
+
outcomes,
|
|
261
|
+
deltas,
|
|
262
|
+
timeSpend,
|
|
263
|
+
lossCauses,
|
|
264
|
+
lossCausesHidden,
|
|
265
|
+
promptStats,
|
|
266
|
+
errors,
|
|
267
|
+
warnings,
|
|
268
|
+
reviewFailures,
|
|
269
|
+
rejections,
|
|
270
|
+
postMergeRejections,
|
|
271
|
+
mergeFailures,
|
|
272
|
+
merged,
|
|
273
|
+
mergedTotal,
|
|
274
|
+
stateChanges,
|
|
275
|
+
stateChangesTotal,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/** Outcome table: one row per role, counts per result that occurred. */
|
|
279
|
+
function outcomeRows(runEvents) {
|
|
221
280
|
const outcomeMap = new Map();
|
|
222
281
|
for (const ev of runEvents) {
|
|
223
282
|
const role = eventRole(ev);
|
|
@@ -229,19 +288,14 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
229
288
|
// sort-ascending-then-reverse idiom accidentally flipped equal-total ties into
|
|
230
289
|
// reverse-alphabetical order; equal-total roles now follow the stated deterministic rule
|
|
231
290
|
// (pinned by the tie test in test/failure-render.test.ts).
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
// Prompt-token telemetry: per role, the median prompt tokens a run sent and how much of
|
|
237
|
-
// the prompt budget went out before the first edit. Only runs carrying the field count, so
|
|
238
|
-
// a pre-feature log leaves the section off entirely.
|
|
239
|
-
const promptStats = promptStatsFor(runEvents);
|
|
240
|
-
// Deltas: current vs preceding window, per role.
|
|
291
|
+
return rankByCount([...outcomeMap.entries()].map(([role, counts]) => ({ role, counts })), (row) => total(row.counts), (row) => row.role);
|
|
292
|
+
}
|
|
293
|
+
/** Deltas: current vs preceding window, per role, ordered by runs descending. */
|
|
294
|
+
function deltaRows(current, prior) {
|
|
241
295
|
const curStats = roleStats(current);
|
|
242
296
|
const prevStats = roleStats(prior);
|
|
243
297
|
const roles = [...new Set([...curStats.keys(), ...prevStats.keys()])];
|
|
244
|
-
|
|
298
|
+
return roles
|
|
245
299
|
.map((role) => {
|
|
246
300
|
const c = curStats.get(role) ?? { runs: 0, errors: 0, quietKills: 0, rejections: 0 };
|
|
247
301
|
const p = prevStats.get(role) ?? { runs: 0, errors: 0, quietKills: 0, rejections: 0 };
|
|
@@ -260,51 +314,65 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
260
314
|
.sort((a, b) => b.runs - a.runs ||
|
|
261
315
|
b.prevRuns - a.prevRuns ||
|
|
262
316
|
a.role.localeCompare(b.role));
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
317
|
+
}
|
|
318
|
+
/** Only runs that ENDED as errors carry a run error: `state.lastError` is shared state the
|
|
319
|
+
* merge slot also writes, so a successful run's `run_end` can carry a leftover merge failure's
|
|
320
|
+
* text (BUGS.md 2026-09-21). `main_red` joins them because its cause is written for that result
|
|
321
|
+
* — the baseline gate's own text (src/baseline/main-red.ts), never a leftover — so the
|
|
322
|
+
* section itemizes the outcome that blocks every merge instead of leaving it a bare count
|
|
323
|
+
* (BUGS.md 2026-09-28). The total therefore equals the Outcome table's error column PLUS its
|
|
324
|
+
* main_red cells; the render's section title says so, and merge failures still surface below.
|
|
325
|
+
* The selection predicate is the Outcome table's own — raw `error`/`main_red` results, no
|
|
326
|
+
* error-text test — so a malformed run_end that skipped the text can never be counted above
|
|
327
|
+
* but itemized nowhere: those events cluster under a placeholder cause, keeping the section's
|
|
328
|
+
* total equal to the tables it cross-checks against (BUGS.md 2026-09-30). */
|
|
329
|
+
function errorSection(runEvents) {
|
|
274
330
|
const errorEvents = runEvents.filter((ev) => ev.result === "error" || ev.result === "main_red");
|
|
275
|
-
|
|
331
|
+
return section(clusterMessages(errorEvents.map((ev) => ({
|
|
276
332
|
message: errorTextOrPlaceholder(ev.error),
|
|
277
333
|
role: eventRole(ev),
|
|
278
334
|
ts: ev.ts,
|
|
279
335
|
})), ERROR_TOP), errorEvents.length);
|
|
280
|
-
|
|
336
|
+
}
|
|
337
|
+
/** Warnings cluster on the merged, often-reworded message; the re-input rewording pools the
|
|
338
|
+
* pre-07b2664a wording with its current form so a window spanning the change counts one cause. */
|
|
339
|
+
function warningSection(current) {
|
|
340
|
+
return messageSection(current.map((ev) => ev.type === "warning" && typeof ev.message === "string"
|
|
281
341
|
? { ...ev, message: ev.message.replace(OLD_MERGE_CHECK_WORDING, "merge check failed (kept for one retry)") }
|
|
282
342
|
: ev), "warning", WARNING_TOP, (m) => URGENT_WARNING.test(m));
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
343
|
+
}
|
|
344
|
+
/** Rejections cluster on (role, reasons[0]) — the field the event feed renders — so two
|
|
345
|
+
* different rejection reasons from the same role stay separate rows, and an already-landed
|
|
346
|
+
* one (ev.afterMerge === true) splits apart from a same-reason pre-merge one so the row names
|
|
347
|
+
* the post-merge class whose rejection stands recorded on main. The section's total counts
|
|
348
|
+
* every `review_rejected` in the window, reasons or not, so it equals the Deltas column and
|
|
349
|
+
* the render's remainder line cross-checks against it. The post-merge count rides along. */
|
|
350
|
+
function rejectionSection(current) {
|
|
289
351
|
const rejectedEvents = current.filter((ev) => ev.type === "review_rejected");
|
|
290
352
|
const postMergeRejections = rejectedEvents.filter((ev) => ev.afterMerge === true).length;
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
353
|
+
return {
|
|
354
|
+
rejections: section(clusterMessages(rejectedEvents
|
|
355
|
+
.filter((ev) => Array.isArray(ev.reasons))
|
|
356
|
+
.map((ev) => {
|
|
357
|
+
const reasons = ev.reasons;
|
|
358
|
+
const message = ev.afterMerge === true ? `post-merge rejection: ${firstReason(reasons)}` : firstReason(reasons);
|
|
359
|
+
return { message, role: eventRole(ev), ts: ev.ts, keyPrefix: `${eventRole(ev)}\u0000` };
|
|
360
|
+
}), REJECTION_TOP), rejectedEvents.length),
|
|
361
|
+
postMergeRejections,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/** Merge review failures: a reviewer that could not return a parseable verdict (a dead
|
|
365
|
+
* backend, a transport error) fails closed and keeps the commit, but its authoring run ends
|
|
366
|
+
* `queued`/`no_change` — so the failure lives on the `review_failed` event, never on
|
|
367
|
+
* `run_end.error`. Clustered on their own so the selfcheck role can see a gate that keeps
|
|
368
|
+
* failing without misreading it as a run error (BUGS.md 2026-09-21). */
|
|
369
|
+
function reviewFailureSection(current) {
|
|
370
|
+
return messageSection(current, "review_failed", REVIEW_FAILURE_TOP);
|
|
371
|
+
}
|
|
372
|
+
/** The newest MERGED_TOP merges, newest first. The pre-slice count rides along, as it does for
|
|
373
|
+
* the state changes below, so the render can name what the cut left out — a busy window must
|
|
374
|
+
* not read as a MERGED_TOP-merge day (BUGS.md 2026-09-23). */
|
|
375
|
+
function mergedCommits(current) {
|
|
308
376
|
const allMerged = current
|
|
309
377
|
.filter((ev) => ev.type === "merged")
|
|
310
378
|
.map((ev) => ({
|
|
@@ -313,56 +381,17 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
313
381
|
summary: typeof ev.summary === "string" ? truncateExample(ev.summary, SUMMARY_MAX) : "",
|
|
314
382
|
}))
|
|
315
383
|
.sort((a, b) => b.ts - a.ts);
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
384
|
+
return { merged: allMerged.slice(0, MERGED_TOP), mergedTotal: allMerged.length };
|
|
385
|
+
}
|
|
386
|
+
/** The harness's own decisions, newest STATE_CHANGE_TOP kept in chronological order. The
|
|
387
|
+
* description is bounded at collection so render stays a pure function of this data. The
|
|
388
|
+
* pre-slice count rides along so the render can say when it is showing a cut, not all of them. */
|
|
389
|
+
function stateChangeRows(current) {
|
|
320
390
|
const allStateChanges = current
|
|
321
391
|
.filter((ev) => STATE_CHANGE_TYPES.has(ev.type))
|
|
322
392
|
.map((ev) => ({ ts: ev.ts, role: eventRole(ev), description: describeStateChange(ev) }))
|
|
323
393
|
.sort((a, b) => a.ts - b.ts);
|
|
324
|
-
|
|
325
|
-
// Judged on the two partitions, not the whole read: the since collector reads from priorFrom's
|
|
326
|
-
// local day key, which can reach events earlier that day but before priorFrom — outside both
|
|
327
|
-
// windows. Counting those as "events" would suppress the empty-window line with a window that
|
|
328
|
-
// in fact held none. The day collector's read is exactly current ∪ prior (whole local days), so
|
|
329
|
-
// this changes nothing there.
|
|
330
|
-
const hasEvents = current.length > 0 || prior.length > 0;
|
|
331
|
-
const oldestEventDate = hasEvents ? oldestRetained : null;
|
|
332
|
-
// Complete only when the reader proves the whole read window (priorFrom..to) is covered:
|
|
333
|
-
// coversFullWindow fires when the oldest retained line predates priorFrom, so both the delta
|
|
334
|
-
// baseline and the current window reach their starts. Every retained event's day is >= priorFrom
|
|
335
|
-
// (the reader filters on it), so an event-side comparison can never strengthen that proof — the
|
|
336
|
-
// former `oldestEventDate < from` arm declared the digest complete while the prior half was still
|
|
337
|
-
// truncated, leaving the delta table to present a skewed baseline as a real trend. Only a proven
|
|
338
|
-
// window can be trusted as idle rather than truncated by rotation.
|
|
339
|
-
const windowComplete = coversFullWindow;
|
|
340
|
-
return {
|
|
341
|
-
...window,
|
|
342
|
-
...(bases === undefined ? {} : { role: bases.join(",") }),
|
|
343
|
-
runs: runEvents.length,
|
|
344
|
-
hasEvents,
|
|
345
|
-
emptyLog: !hasEvents && !coversFullWindow,
|
|
346
|
-
logState,
|
|
347
|
-
partial: hasEvents && !windowComplete,
|
|
348
|
-
oldestEventDate,
|
|
349
|
-
outcomes,
|
|
350
|
-
deltas,
|
|
351
|
-
timeSpend,
|
|
352
|
-
lossCauses,
|
|
353
|
-
lossCausesHidden,
|
|
354
|
-
promptStats,
|
|
355
|
-
errors,
|
|
356
|
-
warnings,
|
|
357
|
-
reviewFailures,
|
|
358
|
-
rejections,
|
|
359
|
-
postMergeRejections,
|
|
360
|
-
mergeFailures,
|
|
361
|
-
merged,
|
|
362
|
-
mergedTotal: allMerged.length,
|
|
363
|
-
stateChanges,
|
|
364
|
-
stateChangesTotal: allStateChanges.length,
|
|
365
|
-
};
|
|
394
|
+
return { stateChanges: allStateChanges.slice(-STATE_CHANGE_TOP), stateChangesTotal: allStateChanges.length };
|
|
366
395
|
}
|
|
367
396
|
/** Median of a non-empty numeric list (the mean of the two middles for an even count). */
|
|
368
397
|
function median(values) {
|
|
@@ -384,12 +413,14 @@ function promptStatsFor(runs) {
|
|
|
384
413
|
if (prompt === 0 && preEdit === 0)
|
|
385
414
|
continue;
|
|
386
415
|
const role = eventRole(ev);
|
|
387
|
-
const acc = getOrCreate(byRole, role, () => ({ prompts: [], promptSum: 0, preEditSum: 0 }));
|
|
416
|
+
const acc = getOrCreate(byRole, role, () => ({ prompts: [], promptSum: 0, preEditSum: 0, cacheReadSum: 0 }));
|
|
388
417
|
if (prompt > 0) {
|
|
389
418
|
acc.prompts.push(prompt);
|
|
390
419
|
acc.promptSum += prompt;
|
|
391
420
|
}
|
|
392
421
|
acc.preEditSum += preEdit;
|
|
422
|
+
const cacheRead = nonNegativeNumber(ev.cacheReadTokens, 0);
|
|
423
|
+
acc.cacheReadSum += cacheRead;
|
|
393
424
|
}
|
|
394
425
|
return [...byRole.entries()]
|
|
395
426
|
.filter(([, acc]) => acc.promptSum > 0)
|
|
@@ -398,6 +429,7 @@ function promptStatsFor(runs) {
|
|
|
398
429
|
runs: acc.prompts.length,
|
|
399
430
|
medianPromptTokens: median(acc.prompts),
|
|
400
431
|
preEditShare: acc.preEditSum / acc.promptSum,
|
|
432
|
+
...(acc.cacheReadSum > 0 ? { cacheReadShare: acc.cacheReadSum / acc.promptSum } : {}),
|
|
401
433
|
}))
|
|
402
434
|
.sort((a, b) => a.role.localeCompare(b.role));
|
|
403
435
|
}
|
|
@@ -119,6 +119,19 @@ function hoursPhrase(ms) {
|
|
|
119
119
|
* I/O, no clock reads. */
|
|
120
120
|
export function renderFailureMarkdown(data) {
|
|
121
121
|
const lines = [];
|
|
122
|
+
pushDigestHeader(lines, data);
|
|
123
|
+
pushStateChanges(lines, data);
|
|
124
|
+
pushOutcomeTable(lines, data);
|
|
125
|
+
pushTimeAndSpend(lines, data);
|
|
126
|
+
pushPromptStats(lines, data);
|
|
127
|
+
pushDeltas(lines, data);
|
|
128
|
+
pushClusterSections(lines, data);
|
|
129
|
+
pushMergedSection(lines, data);
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
}
|
|
132
|
+
/** The digest's title, blank line, window line (bounded/since/day) and log-state lines. Owns the
|
|
133
|
+
* local `bounded` const the empty branch reads, so the since-vs-day split stays in one place. */
|
|
134
|
+
function pushDigestHeader(lines, data) {
|
|
122
135
|
lines.push("# tumwater failure digest");
|
|
123
136
|
lines.push("");
|
|
124
137
|
// The since shape prints the same sections as the day shape but labels its window as an
|
|
@@ -145,10 +158,12 @@ export function renderFailureMarkdown(data) {
|
|
|
145
158
|
}
|
|
146
159
|
else if (data.partial)
|
|
147
160
|
lines.push(`partial: retained log starts ${data.oldestEventDate ?? "?"}`);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
161
|
+
}
|
|
162
|
+
/** The causal frame before the counts: what the team DID in the window, so the reader can ask
|
|
163
|
+
* whether the harness's response was right. Omitted entirely when the window held none. When
|
|
164
|
+
* the newest-N cap cut the window's transitions, the remainder is named — a quiet window (no
|
|
165
|
+
* line) and a truncated one must not look the same. */
|
|
166
|
+
function pushStateChanges(lines, data) {
|
|
152
167
|
if (data.stateChanges.length > 0) {
|
|
153
168
|
lines.push("");
|
|
154
169
|
// A scoped digest retitles the section: its transitions are the named role's, so the
|
|
@@ -161,6 +176,10 @@ export function renderFailureMarkdown(data) {
|
|
|
161
176
|
if (hidden > 0)
|
|
162
177
|
lines.push(`+${hidden} older transitions hidden`);
|
|
163
178
|
}
|
|
179
|
+
}
|
|
180
|
+
/** The outcome table: the column set derived from the window's evidence, then the role × result
|
|
181
|
+
* table, or its placeholder when no run finished. */
|
|
182
|
+
function pushOutcomeTable(lines, data) {
|
|
164
183
|
const cols = columns(data.outcomes);
|
|
165
184
|
lines.push("");
|
|
166
185
|
lines.push("## Outcome by role");
|
|
@@ -176,12 +195,16 @@ export function renderFailureMarkdown(data) {
|
|
|
176
195
|
...cols.map((c) => String(row.counts[c] ?? 0)),
|
|
177
196
|
]), ["left", ...cols.map(() => "right")]));
|
|
178
197
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
198
|
+
}
|
|
199
|
+
/** The same runs priced by what they cost, so a 30-minute timeout outranks ten 200-ms
|
|
200
|
+
* errors that the Outcome table above weighs equally. Two parts: the role × class table of
|
|
201
|
+
* hours and dollars, then the top loss causes by time (an error cluster, a role's
|
|
202
|
+
* no_change total — the quiet loss no cluster names, a review-rejected authoring span, a
|
|
203
|
+
* merge-conflict/merge-blocked authoring span, or a context cut-off (a run truncated at
|
|
204
|
+
* its model's ceiling, a cost of length the harness resumes rather than quiet idleness).
|
|
205
|
+
* Grows only with the role catalog (fixed by config) and the LOSS_TOP cut, so the byte
|
|
206
|
+
* bound argued above holds. */
|
|
207
|
+
function pushTimeAndSpend(lines, data) {
|
|
185
208
|
lines.push("");
|
|
186
209
|
lines.push("## Time and spend by outcome");
|
|
187
210
|
if (data.timeSpend.length === 0) {
|
|
@@ -209,7 +232,9 @@ export function renderFailureMarkdown(data) {
|
|
|
209
232
|
? `refused on ${roleCell(c.roles[0] ?? "?")}${c.example ? ` — ${c.example}` : ""}`
|
|
210
233
|
: c.kind === "merge-conflict" || c.kind === "merge-blocked"
|
|
211
234
|
? `${c.kind} authoring on ${roleCell(c.roles[0] ?? "?")}${c.example ? ` — ${c.example}` : ""}`
|
|
212
|
-
:
|
|
235
|
+
: c.kind === "cutoff"
|
|
236
|
+
? `context cut-off on ${roleCell(c.roles[0] ?? "?")}`
|
|
237
|
+
: `${c.example} (${roleList(c.roles)})`;
|
|
213
238
|
lines.push(`- ${hoursPhrase(c.ms)} · ${usd(c.costUsd)} — ${plural(c.runs, "run")}: ${cause}`);
|
|
214
239
|
}
|
|
215
240
|
// Mark the top-5 cut the way the cluster sections mark theirs: a capped ranking that
|
|
@@ -220,18 +245,28 @@ export function renderFailureMarkdown(data) {
|
|
|
220
245
|
}
|
|
221
246
|
}
|
|
222
247
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
248
|
+
}
|
|
249
|
+
/** Prompt-token telemetry: the per-role median prompt
|
|
250
|
+
* send and how much of the prompt budget goes out before a run's first edit, plus each
|
|
251
|
+
* role's cache-read share of its prompt bill when some run reported cache reads. Omitted
|
|
252
|
+
* entirely when no run_end in the window carries the fields, so old logs and their golden
|
|
253
|
+
* digests render unchanged. */
|
|
254
|
+
function pushPromptStats(lines, data) {
|
|
227
255
|
if (data.promptStats.length > 0) {
|
|
228
256
|
lines.push("");
|
|
229
257
|
lines.push("## Prompt tokens by role");
|
|
230
258
|
for (const row of data.promptStats) {
|
|
231
259
|
const share = percent(row.preEditShare, 1);
|
|
232
|
-
|
|
260
|
+
let line = `- ${roleCell(row.role)}: median ${row.medianPromptTokens} prompt tokens/run · ${share} before first edit`;
|
|
261
|
+
if (row.cacheReadShare !== undefined) {
|
|
262
|
+
line += ` · ${percent(row.cacheReadShare, 1)} cache reads`;
|
|
263
|
+
}
|
|
264
|
+
lines.push(line);
|
|
233
265
|
}
|
|
234
266
|
}
|
|
267
|
+
}
|
|
268
|
+
/** The deltas table: this window's runs/rates vs the preceding one, or its placeholder. */
|
|
269
|
+
function pushDeltas(lines, data) {
|
|
235
270
|
lines.push("");
|
|
236
271
|
lines.push(`## Deltas vs the preceding ${data.sinceMs !== undefined ? durationLabel(data.sinceMs - (data.untilMs ?? 0)) : dayLabel(data.days)}`);
|
|
237
272
|
if (data.deltas.length === 0) {
|
|
@@ -246,6 +281,10 @@ export function renderFailureMarkdown(data) {
|
|
|
246
281
|
rejectionCell(d.prevRuns, d.prevRejections, d.runs, d.rejections),
|
|
247
282
|
])));
|
|
248
283
|
}
|
|
284
|
+
}
|
|
285
|
+
/** The five cluster sections in their current order, plus the post-merge-rejection line between
|
|
286
|
+
* the review/rejection clusters and the merge-failure section. */
|
|
287
|
+
function pushClusterSections(lines, data) {
|
|
249
288
|
// The title restates the section's contract: its total now covers the Outcome table's error
|
|
250
289
|
// column AND its main_red cells (the baseline gate's own cause rides the run, BUGS.md
|
|
251
290
|
// 2026-09-28), so an operator cross-checking counts against the tables above is not misled
|
|
@@ -258,6 +297,9 @@ export function renderFailureMarkdown(data) {
|
|
|
258
297
|
lines.push(`_${plural(data.postMergeRejections, "post-merge rejection", "post-merge rejections")} — the commit already reached main, so it stays there with the reviewer's recorded reasons (never re-reviewed — Forward, never back)_`);
|
|
259
298
|
}
|
|
260
299
|
renderClusters(lines, "Merge failures", data.mergeFailures, "merge failures");
|
|
300
|
+
}
|
|
301
|
+
/** The merged list, with its remainder line when the newest-N cut dropped anything. */
|
|
302
|
+
function pushMergedSection(lines, data) {
|
|
261
303
|
lines.push("");
|
|
262
304
|
lines.push("## Merged in the window");
|
|
263
305
|
if (data.merged.length === 0) {
|
|
@@ -272,7 +314,6 @@ export function renderFailureMarkdown(data) {
|
|
|
272
314
|
if (hidden > 0)
|
|
273
315
|
lines.push(`+${hidden} older merges not listed`);
|
|
274
316
|
}
|
|
275
|
-
return lines.join("\n");
|
|
276
317
|
}
|
|
277
318
|
/** Push one "Top … clusters" section: each cluster as a `**N×** roles · first → last —
|
|
278
319
|
* example` line (the collector supplies the order and the top-N cut), or a single `no <noun> in the
|