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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/** The single-event folding core of the usage report: fold one harness event into a
|
|
2
|
+
* `UsageFold` accumulator (`foldUsageEvent`), with one named helper per event family
|
|
3
|
+
* (run_end, review_rejected, merge_done/merge_failed, …). This is the pure per-event rule,
|
|
4
|
+
* shared by the since-window collector and the per-day window fold — the latter lives in
|
|
5
|
+
* src/report/report-fold.ts, which reads and memoizes the event log (`foldWindowEvents`)
|
|
6
|
+
* and delegates each parsed event here, so the two windows' aggregation rules cannot drift
|
|
7
|
+
* apart. Zero incidental runtime dependencies: node built-ins and the app's own modules
|
|
8
|
+
* only. */
|
|
9
|
+
import { addTo } from "../collections.js";
|
|
10
|
+
import { isPassingFullCheck, isRedFullCheck } from "../build/build-check-events.js";
|
|
11
|
+
import { eventRole, eventUsage } from "../events/event-read.js";
|
|
12
|
+
import { finiteNumber, nonNegativeNumber } from "../files/json-object.js";
|
|
13
|
+
import { OUTCOME_CLASS } from "../failure/time-spend.js";
|
|
14
|
+
import { commitTier } from "../roles/roles.js";
|
|
15
|
+
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
16
|
+
/** Fold one event's `providerCost` map (`Record<provider, {costUsd, notionalCostUsd}>` — the
|
|
17
|
+
* shape run-finalize.ts's run_end and merge-slot.ts's merge_done/merge_failed emit) into the
|
|
18
|
+
* per-provider tallies, one addTo per provider per nonzero figure so zero stays absent. A
|
|
19
|
+
* missing, foreign, or empty map folds nothing, so old logs render byte-identically. */
|
|
20
|
+
function foldProviderCost(target, providerCost) {
|
|
21
|
+
if (typeof providerCost !== "object" || providerCost === null)
|
|
22
|
+
return;
|
|
23
|
+
for (const [provider, c] of Object.entries(providerCost)) {
|
|
24
|
+
if (typeof c !== "object" || c === null)
|
|
25
|
+
continue;
|
|
26
|
+
const entry = c;
|
|
27
|
+
const costUsd = finiteNumber(entry.costUsd, 0);
|
|
28
|
+
const notionalCostUsd = finiteNumber(entry.notionalCostUsd, 0);
|
|
29
|
+
if (costUsd !== 0)
|
|
30
|
+
addTo(target.costByProvider, provider, costUsd);
|
|
31
|
+
if (notionalCostUsd !== 0)
|
|
32
|
+
addTo(target.notionalByProvider, provider, notionalCostUsd);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Push `ms` into the accumulator list a day median reads, when it is positive: the event
|
|
36
|
+
* fields that feed these medians are missing or 0 for the same event kinds (short runs, build
|
|
37
|
+
* pre-checks, precedent legacy events, probe-seam waits), and those contribute nothing — a
|
|
38
|
+
* median over only real durations/wait times. Every median list folds through here, so the
|
|
39
|
+
* "positive-only" rule cannot drift between fields. */
|
|
40
|
+
function pushPositiveMs(list, ms) {
|
|
41
|
+
if (ms > 0)
|
|
42
|
+
list.push(ms);
|
|
43
|
+
}
|
|
44
|
+
/** Fold a run_end into the accumulator: a run (per role and, when the target counts runs, in
|
|
45
|
+
* total), its output/prompt/cache tokens, its cost, and its notional backend share. */
|
|
46
|
+
function foldRunEnd(target, ev) {
|
|
47
|
+
const role = eventRole(ev);
|
|
48
|
+
addTo(target.runsByRole, role, 1);
|
|
49
|
+
// Error-class runs get their own parallel tally — the day report's ``Failed runs by role``
|
|
50
|
+
// line — reusing the digest's OUTCOME_CLASS so "failed" cannot drift from the failure
|
|
51
|
+
// report's meaning. Never subtracted from runsByRole; the two are independent counts.
|
|
52
|
+
if (OUTCOME_CLASS[ev.result] === "error")
|
|
53
|
+
addTo(target.failedRunsByRole, role, 1);
|
|
54
|
+
// A refusal is its own deliberate outcome (src/verdict/refusal.ts), not error-class, so
|
|
55
|
+
// it needs an explicit result check: the one per-role rejection-without-review count a
|
|
56
|
+
// day report should surface beside rejections, revisions and falls. Never subtracted from
|
|
57
|
+
// runsByRole; a refused run still counts as a run.
|
|
58
|
+
if (ev.result === "refused")
|
|
59
|
+
addTo(target.refusalsByRole, role, 1);
|
|
60
|
+
if (target.runs !== undefined)
|
|
61
|
+
target.runs++;
|
|
62
|
+
const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
|
|
63
|
+
pushPositiveMs(target.runDurationsMs, finiteNumber(ev.durationMs, 0));
|
|
64
|
+
addTo(target.tokensByRole, role, tokens);
|
|
65
|
+
// The prompt side of the same run's bill: how many prompt tokens each role sent and how
|
|
66
|
+
// much of that its backend served from cache (run-finalize.ts rides both on run_end, each
|
|
67
|
+
// omitted when zero). Zero-means-absent like tokensByRole. The finite guard is the failure
|
|
68
|
+
// digest's promptStatsFor rule, and the Math.min clamp keeps a torn log's cache share from
|
|
69
|
+
// ever exceeding the prompt total — together they pin a rendered share to <= 100% and keep
|
|
70
|
+
// a non-finite reading (1e999 → Infinity) out of the maps as absent, not NaN.
|
|
71
|
+
const prompt = nonNegativeNumber(ev.promptTokens, 0);
|
|
72
|
+
const cacheRead = Math.min(nonNegativeNumber(ev.cacheReadTokens, 0), prompt);
|
|
73
|
+
if (prompt > 0)
|
|
74
|
+
addTo(target.promptTokensByRole, role, prompt);
|
|
75
|
+
if (cacheRead > 0)
|
|
76
|
+
addTo(target.cacheReadTokensByRole, role, cacheRead);
|
|
77
|
+
target.tokensOut += tokens;
|
|
78
|
+
target.costUsd += costUsd;
|
|
79
|
+
target.notionalCostUsd += notionalCostUsd;
|
|
80
|
+
if (costUsd !== 0)
|
|
81
|
+
addTo(target.costByRole, role, costUsd);
|
|
82
|
+
if (notionalCostUsd !== 0)
|
|
83
|
+
addTo(target.notionalByRole, role, notionalCostUsd);
|
|
84
|
+
// The notional spend's backend (plans/codex-backend.md part 3d/9): the subscription seam
|
|
85
|
+
// whose runs made the run's API-equivalent cost, from the run_end's `agent` (run-finalize
|
|
86
|
+
// carries it when the run reported billing/rate windows) — falling back to "claude" when
|
|
87
|
+
// a legacy window event (no agent, subscription billing) attributed its spend, so the old
|
|
88
|
+
// default stays the report's only backend. A run with no backend and no subscription
|
|
89
|
+
// billing is unattributed and adds no key, so the render keeps its generic line for it.
|
|
90
|
+
const notionalBackend = typeof ev.agent === "string" && ev.agent !== ""
|
|
91
|
+
? ev.agent
|
|
92
|
+
: ev.billing === "subscription"
|
|
93
|
+
? "claude"
|
|
94
|
+
: undefined;
|
|
95
|
+
if (notionalBackend !== undefined && notionalCostUsd !== 0)
|
|
96
|
+
addTo(target.notionalByBackend, notionalBackend, notionalCostUsd);
|
|
97
|
+
// The agent runs' per-provider spend (run-finalize.ts emits providerCost on run_end)
|
|
98
|
+
// — folded per provider beside the per-role maps, so a mixed team's `Cost by provider`
|
|
99
|
+
// line names the expensive seam.
|
|
100
|
+
foldProviderCost(target, ev.providerCost);
|
|
101
|
+
}
|
|
102
|
+
/** Fold a review_rejected: one rejection per event, on the day it happened, per role, with
|
|
103
|
+
* the post-merge subset and a review-duration entry when the rejection carried one. */
|
|
104
|
+
function foldReviewRejected(target, ev) {
|
|
105
|
+
// One rejection per event, on the day it happened, per role. Zero-means-absent: a role
|
|
106
|
+
// that never rejected stays absent, like costByRole.
|
|
107
|
+
addTo(target.rejectionsByRole, eventRole(ev), 1);
|
|
108
|
+
// A rejection that carried afterMerge: true is a post-merge one — the commit had already
|
|
109
|
+
// landed, so the reviewer's turn-back reaches the revert path off main. Kept as its own
|
|
110
|
+
// subset of rejectionsByRole because it is a different, costlier kind of rejection.
|
|
111
|
+
if (ev.afterMerge === true)
|
|
112
|
+
addTo(target.postMergeRejectionsByRole, eventRole(ev), 1);
|
|
113
|
+
// A rejection that carried a positive durationMs means a reviewer actually ran the gate
|
|
114
|
+
// and reached a verdict (post-merge rejections do; the build pre-check contributes no
|
|
115
|
+
// duration), so it joins the review-duration median beside approvals. durationMs: 0 and
|
|
116
|
+
// a duration-less event contribute nothing.
|
|
117
|
+
pushPositiveMs(target.reviewDurationsMs, finiteNumber(ev.durationMs, 0));
|
|
118
|
+
}
|
|
119
|
+
/** Fold a review_verdict approval into the review-duration median. */
|
|
120
|
+
function foldReviewVerdict(target, ev) {
|
|
121
|
+
// An approval that reached a verdict. Every reviewer run that reached a verdict, approval
|
|
122
|
+
// or rejection, is one entry in the review-duration median; review_failed stays out — it
|
|
123
|
+
// has no verdict. Mirrors the rejection branch above, on the same-day count.
|
|
124
|
+
pushPositiveMs(target.reviewDurationsMs, finiteNumber(ev.durationMs, 0));
|
|
125
|
+
}
|
|
126
|
+
/** Fold a revision: one rework round per event, on the day it happened, per role. */
|
|
127
|
+
function foldRevision(target, ev) {
|
|
128
|
+
// One rework round per event, on the day it happened, per role. Zero-means-absent: a role
|
|
129
|
+
// that never reworked a rejected change stays absent, like rejectionsByRole.
|
|
130
|
+
addTo(target.revisionsByRole, eventRole(ev), 1);
|
|
131
|
+
}
|
|
132
|
+
/** Fold a passing full-suite check into the day report's check-time median. */
|
|
133
|
+
function foldPassingCheck(target, ev) {
|
|
134
|
+
// The merge slot's passing full-suite checks, for the day report's check-time median.
|
|
135
|
+
pushPositiveMs(target.checkDurationsMs, finiteNumber(ev.durationMs, 0));
|
|
136
|
+
}
|
|
137
|
+
/** Fold a red full-suite check into the day report's red-check count. */
|
|
138
|
+
function foldRedCheck(target, _ev) {
|
|
139
|
+
// The merge slot's red full-suite checks, for the day report's red-check count.
|
|
140
|
+
target.checkFailures++;
|
|
141
|
+
}
|
|
142
|
+
/** Fold a model_fallback_started: one episode per start event, per role. */
|
|
143
|
+
function foldFallbackStarted(target, ev) {
|
|
144
|
+
// One episode per start event; durations are not summed (an episode can span the window edge).
|
|
145
|
+
addTo(target.fallbacksByRole, eventRole(ev), 1);
|
|
146
|
+
}
|
|
147
|
+
/** Fold a run_deferred: one deferral per event, bucketed by the raw reason code (team-only). */
|
|
148
|
+
function foldDeferred(target, ev) {
|
|
149
|
+
// One deferral per event, a due maintenance run that stood down. Bucket by the raw
|
|
150
|
+
// reason code ("backlog", "plans-full", else "no-work" for a missing/foreign/torn
|
|
151
|
+
// reason — the same fallback deferredReasonText applies in the feed), so the collector
|
|
152
|
+
// stays free of a presentation import and the render prints the code the event carried.
|
|
153
|
+
// Team-only: a reason is not a role.
|
|
154
|
+
const reason = ev.reason === "backlog" || ev.reason === "plans-full" ? ev.reason : "no-work";
|
|
155
|
+
addTo(target.deferredByReason, reason, 1);
|
|
156
|
+
}
|
|
157
|
+
/** Fold a merged commit: one commit split by base role and tier (Work ratio 4/4). */
|
|
158
|
+
function foldMerged(target, ev) {
|
|
159
|
+
target.commits++;
|
|
160
|
+
// Work ratio 4/4: split merged commits by base role and by tier. Instance suffixes fold
|
|
161
|
+
// into their role (`feature-2` counts as feature) so the split does not fragment.
|
|
162
|
+
const role = baseRoleOf(eventRole(ev));
|
|
163
|
+
addTo(target.commitsByRole, role, 1);
|
|
164
|
+
const tier = commitTier(role);
|
|
165
|
+
if (tier === "work")
|
|
166
|
+
target.workCommits++;
|
|
167
|
+
else if (tier === "maintenance")
|
|
168
|
+
target.maintenanceCommits++;
|
|
169
|
+
}
|
|
170
|
+
/** Fold a run_start: one tally per run start on a configured model. */
|
|
171
|
+
function foldRunStarted(target, ev) {
|
|
172
|
+
// One tally per run start on a configured model: the same `model` field the CLI's
|
|
173
|
+
// `run #N started on <model>` line prints, and the only event that carries a model
|
|
174
|
+
// (run_end has none), so a run that started with no configured model contributes no key.
|
|
175
|
+
const model = typeof ev.model === "string" && ev.model.length > 0 ? ev.model : null;
|
|
176
|
+
if (model !== null)
|
|
177
|
+
addTo(target.runsByModel, model, 1);
|
|
178
|
+
}
|
|
179
|
+
/** Fold a provider_wait into the provider-slot wait median. */
|
|
180
|
+
function foldProviderWait(target, ev) {
|
|
181
|
+
// The day report's provider-slot wait median, mirroring the slot_wait push: a wait only
|
|
182
|
+
// event-logged at or above the 30 s threshold counts, so a 0 ms wait (a probe seam) adds
|
|
183
|
+
// nothing and the median reflects real blocked admissions only.
|
|
184
|
+
pushPositiveMs(target.providerWaitsMs, finiteNumber(ev.waitedMs, 0));
|
|
185
|
+
}
|
|
186
|
+
/** Fold a slot_wait into the workspace-slot wait median. */
|
|
187
|
+
function foldSlotWait(target, ev) {
|
|
188
|
+
// The day report's workspace-slot wait median, mirroring the provider_wait push: a wait
|
|
189
|
+
// only event-logged at or above the 30 s threshold counts, so a 0 ms wait (a probe seam)
|
|
190
|
+
// or a pre-waitedMs event adds nothing.
|
|
191
|
+
pushPositiveMs(target.slotWaitsMs, finiteNumber(ev.waitedMs, 0));
|
|
192
|
+
}
|
|
193
|
+
/** Fold a check_wait into the check-slot wait median. */
|
|
194
|
+
function foldCheckWait(target, ev) {
|
|
195
|
+
// The day report's check-slot wait median, mirroring the slot_wait push: only waits logged
|
|
196
|
+
// at or above the 30 s threshold count, so a 0 ms or pre-waitedMs event adds nothing.
|
|
197
|
+
pushPositiveMs(target.checkWaitsMs, finiteNumber(ev.waitedMs, 0));
|
|
198
|
+
}
|
|
199
|
+
/** Fold a merge_done/merge_failed outcome: its run count, its accumulation into the merge
|
|
200
|
+
* fields only (costByRole stays a runs-only breakdown), and its notional/provider shares. */
|
|
201
|
+
function foldMergeOutcome(target, ev) {
|
|
202
|
+
target.mergeRuns = (target.mergeRuns ?? 0) + 1;
|
|
203
|
+
if (ev.type === "merge_failed")
|
|
204
|
+
target.mergeFailed = (target.mergeFailed ?? 0) + 1;
|
|
205
|
+
// Attribute the outcome to the authoring role (writeMergeOutcome logs role: entry.role)
|
|
206
|
+
// folded to base role, the same normalization the merged branch uses, so `feature-2`
|
|
207
|
+
// aggregates into `feature`. A parallel tally beside the team run counts, never
|
|
208
|
+
// subtracted from mergeRuns/mergeFailed.
|
|
209
|
+
const role = baseRoleOf(eventRole(ev));
|
|
210
|
+
if (ev.type === "merge_done") {
|
|
211
|
+
addTo(target.mergedByRole, role, 1);
|
|
212
|
+
// Events logged before queuedMs existed carry none: skipped, not counted as zero.
|
|
213
|
+
pushPositiveMs(target.mergeWaitsMs, finiteNumber(ev.queuedMs, 0));
|
|
214
|
+
// How long the merge slot itself held the change, from the verification's start to the
|
|
215
|
+
// outcome. Like mergeWaitsMs, only a landed merge feeds it, and a pre-durationMs event
|
|
216
|
+
// (or 0) contributes nothing.
|
|
217
|
+
pushPositiveMs(target.mergeDurationsMs, finiteNumber(ev.durationMs, 0));
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
addTo(target.mergeFailedByRole, role, 1);
|
|
221
|
+
// The event's result (writeMergeOutcome's RunResult) names WHY the merge bounced
|
|
222
|
+
// off the gate — rejected/merge_conflict/merge_blocked/aborted. A legacy event with
|
|
223
|
+
// no result folds under `unknown`, so the reason buckets always sum to mergeFailed.
|
|
224
|
+
const reason = typeof ev.result === "string" && ev.result.length > 0 ? ev.result : "unknown";
|
|
225
|
+
addTo(target.mergeFailedByReason, reason, 1);
|
|
226
|
+
}
|
|
227
|
+
const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
|
|
228
|
+
target.mergeTokens = (target.mergeTokens ?? 0) + tokens;
|
|
229
|
+
target.mergeCostUsd = (target.mergeCostUsd ?? 0) + costUsd;
|
|
230
|
+
// A subscription merge's API-equivalent spend joins the notional line, never the
|
|
231
|
+
// budgeted cost totals (plans/agent-backends.md part 6a).
|
|
232
|
+
target.notionalCostUsd += notionalCostUsd;
|
|
233
|
+
// The merge's notional spend per backend (merge-slot.ts emits it on the event, keyed by
|
|
234
|
+
// each subscription run's agent) — folded beside the run-end per-backend map, so a codex
|
|
235
|
+
// merge's subscription spend names codex instead of falling back to the generic line.
|
|
236
|
+
for (const [backend, c] of Object.entries(ev.notionalByBackend ?? {})) {
|
|
237
|
+
const amt = finiteNumber(c, 0);
|
|
238
|
+
if (amt !== 0)
|
|
239
|
+
addTo(target.notionalByBackend, backend, amt);
|
|
240
|
+
}
|
|
241
|
+
// The merge's per-provider spend (merge-slot.ts keys it to its reviewer seam), so a
|
|
242
|
+
// merge's costs appear under the provider that ran the reviewer, matching the author
|
|
243
|
+
// side's foldMergeUsage charge.
|
|
244
|
+
foldProviderCost(target, ev.providerCost);
|
|
245
|
+
}
|
|
246
|
+
/** Fold one event into a usage accumulator by dispatching to a per-family builder —
|
|
247
|
+
* foldRunEnd, foldReviewRejected, foldReviewVerdict, foldRevision, foldPassingCheck,
|
|
248
|
+
* foldRedCheck, foldFallbackStarted, foldDeferred, foldMerged,
|
|
249
|
+
* foldRunStarted, foldProviderWait, foldSlotWait, foldCheckWait or foldMergeOutcome — each
|
|
250
|
+
* named for the family it folds, so how a family folds is found by name. The dispatch holds
|
|
251
|
+
* no accumulation of its own. All behavior lives in the builders: run_end events add a run
|
|
252
|
+
* (per role and, when the target counts runs, in total), their output tokens, and their
|
|
253
|
+
* cost — zero-cost runs contribute nothing, so they leave no costByRole key, keeping the
|
|
254
|
+
* render's "$0 roles are omitted" rule true on the aggregation itself, not just at display
|
|
255
|
+
* time; merged events add a commit; merge_done/merge_failed events add a merge run and
|
|
256
|
+
* accumulate their tokens/cost into the merge fields only — costByRole stays a runs-only
|
|
257
|
+
* breakdown, and the merge line is how the reviewer's share is named. Both collectors fold
|
|
258
|
+
* through this one place, so the two windows' aggregation rules cannot drift apart. */
|
|
259
|
+
export function foldUsageEvent(target, ev) {
|
|
260
|
+
if (ev.type === "run_end")
|
|
261
|
+
foldRunEnd(target, ev);
|
|
262
|
+
else if (ev.type === "review_rejected")
|
|
263
|
+
foldReviewRejected(target, ev);
|
|
264
|
+
else if (ev.type === "review_verdict")
|
|
265
|
+
foldReviewVerdict(target, ev);
|
|
266
|
+
else if (ev.type === "revision")
|
|
267
|
+
foldRevision(target, ev);
|
|
268
|
+
else if (isPassingFullCheck(ev))
|
|
269
|
+
foldPassingCheck(target, ev);
|
|
270
|
+
else if (isRedFullCheck(ev))
|
|
271
|
+
foldRedCheck(target, ev);
|
|
272
|
+
else if (ev.type === "model_fallback_started")
|
|
273
|
+
foldFallbackStarted(target, ev);
|
|
274
|
+
else if (ev.type === "run_deferred")
|
|
275
|
+
foldDeferred(target, ev);
|
|
276
|
+
else if (ev.type === "merged")
|
|
277
|
+
foldMerged(target, ev);
|
|
278
|
+
else if (ev.type === "run_start")
|
|
279
|
+
foldRunStarted(target, ev);
|
|
280
|
+
else if (ev.type === "provider_wait")
|
|
281
|
+
foldProviderWait(target, ev);
|
|
282
|
+
else if (ev.type === "slot_wait")
|
|
283
|
+
foldSlotWait(target, ev);
|
|
284
|
+
else if (ev.type === "check_wait")
|
|
285
|
+
foldCheckWait(target, ev);
|
|
286
|
+
else if (ev.type === "merge_done" || ev.type === "merge_failed")
|
|
287
|
+
foldMergeOutcome(target, ev);
|
|
288
|
+
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* src/report/report-data.ts / src/failure/failure-data.ts, and print. The Markdown renderers
|
|
3
3
|
* live in the pure-render modules they pair with: src/report/report-render.ts and
|
|
4
4
|
* src/failure/failure-render.ts. */
|
|
5
|
-
import { collectReport
|
|
5
|
+
import { collectReport } from "./report-data.js";
|
|
6
|
+
import { collectReportSince } from "./report-since-data.js";
|
|
6
7
|
import { collectFailureReport, collectFailureReportSince } from "../failure/failure-data.js";
|
|
7
8
|
import { renderFailureMarkdown } from "../failure/failure-render.js";
|
|
8
9
|
import { renderReportMarkdown, renderSinceReportMarkdown } from "./report-render.js";
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* evidence of flakiness — it is exactly the attribution this memory exists to avoid making
|
|
11
11
|
* twice. */
|
|
12
12
|
import { DAY_MS } from "../text/datetime.js";
|
|
13
|
-
import { TUMWATER_SOURCE, tryOrWarn } from "../events/events.js";
|
|
13
|
+
import { TUMWATER_SOURCE, tryOrWarn, warnEventBestEffort } from "../events/events.js";
|
|
14
14
|
import { readEventsSince } from "../events/event-window.js";
|
|
15
15
|
import { increment } from "../collections.js";
|
|
16
16
|
import { normalizeClusterKey } from "../failure/failure-cluster.js";
|
|
@@ -65,11 +65,11 @@ export function flakeCounts(events) {
|
|
|
65
65
|
/** The bar at which a normalized check counts as flapping: one warning in the window is a
|
|
66
66
|
* routine one-off the gate's single re-run already absorbed, while two means the check flapped
|
|
67
67
|
* again and pays that re-run on every merge it touches — the one threshold the doctor's `flaky
|
|
68
|
-
* checks` row
|
|
68
|
+
* checks` row reads, folded through
|
|
69
69
|
* repeatedFlakeCounts. */
|
|
70
70
|
const FLAKE_REPEAT_WARN = 2;
|
|
71
71
|
/** The normalized checks in `events` that flapped at least FLAKE_REPEAT_WARN times, most
|
|
72
|
-
* frequent first — the filter flakeCounts feeds
|
|
72
|
+
* frequent first — the filter flakeCounts feeds the doctor's `flaky checks` row, so the
|
|
73
73
|
* threshold lives in the one home. Pure. */
|
|
74
74
|
export function repeatedFlakeCounts(events) {
|
|
75
75
|
return flakeCounts(events).filter(([, n]) => n >= FLAKE_REPEAT_WARN);
|
|
@@ -148,6 +148,25 @@ export function quarantineTests(root, tests, tail) {
|
|
|
148
148
|
return [];
|
|
149
149
|
return added;
|
|
150
150
|
}
|
|
151
|
+
/** Remove the one quarantined test named `test` at the operator's request — the manual twin
|
|
152
|
+
* of releaseQuarantine's automatic release, and `tumwater quarantine --release <n>` its only
|
|
153
|
+
* caller (which resolves the position the list prints down to the exact test first). Returns
|
|
154
|
+
* whether the entry was present and the write succeeded, so the caller can answer a concurrent
|
|
155
|
+
* removal: `false` with no warning and no write for a name not quarantined, or when a write
|
|
156
|
+
* faulted (the quarantineTests write contract: a failed write warns and changes nothing). Logs
|
|
157
|
+
* one `warning` under TUMWATER_SOURCE so the doctor's `flaky checks` row stops
|
|
158
|
+
* excusing the name. */
|
|
159
|
+
export function releaseQuarantinedTest(root, test) {
|
|
160
|
+
const entries = loadQuarantine(root);
|
|
161
|
+
const have = entries.some((e) => e.test === test);
|
|
162
|
+
if (!have)
|
|
163
|
+
return false;
|
|
164
|
+
const removed = entries.filter((e) => e.test !== test);
|
|
165
|
+
if (!tryOrWarn(root, TUMWATER_SOURCE, "write quarantine.json", () => saveQuarantine(root, removed)))
|
|
166
|
+
return false;
|
|
167
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `${test} released from the flake quarantine at the user's request — the gate no longer excuses its failures`);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
151
170
|
/** Record the BUGS.md heading filed for each of `tests`, dropping their kept output. */
|
|
152
171
|
export function markQuarantineFiled(root, bugs) {
|
|
153
172
|
const entries = loadQuarantine(root);
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* (src/cli/cli-review-commands.ts `tumwater reviews --accept <n>`), split out of the CLI so
|
|
3
3
|
* the review queue stays a pure reader/editor and this module owns the destructive half —
|
|
4
4
|
* removing an unattempted entry on purpose. It is the sibling of `tumwater queue --cancel
|
|
5
|
-
* <n>` (src/merge/merge-cancel.ts): the queue's no-third-path gap that the at-risk
|
|
6
|
-
* shared sentence names (
|
|
5
|
+
* <n>` (src/merge/merge-cancel.ts): the queue's no-third-path gap that the doctor's at-risk
|
|
6
|
+
* post-review row's shared sentence names (“…or accept the commit unreviewed deliberately”).
|
|
7
|
+
* It reads the same
|
|
7
8
|
* FIFO list `readPostReviews` numbers 1..N and settles the entry by its commit, so the removed
|
|
8
9
|
* entry is the very one the printed position named. */
|
|
9
10
|
import { readPostReviews, settlePostReview } from "./post-merge-review.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
2
|
+
import { compareCodeUnits } from "../collections.js";
|
|
2
3
|
import { postReviewPath, postReviewWorktreePath, nativeLogPath, piLogPath, reviewRunSessionDir, reviewFixRef } from "../paths.js";
|
|
3
4
|
import { reviewedAfterMerge, reviewRunConfig } from "../config/config-views.js";
|
|
4
5
|
import { logEventBestEffort, TUMWATER_SOURCE, tryOrWarn } from "../events/events.js";
|
|
@@ -19,20 +20,21 @@ import { fixLine } from "../verdict/reply-contract.js";
|
|
|
19
20
|
import { parseVerdict } from "./review-verdict.js";
|
|
20
21
|
/** A commit's review is dropped, with a warning, after this many runs with no verdict. */
|
|
21
22
|
export const POST_REVIEW_FAILURE_LIMIT = 3;
|
|
22
|
-
/** The bar a day's post-merge rejections must reach before the `post-merge
|
|
23
|
-
*
|
|
24
|
-
* a day is a role whose already-landed changes keep being turned back — the
|
|
25
|
-
* for an operator to narrow `review.afterMerge` or raise the
|
|
23
|
+
/** The bar a day's post-merge rejections must reach before the doctor's `post-merge churn` row
|
|
24
|
+
* fires: one post-merge rejection is the tiering's ordinary cost,
|
|
25
|
+
* two in a day is a role whose already-landed changes keep being turned back — the
|
|
26
|
+
* reviewer-fix signal for an operator to narrow `review.afterMerge` or raise the
|
|
27
|
+
* authoring bar. */
|
|
26
28
|
export const POST_MERGE_REJECT_WARN = 2;
|
|
27
29
|
/** How far back the post-merge-churn rule looks — one day, the 24 h event-window convention
|
|
28
|
-
* every event-reading
|
|
30
|
+
* every event-reading doctor row uses. */
|
|
29
31
|
export const POST_MERGE_REJECT_WINDOW_MS = 24 * 60 * 60_000;
|
|
30
32
|
/** Group the `afterMerge: true` `review_rejected` events of `events` by base role — the same
|
|
31
33
|
* `e.type === "review_rejected" && e.afterMerge === true` test the report fold
|
|
32
|
-
* (src/report/report-fold.ts `postMergeRejectionsByRole`) applies, so the churn
|
|
34
|
+
* (src/report/report-fold.ts `postMergeRejectionsByRole`) applies, so the churn row and the
|
|
33
35
|
* report cannot disagree on which rejections are post-merge. The key is the event's base role
|
|
34
36
|
* (`baseRoleOf(eventRole(ev))`), not the raw source: an event's `source` is the runner id
|
|
35
|
-
* (`bugfix-2`), and both the
|
|
37
|
+
* (`bugfix-2`), and both the row's remedy ("narrow `review.afterMerge`") and the scoped
|
|
36
38
|
* report are keyed by base role, so one base role's churn must read as one row. Pre-merge
|
|
37
39
|
* rejections and other event types contribute nothing. */
|
|
38
40
|
export function postMergeRejectionCounts(events) {
|
|
@@ -45,10 +47,10 @@ export function postMergeRejectionCounts(events) {
|
|
|
45
47
|
}
|
|
46
48
|
return [...byRole.entries()]
|
|
47
49
|
.map(([role, count]) => ({ role, count }))
|
|
48
|
-
.sort((a, b) => b.count - a.count || (a.role
|
|
50
|
+
.sort((a, b) => b.count - a.count || compareCodeUnits(a.role, b.role));
|
|
49
51
|
}
|
|
50
|
-
/** The one home of the trigger and selection the `post-merge
|
|
51
|
-
*
|
|
52
|
+
/** The one home of the trigger and selection the doctor's `post-merge churn` row
|
|
53
|
+
* (src/doctor/doctor-project-checks.ts) reads: whether
|
|
52
54
|
* the trailing `POST_MERGE_REJECT_WINDOW_MS` holds at least `POST_MERGE_REJECT_WARN` post-merge
|
|
53
55
|
* `review_rejected` events, and, when it does, which roles and how many each. null when the
|
|
54
56
|
* window holds fewer than the bar, or none — so the two surfaces cannot disagree on when churn
|
|
@@ -195,9 +197,9 @@ export async function reviewMergedCommit(root, config, entry, runGatePi, signal)
|
|
|
195
197
|
logEventBestEffort(root, { source: role, type: "review_verdict", head: commit, reason: verdict.reasons[0], durationMs, afterMerge: true });
|
|
196
198
|
}
|
|
197
199
|
// A rejecting review whose reply carries a FIX: line and whose worktree holds an edit
|
|
198
|
-
// survives as one fix commit on the review ref, kept for
|
|
199
|
-
// main through the ordinary change path. A clean
|
|
200
|
-
// today's outcomes and deletes the ref.
|
|
200
|
+
// survives as one fix commit on the review ref, kept for
|
|
201
|
+
// merge-post-review-drain.ts to land on main through the ordinary change path. A clean
|
|
202
|
+
// worktree or a reply with no FIX line keeps today's outcomes and deletes the ref.
|
|
201
203
|
const paths = pi.ok && !pi.aborted ? fixLine(pi.verdictText ?? "") : null;
|
|
202
204
|
if (paths && verdict.verdict === "reject" && (await gitTry(wt, "status", "--porcelain"))?.trim().length) {
|
|
203
205
|
// The self-fix chain's round is one past the reviewed commit's `Revision:` trailer; at
|
|
@@ -221,7 +223,7 @@ export async function reviewMergedCommit(root, config, entry, runGatePi, signal)
|
|
|
221
223
|
}
|
|
222
224
|
const fix = await commitReviewerFix(wt, commit, verdict.reasons, nextRound);
|
|
223
225
|
// Advance the review-fix ref from C to the new fix commit, so the repair is reachable and
|
|
224
|
-
// the merge-post-review landing (which checks the fix out by this sha) finds it.
|
|
226
|
+
// the merge-post-review-drain landing (which checks the fix out by this sha) finds it.
|
|
225
227
|
await setRef(root, fixRef, fix);
|
|
226
228
|
return { kind: "fixed", fix, reasons: verdict.reasons };
|
|
227
229
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* so an operator can see which tests the gate is excusing and why. Pure over its inputs
|
|
7
7
|
* (no I/O, no clock): every branch has its own unit tests. */
|
|
8
8
|
import { plural } from "../text/phrases.js";
|
|
9
|
+
import { compareCodeUnits } from "../collections.js";
|
|
9
10
|
import { squash } from "../text/text.js";
|
|
10
11
|
import { humanAgo } from "../text/datetime.js";
|
|
11
12
|
import { quarantineWarning } from "../baseline/main-guardrails.js";
|
|
@@ -14,7 +15,7 @@ import { quarantineWarning } from "../baseline/main-guardrails.js";
|
|
|
14
15
|
* through with `""` staying `""`. */
|
|
15
16
|
export function quarantineRows(entries) {
|
|
16
17
|
return [...entries]
|
|
17
|
-
.sort((a, b) => a.firstSeen - b.firstSeen || (a.test
|
|
18
|
+
.sort((a, b) => a.firstSeen - b.firstSeen || compareCodeUnits(a.test, b.test))
|
|
18
19
|
.map((e, i) => ({
|
|
19
20
|
position: i + 1,
|
|
20
21
|
test: e.test,
|
|
@@ -43,12 +44,13 @@ export function renderQuarantine(rows, now) {
|
|
|
43
44
|
}
|
|
44
45
|
return lines.join("\n");
|
|
45
46
|
}
|
|
46
|
-
/** The Markdown section the Failures
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* `tumwater quarantine` (and shares its `quarantineWarning` phrase
|
|
50
|
-
* and the
|
|
51
|
-
* digest but a clean quarantine adds nothing. Pure over its inputs (no I/O, no
|
|
47
|
+
/** The Markdown section the TUI's Failures pane appends below the failure digest — no longer
|
|
48
|
+
* reachable from the dashboard, whose Failures view paints the structured { warning, rows }
|
|
49
|
+
* card (quarantineViewRows) with a Release button per row instead. It reads the same
|
|
50
|
+
* `quarantineRows` rule as `tumwater quarantine` (and shares its `quarantineWarning` phrase
|
|
51
|
+
* when one fires), so the CLI and the TUI cannot disagree. Empty for an empty list, so a
|
|
52
|
+
* window with a digest but a clean quarantine adds nothing. Pure over its inputs (no I/O, no
|
|
53
|
+
* clock). */
|
|
52
54
|
export function renderQuarantineSection(rows, now) {
|
|
53
55
|
if (rows.length === 0)
|
|
54
56
|
return "";
|
|
@@ -56,10 +58,26 @@ export function renderQuarantineSection(rows, now) {
|
|
|
56
58
|
const warning = quarantineWarning(rows, now);
|
|
57
59
|
if (warning !== null)
|
|
58
60
|
lines.push(warning, "");
|
|
59
|
-
for (const r of rows) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
for (const r of quarantineViewRows(rows, now)) {
|
|
62
|
+
// The test name is NOT squashed: the Failures Ctrl+X release editor matches the exact
|
|
63
|
+
// path the list shows (releaseQuarantinedTest(session.root, test)), so truncating here
|
|
64
|
+
// would make a title over the bound impossible to release. `tumwater quarantine` keeps
|
|
65
|
+
// its own numbered squash because --release <n> never needs the full name.
|
|
66
|
+
lines.push(`- ${r.test} — ${r.detail}`);
|
|
63
67
|
}
|
|
64
68
|
return lines.join("\n");
|
|
65
69
|
}
|
|
70
|
+
/** Build the structured rows for the dashboard's quarantine card: `quarantineViewRows` over the
|
|
71
|
+
* sorted `rows`, each `detail` the exact per-row text `renderQuarantineSection` prints after the
|
|
72
|
+
* test. */
|
|
73
|
+
export function quarantineViewRows(rows, now) {
|
|
74
|
+
return rows.map((r) => {
|
|
75
|
+
const filed = r.bug ? `filed: ${squash(r.bug, 80)}` : "not filed";
|
|
76
|
+
return {
|
|
77
|
+
position: r.position,
|
|
78
|
+
test: r.test,
|
|
79
|
+
detail: `first seen ${humanAgo(r.firstSeen, now)}, ` +
|
|
80
|
+
`last seen ${humanAgo(r.lastSeen, now)}, ${plural(r.count, "flake")} — ${filed}`,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -146,7 +146,8 @@ export async function gateBuildPrecheck(ctx, state, head, reject) {
|
|
|
146
146
|
if (main.status === "red") {
|
|
147
147
|
// Main fails too: not this change's failure. Leave the commit (and with it the pin) and
|
|
148
148
|
// do not advance the discard counter — the transport-failure rule (BUGS.md 2026-09-20):
|
|
149
|
-
// nothing judged this diff. main-red.ts's
|
|
149
|
+
// nothing judged this diff. main-red.ts's healer hand-off (`mainRedHealNote`) and the
|
|
150
|
+
// bugfix handoff own the repair,
|
|
150
151
|
// and the orchestrator poll (requeueMainRedPin) re-queues the kept pin once main's head
|
|
151
152
|
// moves; leftover recovery holds it until then.
|
|
152
153
|
const detail = mainRedNotMine(main.sha);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* in roles.ts. Pure catalog data — the shared instruction fragments role prompts embed live in
|
|
5
5
|
* role-guidance.ts (import-only downward, so prompt assembly depends on both without a cycle).
|
|
6
6
|
* roles.ts re-exports ROLES and Role, so every existing importer keeps its import path. */
|
|
7
|
-
import { backlogMoveGuidance, CONVERGENCE_HOLD_MARKER, DECOMPOSITION_GUIDANCE,
|
|
7
|
+
import { backlogMoveGuidance, CHECK_DECLARATION_MARKER, CONVERGENCE_HOLD_MARKER, DECOMPOSITION_GUIDANCE, NEEDS_REVIEW_NOTE, PLAN_QUEUE_GUIDANCE, PLAN_SIZING, searchGuidance, VALIDATION_GAP_GUIDANCE, VALIDATION_GAP_QUERY, VALIDATION_GAP_TALLY, } from "./role-guidance.js";
|
|
8
8
|
import { roleCommitGrep } from "./legacy-role-ids.js";
|
|
9
9
|
import { NOTHING_TO_DO } from "../verdict/reply-contract.js";
|
|
10
10
|
/** The trailer the readme role's commits carry and its `git log --grep` reads back; git parses
|
|
@@ -24,25 +24,25 @@ export const ROLES = [
|
|
|
24
24
|
tier: "default",
|
|
25
25
|
title: "feature implementer",
|
|
26
26
|
find: `Implement the SINGLE most valuable planned feature in PLANS.md that is not yet implemented.
|
|
27
|
+
A \`<no-check>\` block in your prompt names the \`## Planned\` entry marked ${CHECK_DECLARATION_MARKER}
|
|
28
|
+
— that project-check declaration outranks every other planned entry: implement it so a merge
|
|
29
|
+
makes it live (the doctor's \`check\` row ok, \`tumwater status --json\` with no \`noDeclaredCheck\`),
|
|
30
|
+
then mark it done. Do not chase an in-run re-read: while unmerged both still reflect the
|
|
31
|
+
pre-declaration state, so the merge confirms it.
|
|
32
|
+
|
|
27
33
|
1. The prompt's <backlog-index> block lists PLANS.md's \`## Planned\` entries with each
|
|
28
34
|
entry's line range; read the one you pick by that range.
|
|
29
35
|
2. Pick one entry under \`## Planned\`, preferring ones marked ready or with a written plan.
|
|
30
36
|
Skip entries the index block marks \`[blocked: …]\` — \`[blocked: requires …]\` (a
|
|
31
|
-
prerequisite has not merged), \`[blocked: refused]\`, \`[blocked: needs review]
|
|
32
|
-
|
|
33
|
-
entries already carrying a **Needs review …** note
|
|
37
|
+
prerequisite has not merged), \`[blocked: refused]\`, \`[blocked: needs review]\`.
|
|
38
|
+
Skip plans whose entry carries a Refused note, and skip
|
|
39
|
+
entries already carrying a **Needs review …** note. If PLANS.md
|
|
34
40
|
is empty or everything is done, there is nothing to do.
|
|
35
41
|
3. Read only the chosen entry's line range and the code it names.
|
|
36
42
|
4. A plan too large to finish in this run is not split by you: append the note
|
|
37
43
|
${NEEDS_REVIEW_NOTE}
|
|
38
44
|
under its heading, skip it, and implement the next available plan that fits. You may mark
|
|
39
|
-
several oversized entries while scanning, but land exactly one plan.
|
|
40
|
-
— \`${HELD_TAG_PREFIX}<N>)\` on a Needs-review note, \`${HELD_TAG_PREFIX}<H>)\` on a
|
|
41
|
-
Needs-replan note — is ONE past the count already on the entry: read your own or an earlier
|
|
42
|
-
mark's \`${HELD_TAG_PREFIX}N)\` tag, else the \`${PRIOR_HELD_PREFIX}<N>**\` line a lift left
|
|
43
|
-
behind, else start at 1 — never reset it while the same plan ping-pongs, so three holds
|
|
44
|
-
without a merge reach \`${HELD_TAG_PREFIX}3)\`. A plan that merged and left \`## Planned\`
|
|
45
|
-
carries no marker — the convergence backstop starts fresh there.
|
|
45
|
+
several oversized entries while scanning, but land exactly one plan.
|
|
46
46
|
5. Implement it completely: code, tests, and any docs. The reviewer checks your diff against
|
|
47
47
|
the entry's files-touched list and acceptance criteria, so land everything the entry
|
|
48
48
|
promises, or update the entry to say what changed and why. When a plan's anchors no longer
|
|
@@ -64,8 +64,8 @@ than forcing it.`,
|
|
|
64
64
|
latent-bug hunt at the end instead.
|
|
65
65
|
2. Reproduce it if possible: a failing test or a scratch script. When it cannot be reproduced
|
|
66
66
|
and this run made its failure message name the cause next time (or found it already does),
|
|
67
|
-
record a Progress note, write
|
|
68
|
-
heading, and end the run. Do not keep investigating without new evidence.
|
|
67
|
+
record a Progress note, write a **Needs review <YYYY-MM-DD> by bugfix: <what the next failure's message
|
|
68
|
+
will show>** note as its own line under the heading, and end the run. Do not keep investigating without new evidence.
|
|
69
69
|
3. Fix it, and add a regression test that fails without the fix — run that test to watch it
|
|
70
70
|
fail before the fix and pass after.
|
|
71
71
|
4. Mark it fixed in BUGS.md — ${backlogMoveGuidance("BUGS.md", "## Open", "## Fixed", "BUGS-FIXED.md", "fixed")}
|
|
@@ -92,10 +92,17 @@ within that budget, there is nothing to do.`,
|
|
|
92
92
|
find: `Write ONE concrete plan for what this project needs next. Do not implement it.
|
|
93
93
|
1. Check what is already waiting: the prompt's <backlog-index> block lists PLANS.md's
|
|
94
94
|
\`## Planned\` entries with each entry's line range.
|
|
95
|
+
- A \`<no-check>\` block in your prompt (this project declares no check and no declaration
|
|
96
|
+
plan waits) is this run's task over adding another plan: write the declaration entry it
|
|
97
|
+
names, headed \`### Declare the project's deterministic check\` and marked on its own line
|
|
98
|
+
with ${CHECK_DECLARATION_MARKER}, before anything else — the eligible-plan stop below
|
|
99
|
+
yields to it within an admitted run (a full plan backlog can still defer the plan role
|
|
100
|
+
before this prompt is built). A Needs-review lift and an assigned claim still
|
|
101
|
+
come first.
|
|
95
102
|
- A plan carrying a ${NEEDS_REVIEW_NOTE} note outranks adding another plan: split it into
|
|
96
103
|
independently mergeable sub-plans that cross-reference each other (per PLAN_SIZING below,
|
|
97
104
|
each with its own acceptance criteria), then remove the note. That split is this run's task.
|
|
98
|
-
- A BUGS.md \`## Open\` entry carrying a ${NEEDS_REVIEW_NOTE}
|
|
105
|
+
- A BUGS.md \`## Open\` entry carrying a ${NEEDS_REVIEW_NOTE} note
|
|
99
106
|
(the feature or bugfix role parked it as too large for one run) also outranks adding another
|
|
100
107
|
plan: resolve it in the same run by splitting it into independently fixable sub-bugs (each
|
|
101
108
|
its own BUGS.md heading) then removing the note, or by lifting the note when the work does
|
|
@@ -106,19 +113,13 @@ within that budget, there is nothing to do.`,
|
|
|
106
113
|
the quoted objections and the code they name and rewrite the plan so an implementation
|
|
107
114
|
following it would answer each — correcting the approach or the anchors, tightening the
|
|
108
115
|
acceptance criteria, or splitting it per PLAN_SIZING — then remove the note.
|
|
109
|
-
- A plan carrying a
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
\`**Previously held N**\` line on the surviving parent and every child part the split
|
|
117
|
-
produces, then write \`${PRIOR_HELD_PREFIX}<N>**\` at the lifted note's count on the
|
|
118
|
-
parent and copy it to each child, so exactly one marker survives and heldCount reads the
|
|
119
|
-
lifted count everywhere — a merged parent (one whose plan left \`## Planned\`) leaves no
|
|
120
|
-
marker, and a note with no \`(held N)\` tag (the automatic convergence hold) is lifted
|
|
121
|
-
without writing a \`**Previously held 0**\` marker.
|
|
116
|
+
- A plan carrying a **Needs review …** note that quotes the reviewer's surviving objections
|
|
117
|
+
(a final-round rejection — \`rejected after <N> review rounds\`) also outranks adding another
|
|
118
|
+
plan: read the quoted objections and the code they name, then rewrite the plan so an
|
|
119
|
+
implementation following it would answer them — correcting the approach or the anchors,
|
|
120
|
+
tightening the acceptance criteria, splitting it per PLAN_SIZING, or refusing it with the
|
|
121
|
+
objection recorded when it should not be done — and remove both the note and the quoted
|
|
122
|
+
objections.
|
|
122
123
|
- Otherwise, when PLANS.md \`## Planned\` already holds one more eligible plan than there
|
|
123
124
|
are feature instances (one waiting per instance, plus one for the next planner run),
|
|
124
125
|
end with ${NOTHING_TO_DO} — feature has work, and a waiting plan is refined by the
|
|
@@ -418,8 +419,8 @@ own response failing, so it is fileable as it stands under rule 1.
|
|
|
418
419
|
outranks one that burned seconds — cite its cost when it is the reason you file.
|
|
419
420
|
4. Before filing, check BUGS.md's ## Open section and \`git log ${roleCommitGrep("selfcheck")}\`
|
|
420
421
|
for an entry that already names this cluster — no duplicate filings. When that Open entry
|
|
421
|
-
carries a
|
|
422
|
-
note's date, add the new occurrence under the entry (time, merged commit, failure message)
|
|
422
|
+
carries a **Needs review …** note naming what would make the failure show itself and the cluster
|
|
423
|
+
failed again after the note's date, add the new occurrence under the entry (time, merged commit, failure message)
|
|
423
424
|
and delete the note line, instead of filing a duplicate.
|
|
424
425
|
5. File it: cite the cluster's normalized key plus the correlated \`merged\` commit in the
|
|
425
426
|
entry, so the bugfix role can reproduce it.
|