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
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
// vocabulary: plain
|
|
2
|
+
/** Event-log folding for the usage report: turn raw harness events (run_end/merged/…) into
|
|
3
|
+
* per-day and whole-window `UsageFold` accumulators, memoizing the day-keyed read per
|
|
4
|
+
* (root, fromKey) so a dashboard polling /api/report only folds appended lines instead of
|
|
5
|
+
* re-reading the window. The two window collectors (collectReport / collectReportSince) in
|
|
6
|
+
* src/report/report-data.ts fold through this one shared layer, so the aggregation rules the
|
|
7
|
+
* --since and day windows present cannot drift apart; it lives here matching its own test
|
|
8
|
+
* (test/report-fold-cache.test.ts). Zero incidental runtime dependencies: node built-ins and
|
|
9
|
+
* the app's own modules only. */
|
|
10
|
+
import { sameStatIdentity, statOrNull } from "../files/files.js";
|
|
11
|
+
import { readWindowEvents } from "../events/event-window.js";
|
|
12
|
+
import { eventDayKey, eventRole, eventUsage, parseEventLine } from "../events/event-read.js";
|
|
13
|
+
import { readCompleteLines } from "../files/tail.js";
|
|
14
|
+
import { eventsLogPath } from "../paths.js";
|
|
15
|
+
import { isPassingFullCheck, isRedFullCheck } from "../build/build-check-events.js";
|
|
16
|
+
import { addTo, memoSetLru } from "../collections.js";
|
|
17
|
+
import { finiteNumber } from "../files/json-object.js";
|
|
18
|
+
import { OUTCOME_CLASS } from "../failure/time-spend.js";
|
|
19
|
+
import { baseRoleOf } from "../roles/loop-ids.js";
|
|
20
|
+
import { commitTier } from "../roles/roles.js";
|
|
21
|
+
/** Fold one event's `providerCost` map (`Record<provider, {costUsd, notionalCostUsd}>` — the
|
|
22
|
+
* shape tick-finalize.ts's run_end and merge-slot.ts's merge_done/merge_failed emit) into the
|
|
23
|
+
* per-provider tallies, one addTo per provider per nonzero figure so zero stays absent. A
|
|
24
|
+
* missing, foreign, or empty map folds nothing, so old logs render byte-identically. */
|
|
25
|
+
function foldProviderCost(target, providerCost) {
|
|
26
|
+
if (typeof providerCost !== "object" || providerCost === null)
|
|
27
|
+
return;
|
|
28
|
+
for (const [provider, c] of Object.entries(providerCost)) {
|
|
29
|
+
if (typeof c !== "object" || c === null)
|
|
30
|
+
continue;
|
|
31
|
+
const entry = c;
|
|
32
|
+
const costUsd = finiteNumber(entry.costUsd, 0);
|
|
33
|
+
const notionalCostUsd = finiteNumber(entry.notionalCostUsd, 0);
|
|
34
|
+
if (costUsd !== 0)
|
|
35
|
+
addTo(target.costByProvider, provider, costUsd);
|
|
36
|
+
if (notionalCostUsd !== 0)
|
|
37
|
+
addTo(target.notionalByProvider, provider, notionalCostUsd);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Push a provider- or workspace-slot wait's ms into the accumulator list a median reads,
|
|
41
|
+
* when the event carried a positive wait: a wait is only event-logged at or above the 30 s
|
|
42
|
+
* threshold, so a 0 ms wait (a probe seam) or a pre-waitedMs event adds nothing and the two
|
|
43
|
+
* wait medians reflect real blocked admissions only. */
|
|
44
|
+
function pushPositiveWaitedMs(list, ev) {
|
|
45
|
+
const waited = finiteNumber(ev.waitedMs, 0);
|
|
46
|
+
if (waited > 0)
|
|
47
|
+
list.push(waited);
|
|
48
|
+
}
|
|
49
|
+
/** Fold one event into a usage accumulator: run_end events add a tick (per role and, when
|
|
50
|
+
* the target counts ticks, in total), their output tokens, and their cost — zero-cost ticks
|
|
51
|
+
* contribute nothing, so they leave no costByRole key, keeping the render's "$0 roles are
|
|
52
|
+
* omitted" rule true on the aggregation itself, not just at display time; merged events add
|
|
53
|
+
* a commit; merge_done/merge_failed events add a merge run and
|
|
54
|
+
* accumulate their tokens/cost into the merge fields only — costByRole stays a ticks-only
|
|
55
|
+
* breakdown, and the merge line is how the reviewer's share is named. Both collectors fold
|
|
56
|
+
* through this one place, so the two windows' aggregation rules cannot drift apart. */
|
|
57
|
+
export function foldUsageEvent(target, ev) {
|
|
58
|
+
if (ev.type === "run_end") {
|
|
59
|
+
const role = eventRole(ev);
|
|
60
|
+
addTo(target.runsByRole, role, 1);
|
|
61
|
+
// Error-class ticks get their own parallel tally — the day report's ``Failed ticks by role``
|
|
62
|
+
// line — reusing the digest's OUTCOME_CLASS so "failed" cannot drift from the failure
|
|
63
|
+
// report's meaning. Never subtracted from runsByRole; the two are independent counts.
|
|
64
|
+
if (OUTCOME_CLASS[ev.result] === "error")
|
|
65
|
+
addTo(target.failedRunsByRole, role, 1);
|
|
66
|
+
// A refusal is its own deliberate outcome (src/verdict/refusal.ts), not error-class, so
|
|
67
|
+
// it needs an explicit result check: the one per-role rejection-without-review count a
|
|
68
|
+
// day report should surface beside rejections, revisions and falls. Never subtracted from
|
|
69
|
+
// runsByRole; a refused tick still counts as a run.
|
|
70
|
+
if (ev.result === "refused")
|
|
71
|
+
addTo(target.refusalsByRole, role, 1);
|
|
72
|
+
if (target.runs !== undefined)
|
|
73
|
+
target.runs++;
|
|
74
|
+
const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
|
|
75
|
+
addTo(target.tokensByRole, role, tokens);
|
|
76
|
+
target.tokensOut += tokens;
|
|
77
|
+
target.costUsd += costUsd;
|
|
78
|
+
target.notionalCostUsd += notionalCostUsd;
|
|
79
|
+
if (costUsd !== 0)
|
|
80
|
+
addTo(target.costByRole, role, costUsd);
|
|
81
|
+
if (notionalCostUsd !== 0)
|
|
82
|
+
addTo(target.notionalByRole, role, notionalCostUsd);
|
|
83
|
+
// The tick's own runs' per-provider spend (tick-finalize.ts emits providerCost on run_end)
|
|
84
|
+
// — folded per provider beside the per-role maps, so a mixed team's `Cost by provider`
|
|
85
|
+
// line names the expensive seam.
|
|
86
|
+
foldProviderCost(target, ev.providerCost);
|
|
87
|
+
}
|
|
88
|
+
else if (ev.type === "review_rejected") {
|
|
89
|
+
// One rejection per event, on the day it happened, per loop. Zero-means-absent: a role
|
|
90
|
+
// that never rejected stays absent, like costByRole.
|
|
91
|
+
addTo(target.rejectionsByRole, eventRole(ev), 1);
|
|
92
|
+
}
|
|
93
|
+
else if (ev.type === "revision") {
|
|
94
|
+
// One rework round per event, on the day it happened, per loop. Zero-means-absent: a role
|
|
95
|
+
// that never reworked a rejected change stays absent, like rejectionsByRole.
|
|
96
|
+
addTo(target.revisionsByRole, eventRole(ev), 1);
|
|
97
|
+
}
|
|
98
|
+
else if (isPassingFullCheck(ev)) {
|
|
99
|
+
// The merge slot's passing full-suite checks, for the day report's check-time median.
|
|
100
|
+
const durationMs = finiteNumber(ev.durationMs, 0);
|
|
101
|
+
if (durationMs > 0)
|
|
102
|
+
target.checkDurationsMs.push(durationMs);
|
|
103
|
+
}
|
|
104
|
+
else if (isRedFullCheck(ev)) {
|
|
105
|
+
// The merge slot's red full-suite checks, for the day report's red-check count.
|
|
106
|
+
target.checkFailures++;
|
|
107
|
+
}
|
|
108
|
+
else if (ev.type === "model_fallback_started") {
|
|
109
|
+
// One episode per start event; durations are not summed (an episode can span the window edge).
|
|
110
|
+
addTo(target.fallbacksByRole, eventRole(ev), 1);
|
|
111
|
+
}
|
|
112
|
+
else if (ev.type === "image_reroute") {
|
|
113
|
+
// One reroute per event, preflight or after a blind read.
|
|
114
|
+
addTo(target.imageReroutesByRole, eventRole(ev), 1);
|
|
115
|
+
}
|
|
116
|
+
else if (ev.type === "run_deferred") {
|
|
117
|
+
// One deferral per event, a due maintenance tick that stood down. Bucket by the raw
|
|
118
|
+
// reason code ("backlog", "plans-full", else "no-work" for a missing/foreign/torn
|
|
119
|
+
// reason — the same fallback deferredReasonText applies in the feed), so the collector
|
|
120
|
+
// stays free of a presentation import and the render prints the code the event carried.
|
|
121
|
+
// Team-only: a reason is not a role.
|
|
122
|
+
const reason = ev.reason === "backlog" || ev.reason === "plans-full" ? ev.reason : "no-work";
|
|
123
|
+
addTo(target.deferredByReason, reason, 1);
|
|
124
|
+
}
|
|
125
|
+
else if (ev.type === "merged") {
|
|
126
|
+
target.commits++;
|
|
127
|
+
// Work ratio 4/4: split merged commits by base role and by tier. Instance suffixes fold
|
|
128
|
+
// into their role (`feature-2` counts as feature) so the split does not fragment.
|
|
129
|
+
const role = baseRoleOf(eventRole(ev));
|
|
130
|
+
addTo(target.commitsByRole, role, 1);
|
|
131
|
+
const tier = commitTier(role);
|
|
132
|
+
if (tier === "work")
|
|
133
|
+
target.workCommits++;
|
|
134
|
+
else if (tier === "maintenance")
|
|
135
|
+
target.maintenanceCommits++;
|
|
136
|
+
}
|
|
137
|
+
else if (ev.type === "run_start") {
|
|
138
|
+
// One tally per run start on a configured model: the same `model` field the CLI's
|
|
139
|
+
// `run #N started on <model>` line prints, and the only event that carries a model
|
|
140
|
+
// (run_end has none), so a run that started with no configured model contributes no key.
|
|
141
|
+
const model = typeof ev.model === "string" && ev.model.length > 0 ? ev.model : null;
|
|
142
|
+
if (model !== null)
|
|
143
|
+
addTo(target.runsByModel, model, 1);
|
|
144
|
+
}
|
|
145
|
+
else if (ev.type === "provider_wait") {
|
|
146
|
+
// The day report's provider-slot wait median, mirroring the slot_wait push: a wait only
|
|
147
|
+
// event-logged at or above the 30 s threshold counts, so a 0 ms wait (a probe seam) adds
|
|
148
|
+
// nothing and the median reflects real blocked admissions only.
|
|
149
|
+
pushPositiveWaitedMs(target.providerWaitsMs, ev);
|
|
150
|
+
}
|
|
151
|
+
else if (ev.type === "slot_wait") {
|
|
152
|
+
// The day report's workspace-slot wait median, mirroring the provider_wait push: a wait
|
|
153
|
+
// only event-logged at or above the 30 s threshold counts, so a 0 ms wait (a probe seam)
|
|
154
|
+
// or a pre-waitedMs event adds nothing.
|
|
155
|
+
pushPositiveWaitedMs(target.slotWaitsMs, ev);
|
|
156
|
+
}
|
|
157
|
+
else if (ev.type === "merge_done" || ev.type === "merge_failed") {
|
|
158
|
+
target.mergeRuns = (target.mergeRuns ?? 0) + 1;
|
|
159
|
+
if (ev.type === "merge_failed")
|
|
160
|
+
target.mergeFailed = (target.mergeFailed ?? 0) + 1;
|
|
161
|
+
// Attribute the outcome to the authoring role (writeMergeOutcome logs loop: entry.role)
|
|
162
|
+
// folded to base role, the same normalization the merged branch uses, so `feature-2`
|
|
163
|
+
// aggregates into `feature`. A parallel tally beside the team run counts, never
|
|
164
|
+
// subtracted from mergeRuns/mergeFailed.
|
|
165
|
+
const role = baseRoleOf(eventRole(ev));
|
|
166
|
+
if (ev.type === "merge_done") {
|
|
167
|
+
addTo(target.mergedByRole, role, 1);
|
|
168
|
+
// Events logged before queuedMs existed carry none: skipped, not counted as zero.
|
|
169
|
+
const waited = finiteNumber(ev.queuedMs, 0);
|
|
170
|
+
if (waited > 0)
|
|
171
|
+
target.mergeWaitsMs.push(waited);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
addTo(target.mergeFailedByRole, role, 1);
|
|
175
|
+
// The event's result (writeMergeOutcome's TickResult) names WHY the merge bounced
|
|
176
|
+
// off the gate — rejected/merge_conflict/merge_blocked/aborted. A legacy event with
|
|
177
|
+
// no result folds under `unknown`, so the reason buckets always sum to mergeFailed.
|
|
178
|
+
const reason = typeof ev.result === "string" && ev.result.length > 0 ? ev.result : "unknown";
|
|
179
|
+
addTo(target.mergeFailedByReason, reason, 1);
|
|
180
|
+
}
|
|
181
|
+
const { tokens, costUsd, notionalCostUsd } = eventUsage(ev);
|
|
182
|
+
target.mergeTokens = (target.mergeTokens ?? 0) + tokens;
|
|
183
|
+
target.mergeCostUsd = (target.mergeCostUsd ?? 0) + costUsd;
|
|
184
|
+
// A subscription merge's API-equivalent spend joins the notional line, never the
|
|
185
|
+
// budgeted cost totals (plans/agent-backends.md part 6a).
|
|
186
|
+
target.notionalCostUsd += notionalCostUsd;
|
|
187
|
+
// The merge's per-provider spend (merge-slot.ts keys it to its reviewer seam), so a
|
|
188
|
+
// merge's costs appear under the provider that ran the reviewer, matching the author
|
|
189
|
+
// side's foldMergeUsage charge.
|
|
190
|
+
foldProviderCost(target, ev.providerCost);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
/** Bounded memo of window folds. Eight entries cover a dashboard flipping among a few day
|
|
194
|
+
* counts plus the midnight rollover's new fromKey; oldest-insertion eviction (Map preserves
|
|
195
|
+
* insertion order) keeps the memory bound to a handful of per-day accumulator maps. */
|
|
196
|
+
const reportFoldCache = new Map();
|
|
197
|
+
const REPORT_FOLD_CACHE_MAX = 8;
|
|
198
|
+
/** The empty commit-family counters every accumulator starts from: the total and per-base-role
|
|
199
|
+
* merged-commit count plus the work/maintenance split (Work ratio 4/4). Four shapes carry them —
|
|
200
|
+
* DayFold, UsageFold, the ReportDay series slots, and the window totals — so the empty quartet
|
|
201
|
+
* lives once here and a new commit counter is added in one place. */
|
|
202
|
+
export function zeroCommitCounts() {
|
|
203
|
+
return { commits: 0, commitsByRole: {}, workCommits: 0, maintenanceCommits: 0 };
|
|
204
|
+
}
|
|
205
|
+
/** The empty merge-run counters every accumulator starts from: the merge slot's run count
|
|
206
|
+
* and its own token/cost share (foldUsageEvent's merge_done/merge_failed branch). Four shapes carry
|
|
207
|
+
* them — DayFold, UsageFold, the scoped window totals, and the window totals — so the empty
|
|
208
|
+
* triple lives once here and a new merge counter is added in one place. */
|
|
209
|
+
export function zeroMergeCounts() {
|
|
210
|
+
return { mergeRuns: 0, mergeTokens: 0, mergeCostUsd: 0 };
|
|
211
|
+
}
|
|
212
|
+
/** Sum a per-loop-id count/cost map over one base role: instance keys (`feature-2`) fold into
|
|
213
|
+
* their base (`feature`), so a scoped report aggregates a whole role group exactly as the
|
|
214
|
+
* per-role cap's group does. Ticks/tokens/cost are keyed by exact loop id while commits are
|
|
215
|
+
* already base-role keyed; running every key through `baseRoleOf` reads both shapes the same
|
|
216
|
+
* and returns 0 when nothing matches. */
|
|
217
|
+
export function roleSum(record, role) {
|
|
218
|
+
let sum = 0;
|
|
219
|
+
for (const [key, n] of Object.entries(record)) {
|
|
220
|
+
if (baseRoleOf(key) === role)
|
|
221
|
+
sum += n;
|
|
222
|
+
}
|
|
223
|
+
return sum;
|
|
224
|
+
}
|
|
225
|
+
/** Narrow a per-loop-id count/cost map to one base role, keyed by the base role; a role absent
|
|
226
|
+
* from the map (no ticks, or no nonzero cost) yields an empty map, never a zero entry — the
|
|
227
|
+
* same zero-means-absent rule the team render applies. Used by the role-scoped collectors so a
|
|
228
|
+
* scoped series derives from the fold's per-role maps without inventing entries. */
|
|
229
|
+
export function narrowToRole(record, role) {
|
|
230
|
+
const n = roleSum(record, role);
|
|
231
|
+
return n === 0 ? {} : { [role]: n };
|
|
232
|
+
}
|
|
233
|
+
/** A per-loop map scoped to one base role for a report: the full map as-is when no scope is
|
|
234
|
+
* set (a team report), else the same map narrowed to `base` — narrowToRole's zero-means-absent
|
|
235
|
+
* rule, so a scoped report never invents a zero entry. The one home of the "keep everything,
|
|
236
|
+
* or keep only the named base role" rule: the 13 per-role/pair fields of the `--since` report's
|
|
237
|
+
* output all shape through it, so the scoping rule cannot drift between them. */
|
|
238
|
+
export function scopedByRole(record, base) {
|
|
239
|
+
return base === undefined ? record : narrowToRole(record, base);
|
|
240
|
+
}
|
|
241
|
+
function dayFoldFor(entry, dayKey) {
|
|
242
|
+
let fold = entry.days.get(dayKey);
|
|
243
|
+
if (!fold) {
|
|
244
|
+
fold = {
|
|
245
|
+
tokensOut: 0,
|
|
246
|
+
...zeroCommitCounts(),
|
|
247
|
+
costUsd: 0,
|
|
248
|
+
notionalCostUsd: 0,
|
|
249
|
+
runsByRole: {},
|
|
250
|
+
rejectionsByRole: {},
|
|
251
|
+
revisionsByRole: {},
|
|
252
|
+
refusalsByRole: {},
|
|
253
|
+
checkDurationsMs: [],
|
|
254
|
+
checkFailures: 0,
|
|
255
|
+
mergeWaitsMs: [],
|
|
256
|
+
providerWaitsMs: [],
|
|
257
|
+
slotWaitsMs: [],
|
|
258
|
+
failedRunsByRole: {},
|
|
259
|
+
fallbacksByRole: {},
|
|
260
|
+
imageReroutesByRole: {},
|
|
261
|
+
tokensByRole: {},
|
|
262
|
+
costByRole: {},
|
|
263
|
+
notionalByRole: {},
|
|
264
|
+
...zeroMergeCounts(),
|
|
265
|
+
mergedByRole: {},
|
|
266
|
+
mergeFailedByRole: {},
|
|
267
|
+
mergeFailedByReason: {},
|
|
268
|
+
costByProvider: {},
|
|
269
|
+
notionalByProvider: {},
|
|
270
|
+
runsByModel: {},
|
|
271
|
+
deferredByReason: {},
|
|
272
|
+
};
|
|
273
|
+
entry.days.set(dayKey, fold);
|
|
274
|
+
}
|
|
275
|
+
return fold;
|
|
276
|
+
}
|
|
277
|
+
/** Fold one already-parsed event into a cache entry: only in-window events (a non-null day
|
|
278
|
+
* on or after fromKey) count, matching readWindowEvents's event list; the first such event
|
|
279
|
+
* seen takes firstEventTs. The one home of the fold body, shared by the incremental append
|
|
280
|
+
* path (foldLineInto) and the full re-read path, so the two can never drift. */
|
|
281
|
+
function foldEventInto(entry, ev) {
|
|
282
|
+
const dayKey = eventDayKey(ev);
|
|
283
|
+
if (dayKey === null || dayKey < entry.fromKey)
|
|
284
|
+
return;
|
|
285
|
+
if (!entry.hadEvents) {
|
|
286
|
+
entry.hadEvents = true;
|
|
287
|
+
entry.firstEventTs = typeof ev.ts === "number" ? ev.ts : undefined;
|
|
288
|
+
}
|
|
289
|
+
foldUsageEvent(dayFoldFor(entry, dayKey), ev);
|
|
290
|
+
}
|
|
291
|
+
/** Parse one appended log line and fold it; a torn or non-event line folds nothing. */
|
|
292
|
+
function foldLineInto(entry, line) {
|
|
293
|
+
const ev = parseEventLine(line);
|
|
294
|
+
if (ev)
|
|
295
|
+
foldEventInto(entry, ev);
|
|
296
|
+
}
|
|
297
|
+
/** The event-log half of collectReport, memoized: a dashboard re-fetching /api/report every
|
|
298
|
+
* poll re-read and re-parsed the whole window's log bytes each time (~6.8 MB and tens of ms
|
|
299
|
+
* on this repo's own day log, growing toward the 16 MB rotation cap). Now a request whose
|
|
300
|
+
* log has not changed costs one stat; growth folds only the appended complete lines
|
|
301
|
+
* (readCompleteLines holds back a torn trailing write, so it is folded whole next call).
|
|
302
|
+
* Same size with a changed mtime, a shrunken file (rotation or rewrite), or a new fromKey
|
|
303
|
+
* falls back to the full readWindowEvents pass — the fallback re-derives everything, so a
|
|
304
|
+
* mis-fold can only come from the append assumption, and rotation's whole-file replace
|
|
305
|
+
* always hits the shrink arm. The entry's coverage parts are pinned by the cache key:
|
|
306
|
+
* fromKey and file bytes decide them, so an advancing window re-derives fresh instead of
|
|
307
|
+
* carrying yesterday's proof forward. */
|
|
308
|
+
export function foldWindowEvents(root, fromKey) {
|
|
309
|
+
const key = `${root}\0${fromKey}`;
|
|
310
|
+
const liveStat = statOrNull(eventsLogPath(root));
|
|
311
|
+
const size = liveStat?.size ?? 0;
|
|
312
|
+
const mtimeMs = liveStat?.mtimeMs ?? 0;
|
|
313
|
+
const dev = liveStat?.dev ?? 0;
|
|
314
|
+
const ino = liveStat?.ino ?? 0;
|
|
315
|
+
const cached = reportFoldCache.get(key);
|
|
316
|
+
if (cached &&
|
|
317
|
+
sameStatIdentity({ dev, ino, mtimeMs, size }, { dev: cached.dev, ino: cached.ino, mtimeMs: cached.mtimeMs, size: cached.offset })) {
|
|
318
|
+
memoSetLru(reportFoldCache, key, cached, REPORT_FOLD_CACHE_MAX);
|
|
319
|
+
return cached;
|
|
320
|
+
}
|
|
321
|
+
if (cached && size > cached.offset && dev === cached.dev && ino === cached.ino) {
|
|
322
|
+
const { lines, end } = readCompleteLines(eventsLogPath(root), cached.offset, size);
|
|
323
|
+
if (end > cached.offset) {
|
|
324
|
+
for (const line of lines)
|
|
325
|
+
foldLineInto(cached, line);
|
|
326
|
+
cached.offset = end;
|
|
327
|
+
cached.mtimeMs = mtimeMs;
|
|
328
|
+
return cached;
|
|
329
|
+
}
|
|
330
|
+
// Grown but no complete line yet (a torn write in flight): reuse the folds; the next
|
|
331
|
+
// request sees the completed line and folds it then.
|
|
332
|
+
return cached;
|
|
333
|
+
}
|
|
334
|
+
const raw = readWindowEvents(root, fromKey);
|
|
335
|
+
const entry = {
|
|
336
|
+
root,
|
|
337
|
+
fromKey,
|
|
338
|
+
// The scan's own end, not the outer `size`: an event appended between that stat and the
|
|
339
|
+
// read is already folded, and seeding the offset from the stale size would fold it again
|
|
340
|
+
// on the next append (readTailTextWithEnd's coveredEnd is exactly where the scan stopped).
|
|
341
|
+
offset: raw.liveEnd,
|
|
342
|
+
mtimeMs,
|
|
343
|
+
dev,
|
|
344
|
+
ino,
|
|
345
|
+
fileCovers: raw.coversFullWindow,
|
|
346
|
+
hadEvents: false,
|
|
347
|
+
days: new Map(),
|
|
348
|
+
};
|
|
349
|
+
for (const ev of raw.events)
|
|
350
|
+
foldEventInto(entry, ev);
|
|
351
|
+
memoSetLru(reportFoldCache, key, entry, REPORT_FOLD_CACHE_MAX);
|
|
352
|
+
return entry;
|
|
353
|
+
}
|