tumwater 0.1.3 → 0.2.0
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 +63 -28
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-eligibility.js +332 -43
- package/dist/src/backlog/backlog-md.js +79 -17
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +178 -441
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +32 -30
- package/dist/src/{brief-edit.js → brief/brief-edit.js} +12 -11
- package/dist/src/{brief.js → brief/brief.js} +33 -8
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +13 -11
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +9 -7
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +43 -10
- package/dist/src/cli/cli-editor.js +13 -1
- package/dist/src/cli/cli-flag-specs.js +31 -6
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-query-commands.js +439 -56
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +124 -68
- package/dist/src/cli/completion.js +70 -12
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +264 -124
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +13 -224
- package/dist/src/cli.js +78 -36
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +107 -325
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +494 -68
- package/dist/src/doctor/doctor-checks.js +509 -58
- package/dist/src/doctor/doctor-holds.js +139 -78
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +2 -3
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +76 -25
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +219 -106
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +80 -30
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +6 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +79 -59
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -716
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +105 -54
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +25 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +179 -104
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +29 -70
- package/dist/src/inbox/inbox-attachments.js +66 -10
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +114 -73
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +104 -50
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +130 -13
- package/dist/src/loop/loop-tick.js +190 -88
- package/dist/src/loop/loop.js +68 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -20
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +113 -45
- package/dist/src/operator/operator-intent.js +74 -53
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +47 -28
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +74 -43
- package/dist/src/orchestrator/orchestrator.js +149 -96
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +124 -38
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +4 -3
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +36 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +29 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +259 -254
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +235 -44
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +4 -3
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +55 -34
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +109 -131
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +16 -14
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +15 -8
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +28 -25
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +72 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +50 -39
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +84 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +39 -38
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +142 -25
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +23 -22
- package/dist/src/tick/tick-detail.js +37 -24
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +67 -53
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +75 -10
- package/dist/src/ui/badges.js +113 -69
- package/dist/src/ui/gui/gui-client-boot.js +202 -29
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +94 -37
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +90 -47
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +60 -43
- package/dist/src/ui/gui/gui-client-operator.js +73 -43
- package/dist/src/ui/gui/gui-client-pending.js +34 -31
- package/dist/src/ui/gui/gui-client-report.js +330 -47
- package/dist/src/ui/gui/gui-client-settings.js +126 -18
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +190 -74
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +18 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +176 -102
- package/dist/src/ui/status-payload.js +114 -36
- package/dist/src/ui/status-render.js +121 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +9 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +61 -57
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +44 -50
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +87 -1821
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +52 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +44 -26
- package/dist/src/verdict/fix-claim.js +56 -33
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +43 -11
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -56
- package/dist/src/landing/landing-slot.js +0 -256
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { loadConfigSafe } from "../config/config.js";
|
|
3
|
-
import { subscribeEvents, warnEventBestEffort } from "./events.js";
|
|
3
|
+
import { subscribeEvents, TUMWATER_SOURCE, warnEventBestEffort } from "./events.js";
|
|
4
4
|
import { formatEvent } from "./event-format.js";
|
|
5
5
|
import { errorMessage, stripTerminalControls } from "../text/text.js";
|
|
6
6
|
import { childEnv } from "../process/process.js";
|
|
@@ -11,43 +11,46 @@ import { childEnv } from "../process/process.js";
|
|
|
11
11
|
* Scope, stated plainly: the notifier subscribes via subscribeEvents (src/events/events.ts), which
|
|
12
12
|
* only sees events THIS process logs. Every allowlisted type but `supervisor_exit` is logged by
|
|
13
13
|
* the orchestrator's own process (budget-gates.ts, gate-polls.ts → streak-gate.ts,
|
|
14
|
-
* role-cap-gates.ts and disk-gate.ts, src/baseline/main-red.ts, src/
|
|
14
|
+
* role-cap-gates.ts and disk-gate.ts, src/baseline/main-red.ts, src/team/team-polls.ts,
|
|
15
|
+
* merge-slot.ts, merge-questions.ts,
|
|
15
16
|
* src/redeploy/redeployer.ts). `supervisor_exit` is logged by the supervisor after the
|
|
16
|
-
* orchestrator is gone, so src/cli/cli-run.ts pages it directly with
|
|
17
|
+
* orchestrator is gone, so src/cli/cli-run.ts pages it directly with notifyTeamDown; events
|
|
18
|
+
* appended by
|
|
17
19
|
* operator CLI commands go straight to the file and are out of scope by design.
|
|
18
20
|
*
|
|
19
21
|
* One knob, an opinionated fixed allowlist, no per-event selection — a command that can post
|
|
20
22
|
* anywhere (a desktop notification, a webhook, a phone push) composes it with the environment
|
|
21
|
-
* the harness hands it: TUMWATER_EVENT_TYPE,
|
|
23
|
+
* the harness hands it: TUMWATER_EVENT_TYPE, TUMWATER_EVENT_SOURCE, and TUMWATER_EVENT_MESSAGE
|
|
22
24
|
* (the exact line `tumwater logs` renders, via formatEvent). */
|
|
23
25
|
export const NOTIFY_EVENT_TYPES = [
|
|
24
26
|
"budget_warning", // daily spend crossed 80% of the cap, gate still open — time to raise it or fix the fallback before the page below
|
|
25
27
|
"role_cap_warning", // one role's own daily spend crossed 80% of its cap, still ticking — time to raise it before the page below
|
|
26
28
|
"budget_paused", // spend cap hit with no usable free fallback — role loops are stopped
|
|
27
29
|
"claude_window_warning", // the claude.ai subscription window first reported allowed_warning — subscription claude seams still tick; the heads-up before the hold below
|
|
28
|
-
"claude_window_limited", // a rejected subscription window holds every subscription claude seam until it resets — the subscription
|
|
30
|
+
"claude_window_limited", // a rejected subscription window holds every subscription claude seam until it resets — the subscription team's binding stop
|
|
29
31
|
"role_streak_paused", // the error-streak circuit breaker auto-paused a role
|
|
30
32
|
"role_cap_paused", // the role's own daily cost cap is reached — it stops starting ticks
|
|
31
33
|
"disk_low", // free space on the worktrees volume dropped below diskHoldGB — new work is held
|
|
32
34
|
"main_red_hold", // main's own check is red — every code loop is held until a fix lands
|
|
33
|
-
"
|
|
34
|
-
"restart_blocked", // the self-redeploy for main is blocked and latched — the
|
|
35
|
-
"question_posted", // a
|
|
36
|
-
"entry_refused", // a refusal note
|
|
37
|
-
"supervisor_exit", // the supervisor gave up — the
|
|
35
|
+
"merge_failed", // the merge slot finished without merging a change
|
|
36
|
+
"restart_blocked", // the self-redeploy for main is blocked and latched — the team is stuck behind it
|
|
37
|
+
"question_posted", // a merge posted a new QUESTIONS.md ## Open entry — only a human can answer it
|
|
38
|
+
"entry_refused", // a refusal note merged — the entry is held until a human or the director releases it
|
|
39
|
+
"supervisor_exit", // the supervisor gave up — the team is down; paged from the supervisor process by notifyTeamDown, since no orchestrator is left to hear it
|
|
38
40
|
];
|
|
39
|
-
/** Same-type events within this gap spawn the command
|
|
40
|
-
* `
|
|
41
|
+
/** Same-type events within this gap spawn the command at most once per gap: a burst of
|
|
42
|
+
* `merge_failed` events pages once at once and once more when the gap ends, carrying the newest
|
|
43
|
+
* held event and how many were held. Keyed per type, so one type's page never suppresses
|
|
41
44
|
* another's. */
|
|
42
45
|
export const NOTIFY_MIN_GAP_MS = 60_000;
|
|
43
46
|
/** The child environment the notify hook runs with: the three TUMWATER_EVENT_* variables over
|
|
44
47
|
* the harness's own environment. One home for the variable names, shared by the notifier's
|
|
45
48
|
* spawn and the `tumwater notify --test` diagnostic (src/cli/notify-command.ts), so the two
|
|
46
49
|
* cannot drift on what a hook sees. */
|
|
47
|
-
export function notifyCommandEnv(type,
|
|
50
|
+
export function notifyCommandEnv(type, source, message) {
|
|
48
51
|
return childEnv({
|
|
49
52
|
TUMWATER_EVENT_TYPE: type,
|
|
50
|
-
|
|
53
|
+
TUMWATER_EVENT_SOURCE: source,
|
|
51
54
|
TUMWATER_EVENT_MESSAGE: message,
|
|
52
55
|
});
|
|
53
56
|
}
|
|
@@ -57,7 +60,7 @@ export function notifyCommandEnv(type, loop, message) {
|
|
|
57
60
|
const NOTIFY_FAILURE_PREFIX = "notify command ";
|
|
58
61
|
/** True for a warning that spawnNotifyCommand logged for a failed page. */
|
|
59
62
|
export function isNotifyFailure(ev) {
|
|
60
|
-
return ev.type === "warning" && ev.
|
|
63
|
+
return ev.type === "warning" && ev.source === TUMWATER_SOURCE && typeof ev.message === "string" && ev.message.startsWith(NOTIFY_FAILURE_PREFIX);
|
|
61
64
|
}
|
|
62
65
|
/** The warning for a notify hook that did not exit cleanly, or undefined for exit code 0.
|
|
63
66
|
* Pure, so the wording is testable without a spawn. */
|
|
@@ -67,16 +70,24 @@ export function notifyExitWarning(type, code, signal) {
|
|
|
67
70
|
const hint = "check it with `tumwater notify --test`";
|
|
68
71
|
if (code === null)
|
|
69
72
|
return `${NOTIFY_FAILURE_PREFIX}was killed by ${signal} paging ${type} — ${hint}`;
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
return `${NOTIFY_FAILURE_PREFIX}exited with code ${code}${notifyExitGloss(code)} paging ${type} — ${hint}`;
|
|
74
|
+
}
|
|
75
|
+
/** The sh launch-failure gloss for a notify hook that could not start: 127 is sh's "command
|
|
76
|
+
* not found", 126 its "found but not executable" — most often the configured `notify` command
|
|
77
|
+
* is a typo or an uninstalled/non-executable binary, so a bare "code 127" hides the fix the
|
|
78
|
+
* gloss names. Shared by the notifier's warning (notifyExitWarning above) and `tumwater
|
|
79
|
+
* notify --test`'s diagnostic (src/cli/notify-command.ts) so both surfaces name the same fix
|
|
80
|
+
* for a hook that cannot launch. Any other code gets no gloss. */
|
|
81
|
+
export function notifyExitGloss(code) {
|
|
82
|
+
return code === 127 ? " (command not found)" : code === 126 ? " (found but not executable)" : "";
|
|
72
83
|
}
|
|
73
84
|
/** Spawn the notify command for one event: detached with stdio ignored, so it outlives the
|
|
74
85
|
* caller, and never awaited. A failed spawn or a nonzero exit is one warning, never a throw. */
|
|
75
|
-
function spawnNotifyCommand(root, command, event) {
|
|
86
|
+
function spawnNotifyCommand(root, command, event, suffix = "") {
|
|
76
87
|
try {
|
|
77
88
|
const child = spawn(command, [], {
|
|
78
89
|
shell: true,
|
|
79
|
-
env: notifyCommandEnv(event.type, event.
|
|
90
|
+
env: notifyCommandEnv(event.type, event.source, stripTerminalControls(formatEvent(event)) + suffix),
|
|
80
91
|
stdio: "ignore",
|
|
81
92
|
detached: true,
|
|
82
93
|
});
|
|
@@ -85,26 +96,26 @@ function spawnNotifyCommand(root, command, event) {
|
|
|
85
96
|
// exceeds the OS arg limit): one warning names it and the listener keeps working. A
|
|
86
97
|
// command that starts but exits nonzero is reported by the exit listener below.
|
|
87
98
|
child.on("error", (err) => {
|
|
88
|
-
warnEventBestEffort(root,
|
|
99
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `${NOTIFY_FAILURE_PREFIX}could not start: ${err.message}`);
|
|
89
100
|
});
|
|
90
101
|
// After unref() this listener does not keep the process alive: a hook that outlives the
|
|
91
102
|
// caller logs nothing.
|
|
92
103
|
child.on("exit", (code, signal) => {
|
|
93
104
|
const message = notifyExitWarning(event.type, code, signal);
|
|
94
105
|
if (message)
|
|
95
|
-
warnEventBestEffort(root,
|
|
106
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, message);
|
|
96
107
|
});
|
|
97
108
|
}
|
|
98
109
|
catch (err) {
|
|
99
|
-
warnEventBestEffort(root,
|
|
110
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `${NOTIFY_FAILURE_PREFIX}could not start: ${errorMessage(err)}`);
|
|
100
111
|
}
|
|
101
112
|
}
|
|
102
|
-
/** Page the configured notify command for the
|
|
113
|
+
/** Page the configured notify command for the team going down. The supervisor process calls
|
|
103
114
|
* this directly, because the orchestrator generation that runs newNotifier is already gone.
|
|
104
115
|
* Reads the command fresh from tumwater.json; a missing or invalid file or an absent `notify`
|
|
105
|
-
* returns silently (the
|
|
116
|
+
* returns silently (the team-down cause may be that very file), and there is no throttle —
|
|
106
117
|
* the supervisor exits right after this one event. */
|
|
107
|
-
export function
|
|
118
|
+
export function notifyTeamDown(root, event) {
|
|
108
119
|
const { config } = loadConfigSafe(root);
|
|
109
120
|
const command = config?.notify;
|
|
110
121
|
if (typeof command !== "string" || command === "")
|
|
@@ -114,13 +125,34 @@ export function notifyFleetDown(root, event) {
|
|
|
114
125
|
/** Subscribe to this process's event feed and spawn the configured command for allowlisted
|
|
115
126
|
* events. Fire-and-forget: the spawn is never awaited from the poll loop — the command runs
|
|
116
127
|
* detached with stdio ignored, so it outlives whatever the orchestrator does next. A nonzero
|
|
117
|
-
* exit or kill is logged as one warning and the page is not retried. `"warning"` is
|
|
118
|
-
* the allowlist, so a failure warning can never recurse into another
|
|
128
|
+
* exit or kill is logged as one warning and the page is not retried. `"warning"` is
|
|
129
|
+
* deliberately not on the allowlist, so a failure warning can never recurse into another
|
|
130
|
+
* spawn. A same-type event inside the gap is held, not dropped: the newest one is paged when
|
|
131
|
+
* the gap ends (or at `dispose`), with a count of the others held with it. */
|
|
119
132
|
export function newNotifier(root, minGapMs = NOTIFY_MIN_GAP_MS,
|
|
120
133
|
/** The throttle's clock — a test seam; production reads the wall clock. */
|
|
121
|
-
now = Date.now
|
|
134
|
+
now = Date.now,
|
|
135
|
+
/** Runs `fn` after `delayMs` and returns a cancel function — a test seam; production uses an
|
|
136
|
+
* unref'd timer so a held page never keeps the process alive. */
|
|
137
|
+
schedule = (fn, delayMs) => {
|
|
138
|
+
const timer = setTimeout(fn, delayMs);
|
|
139
|
+
timer.unref();
|
|
140
|
+
return () => clearTimeout(timer);
|
|
141
|
+
}) {
|
|
122
142
|
let command;
|
|
123
143
|
const lastSpawnAt = new Map();
|
|
144
|
+
const pending = new Map();
|
|
145
|
+
const flush = (type) => {
|
|
146
|
+
const entry = pending.get(type);
|
|
147
|
+
if (!entry)
|
|
148
|
+
return;
|
|
149
|
+
pending.delete(type);
|
|
150
|
+
if (typeof command !== "string" || command === "")
|
|
151
|
+
return;
|
|
152
|
+
lastSpawnAt.set(type, now());
|
|
153
|
+
const suffix = entry.held > 1 ? ` (and ${entry.held - 1} more ${type} event(s) in the same minute; \`tumwater logs\` lists them)` : "";
|
|
154
|
+
spawnNotifyCommand(root, command, entry.event, suffix);
|
|
155
|
+
};
|
|
124
156
|
const unsubscribe = subscribeEvents((event) => {
|
|
125
157
|
if (typeof command !== "string" || command === "")
|
|
126
158
|
return;
|
|
@@ -129,8 +161,20 @@ now = Date.now) {
|
|
|
129
161
|
const type = event.type;
|
|
130
162
|
const at = now();
|
|
131
163
|
const last = lastSpawnAt.get(type);
|
|
132
|
-
if (last !== undefined && at - last < minGapMs)
|
|
164
|
+
if (last !== undefined && at - last < minGapMs) {
|
|
165
|
+
const held = pending.get(type);
|
|
166
|
+
if (held) {
|
|
167
|
+
held.event = event;
|
|
168
|
+
held.held += 1;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
pending.set(type, { event, held: 1, cancel: schedule(() => flush(type), last + minGapMs - at) });
|
|
172
|
+
}
|
|
133
173
|
return;
|
|
174
|
+
}
|
|
175
|
+
// Past the gap: this event pages now and supersedes anything still held for the type.
|
|
176
|
+
pending.get(type)?.cancel();
|
|
177
|
+
pending.delete(type);
|
|
134
178
|
lastSpawnAt.set(type, at);
|
|
135
179
|
spawnNotifyCommand(root, command, event);
|
|
136
180
|
});
|
|
@@ -138,6 +182,12 @@ now = Date.now) {
|
|
|
138
182
|
update(config) {
|
|
139
183
|
command = config.notify;
|
|
140
184
|
},
|
|
141
|
-
dispose
|
|
185
|
+
dispose() {
|
|
186
|
+
unsubscribe();
|
|
187
|
+
for (const [type, entry] of [...pending]) {
|
|
188
|
+
entry.cancel();
|
|
189
|
+
flush(type);
|
|
190
|
+
}
|
|
191
|
+
},
|
|
142
192
|
};
|
|
143
193
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** The cluster-key rules shared by everything that groups failure messages into causes: the
|
|
2
2
|
* failure digest's collection (failure-data.ts) and the error-storm reducer
|
|
3
|
-
* (src/
|
|
3
|
+
* (src/team/error-storm.ts) both count by these keys, so the normalization, the two
|
|
4
4
|
* tick-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. */
|
|
@@ -12,7 +12,7 @@ import { cutSplitsSurrogatePair } from "../text/text.js";
|
|
|
12
12
|
export const EXAMPLE_MAX = 120;
|
|
13
13
|
/** The example and cluster key every message-less failure takes: errorTextOrPlaceholder
|
|
14
14
|
* returns it for the digest's error clusters (failure-data.ts) and its loss ranking
|
|
15
|
-
* (time-spend.ts), so a broken `
|
|
15
|
+
* (time-spend.ts), so a broken `run_end` that skipped its error text reads the same on both
|
|
16
16
|
* surfaces. */
|
|
17
17
|
const NO_ERROR_TEXT = "(no error text recorded)";
|
|
18
18
|
/** The error text a failure event carries, or NO_ERROR_TEXT when it carries none — the field
|
|
@@ -49,7 +49,7 @@ export function truncateExample(message, max = EXAMPLE_MAX) {
|
|
|
49
49
|
* array in ascending localeCompare order, so equal inputs always render identically no matter
|
|
50
50
|
* the Set's insertion order. One home for the rule the failure digest's clusters
|
|
51
51
|
* (clusterMessages) and loss causes (failure-data.ts) and the error-storm warning
|
|
52
|
-
* (src/
|
|
52
|
+
* (src/team/error-storm.ts) all share — an inline `.sort()` without the comparator would
|
|
53
53
|
* silently pick code-unit order instead and let the two rules drift. */
|
|
54
54
|
export function sortedRoles(roles) {
|
|
55
55
|
return [...roles].sort((a, b) => a.localeCompare(b));
|
|
@@ -78,7 +78,7 @@ export function normalizeClusterKey(message) {
|
|
|
78
78
|
}
|
|
79
79
|
/** normalizeClusterKey's rendering of the two tick-timeout errors src/agent/run-agent.ts emits —
|
|
80
80
|
* the plain kill and the still-making-progress variant (session and worktree edits preserved
|
|
81
|
-
* for resume, BUGS.md 2026-09-29). Both are one cause pointing at one knob (
|
|
81
|
+
* for resume, BUGS.md 2026-09-29). Both are one cause pointing at one knob (runTimeoutSeconds),
|
|
82
82
|
* so every consumer that counts by cluster key pools them under the plain one. */
|
|
83
83
|
export const TICK_TIMEOUT_KEY = "timed out after <dur>";
|
|
84
84
|
const TICK_TIMEOUT_PROGRESSING_KEY = "timed out after <dur> while still making progress — session and worktree edits preserved for resume";
|
|
@@ -94,9 +94,9 @@ const TICK_TIMEOUT_PROGRESSING_REVIEW_KEY = "timed out after <dur> while still m
|
|
|
94
94
|
export const TICK_TIMEOUT_PROGRESSING_REVIEW_RESUME_KEY = "timed out after <dur> while still making progress — the commit is kept; the next attempt continues this review";
|
|
95
95
|
/** The known progressing-timeout wordings and the key each pools under. The two review
|
|
96
96
|
* wordings name one cause on one knob (review.timeoutSeconds): a still-making-progress kill
|
|
97
|
-
* pools into the plain tick-timeout row (a mixed
|
|
97
|
+
* pools into the plain tick-timeout row (a mixed team of plain and progressing kills is one
|
|
98
98
|
* cause's agent-hours, not two half-size rows the top-N cut can drop — the same pooling the
|
|
99
|
-
* error-storm reducer applies, src/
|
|
99
|
+
* error-storm reducer applies, src/team/error-storm.ts), and a resumed review's wording maps
|
|
100
100
|
* to itself, because continuing the timed-out session is a different cause from a from-scratch
|
|
101
101
|
* re-review. Enumerated so a future wording edit lands in
|
|
102
102
|
* this table rather than silently changing which row its timeouts count under. */
|
|
@@ -114,9 +114,10 @@ export function poolTimeoutKey(key) {
|
|
|
114
114
|
* rides lastSeen: a cluster that outlived a config change labels itself with the message as
|
|
115
115
|
* it happens now, not the retired value it first emitted (BUGS.md 2026-09-30). `keyPrefix`
|
|
116
116
|
* scopes a cluster to something the message itself omits (rejections key on role too) without
|
|
117
|
-
* polluting the verbatim example.
|
|
117
|
+
* polluting the verbatim example. `urgent` marks messages that outrank the count.
|
|
118
|
+
* Returns the top-N clusters plus how many fell past the cut,
|
|
118
119
|
* so the render can mark the truncation instead of presenting the survivors as the whole. */
|
|
119
|
-
export function clusterMessages(messages, top) {
|
|
120
|
+
export function clusterMessages(messages, top, urgent) {
|
|
120
121
|
const drafts = new Map();
|
|
121
122
|
for (const { message, role, ts, keyPrefix } of messages) {
|
|
122
123
|
const key = `${keyPrefix ?? ""}${poolTimeoutKey(normalizeClusterKey(message))}`;
|
|
@@ -142,7 +143,9 @@ export function clusterMessages(messages, top) {
|
|
|
142
143
|
});
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
|
-
|
|
146
|
+
// Clusters whose example is `urgent` rank ahead of every other, whatever their count, so a
|
|
147
|
+
// once-per-window singleton survives the cut past recurring noise.
|
|
148
|
+
const sorted = rankByCount(drafts.values(), (d) => d.count + (urgent?.(d.example) ? Number.MAX_SAFE_INTEGER / 2 : 0), (d) => d.key);
|
|
146
149
|
return {
|
|
147
150
|
clusters: sorted.slice(0, top).map((d) => ({
|
|
148
151
|
key: d.key,
|
|
@@ -5,19 +5,34 @@ import { timeAndSpend } from "./time-spend.js";
|
|
|
5
5
|
import { dayAt, dayKey, dayWindow, formatDate } from "../text/datetime.js";
|
|
6
6
|
import { describeStateChange, STATE_CHANGE_TOP, STATE_CHANGE_TYPES } from "./failure-state-change.js";
|
|
7
7
|
import { clusterMessages, errorTextOrPlaceholder, truncateExample } from "./failure-cluster.js";
|
|
8
|
-
import { addTo, getOrCreate, rankByCount } from "../collections.js";
|
|
8
|
+
import { addTo, getOrCreate, memoSetLru, rankByCount } from "../collections.js";
|
|
9
9
|
import { firstReason } from "../text/phrases.js";
|
|
10
10
|
import { sameStatIdentity, statOrNull } from "../files/files.js";
|
|
11
11
|
import { nonNegativeNumber } from "../files/json-object.js";
|
|
12
12
|
import { eventsLogPath } from "../paths.js";
|
|
13
13
|
/** Caps that keep the digest bounded regardless of how bad the window was — the top-N
|
|
14
|
-
* clusters, one trimmed example each, and the newest N
|
|
14
|
+
* clusters, one trimmed example each, and the newest N merged commits. See the render-doc
|
|
15
15
|
* byte bound below for what these buy. */
|
|
16
16
|
const ERROR_TOP = 7;
|
|
17
17
|
const WARNING_TOP = 7;
|
|
18
|
+
/** Warnings that name a failing check or a stalled tool call. Each is a once-per-window
|
|
19
|
+
* singleton, so ranking by count alone lets recurring housekeeping lines push it out of the
|
|
20
|
+
* digest; these rank first. */
|
|
21
|
+
const URGENT_WARNING = /^(?:landing check red|merge check failed|tool call stalled)/;
|
|
22
|
+
/** The pre-07b2664a wording of the merge-check warning, pooled under the current one so a
|
|
23
|
+
* window spanning the reword counts one cause, not two. */
|
|
24
|
+
const OLD_MERGE_CHECK_WORDING = /^landing check red \(kept for one re-land\)/;
|
|
18
25
|
const REVIEW_FAILURE_TOP = 5;
|
|
19
26
|
const REJECTION_TOP = 5;
|
|
20
|
-
|
|
27
|
+
/** Blocked merges are rare and each one is a signal, so the section gets its own cut, roomier
|
|
28
|
+
* than the other cluster sections'; the digest stays bounded and the render marks any clusters
|
|
29
|
+
* the cut hides. */
|
|
30
|
+
const MERGE_FAILURE_TOP = 10;
|
|
31
|
+
/** The cause text for a blocked or conflicted merge whose role logged no warning since its
|
|
32
|
+
* previous merge outcome — a block with no stated reason is a signal in itself, so it clusters
|
|
33
|
+
* on its own instead of hiding. */
|
|
34
|
+
const NO_MERGE_REASON = "no reason recorded";
|
|
35
|
+
const MERGED_TOP = 10;
|
|
21
36
|
const SUMMARY_MAX = 100;
|
|
22
37
|
/** Keep only events that belong to one base role, so a scoped digest aggregates instances
|
|
23
38
|
* (`feature-2`) into their group exactly as `report --role` and the per-role cap do. Loop
|
|
@@ -44,21 +59,56 @@ function section(clustered, total) {
|
|
|
44
59
|
* section does not render through it — its text lives on `error`, not `message`, with a
|
|
45
60
|
* fallback phrase — and rejections key on reasons[0] and prefix their cluster keys, so both
|
|
46
61
|
* keep their own blocks. */
|
|
47
|
-
function messageSection(events, type, top) {
|
|
62
|
+
function messageSection(events, type, top, urgent) {
|
|
48
63
|
const selected = events.filter((ev) => ev.type === type && typeof ev.message === "string" && ev.message !== "");
|
|
49
|
-
return section(clusterMessages(selected.map((ev) => ({ message: ev.message, role: eventRole(ev), ts: ev.ts })), top), selected.length);
|
|
64
|
+
return section(clusterMessages(selected.map((ev) => ({ message: ev.message, role: eventRole(ev), ts: ev.ts })), top, urgent), selected.length);
|
|
65
|
+
}
|
|
66
|
+
/** The merge-failure section: each blocked or conflicted `merge_failed` clusters on
|
|
67
|
+
* (role, result, reason).
|
|
68
|
+
* `merge_failed` carries no message, so the reason is the same role's nearest preceding
|
|
69
|
+
* `warning` (a red merge check, a structure or fix-claim block) logged after that role's
|
|
70
|
+
* previous merge outcome; with none, the placeholder. The total counts every blocked or
|
|
71
|
+
* conflicted `merge_failed` in the window. */
|
|
72
|
+
function mergeFailureSection(events) {
|
|
73
|
+
const ordered = [...events].sort((a, b) => a.ts - b.ts);
|
|
74
|
+
const lastWarning = new Map();
|
|
75
|
+
const messages = [];
|
|
76
|
+
let total = 0;
|
|
77
|
+
for (const ev of ordered) {
|
|
78
|
+
const role = eventRole(ev);
|
|
79
|
+
if (ev.type === "warning" && typeof ev.message === "string" && ev.message !== "") {
|
|
80
|
+
lastWarning.set(role, ev.message.replace(OLD_MERGE_CHECK_WORDING, "merge check failed (kept for one retry)"));
|
|
81
|
+
}
|
|
82
|
+
else if (ev.type === "merge_done") {
|
|
83
|
+
lastWarning.delete(role);
|
|
84
|
+
}
|
|
85
|
+
else if (ev.type === "merge_failed") {
|
|
86
|
+
const result = typeof ev.result === "string" ? ev.result : "";
|
|
87
|
+
// Rejected and review_error merges have their own sections; only a block or a conflict
|
|
88
|
+
// is unaccounted for here.
|
|
89
|
+
if (result !== "merge_blocked" && result !== "merge_conflict") {
|
|
90
|
+
lastWarning.delete(role);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
total++;
|
|
94
|
+
const reason = lastWarning.get(role) ?? NO_MERGE_REASON;
|
|
95
|
+
lastWarning.delete(role);
|
|
96
|
+
messages.push({ message: `${result} — ${reason}`, role, ts: ev.ts, keyPrefix: `${role}\u0000` });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return section(clusterMessages(messages, MERGE_FAILURE_TOP), total);
|
|
50
100
|
}
|
|
51
101
|
function roleStats(events) {
|
|
52
102
|
const byRole = new Map();
|
|
53
|
-
const statsFor = (role) => getOrCreate(byRole, role, () => ({
|
|
103
|
+
const statsFor = (role) => getOrCreate(byRole, role, () => ({ runs: 0, errors: 0, quietKills: 0, rejections: 0 }));
|
|
54
104
|
for (const ev of events) {
|
|
55
|
-
// Ticks are
|
|
105
|
+
// Ticks are run_end events; a rejection is now recorded by the merge slot, AFTER the
|
|
56
106
|
// authoring tick has already ended `queued` (plans/merge-queue.md 3/5). review_rejected is
|
|
57
107
|
// the one event every reject path logs exactly once (src/review/review.ts), so it — not a
|
|
58
|
-
//
|
|
59
|
-
if (ev.type === "
|
|
108
|
+
// run_end result no production path emits — is the rejection source.
|
|
109
|
+
if (ev.type === "run_end") {
|
|
60
110
|
const stats = statsFor(eventRole(ev));
|
|
61
|
-
stats.
|
|
111
|
+
stats.runs++;
|
|
62
112
|
if (ev.result === "error")
|
|
63
113
|
stats.errors++;
|
|
64
114
|
else if (ev.result === "quiet_killed")
|
|
@@ -75,7 +125,7 @@ function roleStats(events) {
|
|
|
75
125
|
* collector re-reads and re-parses up to 2× the window's log bytes each time (~4 MB on this
|
|
76
126
|
* repo's own log, growing toward the rotation cap). An unchanged log and window cost one stat
|
|
77
127
|
* and return the prior result; any change — an append, a rotation, or a new local day —
|
|
78
|
-
* re-derives exactly as uncached. Mirrors report-
|
|
128
|
+
* re-derives exactly as uncached. Mirrors report-fold.ts's foldWindowEvents, which already
|
|
79
129
|
* spares the usage report the same re-read. */
|
|
80
130
|
const failureDigestCache = new Map();
|
|
81
131
|
const FAILURE_DIGEST_CACHE_MAX = 8;
|
|
@@ -95,7 +145,7 @@ export function collectFailureReport(root, days, role) {
|
|
|
95
145
|
const { from, to } = dayWindow(days, now);
|
|
96
146
|
const priorFrom = formatDate(dayAt(days * 2 - 1, now));
|
|
97
147
|
const base = role === undefined ? undefined : baseRoleOf(role);
|
|
98
|
-
// The base role rides the key, so a scoped and a
|
|
148
|
+
// The base role rides the key, so a scoped and a team digest of the same log cannot
|
|
99
149
|
// collide in the memo.
|
|
100
150
|
const key = `${root}\u0000${days}\u0000${base ?? ""}`;
|
|
101
151
|
const stat = digestStat(root);
|
|
@@ -104,8 +154,7 @@ export function collectFailureReport(root, days, role) {
|
|
|
104
154
|
cached.to === to &&
|
|
105
155
|
cached.priorFrom === priorFrom &&
|
|
106
156
|
sameStatIdentity(cached.stat, stat)) {
|
|
107
|
-
failureDigestCache
|
|
108
|
-
failureDigestCache.set(key, cached);
|
|
157
|
+
memoSetLru(failureDigestCache, key, cached, FAILURE_DIGEST_CACHE_MAX);
|
|
109
158
|
return cached.data;
|
|
110
159
|
}
|
|
111
160
|
const { events, coversFullWindow, logState } = readWindowEvents(root, priorFrom);
|
|
@@ -121,12 +170,7 @@ export function collectFailureReport(root, days, role) {
|
|
|
121
170
|
prior.push(ev);
|
|
122
171
|
}
|
|
123
172
|
const data = assembleFailureReport(current, prior, events, coversFullWindow, logState, { days, from, to }, role);
|
|
124
|
-
failureDigestCache
|
|
125
|
-
if (failureDigestCache.size > FAILURE_DIGEST_CACHE_MAX) {
|
|
126
|
-
const oldest = failureDigestCache.keys().next().value;
|
|
127
|
-
if (oldest !== undefined)
|
|
128
|
-
failureDigestCache.delete(oldest);
|
|
129
|
-
}
|
|
173
|
+
memoSetLru(failureDigestCache, key, { from, to, priorFrom, stat, data }, FAILURE_DIGEST_CACHE_MAX);
|
|
130
174
|
return data;
|
|
131
175
|
}
|
|
132
176
|
/** Collect the digest over the trailing `sinceMs` window ending now, partitioning the read into
|
|
@@ -164,7 +208,7 @@ export function collectFailureReportSince(root, sinceMs, role) {
|
|
|
164
208
|
* exist twice. `window` carries the labels the render branches on. */
|
|
165
209
|
function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWindow, logState, window, role) {
|
|
166
210
|
// Scope first when a role was named: every section below derives from these partitions, so
|
|
167
|
-
// narrowing them at the source scopes outcomes, deltas, clusters,
|
|
211
|
+
// narrowing them at the source scopes outcomes, deltas, clusters, merges, and time/spend at
|
|
168
212
|
// once. The unfiltered read's first event is kept for the retention line, so a scoped partial
|
|
169
213
|
// window still names when the log starts, not when the scoped role's own first tick does.
|
|
170
214
|
const base = role === undefined ? undefined : baseRoleOf(role);
|
|
@@ -172,7 +216,7 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
172
216
|
const prior = base === undefined ? priorRaw : scopeToRole(priorRaw, base);
|
|
173
217
|
const allEvents = base === undefined ? allEventsRaw : scopeToRole(allEventsRaw, base);
|
|
174
218
|
const oldestRetained = allEventsRaw.length > 0 ? eventDayKey(allEventsRaw[0]) : null;
|
|
175
|
-
const tickEvents = current.filter((ev) => ev.type === "
|
|
219
|
+
const tickEvents = current.filter((ev) => ev.type === "run_end");
|
|
176
220
|
// Outcome table: one row per role, counts per result that occurred.
|
|
177
221
|
const outcomeMap = new Map();
|
|
178
222
|
for (const ev of tickEvents) {
|
|
@@ -186,11 +230,11 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
186
230
|
// reverse-alphabetical order; equal-total roles now follow the stated deterministic rule
|
|
187
231
|
// (pinned by the tie test in test/failure-render.test.ts).
|
|
188
232
|
const outcomes = rankByCount([...outcomeMap.entries()].map(([role, counts]) => ({ role, counts })), (row) => total(row.counts), (row) => row.role);
|
|
189
|
-
// Time and spend: the same
|
|
233
|
+
// Time and spend: the same runs priced by wall-clock span and cost, per role × outcome
|
|
190
234
|
// class, plus the loss ranking that weighs causes by agent-hours rather than tick counts.
|
|
191
235
|
const { timeSpend, lossCauses, lossCausesHidden } = timeAndSpend(tickEvents, allEvents);
|
|
192
236
|
// Prompt-token telemetry: per role, the median prompt tokens a tick sent and how much of
|
|
193
|
-
// the prompt budget went out before the first edit. Only
|
|
237
|
+
// the prompt budget went out before the first edit. Only runs carrying the field count, so
|
|
194
238
|
// a pre-feature log leaves the section off entirely.
|
|
195
239
|
const promptStats = promptStatsFor(tickEvents);
|
|
196
240
|
// Deltas: current vs preceding window, per role.
|
|
@@ -199,12 +243,12 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
199
243
|
const roles = [...new Set([...curStats.keys(), ...prevStats.keys()])];
|
|
200
244
|
const deltas = roles
|
|
201
245
|
.map((role) => {
|
|
202
|
-
const c = curStats.get(role) ?? {
|
|
203
|
-
const p = prevStats.get(role) ?? {
|
|
246
|
+
const c = curStats.get(role) ?? { runs: 0, errors: 0, quietKills: 0, rejections: 0 };
|
|
247
|
+
const p = prevStats.get(role) ?? { runs: 0, errors: 0, quietKills: 0, rejections: 0 };
|
|
204
248
|
return {
|
|
205
249
|
role,
|
|
206
|
-
|
|
207
|
-
|
|
250
|
+
prevRuns: p.runs,
|
|
251
|
+
runs: c.runs,
|
|
208
252
|
prevErrors: p.errors,
|
|
209
253
|
errors: c.errors,
|
|
210
254
|
prevQuietKills: p.quietKills,
|
|
@@ -213,18 +257,18 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
213
257
|
rejections: c.rejections,
|
|
214
258
|
};
|
|
215
259
|
})
|
|
216
|
-
.sort((a, b) => b.
|
|
217
|
-
b.
|
|
260
|
+
.sort((a, b) => b.runs - a.runs ||
|
|
261
|
+
b.prevRuns - a.prevRuns ||
|
|
218
262
|
a.role.localeCompare(b.role));
|
|
219
|
-
// Only
|
|
220
|
-
//
|
|
263
|
+
// Only runs that ENDED as errors carry a tick error: `state.lastError` is shared state the
|
|
264
|
+
// merge slot also writes, so a successful tick's `run_end` can carry a leftover merge failure's
|
|
221
265
|
// text (BUGS.md 2026-09-21). `main_red` joins them because its cause is written for that result
|
|
222
266
|
// — the baseline gate's own text (src/baseline/main-red.ts), never a leftover — so the
|
|
223
267
|
// section itemizes the outcome that blocks every merge instead of leaving it a bare count
|
|
224
268
|
// (BUGS.md 2026-09-28). The total therefore equals the Outcome table's error column PLUS its
|
|
225
|
-
// main_red cells; the render's section title says so, and
|
|
269
|
+
// main_red cells; the render's section title says so, and merge failures still surface below.
|
|
226
270
|
// The selection predicate is the Outcome table's own — raw `error`/`main_red` results, no
|
|
227
|
-
// error-text test — so a malformed
|
|
271
|
+
// error-text test — so a malformed run_end that skipped the text can never be counted above
|
|
228
272
|
// but itemized nowhere: those events cluster under a placeholder cause, keeping the section's
|
|
229
273
|
// total equal to the tables it cross-checks against (BUGS.md 2026-09-30).
|
|
230
274
|
const errorEvents = tickEvents.filter((ev) => ev.result === "error" || ev.result === "main_red");
|
|
@@ -233,7 +277,9 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
233
277
|
role: eventRole(ev),
|
|
234
278
|
ts: ev.ts,
|
|
235
279
|
})), ERROR_TOP), errorEvents.length);
|
|
236
|
-
const warnings = messageSection(current
|
|
280
|
+
const warnings = messageSection(current.map((ev) => ev.type === "warning" && typeof ev.message === "string"
|
|
281
|
+
? { ...ev, message: ev.message.replace(OLD_MERGE_CHECK_WORDING, "merge check failed (kept for one retry)") }
|
|
282
|
+
: ev), "warning", WARNING_TOP, (m) => URGENT_WARNING.test(m));
|
|
237
283
|
// Rejections cluster on (role, reasons[0]) — the field the event feed renders — so two
|
|
238
284
|
// different rejection reasons from the same role stay separate rows. The section's total
|
|
239
285
|
// counts every `review_rejected` in the window, reasons or not, so it equals the Deltas
|
|
@@ -245,16 +291,17 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
245
291
|
const reasons = ev.reasons;
|
|
246
292
|
return { message: firstReason(reasons), role: eventRole(ev), ts: ev.ts, keyPrefix: `${eventRole(ev)}\u0000` };
|
|
247
293
|
}), REJECTION_TOP), rejectedEvents.length);
|
|
248
|
-
|
|
294
|
+
const mergeFailures = mergeFailureSection(current);
|
|
295
|
+
// Merge review failures: a reviewer that could not return a parseable verdict (a dead
|
|
249
296
|
// backend, a transport error) fails closed and keeps the commit, but its authoring tick ends
|
|
250
297
|
// `queued`/`no_change` — so the failure lives on the `review_failed` event, never on
|
|
251
|
-
// `
|
|
298
|
+
// `run_end.error`. Clustered on their own so the telemetry role can see a gate that keeps
|
|
252
299
|
// failing without misreading it as a tick error (BUGS.md 2026-09-21).
|
|
253
300
|
const reviewFailures = messageSection(current, "review_failed", REVIEW_FAILURE_TOP);
|
|
254
|
-
// The newest
|
|
301
|
+
// The newest MERGED_TOP merges, newest first. The pre-slice count rides along, as it does for
|
|
255
302
|
// the state changes below, so the render can name what the cut left out — a busy window must
|
|
256
|
-
// not read as a
|
|
257
|
-
const
|
|
303
|
+
// not read as a MERGED_TOP-merge day (BUGS.md 2026-09-23).
|
|
304
|
+
const allMerged = current
|
|
258
305
|
.filter((ev) => ev.type === "merged")
|
|
259
306
|
.map((ev) => ({
|
|
260
307
|
ts: ev.ts,
|
|
@@ -262,7 +309,7 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
262
309
|
summary: typeof ev.summary === "string" ? truncateExample(ev.summary, SUMMARY_MAX) : "",
|
|
263
310
|
}))
|
|
264
311
|
.sort((a, b) => b.ts - a.ts);
|
|
265
|
-
const
|
|
312
|
+
const merged = allMerged.slice(0, MERGED_TOP);
|
|
266
313
|
// The harness's own decisions, newest STATE_CHANGE_TOP kept in chronological order. The
|
|
267
314
|
// description is bounded at collection so render stays a pure function of this data. The
|
|
268
315
|
// pre-slice count rides along so the render can say when it is showing a cut, not all of them.
|
|
@@ -289,7 +336,7 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
289
336
|
return {
|
|
290
337
|
...window,
|
|
291
338
|
...(base === undefined ? {} : { role: base }),
|
|
292
|
-
|
|
339
|
+
runs: tickEvents.length,
|
|
293
340
|
hasEvents,
|
|
294
341
|
emptyLog: !hasEvents && !coversFullWindow,
|
|
295
342
|
logState,
|
|
@@ -305,8 +352,9 @@ function assembleFailureReport(currentRaw, priorRaw, allEventsRaw, coversFullWin
|
|
|
305
352
|
warnings,
|
|
306
353
|
reviewFailures,
|
|
307
354
|
rejections,
|
|
308
|
-
|
|
309
|
-
|
|
355
|
+
mergeFailures,
|
|
356
|
+
merged,
|
|
357
|
+
mergedTotal: allMerged.length,
|
|
310
358
|
stateChanges,
|
|
311
359
|
stateChangesTotal: allStateChanges.length,
|
|
312
360
|
};
|
|
@@ -317,15 +365,15 @@ function median(values) {
|
|
|
317
365
|
const mid = Math.floor(sorted.length / 2);
|
|
318
366
|
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
319
367
|
}
|
|
320
|
-
/** Per-role prompt-token stats from the window's
|
|
368
|
+
/** Per-role prompt-token stats from the window's run_end events. Ticks without a
|
|
321
369
|
* `promptTokens` reading contribute nothing (and skip a role entirely), so the section only
|
|
322
370
|
* appears once some tick ran on a build that records the field. Both reads go through
|
|
323
371
|
* nonNegativeNumber like every other event-log number: a torn or hand-edited line whose
|
|
324
372
|
* `promptTokens` is `1e999` (JSON parses to Infinity) would otherwise land in the median and
|
|
325
373
|
* the `preEditSum / promptSum` ratio as a non-finite value no report can render. */
|
|
326
|
-
function promptStatsFor(
|
|
374
|
+
function promptStatsFor(runs) {
|
|
327
375
|
const byRole = new Map();
|
|
328
|
-
for (const ev of
|
|
376
|
+
for (const ev of runs) {
|
|
329
377
|
const prompt = nonNegativeNumber(ev.promptTokens, 0);
|
|
330
378
|
const preEdit = nonNegativeNumber(ev.preEditPromptTokens, 0);
|
|
331
379
|
if (prompt === 0 && preEdit === 0)
|
|
@@ -342,7 +390,7 @@ function promptStatsFor(ticks) {
|
|
|
342
390
|
.filter(([, acc]) => acc.promptSum > 0)
|
|
343
391
|
.map(([role, acc]) => ({
|
|
344
392
|
role,
|
|
345
|
-
|
|
393
|
+
runs: acc.prompts.length,
|
|
346
394
|
medianPromptTokens: median(acc.prompts),
|
|
347
395
|
preEditShare: acc.preEditSum / acc.promptSum,
|
|
348
396
|
}))
|