tumwater 0.1.2 → 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 +74 -108
- 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-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- 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 +197 -319
- 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 +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- 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 +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- 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 +29 -224
- package/dist/src/cli.js +121 -40
- 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 +119 -337
- 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 +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- 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 +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- 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 +137 -38
- 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 +12 -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 +86 -55
- 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 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- 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 +28 -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 +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- 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 +170 -80
- 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 +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -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 -21
- 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 +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- 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 +128 -37
- 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 +47 -43
- 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 +47 -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 +35 -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 +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- 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 +81 -39
- 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 +125 -135
- 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 +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- 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 +77 -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 +68 -33
- 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 +86 -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} +62 -37
- 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 +147 -26
- 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 +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- 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 +91 -47
- 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 +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- 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 +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -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 +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -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 +11 -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 +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- 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 +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- 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 -31
- package/dist/src/landing/landing-slot.js +0 -246
- 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,10 +1,12 @@
|
|
|
1
|
-
import { addTo, rankCountEntries } from "../collections.js";
|
|
1
|
+
import { addTo, rankCountEntries, sortByValueDesc } from "../collections.js";
|
|
2
2
|
import { compactTokens, usd } from "../text/format.js";
|
|
3
3
|
import { durationLabel } from "../cli/cli-args.js";
|
|
4
4
|
import { monthDay, reportWindow, sinceWindowLine } from "../text/datetime.js";
|
|
5
5
|
import { eventsRotationLabel } from "../events/events.js";
|
|
6
6
|
import { SPARSE_WINDOW_NOTE } from "../events/event-window.js";
|
|
7
|
+
import { shortSpanPhrase } from "../text/phrases.js";
|
|
7
8
|
import { markdownTable } from "../text/markdown.js";
|
|
9
|
+
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
8
10
|
/** Bar width for one day: up to 20 blocks scaled to the window's max tokensOut —
|
|
9
11
|
* round(20·v/max), min 1 when v > 0. */
|
|
10
12
|
function barWidth(v, max) {
|
|
@@ -20,7 +22,7 @@ function rankedRoleMap(totals) {
|
|
|
20
22
|
return rankCountEntries(Object.entries(totals));
|
|
21
23
|
}
|
|
22
24
|
/** Window totals per role: fold the per-day role maps (any `Record<string, number>` day field,
|
|
23
|
-
* e.g.
|
|
25
|
+
* e.g. runsByRole or costByRole) across the series, then rank through rankedRoleMap.
|
|
24
26
|
* Callers filter/format the ranked pairs. */
|
|
25
27
|
function rankedRoleTotals(series, pick) {
|
|
26
28
|
const totals = {};
|
|
@@ -35,43 +37,72 @@ function rankedRoleTotals(series, pick) {
|
|
|
35
37
|
function rankedRoleLine(label, pairs, fmt) {
|
|
36
38
|
return `**${label}:** ${pairs.length === 0 ? "-" : pairs.map(([r, n]) => `${r} — ${fmt(n)}`).join(" · ")}`;
|
|
37
39
|
}
|
|
40
|
+
/** The per-day-median line of the team day report — "MMM D <span>" cells for each day that
|
|
41
|
+
* carries a value, joined under a bold title, or undefined when no day has one (zero means
|
|
42
|
+
* absent). The one home of that cameo rendering, shared by the merge check-time and merge-wait
|
|
43
|
+
* medians so their date+span cells and title line cannot drift; the planned provider-slot wait
|
|
44
|
+
* median is a third caller of the same rule. */
|
|
45
|
+
function medianLine(series, title, pick) {
|
|
46
|
+
const cells = series.flatMap((d) => {
|
|
47
|
+
const v = pick(d);
|
|
48
|
+
return v === undefined ? [] : [`${monthDay(d.date)} ${shortSpanPhrase(v)}`];
|
|
49
|
+
});
|
|
50
|
+
return cells.length > 0 ? `**${title}:** ${cells.join(" · ")}` : undefined;
|
|
51
|
+
}
|
|
52
|
+
/** Push the `Cost by provider` and `Estimated by provider` lines of the per-provider spend
|
|
53
|
+
* breakdown (plans: per-provider spend), from the caller's already-ranked real- and notional-
|
|
54
|
+
* spend pairs. Role scoping is the caller's `role === undefined` gate — this helper only
|
|
55
|
+
* renders. Zero-means-absent: a provider with no spend prints no line, and both lanes format
|
|
56
|
+
* with usd so the day and --since windows cannot drift (the --since path ranks via rankedRoleMap,
|
|
57
|
+
* the day path via rankedRoleTotals). */
|
|
58
|
+
function pushProviderCostLines(lines, rankCost, rankNotional) {
|
|
59
|
+
const providers = rankCost().filter(([, c]) => c > 0);
|
|
60
|
+
if (providers.length > 0)
|
|
61
|
+
lines.push(rankedRoleLine("Cost by provider", providers, usd));
|
|
62
|
+
const notionalProviders = rankNotional().filter(([, c]) => c > 0);
|
|
63
|
+
if (notionalProviders.length > 0)
|
|
64
|
+
lines.push(rankedRoleLine("Estimated by provider", notionalProviders, usd));
|
|
65
|
+
}
|
|
38
66
|
/** The "**Totals:** …" line both renders print — one home so field order, units, and the ·
|
|
39
67
|
* separators cannot drift between the day report and the --since window. `tail` carries the
|
|
40
|
-
* cells only one shape has: the day report's features/bugs counts
|
|
41
|
-
* subdivide their day-granular dates, so the --since render passes
|
|
68
|
+
* cells only one shape has: the day report's features/bugs counts and the filing counts
|
|
69
|
+
* (a sub-day window cannot subdivide their day-granular dates, so the --since render passes
|
|
70
|
+
* nothing). */
|
|
42
71
|
function totalsLine(t, tail = "") {
|
|
43
|
-
return `**Totals:** ${compactTokens(t.tokensOut)} output tokens · ${t.
|
|
72
|
+
return `**Totals:** ${compactTokens(t.tokensOut)} output tokens · ${t.runs} runs · ${t.commits} commits · ${usd(t.costUsd)}${tail}`;
|
|
44
73
|
}
|
|
45
|
-
/** The "of which
|
|
46
|
-
* wording. The caller decides when it prints: zero-means-absent, so a
|
|
47
|
-
* spend renders exactly as before (the budget charges the
|
|
74
|
+
/** The "of which merge runs …" line under a totals line — one home for the merge share's
|
|
75
|
+
* wording. The caller decides when it prints: zero-means-absent, so a team with no merge
|
|
76
|
+
* spend renders exactly as before (the budget charges the merge slot's runs too, so the
|
|
48
77
|
* totals include them — this line says how much of the spend was reviewer + conflict
|
|
49
78
|
* resolution work). */
|
|
50
|
-
function
|
|
51
|
-
|
|
79
|
+
function mergeShareLine(t) {
|
|
80
|
+
const failed = (t.mergeFailed ?? 0) > 0 ? ` (${t.mergeFailed} failed)` : "";
|
|
81
|
+
return `of which merge runs: ${t.mergeRuns} runs${failed} · ${compactTokens(t.mergeTokens)} tokens · ${usd(t.mergeCostUsd)} (reviewer + conflict resolution)`;
|
|
52
82
|
}
|
|
53
83
|
/** The notional-spend line (plans/agent-backends.md part 6a): the API-equivalent cost of
|
|
54
84
|
* claude.ai subscription runs folded into the window. Never part of the totals' costUsd,
|
|
55
85
|
* because a subscription run is exempt from the daily dollar cap; the caller prints it only
|
|
56
|
-
* when a subscription run actually folded in (zero means absent, like the
|
|
57
|
-
* an API-only
|
|
86
|
+
* when a subscription run actually folded in (zero means absent, like the merge line), so
|
|
87
|
+
* an API-only team's report is unchanged. */
|
|
58
88
|
function notionalLine(t) {
|
|
59
|
-
return `claude subscription, API-equivalent: ${usd(t.notionalCostUsd ?? 0)}
|
|
89
|
+
return `claude subscription, API-equivalent: ${usd(t.notionalCostUsd ?? 0)} estimated`;
|
|
60
90
|
}
|
|
61
|
-
/** The scope note a `--role` report prints:
|
|
62
|
-
* features
|
|
63
|
-
*
|
|
91
|
+
/** The scope note a `--role` report prints: merge runs belong to the merge slot and the
|
|
92
|
+
* day-granular backlog tallies (features done / bugs fixed / plans added / bugs filed) come
|
|
93
|
+
* from day-granular backlog-file dates, so none can
|
|
94
|
+
* be scoped to a role and all are omitted from the scoped totals. Shared by the day and
|
|
64
95
|
* --since renders so the two say the same thing. */
|
|
65
|
-
const ROLE_SCOPE_NOTE = "role scope:
|
|
96
|
+
const ROLE_SCOPE_NOTE = "role scope: merge runs and backlog tallies (features done / bugs fixed / plans added / bugs filed) are team-wide and omitted";
|
|
66
97
|
/** The summary block both report shapes print, in the same order: the totals line (carrying
|
|
67
|
-
* the caller's extra `tail` cells), the
|
|
68
|
-
* nonzero (zero means absent, so an API-only or
|
|
98
|
+
* the caller's extra `tail` cells), the merge-share line and the notional line when each is
|
|
99
|
+
* nonzero (zero means absent, so an API-only or merge-free window renders as before), and the
|
|
69
100
|
* role-scope note when the report is scoped. One home so the day report and the --since window
|
|
70
101
|
* cannot drift on the order, the omission predicates, or the scope note. */
|
|
71
102
|
function pushReportSummary(lines, t, tail = "", scoped = false) {
|
|
72
103
|
lines.push(totalsLine(t, tail));
|
|
73
|
-
if (t.
|
|
74
|
-
lines.push(
|
|
104
|
+
if (t.mergeRuns > 0)
|
|
105
|
+
lines.push(mergeShareLine(t));
|
|
75
106
|
if ((t.notionalCostUsd ?? 0) > 0)
|
|
76
107
|
lines.push(notionalLine(t));
|
|
77
108
|
if (scoped)
|
|
@@ -90,15 +121,82 @@ export function renderSinceReportMarkdown(data) {
|
|
|
90
121
|
lines.push(`role: ${data.role}`);
|
|
91
122
|
lines.push("");
|
|
92
123
|
const t = data.totals;
|
|
93
|
-
// Same totals voice as the day report, minus the features/bugs cells: those tallies
|
|
94
|
-
// from day-granular backlog-file dates that cannot subdivide a sub-day window, so the
|
|
95
|
-
// line below states the omission instead of showing a day-rounded number.
|
|
124
|
+
// Same totals voice as the day report, minus the features/bugs and filing cells: those tallies
|
|
125
|
+
// come from day-granular backlog-file dates that cannot subdivide a sub-day window, so the
|
|
126
|
+
// final line below states the omission instead of showing a day-rounded number.
|
|
96
127
|
pushReportSummary(lines, t, "", data.role !== undefined);
|
|
97
128
|
lines.push("");
|
|
98
|
-
lines.push(rankedRoleLine("
|
|
129
|
+
lines.push(rankedRoleLine("Runs by role", rankedRoleMap(data.runsByRole), String));
|
|
130
|
+
// Same breakdown for token spend: ranked by tokens desc then name asc, zero-token roles
|
|
131
|
+
// omitted — an all-zero window renders "-" like the other lines. Output tokens are the
|
|
132
|
+
// resource the budget charges, so this is the line an operator reads to find which role's
|
|
133
|
+
// prompt loop burned the window's tokens.
|
|
134
|
+
lines.push(rankedRoleLine("Tokens by role", rankedRoleMap(data.tokensByRole).filter(([, n]) => n > 0), compactTokens));
|
|
99
135
|
// Same breakdown for spend: ranked by spend desc then name asc, zero-spend roles omitted —
|
|
100
136
|
// an all-zero window renders "-" like the ticks line.
|
|
101
137
|
lines.push(rankedRoleLine("Cost by role", rankedRoleMap(data.costByRole).filter(([, c]) => c > 0), usd));
|
|
138
|
+
// The same breakdown per PROVIDER (plans: per-provider spend), gated to the team report
|
|
139
|
+
// like its day sibling. SinceReport.costByProvider is always present (empty when none), so
|
|
140
|
+
// the zero-means-absent gate prints no line for a window with no provider spend.
|
|
141
|
+
if (data.role === undefined) {
|
|
142
|
+
pushProviderCostLines(lines, () => rankedRoleMap(data.costByProvider), () => rankedRoleMap(data.notionalByProvider));
|
|
143
|
+
// The same breakdown per MODEL (each run_start's agent-qualified selector — the model a
|
|
144
|
+
// team mixing providers, per-role overrides and fallbacks actually ran on), gated to the
|
|
145
|
+
// team report like the provider lines: a model is not a role, so a role-scoped window
|
|
146
|
+
// omits it. SinceReport.runsByModel is always present, so a window with no modelled
|
|
147
|
+
// start prints no line (keeping a model-less day's output unchanged), exactly as the
|
|
148
|
+
// provider lines stay absent when no provider spent.
|
|
149
|
+
const modelPairs = rankedRoleMap(data.runsByModel).filter(([, n]) => n > 0);
|
|
150
|
+
if (modelPairs.length > 0)
|
|
151
|
+
lines.push(rankedRoleLine("Runs by model", modelPairs, String));
|
|
152
|
+
}
|
|
153
|
+
// The same breakdown for review friction: rejections per role from review_rejected events;
|
|
154
|
+
// a window with none renders "-" like the other lines.
|
|
155
|
+
lines.push(rankedRoleLine("Rejections by role", rankedRoleMap(data.rejectionsByRole).filter(([, r]) => r > 0), String));
|
|
156
|
+
lines.push(rankedRoleLine("Revisions by role", rankedRoleMap(data.revisionsByRole).filter(([, r]) => r > 0), String));
|
|
157
|
+
lines.push(rankedRoleLine("Refusals by role", rankedRoleMap(data.refusalsByRole).filter(([, r]) => r > 0), String));
|
|
158
|
+
// The same breakdown for outright failures: error-class run_ends per role under the
|
|
159
|
+
// digest's OUTCOME_CLASS — the ticks that burned agent time and merged nothing without
|
|
160
|
+
// going through review, which rejections/revisions would miss entirely. A window with
|
|
161
|
+
// none renders "-" like the other lines.
|
|
162
|
+
lines.push(rankedRoleLine("Failed runs by role", rankedRoleMap(data.failedRunsByRole).filter(([, n]) => n > 0), String));
|
|
163
|
+
lines.push(rankedRoleLine("Fallbacks by role", rankedRoleMap(data.fallbacksByRole).filter(([, n]) => n > 0), String));
|
|
164
|
+
lines.push(rankedRoleLine("Image reroutes by role", rankedRoleMap(data.imageReroutesByRole).filter(([, n]) => n > 0), String));
|
|
165
|
+
// Deferred maintenance ticks per reason code, the --since twin of the day render's line:
|
|
166
|
+
// how many due maintenance runs stood down and why (backlog / no-work / plans-full).
|
|
167
|
+
// Gated to the team report — a reason is not a role, so a role-scoped window omits it.
|
|
168
|
+
// Zero-means-absent: a window with no deferral prints no line.
|
|
169
|
+
if (data.role === undefined) {
|
|
170
|
+
const deferred = rankedRoleMap(data.deferredByReason ?? {}).filter(([, n]) => n > 0);
|
|
171
|
+
if (deferred.length > 0)
|
|
172
|
+
lines.push(rankedRoleLine("Deferred runs by reason", deferred, String));
|
|
173
|
+
}
|
|
174
|
+
// The merged-commits split by base role, matching the day render's unfiltered line: the
|
|
175
|
+
// window's totals.commits carries the count; this line says which role's commits it was.
|
|
176
|
+
lines.push(rankedRoleLine("Commits by role", rankedRoleMap(data.commitsByRole), String));
|
|
177
|
+
// The merged-outcomes split by base role, mirroring the day render's line: which roles'
|
|
178
|
+
// changes reach main and which bounce off the gate (merge_failed). Gated to the team
|
|
179
|
+
// report — a role-scoped window omits merge (ROLE_SCOPE_NOTE), so the per-role
|
|
180
|
+
// attribution would contradict it. Zero-means-absent: the failures line prints only when
|
|
181
|
+
// some role has one; a merge-free window renders `-` like the other lines.
|
|
182
|
+
if (data.role === undefined) {
|
|
183
|
+
lines.push(rankedRoleLine("Merged by role", rankedRoleMap(data.mergedByRole), String));
|
|
184
|
+
const mergeFailures = rankedRoleMap(data.mergeFailedByRole).filter(([, n]) => n > 0);
|
|
185
|
+
if (mergeFailures.length > 0)
|
|
186
|
+
lines.push(rankedRoleLine("Merge failures by role", mergeFailures, String));
|
|
187
|
+
// WHY the window's merges failed, from each merge_failed's result code — the day
|
|
188
|
+
// report's reason twin of the role line above, so an operator can tell a review
|
|
189
|
+
// rejection (fix the change) from a conflict/block (rebase) from a shutdown abort
|
|
190
|
+
// (capacity). Ranked the same way (count desc then code asc), reason-free only.
|
|
191
|
+
const mergeFailureReasons = rankedRoleMap(data.mergeFailedByReason ?? {}).filter(([, n]) => n > 0);
|
|
192
|
+
if (mergeFailureReasons.length > 0)
|
|
193
|
+
lines.push(rankedRoleLine("Merge failures by reason", mergeFailureReasons, String));
|
|
194
|
+
}
|
|
195
|
+
// Subscription spend is notional: its own line, only when the window has any — the same
|
|
196
|
+
// conditional/filtered `usd` form the day render's "Estimated by role" uses.
|
|
197
|
+
if (data.notionalByRole !== undefined) {
|
|
198
|
+
lines.push(rankedRoleLine("Estimated by role", rankedRoleMap(data.notionalByRole).filter(([, c]) => c > 0), usd));
|
|
199
|
+
}
|
|
102
200
|
lines.push("");
|
|
103
201
|
// Exactly one final line, chosen by the collector's coverage proof: either a hedged note
|
|
104
202
|
// that stays true whenever it prints (the oldest retained event lies inside the window —
|
|
@@ -110,9 +208,47 @@ export function renderSinceReportMarkdown(data) {
|
|
|
110
208
|
if (!data.coversFullWindow)
|
|
111
209
|
lines.push(SPARSE_WINDOW_NOTE);
|
|
112
210
|
else if (data.role === undefined)
|
|
113
|
-
lines.push("backlog tallies (features done / bugs fixed) need the day report (--days)");
|
|
211
|
+
lines.push("backlog tallies (features done / bugs fixed / plans added / bugs filed) need the day report (--days)");
|
|
114
212
|
return lines.join("\n");
|
|
115
213
|
}
|
|
214
|
+
/** Cost per merged commit by base role. Spend comes from costByRole (keyed by loop id, folded to
|
|
215
|
+
* its base role so `feature-2` joins `feature`), commits from commitsByRole (already base-role
|
|
216
|
+
* keyed). `merged` is [role, cost per commit, commits] for roles with both, ranked by cost per
|
|
217
|
+
* commit desc then name asc; `idle` is roles that spent and merged nothing, ranked by spend.
|
|
218
|
+
* Merge spend (the reviewer's) and notional subscription spend stay out by default, matching
|
|
219
|
+
* `**Cost by role:**` (`pick` selects another per-loop-id spend map, e.g. notionalByRole); a
|
|
220
|
+
* role with commits but no spend in the picked map appears in neither list. */
|
|
221
|
+
function costPerCommitPairs(series, pick = (d) => d.costByRole) {
|
|
222
|
+
const cost = {};
|
|
223
|
+
const commits = {};
|
|
224
|
+
for (const d of series) {
|
|
225
|
+
for (const [id, c] of Object.entries(pick(d)))
|
|
226
|
+
addTo(cost, baseRoleOf(id), c);
|
|
227
|
+
for (const [role, n] of Object.entries(d.commitsByRole ?? {}))
|
|
228
|
+
addTo(commits, role, n);
|
|
229
|
+
}
|
|
230
|
+
const mergeDone = [];
|
|
231
|
+
const idleCost = [];
|
|
232
|
+
for (const [role, c] of Object.entries(cost)) {
|
|
233
|
+
if (c <= 0)
|
|
234
|
+
continue;
|
|
235
|
+
const n = commits[role] ?? 0;
|
|
236
|
+
if (n > 0)
|
|
237
|
+
mergeDone.push([role, c / n, n]);
|
|
238
|
+
else
|
|
239
|
+
idleCost.push([role, c]);
|
|
240
|
+
}
|
|
241
|
+
sortByValueDesc(mergeDone);
|
|
242
|
+
return { mergeDone, idle: rankCountEntries(idleCost) };
|
|
243
|
+
}
|
|
244
|
+
/** One `**<label>:** …` line from costPerCommitPairs output; `-` when there is nothing. */
|
|
245
|
+
function costPerCommitLine(label, pairs) {
|
|
246
|
+
const parts = [
|
|
247
|
+
...pairs.mergeDone.map(([r, per, n]) => `${r} — ${usd(per)} (${n})`),
|
|
248
|
+
...pairs.idle.map(([r, c]) => `${r} — ${usd(c)}, no commits`),
|
|
249
|
+
];
|
|
250
|
+
return `**${label}:** ${parts.length === 0 ? "-" : parts.join(" · ")}`;
|
|
251
|
+
}
|
|
116
252
|
/** Render a report as Markdown — the pinned shape the CLI prints and the TUI usage pane
|
|
117
253
|
* (report 3/3) reuses; the GUI tab (report 2/3) renders its own SVG charts from the same
|
|
118
254
|
* ReportData instead. Pure function of ReportData: no I/O, no clock reads. */
|
|
@@ -125,33 +261,134 @@ export function renderReportMarkdown(data) {
|
|
|
125
261
|
lines.push(`role: ${data.role}`);
|
|
126
262
|
lines.push("");
|
|
127
263
|
const t = data.totals;
|
|
128
|
-
// A role-scoped report drops the
|
|
129
|
-
// dates carry no role — and states the omission through ROLE_SCOPE_NOTE below
|
|
130
|
-
const
|
|
264
|
+
// A role-scoped report drops the team-only features/bugs and filing cells from the totals
|
|
265
|
+
// line — their dates carry no role — and states the omission through ROLE_SCOPE_NOTE below.
|
|
266
|
+
const openedTail = (t.plansAdded ?? 0) + (t.bugsFiled ?? 0) > 0
|
|
267
|
+
? ` · ${t.plansAdded ?? 0} plans added · ${t.bugsFiled ?? 0} bugs filed`
|
|
268
|
+
: "";
|
|
269
|
+
const tail = data.role === undefined
|
|
270
|
+
? ` · ${t.featuresDone} features done · ${t.bugsFixed} bugs fixed${openedTail}`
|
|
271
|
+
: "";
|
|
131
272
|
pushReportSummary(lines, t, tail, data.role !== undefined);
|
|
132
273
|
lines.push("");
|
|
133
274
|
const maxTokens = data.series.reduce((m, d) => Math.max(m, d.tokensOut), 0);
|
|
134
|
-
lines.push(...markdownTable(["day", "tokens out", "
|
|
135
|
-
const ticks = Object.values(d.
|
|
275
|
+
lines.push(...markdownTable(["day", "tokens out", "runs", "commits", "cost"], data.series.map((d) => {
|
|
276
|
+
const ticks = Object.values(d.runsByRole).reduce((a, b) => a + b, 0);
|
|
136
277
|
const w = maxTokens > 0 ? barWidth(d.tokensOut, maxTokens) : 0;
|
|
137
278
|
const bar = w > 0 ? ` ${"█".repeat(w)}` : ""; // Zero days carry no bar (and no stray space).
|
|
138
279
|
return [monthDay(d.date), `${compactTokens(d.tokensOut)}${bar}`, String(ticks), String(d.commits), usd(d.costUsd)];
|
|
139
280
|
}), ["left", "right", "right", "right", "right"]));
|
|
140
281
|
lines.push("");
|
|
141
|
-
lines.push(rankedRoleLine("
|
|
282
|
+
lines.push(rankedRoleLine("Runs by role", rankedRoleTotals(data.series, (d) => d.runsByRole), String));
|
|
283
|
+
// The same breakdown for output tokens (the resource the budget charges): window totals per
|
|
284
|
+
// role from tokensByRole, ranked like the ticks line. Zero-token roles are omitted; an
|
|
285
|
+
// all-zero window renders "-" like the ticks line.
|
|
286
|
+
lines.push(rankedRoleLine("Tokens by role", rankedRoleTotals(data.series, (d) => d.tokensByRole ?? {}).filter(([, n]) => n > 0), compactTokens));
|
|
142
287
|
// The same breakdown for spend: window totals per role from costByRole, ranked by spend
|
|
143
288
|
// desc then name asc — spend is what the operator acts on (an operator tuning per-role
|
|
144
289
|
// intervals wants the burning loop first, not the busiest one). Zero-spend roles are
|
|
145
290
|
// omitted; an all-zero window renders "-" like the ticks line.
|
|
146
291
|
lines.push(rankedRoleLine("Cost by role", rankedRoleTotals(data.series, (d) => d.costByRole).filter(([, c]) => c > 0), usd));
|
|
147
|
-
//
|
|
292
|
+
// The same breakdown per PROVIDER (plans: per-provider spend): which seam of a mixed team
|
|
293
|
+
// spent what, straight from the events' providerCost maps (run_end's own runs and the
|
|
294
|
+
// merge's reviewer seam). Gated to the team report — a provider is not a role, so a
|
|
295
|
+
// role-scoped report omits it (ROLE_SCOPE_NOTE covers the omission). Zero-means-absent: a
|
|
296
|
+
// window with no provider spend prints no line. Ranked and formatted exactly like Cost by
|
|
297
|
+
// role.
|
|
298
|
+
if (data.role === undefined) {
|
|
299
|
+
pushProviderCostLines(lines, () => rankedRoleTotals(data.series, (d) => d.costByProvider ?? {}), () => rankedRoleTotals(data.series, (d) => d.notionalByProvider ?? {}));
|
|
300
|
+
// The same breakdown per MODEL (each run_start's agent-qualified selector — the model a
|
|
301
|
+
// team mixing providers, per-role overrides and fallbacks actually ran on), gated to the
|
|
302
|
+
// team report like the provider lines: a model is not a role, so a role-scoped report
|
|
303
|
+
// omits it (ROLE_SCOPE_NOTE covers the omission), and a window with no modelled start
|
|
304
|
+
// prints no line (keeping a model-less day's output unchanged), exactly as the provider
|
|
305
|
+
// lines stay absent when no provider spent. Ranked and formatted like Cost by role.
|
|
306
|
+
const modelPairs = rankedRoleTotals(data.series, (d) => d.runsByModel ?? {}).filter(([, n]) => n > 0);
|
|
307
|
+
if (modelPairs.length > 0)
|
|
308
|
+
lines.push(rankedRoleLine("Runs by model", modelPairs, String));
|
|
309
|
+
}
|
|
310
|
+
// The same breakdown for review friction: window rejections per role from review_rejected
|
|
311
|
+
// events, ranked like the ticks line; a window with none renders "-".
|
|
312
|
+
lines.push(rankedRoleLine("Rejections by role", rankedRoleTotals(data.series, (d) => d.rejectionsByRole ?? {}).filter(([, r]) => r > 0), String));
|
|
313
|
+
lines.push(rankedRoleLine("Revisions by role", rankedRoleTotals(data.series, (d) => d.revisionsByRole ?? {}).filter(([, r]) => r > 0), String));
|
|
314
|
+
lines.push(rankedRoleLine("Refusals by role", rankedRoleTotals(data.series, (d) => d.refusalsByRole ?? {}).filter(([, r]) => r > 0), String));
|
|
315
|
+
// The same breakdown for outright failures: error-class run_ends per role under the
|
|
316
|
+
// digest's OUTCOME_CLASS — ticks that burned agent time and merged nothing without going
|
|
317
|
+
// through review, which rejections/revisions would miss entirely. Zero-means-absent, so a
|
|
318
|
+
// failure-free window renders no line and an all-zero window renders "-" like the others.
|
|
319
|
+
lines.push(rankedRoleLine("Failed runs by role", rankedRoleTotals(data.series, (d) => d.failedRunsByRole ?? {}).filter(([, n]) => n > 0), String));
|
|
320
|
+
lines.push(rankedRoleLine("Fallbacks by role", rankedRoleTotals(data.series, (d) => d.fallbacksByRole ?? {}).filter(([, n]) => n > 0), String));
|
|
321
|
+
lines.push(rankedRoleLine("Image reroutes by role", rankedRoleTotals(data.series, (d) => d.imageReroutesByRole ?? {}).filter(([, n]) => n > 0), String));
|
|
322
|
+
// Deferred maintenance ticks per reason code — how many due maintenance runs stood down
|
|
323
|
+
// and why, so an idle day reads as backlog-open / no-work / plans-full rather than blank,
|
|
324
|
+
// the per-role breakdown of the deferred maintenances. Gated to the team report — a
|
|
325
|
+
// reason is not a role (ROLE_SCOPE_NOTE covers the omission). Zero-means-absent: a window
|
|
326
|
+
// with no deferral prints no line, so a deferral-free day's output is unchanged.
|
|
327
|
+
if (data.role === undefined) {
|
|
328
|
+
const deferred = rankedRoleTotals(data.series, (d) => d.deferredByReason ?? {}).filter(([, n]) => n > 0);
|
|
329
|
+
if (deferred.length > 0)
|
|
330
|
+
lines.push(rankedRoleLine("Deferred runs by reason", deferred, String));
|
|
331
|
+
}
|
|
332
|
+
// Work ratio 4/4: the merged commits split by base role and by tier. The totals line's
|
|
148
333
|
// `commits` carries the count; these lines say who made it and how much of it was work.
|
|
149
334
|
lines.push(rankedRoleLine("Commits by role", rankedRoleTotals(data.series, (d) => d.commitsByRole ?? {}), String));
|
|
335
|
+
// The merged-outcomes split by base role: which authoring roles' changes reach main through
|
|
336
|
+
// the merge slot, and — the new signal — which roles' changes bounce off the gate (a
|
|
337
|
+
// merge_failed: review rejection, under-cap review, a conflict, a blocked fast-forward, a
|
|
338
|
+
// shutdown abort). Gated to the team report — a role-scoped report omits merge runs
|
|
339
|
+
// (they are the merge slot's, per ROLE_SCOPE_NOTE), so the per-role attribution would
|
|
340
|
+
// contradict that stance. Zero-means-absent: a window with merges renders Merged by role;
|
|
341
|
+
// the failures line prints only when some role actually has one.
|
|
342
|
+
if (data.role === undefined) {
|
|
343
|
+
const mergeDone = rankedRoleTotals(data.series, (d) => d.mergedByRole ?? {});
|
|
344
|
+
lines.push(rankedRoleLine("Merged by role", mergeDone, String));
|
|
345
|
+
const mergeFailures = rankedRoleTotals(data.series, (d) => d.mergeFailedByRole ?? {}).filter(([, n]) => n > 0);
|
|
346
|
+
if (mergeFailures.length > 0)
|
|
347
|
+
lines.push(rankedRoleLine("Merge failures by role", mergeFailures, String));
|
|
348
|
+
// The --since reason twin, folded across the series the same way; a reason-free window
|
|
349
|
+
// prints no line, so its output is byte-for-byte unchanged.
|
|
350
|
+
const mergeFailureReasons = rankedRoleTotals(data.series, (d) => d.mergeFailedByReason ?? {}).filter(([, n]) => n > 0);
|
|
351
|
+
if (mergeFailureReasons.length > 0)
|
|
352
|
+
lines.push(rankedRoleLine("Merge failures by reason", mergeFailureReasons, String));
|
|
353
|
+
// Per-day median passing check time, so a suite slowing over days shows; days without a
|
|
354
|
+
// check are left out, and no line prints when no day has one (zero means absent).
|
|
355
|
+
const medians = medianLine(data.series, "Merge check median", (d) => d.checkMedianMs);
|
|
356
|
+
if (medians !== undefined)
|
|
357
|
+
lines.push(medians);
|
|
358
|
+
// Per-day count of red full-suite checks, beside the passing medians above; a day whose
|
|
359
|
+
// suite went red before passing shows here. Zero-absent: no line when no day fails.
|
|
360
|
+
const reds = data.series.flatMap((d) => d.checkFailures === undefined ? [] : [`${monthDay(d.date)} ×${d.checkFailures}`]);
|
|
361
|
+
if (reds.length > 0)
|
|
362
|
+
lines.push(`**Red merge checks:** ${reds.join(" · ")}`);
|
|
363
|
+
const waits = medianLine(data.series, "Merge wait median", (d) => d.mergeWaitMedianMs);
|
|
364
|
+
if (waits !== undefined)
|
|
365
|
+
lines.push(waits);
|
|
366
|
+
const providerWaits = medianLine(data.series, "Provider wait median", (d) => d.providerWaitMedianMs);
|
|
367
|
+
if (providerWaits !== undefined)
|
|
368
|
+
lines.push(providerWaits);
|
|
369
|
+
const slotWaits = medianLine(data.series, "Workspace wait median", (d) => d.slotWaitMedianMs);
|
|
370
|
+
if (slotWaits !== undefined)
|
|
371
|
+
lines.push(slotWaits);
|
|
372
|
+
}
|
|
373
|
+
lines.push(costPerCommitLine("Cost per commit", costPerCommitPairs(data.series)));
|
|
374
|
+
// Subscription spend is notional: its own two lines, only when the window has any.
|
|
375
|
+
if ((t.notionalCostUsd ?? 0) > 0) {
|
|
376
|
+
const notionalOf = (d) => d.notionalByRole ?? {};
|
|
377
|
+
lines.push(rankedRoleLine("Estimated by role", rankedRoleTotals(data.series, notionalOf).filter(([, c]) => c > 0), usd));
|
|
378
|
+
lines.push(costPerCommitLine("Estimated per commit", costPerCommitPairs(data.series, notionalOf)));
|
|
379
|
+
}
|
|
150
380
|
// Commits per backlog item is day-granular backlog data (features done + bugs fixed), so it
|
|
151
381
|
// belongs only to the day report; zero items means the denominator is absent, not NaN.
|
|
152
382
|
const items = t.featuresDone + t.bugsFixed;
|
|
153
383
|
const perItem = items > 0 ? ` · ${(t.commits / items).toFixed(1)} commits per backlog item` : "";
|
|
154
384
|
lines.push(`**Commits:** ${t.workCommits ?? 0} work / ${t.maintenanceCommits ?? 0} maintenance${perItem}`);
|
|
385
|
+
// Where the bugfix work was hardest to confirm: the window's fixed bugs tallied by their
|
|
386
|
+
// `Validation gap` tag, so the team can see where its own test infrastructure was weakest
|
|
387
|
+
// (recorded first, consumed now). Team-only (a tag has no role) and zero-means-absent: no
|
|
388
|
+
// line and no totals key for a role-scoped report or a window with no tagged fixed bug.
|
|
389
|
+
if (data.role === undefined && t.validationGapsByTag && Object.keys(t.validationGapsByTag).length > 0) {
|
|
390
|
+
lines.push(rankedRoleLine("Validation gaps", rankedRoleMap(t.validationGapsByTag), String));
|
|
391
|
+
}
|
|
155
392
|
// The shared SPARSE_WINDOW_NOTE (one voice across the report's two windows and the other
|
|
156
393
|
// windowed renders): covered is false only when events were aggregated from a log whose
|
|
157
394
|
// oldest retained event lies inside the window, so the hedged sentence stays true whenever
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export function cappedRequestTimeouts(cfg, tickCapS, quietCapS) {
|
|
2
2
|
return {
|
|
3
|
-
|
|
3
|
+
runTimeoutSeconds: Math.min(cfg.runTimeoutSeconds, tickCapS),
|
|
4
4
|
quietTimeoutSeconds: cfg.quietTimeoutSeconds > 0 ? Math.min(cfg.quietTimeoutSeconds, quietCapS) : quietCapS,
|
|
5
5
|
};
|
|
6
6
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** The review gate's memory of its own flake warnings — split out of review-precheck.ts so the
|
|
2
2
|
* "has this exact failure already passed on a re-run?" question lives on its own and can be
|
|
3
3
|
* unit-tested over synthetic events. The gate re-runs a failed check once (BUGS.md 2026-09-23) and
|
|
4
|
-
* logs `gate check failed then passed on retry — flaky: <headline>` when it passes
|
|
4
|
+
* logs `gate check failed then passed on retry — flaky: <headline>` when it passes (the in-lock
|
|
5
|
+
* merge check's kept-pin retry logs the same with a `merge check` prefix); a later
|
|
5
6
|
* repeat of that same headline is the flake firing again, not a verdict about whichever change
|
|
6
7
|
* happened to hit it (BUGS.md 2026-10-06). The flake is matched by the shared failure-cluster
|
|
7
8
|
* normalization so a duration, path, SHA, timestamp, or bare integer difference does not hide the
|
|
@@ -9,34 +10,53 @@
|
|
|
9
10
|
* evidence of flakiness — it is exactly the attribution this memory exists to avoid making
|
|
10
11
|
* twice. */
|
|
11
12
|
import { readEventsSince } from "../events/event-window.js";
|
|
13
|
+
import { increment } from "../collections.js";
|
|
12
14
|
import { normalizeClusterKey } from "../failure/failure-cluster.js";
|
|
13
15
|
/** How far back the gate looks for its own flake warnings. One local day: the example in
|
|
14
16
|
* BUGS.md 2026-10-06 recorded a flake at 10:50 and rejected an unrelated change on the same
|
|
15
17
|
* headline at 15:57, and a day is short enough that a retired flake stops excusing failures
|
|
16
|
-
* while still covering a
|
|
17
|
-
|
|
18
|
+
* while still covering a team's working span. Exported so the doctor's `flaky checks` check
|
|
19
|
+
* and the gate share the one window and cannot drift. */
|
|
20
|
+
export const KNOWN_FLAKE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
18
21
|
/** The exact prefix review-precheck.ts writes before a flake's failure headline. Exported so
|
|
19
22
|
* the writer and reader cannot drift over the separator. */
|
|
20
23
|
export const FLAKY_WARNING_PREFIX = "gate check failed then passed on retry — flaky: ";
|
|
21
|
-
/** The
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
/** The prefix merge-check-failures.ts writes when an in-lock merge check that failed passes on
|
|
25
|
+
* its kept-pin retry — the merge-side twin of FLAKY_WARNING_PREFIX, read by the same consumers. */
|
|
26
|
+
export const MERGE_FLAKY_WARNING_PREFIX = "merge check failed then passed on retry — flaky: ";
|
|
27
|
+
/** The normalized cluster keys of every flake warning in `events` — the shared extraction
|
|
28
|
+
* both the gate's flakeKeys and the doctor's `flaky checks` check fold from the log, so the
|
|
29
|
+
* prefix slice-offset and original cluster normalization live once and cannot drift. Non-
|
|
30
|
+
* warning events and warnings whose message is not a string are ignored; `cutoffMs`, when
|
|
31
|
+
* given, also drops events timestamped strictly before it. Pure over its inputs (no I/O, no
|
|
32
|
+
* clock) so the matching rules have their own unit tests. */
|
|
33
|
+
function flakeWarningKeys(events, cutoffMs) {
|
|
34
|
+
const keys = [];
|
|
28
35
|
for (const e of events) {
|
|
29
36
|
if (e.type !== "warning" || typeof e.message !== "string")
|
|
30
37
|
continue;
|
|
31
|
-
if (typeof e.ts === "number" && e.ts <
|
|
38
|
+
if (cutoffMs !== undefined && typeof e.ts === "number" && e.ts < cutoffMs)
|
|
32
39
|
continue;
|
|
33
|
-
const
|
|
34
|
-
|
|
40
|
+
const message = e.message;
|
|
41
|
+
const prefix = [FLAKY_WARNING_PREFIX, MERGE_FLAKY_WARNING_PREFIX].find((p) => message.includes(p));
|
|
42
|
+
if (prefix === undefined)
|
|
35
43
|
continue;
|
|
36
|
-
keys.
|
|
44
|
+
keys.push(normalizeClusterKey(message.slice(message.indexOf(prefix) + prefix.length)));
|
|
37
45
|
}
|
|
38
46
|
return keys;
|
|
39
47
|
}
|
|
48
|
+
/** Each normalized flake headline in `events` with how often it was warned, most frequent first;
|
|
49
|
+
* ties keep first-seen order. No cutoff: callers pass an already-windowed read. Pure. */
|
|
50
|
+
export function flakeCounts(events) {
|
|
51
|
+
const counts = new Map();
|
|
52
|
+
for (const key of flakeWarningKeys(events))
|
|
53
|
+
increment(counts, key);
|
|
54
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]);
|
|
55
|
+
}
|
|
56
|
+
/** A Set of the flakeWarningKeys at or after `now - windowMs` of the gate's own window. */
|
|
57
|
+
export function flakeKeys(events, now, windowMs = KNOWN_FLAKE_WINDOW_MS) {
|
|
58
|
+
return new Set(flakeWarningKeys(events, now - windowMs));
|
|
59
|
+
}
|
|
40
60
|
/** Whether `headline` matches a flake warning the gate logged within the last
|
|
41
61
|
* KNOWN_FLAKE_WINDOW_MS, read from the live log and its one archived generation. */
|
|
42
62
|
export function isKnownFlake(root, headline) {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promptBlockOf } from "../brief/brief.js";
|
|
4
|
+
import { readTextOrNull } from "../files/files.js";
|
|
5
|
+
import { changeBaseRev, fileContentAt } from "../git/git.js";
|
|
6
|
+
import { briefCandidates } from "../paths.js";
|
|
7
|
+
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
8
|
+
/** The brief files that may carry the initial prompt block: the basenames of `briefCandidates`. */
|
|
9
|
+
const BRIEF_FILES = briefCandidates("").map((p) => path.basename(p));
|
|
10
|
+
/** The roles that may edit PRINCIPLES.md. */
|
|
11
|
+
const PRINCIPLES_ROLES = new Set(["director", "steward"]);
|
|
12
|
+
/** The deterministic gate/stage finding for a change that edits what the loop prompts forbid
|
|
13
|
+
* editing: the initial prompt block of a brief file (changed, removed, or the file deleted —
|
|
14
|
+
* a block that only moves passes), or PRINCIPLES.md from a role other than director or
|
|
15
|
+
* steward. undefined when neither happened. Reads the worktree and the change's merge-base
|
|
16
|
+
* like backlogStructureReason; a missing file reads as empty and never throws. */
|
|
17
|
+
export async function protectedEditReason(wt, mainBranch, files, role) {
|
|
18
|
+
const briefs = BRIEF_FILES.filter((f) => files.includes(f));
|
|
19
|
+
if (briefs.length > 0) {
|
|
20
|
+
const baseRev = await changeBaseRev(wt, mainBranch);
|
|
21
|
+
for (const file of briefs) {
|
|
22
|
+
const base = promptBlockOf(await fileContentAt(wt, baseRev, file));
|
|
23
|
+
if (base === null)
|
|
24
|
+
continue;
|
|
25
|
+
const head = promptBlockOf(readTextOrNull(path.join(wt, file)) ?? "");
|
|
26
|
+
if (head !== base)
|
|
27
|
+
return `${file} changes the brief between the tumwater:prompt markers — only you edit it (tumwater brief); restore the block`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (files.includes("PRINCIPLES.md") && !PRINCIPLES_ROLES.has(baseRoleOf(role)))
|
|
31
|
+
return `PRINCIPLES.md is edited by the ${role} role — only the director and steward roles may edit it; record an objection in PLANS.md instead and revert the edit`;
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { agentSessionResumable } from "../agent/agent-backend.js";
|
|
2
|
-
import { reviewRunConfig } from "../config/config-views.js";
|
|
3
|
-
import { warnEventBestEffort } from "../events/events.js";
|
|
2
|
+
import { reviewRunConfig, resolvedAgentConfig } from "../config/config-views.js";
|
|
3
|
+
import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
4
4
|
import { nativeLogPath, piLogPath } from "../paths.js";
|
|
5
5
|
import { buildNoRerunPrompt, buildVerdictRequestPrompt } from "../prompt/gate-prompts.js";
|
|
6
6
|
import { cappedRequestTimeouts } from "../request-timeouts.js";
|
|
@@ -17,8 +17,9 @@ async function runFollowupTurn(ctx, opts) {
|
|
|
17
17
|
const cfg = reviewRunConfig(ctx.config);
|
|
18
18
|
// The verdict/re-run follow-up continues the review session the gate just wrote. The
|
|
19
19
|
// backend's own resumable rule keeps a newer line of another kind from being the session a
|
|
20
|
-
// pi continue would actually reach; claude resumes its recorded id
|
|
21
|
-
|
|
20
|
+
// pi continue would actually reach; claude resumes its recorded id (looked up under the
|
|
21
|
+
// team's config dir, so a team that set one continues under it).
|
|
22
|
+
if (!agentSessionResumable(dir, { kind: "gate", agent: cfg.agent ?? "pi" }, resolvedAgentConfig(ctx.config, "claude").configDir)) {
|
|
22
23
|
return null;
|
|
23
24
|
}
|
|
24
25
|
return ctx.runGatePi({
|
|
@@ -39,6 +40,17 @@ async function runFollowupTurn(ctx, opts) {
|
|
|
39
40
|
label: opts.label,
|
|
40
41
|
signal: ctx.signal,
|
|
41
42
|
onToolCallStalled: (message) => warnEventBestEffort(ctx.root, ctx.role, message),
|
|
43
|
+
// A gate run that read an image its model cannot see is logged, not killed (plans/
|
|
44
|
+
// image-input.md "Detecting a blind read") — same shape as the main review run in
|
|
45
|
+
// review.ts.
|
|
46
|
+
onBlindRead: (path, action) => logEventBestEffort(ctx.root, {
|
|
47
|
+
source: ctx.role,
|
|
48
|
+
type: "image_blind",
|
|
49
|
+
run: ctx.tick,
|
|
50
|
+
path,
|
|
51
|
+
model: cfg.model,
|
|
52
|
+
action,
|
|
53
|
+
}),
|
|
42
54
|
...(opts.onToolCallStart ? { onToolCallStart: opts.onToolCallStart } : {}),
|
|
43
55
|
});
|
|
44
56
|
}
|