tumwater 0.4.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +5 -2
- package/dist/src/agent/backend-traits.js +21 -0
- package/dist/src/agent/claude/claude-backend.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +181 -182
- package/dist/src/agent/claude/{write-guard-hook.js → claude-write-guard-hook.js} +1 -1
- package/dist/src/agent/claude/claude-write-guard.js +5 -4
- package/dist/src/agent/codex/codex-backend.js +1 -1
- package/dist/src/agent/codex/codex-rollout.js +48 -45
- package/dist/src/agent/codex/codex-stream.js +269 -215
- package/dist/src/agent/codex/{write-guard-hook.js → codex-write-guard-hook.js} +2 -1
- package/dist/src/agent/codex/codex-write-guard.js +3 -3
- package/dist/src/agent/image-input.js +18 -87
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +2 -2
- package/dist/src/agent/opencode/opencode-catalog.js +2 -2
- package/dist/src/agent/opencode/opencode-config.js +2 -2
- package/dist/src/agent/opencode/opencode-stream.js +101 -92
- package/dist/src/agent/run-agent.js +76 -382
- package/dist/src/agent/run-settle.js +25 -15
- package/dist/src/agent/run-spawn.js +226 -0
- package/dist/src/agent/run-stream.js +185 -0
- package/dist/src/agent/session-ledger.js +16 -0
- package/dist/src/agent/write-guard.js +5 -3
- package/dist/src/backlog/backlog-archive.js +86 -170
- package/dist/src/backlog/backlog-cli-view.js +89 -70
- package/dist/src/backlog/backlog-eligibility.js +51 -59
- package/dist/src/backlog/backlog-file-bugs.js +133 -0
- package/dist/src/backlog/backlog-index-data.js +159 -0
- package/dist/src/backlog/backlog-index.js +7 -5
- package/dist/src/backlog/backlog-md.js +39 -28
- package/dist/src/backlog/backlog-release.js +21 -36
- package/dist/src/backlog/backlog-render.js +1 -1
- package/dist/src/backlog/backlog-structure.js +14 -145
- package/dist/src/backlog/backlog-write.js +31 -18
- package/dist/src/backlog/backlog.js +14 -4
- package/dist/src/backlog/question-answer.js +33 -15
- package/dist/src/baseline/main-baseline.js +71 -26
- package/dist/src/baseline/main-guardrails.js +76 -15
- package/dist/src/baseline/main-red.js +45 -94
- package/dist/src/baseline/main-watch.js +9 -3
- package/dist/src/brief/brief.js +40 -0
- package/dist/src/budget/budget-fallback-view.js +50 -8
- package/dist/src/budget/budget-wiring.js +15 -11
- package/dist/src/budget/budget.js +3 -2
- package/dist/src/build/build-check-events.js +4 -2
- package/dist/src/build/build-check-report.js +2 -1
- package/dist/src/build/build-check-scoped.js +70 -41
- package/dist/src/change/change-data.js +4 -2
- package/dist/src/cli/cli-args.js +9 -0
- package/dist/src/cli/cli-command-args.js +199 -98
- package/dist/src/cli/cli-flag-specs.js +6 -4
- package/dist/src/cli/cli-marker-commands.js +2 -2
- package/dist/src/cli/cli-query-commands.js +41 -6
- package/dist/src/cli/cli-review-commands.js +2 -1
- package/dist/src/cli/cli-role-command.js +46 -92
- package/dist/src/cli/cli-start.js +123 -47
- package/dist/src/cli/follow-view.js +3 -1
- package/dist/src/cli/help.js +19 -12
- package/dist/src/cli/log-commands.js +128 -107
- package/dist/src/cli/question-commands.js +82 -63
- package/dist/src/cli.js +396 -343
- package/dist/src/collections.js +46 -9
- package/dist/src/config/config-fallbacks.js +0 -4
- package/dist/src/config/config-live.js +179 -147
- package/dist/src/config/config-request.js +176 -51
- package/dist/src/config/config-validation-roles.js +271 -0
- package/dist/src/config/config-validation.js +10 -205
- package/dist/src/config/config-views.js +2 -7
- package/dist/src/config/config-write.js +205 -175
- package/dist/src/config/config.js +2 -1
- package/dist/src/doctor/doctor-backlog-bugs.js +24 -122
- package/dist/src/doctor/doctor-backlog-plans.js +24 -25
- package/dist/src/doctor/doctor-backlog.js +34 -42
- package/dist/src/doctor/doctor-check-names.js +4 -2
- package/dist/src/doctor/doctor-checks.js +16 -4
- package/dist/src/doctor/doctor-claude-checks.js +2 -2
- package/dist/src/doctor/doctor-codex-checks.js +9 -20
- package/dist/src/doctor/doctor-contention-checks.js +52 -19
- package/dist/src/doctor/doctor-holds.js +22 -20
- package/dist/src/doctor/doctor-model-checks.js +8 -184
- package/dist/src/doctor/doctor-notify.js +2 -5
- package/dist/src/doctor/doctor-opencode-checks.js +90 -41
- package/dist/src/doctor/doctor-principles.js +26 -0
- package/dist/src/doctor/doctor-project-checks.js +69 -19
- package/dist/src/doctor/doctor-role-notes.js +33 -0
- package/dist/src/doctor/doctor.js +13 -6
- package/dist/src/doctor/probe.js +1 -1
- package/dist/src/events/event-format.js +307 -162
- package/dist/src/events/event-labels.js +0 -1
- package/dist/src/events/event-legacy.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +15 -2
- package/dist/src/events/notify.js +1 -1
- package/dist/src/failure/failure-cluster.js +4 -3
- package/dist/src/failure/failure-data.js +126 -94
- package/dist/src/failure/failure-render.js +58 -17
- package/dist/src/failure/failure-state-change.js +308 -27
- package/dist/src/failure/time-spend.js +139 -106
- package/dist/src/files/files.js +7 -0
- package/dist/src/files/json-object.js +15 -2
- package/dist/src/gates/budget-gates.js +90 -68
- package/dist/src/gates/gate-polls.js +92 -61
- package/dist/src/gates/role-cap-gates.js +1 -1
- package/dist/src/gates/startup-gate.js +28 -28
- package/dist/src/gates/streak-gate.js +1 -1
- package/dist/src/git/git.js +1 -1
- package/dist/src/git/slot-salvage.js +9 -9
- package/dist/src/git/slots-state.js +12 -3
- package/dist/src/git/worktree-pool-startup.js +127 -0
- package/dist/src/git/worktree-pool.js +1 -120
- package/dist/src/gui/gui-admin-endpoints.js +3 -4
- package/dist/src/gui/gui-args.js +1 -1
- package/dist/src/gui/gui-backlog-commands.js +20 -11
- package/dist/src/gui/gui-backlog-endpoints.js +2 -8
- package/dist/src/gui/gui-config-commands.js +51 -76
- package/dist/src/gui/gui-endpoint-commands.js +4 -2
- package/dist/src/gui/gui-endpoint-common.js +4 -3
- package/dist/src/gui/gui-history-endpoints.js +36 -27
- package/dist/src/gui/gui-operator-commands.js +43 -17
- package/dist/src/gui/gui-prompt-commands.js +27 -21
- package/dist/src/gui/gui-quarantine-commands.js +30 -0
- package/dist/src/gui/gui-report-endpoints.js +13 -9
- package/dist/src/gui/gui-server.js +5 -3
- package/dist/src/gui/gui-team-commands.js +1 -1
- package/dist/src/history/history-data.js +7 -1
- package/dist/src/history/history-render.js +15 -10
- package/dist/src/history/history.js +103 -68
- package/dist/src/inbox/inbox-submit.js +1 -5
- package/dist/src/inbox/pending-prompt.js +3 -2
- package/dist/src/inbox/prompt-commands.js +194 -161
- package/dist/src/init/init.js +104 -25
- package/dist/src/merge/merge-core.js +1 -1
- package/dist/src/merge/merge-drain.js +1 -1
- package/dist/src/merge/merge-git.js +5 -5
- package/dist/src/merge/{merge-post-review.js → merge-post-review-drain.js} +1 -1
- package/dist/src/merge/merge-queue.js +2 -1
- package/dist/src/merge/merge-to-main.js +1 -1
- package/dist/src/merge/merge-verify.js +3 -2
- package/dist/src/operator/operator-commands.js +84 -52
- package/dist/src/operator/operator-intent.js +26 -3
- package/dist/src/operator/retire.js +2 -1
- package/dist/src/operator/revert.js +18 -12
- package/dist/src/orchestrator/orchestrator-poll.js +2 -2
- package/dist/src/orchestrator/orchestrator.js +1 -1
- package/dist/src/orchestrator/retention.js +6 -5
- package/dist/src/paths.js +10 -10
- package/dist/src/pi/pi-bin.js +2 -2
- package/dist/src/pi/pi-models.js +9 -0
- package/dist/src/pi/pi-stream.js +63 -41
- package/dist/src/pi/pi-watchdogs.js +84 -49
- package/dist/src/pi-extension/context-shake.js +55 -31
- package/dist/src/pi-extension/full-output.js +2 -3
- package/dist/src/process/process.js +24 -1
- package/dist/src/process/run-marker.js +2 -2
- package/dist/src/process/supervisor.js +24 -5
- package/dist/src/prompt/gate-notes.js +41 -16
- package/dist/src/prompt/notes-backlog.js +22 -29
- package/dist/src/prompt/principles.js +11 -0
- package/dist/src/prompt/prompt-followup.js +4 -12
- package/dist/src/prompt/prompt.js +15 -12
- package/dist/src/redeploy/redeploy-probes.js +11 -1
- package/dist/src/redeploy/redeployer.js +54 -7
- package/dist/src/redeploy/self-reload.js +7 -2
- package/dist/src/report/report-data.js +224 -320
- package/dist/src/report/report-fold.js +23 -231
- package/dist/src/report/report-main-red.js +70 -3
- package/dist/src/report/report-render.js +181 -48
- package/dist/src/report/report-since-data.js +218 -0
- package/dist/src/report/report-usage-fold.js +288 -0
- package/dist/src/report/report.js +2 -1
- package/dist/src/review/known-flakes.js +22 -3
- package/dist/src/review/post-merge-review-accept.js +3 -2
- package/dist/src/review/post-merge-review.js +16 -14
- package/dist/src/review/quarantine-render.js +29 -11
- package/dist/src/review/review-precheck.js +2 -1
- package/dist/src/roles/role-catalog.js +30 -29
- package/dist/src/roles/role-guidance.js +26 -43
- package/dist/src/roles/role-render.js +19 -0
- package/dist/src/roles/role-view.js +19 -5
- package/dist/src/roles/roles.js +2 -22
- package/dist/src/run/ci-parity.js +5 -6
- package/dist/src/run/feedback-filings.js +10 -1
- package/dist/src/run/qa-coverage.js +4 -4
- package/dist/src/run/run-apply.js +14 -2
- package/dist/src/run/run-detail-data.js +50 -10
- package/dist/src/run/run-detail-render.js +32 -1
- package/dist/src/run/run-detail.js +44 -22
- package/dist/src/run/run-dismiss.js +43 -0
- package/dist/src/run/run-finalize.js +75 -46
- package/dist/src/run/run-prompt.js +271 -187
- package/dist/src/run/run-resume.js +32 -31
- package/dist/src/run/run-stage.js +116 -64
- package/dist/src/run/run-timing.js +18 -4
- package/dist/src/run/run-verdict.js +119 -83
- package/dist/src/run/stage-backlog-checks.js +4 -31
- package/dist/src/run/stage-check.js +4 -12
- package/dist/src/run/stage-checks-shared.js +15 -0
- package/dist/src/run/stage-name-checks.js +3 -13
- package/dist/src/run/stage-path-checks.js +26 -29
- package/dist/src/runner/leftover.js +13 -4
- package/dist/src/runner/revision.js +2 -2
- package/dist/src/runner/runner-fallback.js +1 -1
- package/dist/src/runner/runner-outcome.js +0 -5
- package/dist/src/runner/runner-pi.js +72 -49
- package/dist/src/runner/runner-run.js +19 -102
- package/dist/src/runner/runner-state-load.js +0 -10
- package/dist/src/runner/runner.js +1 -32
- package/dist/src/scheduling/claims.js +4 -11
- package/dist/src/status/status-data.js +140 -91
- package/dist/src/status/status-polls.js +41 -5
- package/dist/src/status/status-role-notes.js +53 -0
- package/dist/src/status/status-roles.js +10 -37
- package/dist/src/text/datetime.js +8 -1
- package/dist/src/text/format.js +13 -1
- package/dist/src/text/phrases.js +16 -2
- package/dist/src/text/text.js +12 -0
- package/dist/src/ui/alert-feeds.js +115 -0
- package/dist/src/ui/badges.js +3 -4
- package/dist/src/ui/gui/gui-client-boot.js +78 -57
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -3
- package/dist/src/ui/gui/gui-client-drawer.js +18 -2
- package/dist/src/ui/gui/gui-client-feed.js +3 -8
- package/dist/src/ui/gui/gui-client-history.js +16 -16
- package/dist/src/ui/gui/gui-client-model.js +9 -9
- package/dist/src/ui/gui/gui-client-operator.js +6 -10
- package/dist/src/ui/gui/gui-client-pending.js +4 -4
- package/dist/src/ui/gui/gui-client-report-chart.js +111 -10
- package/dist/src/ui/gui/gui-client-report.js +70 -11
- package/dist/src/ui/gui/gui-client-roles.js +52 -12
- package/dist/src/ui/gui/gui-client-settings.js +31 -26
- package/dist/src/ui/gui/gui-client-team.js +28 -2
- package/dist/src/ui/gui/gui-client.js +33 -4
- package/dist/src/ui/gui/gui-icons.js +7 -1
- package/dist/src/ui/progress-data.js +2 -2
- package/dist/src/ui/run-progress-model.js +2 -2
- package/dist/src/ui/status-model.js +26 -16
- package/dist/src/ui/status-payload.js +149 -326
- package/dist/src/ui/status-render.js +131 -80
- package/dist/src/ui/team-alert-backlog.js +236 -0
- package/dist/src/ui/team-alert-shared.js +92 -0
- package/dist/src/ui/team-alert-status.js +326 -0
- package/dist/src/ui/team-alert-tail.js +238 -0
- package/dist/src/ui/team-alerts.js +16 -984
- package/dist/src/ui/transcript-tail.js +137 -94
- package/dist/src/ui/tui/tui-backlog.js +1 -1
- package/dist/src/ui/tui/tui-editor-answer.js +54 -44
- package/dist/src/ui/tui/tui-editor-attach.js +74 -63
- package/dist/src/ui/tui/tui-editor-backends.js +62 -56
- package/dist/src/ui/tui/tui-editor-brief.js +64 -54
- package/dist/src/ui/tui/tui-editor-budget.js +106 -89
- package/dist/src/ui/tui/tui-editor-config.js +2 -4
- package/dist/src/ui/tui/tui-editor-ctx.js +12 -1
- package/dist/src/ui/tui/tui-editor-entry.js +171 -153
- package/dist/src/ui/tui/tui-editor-instances.js +2 -1
- package/dist/src/ui/tui/tui-editor-instructions.js +50 -107
- package/dist/src/ui/tui/tui-editor-interval.js +39 -112
- package/dist/src/ui/tui/tui-editor-notes.js +28 -84
- package/dist/src/ui/tui/tui-editor-pause.js +210 -183
- package/dist/src/ui/tui/tui-editor-prompt.js +46 -35
- package/dist/src/ui/tui/tui-editor-queued.js +74 -64
- package/dist/src/ui/tui/tui-editor-quiet.js +122 -108
- package/dist/src/ui/tui/tui-editor-release.js +85 -0
- package/dist/src/ui/tui/tui-editor-retire.js +57 -46
- package/dist/src/ui/tui/tui-editor-role-enabled.js +90 -80
- package/dist/src/ui/tui/tui-editor-role-file.js +87 -0
- package/dist/src/ui/tui/tui-editor-role-key.js +50 -42
- package/dist/src/ui/tui/tui-editor-search.js +70 -58
- package/dist/src/ui/tui/tui-editor-session.js +3 -0
- package/dist/src/ui/tui/tui-editors.js +4 -1
- package/dist/src/ui/tui/tui-frame.js +7 -3
- package/dist/src/ui/tui/tui-history.js +12 -10
- package/dist/src/ui/tui/tui-keys.js +6 -1
- package/dist/src/ui/tui/tui-pane-activity.js +62 -50
- package/dist/src/ui/tui/tui-pane-backlog.js +4 -4
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +45 -26
- package/dist/src/ui/tui/tui-pane-history.js +296 -264
- package/dist/src/ui/tui/tui-pane-report.js +74 -57
- package/dist/src/ui/tui/tui-pane-session.js +6 -2
- package/dist/src/ui/tui/tui-pane-transcript.js +158 -131
- package/dist/src/ui/tui/tui-pane.js +201 -174
- package/dist/src/ui/tui/tui-panes.js +211 -148
- package/dist/src/ui/tui/tui.js +4 -4
- package/dist/src/verdict/fix-claim.js +18 -6
- package/dist/src/verdict/refusal.js +32 -7
- package/package.json +1 -1
- package/dist/src/doctor/image-probe.js +0 -257
- /package/dist/src/gui/{http-body.js → gui-http-body.js} +0 -0
- /package/dist/src/merge/{backlog-conflicts.js → merge-backlog-conflicts.js} +0 -0
|
@@ -121,62 +121,45 @@ export const VALIDATION_GAP_TALLY = `One query counts both the verbatim line and
|
|
|
121
121
|
export const VALIDATION_GAP_QUERY = `grep -snE '\\*\\*Validation gap:\\*\\* (${VALIDATION_GAP_TAGS.filter((t) => t !== "none").join("|")})' BUGS.md BUGS-FIXED.md | head -20`;
|
|
122
122
|
/** The literal prefix every Needs-review note starts with. Exported so a reader that only has
|
|
123
123
|
* to recognize one (the backlog index's ` [blocked: needs review]` mark) can match on it instead of
|
|
124
|
-
* parsing the date or the wording out of the full template
|
|
125
|
-
*
|
|
124
|
+
* parsing the date or the wording out of the full template. The rejected-final-round note the
|
|
125
|
+
* feature role appends (gate-notes.ts) also begins with this prefix, so it reads as a Needs-review
|
|
126
|
+
* note and is classified by entryHold accordingly. */
|
|
126
127
|
export const NEEDS_REVIEW_PREFIX = `**Needs review `;
|
|
127
|
-
/** The tag text before
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
128
|
+
/** The tag text before a held count, read by the backlog (backlog-eligibility's heldCount) so
|
|
129
|
+
* the note form and the parser cannot drift. No writer emits `(held N)` anymore — the feature
|
|
130
|
+
* note is flat — but heldCount still parses the tag from a Needs-review note line already on
|
|
131
|
+
* disk until part 2f/6 removes the reader. */
|
|
131
132
|
export const HELD_TAG_PREFIX = `(held `;
|
|
132
133
|
/** The markdown note the feature role appends to a plan it cannot finish in one run, so the plan
|
|
133
134
|
* role can split it. Mirrors the **Refused …** note convention: a note the next fresh run reads,
|
|
134
135
|
* with no code parsing it. Defined once so the feature and plan texts and the director's routing
|
|
135
|
-
* clause cannot drift.
|
|
136
|
-
*
|
|
137
|
-
|
|
138
|
-
export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run ${HELD_TAG_PREFIX}<N>)**`;
|
|
136
|
+
* clause cannot drift. No writer carries a `(held <N>)` count anymore: a too-large note stays
|
|
137
|
+
* flat, so a new hold never escalates to the operator's desk. */
|
|
138
|
+
export const NEEDS_REVIEW_NOTE = `${NEEDS_REVIEW_PREFIX}<YYYY-MM-DD> by feature: too large for one run**`;
|
|
139
139
|
/** The literal reason the automatic convergence hold writes into a held plan's note
|
|
140
140
|
* (holdEntryAfterExhaustion, src/runner/revision.ts, builds the note as
|
|
141
141
|
* `**Needs review … by <role>: could not converge — <surviving objections>**`, INV-BH4).
|
|
142
142
|
* Exported so the writer and the reader share one literal: it sits between holdEntryAfterExhaustion
|
|
143
143
|
* (which writes the note) and the plan role's charter (role-catalog.ts `find`) which reads it, so
|
|
144
144
|
* reason to recognize the automatic hold as distinct from the feature role's too-large
|
|
145
|
-
* ${NEEDS_REVIEW_NOTE} note
|
|
146
|
-
*
|
|
147
|
-
*
|
|
145
|
+
* ${NEEDS_REVIEW_NOTE} note — it carries no `could not converge` text, so the charter routes
|
|
146
|
+
* each hold to its own action without matching on a second literal. Add no second spelling of
|
|
147
|
+
* this text. */
|
|
148
148
|
export const CONVERGENCE_HOLD_MARKER = "could not converge";
|
|
149
|
-
/** The marker a lifted hold leaves behind, so a split or rewrite
|
|
150
|
-
* letting it reset: `**Previously held N**` on the note's line.
|
|
151
|
-
*
|
|
152
|
-
*
|
|
149
|
+
/** The marker a lifted hold leaves behind, so a split or rewrite carried the count instead of
|
|
150
|
+
* letting it reset: `**Previously held N**` on the note's line. No writer emits this anymore (the
|
|
151
|
+
* plan role's split no longer carries a count) and heldCount no longer reads it, so it is inert;
|
|
152
|
+
* withoutHeldForOperatorNotes still drops a surviving `**Previously held N−1**` line beside a
|
|
153
|
+
* released at-threshold note until part 2f/6 retires the count machinery. */
|
|
153
154
|
export const PRIOR_HELD_PREFIX = `**Previously held `;
|
|
154
|
-
/** The literal marker
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
*
|
|
161
|
-
|
|
162
|
-
export const NEEDS_REPLAN_PREFIX = `**Needs replan `;
|
|
163
|
-
/** The markdown note the feature role appends under a plan whose change was rejected after its
|
|
164
|
-
* final revision round, carrying the reviewer's objections back to the strong-tier plan role
|
|
165
|
-
* instead of letting feature re-author the same plan. Same convention as NEEDS_REVIEW_NOTE: a
|
|
166
|
-
* note a later fresh run reads, with no code parsing it. The trailing `(held <H>)` tag
|
|
167
|
-
* (HELD_TAG_PREFIX) is the live held count, one past the count surviving on the entry; the
|
|
168
|
-
* placeholder is `<H>` (for held) so it reads apart from the `<N>` review-round count earlier
|
|
169
|
-
* in the same line. It sits inside the line, so NEEDS_REPLAN_PREFIX still recognizes the note. */
|
|
170
|
-
export const NEEDS_REPLAN_NOTE = `${NEEDS_REPLAN_PREFIX}<YYYY-MM-DD> by feature: rejected after <N> review rounds ${HELD_TAG_PREFIX}<H>)**`;
|
|
171
|
-
/** The literal prefix every Waiting-for-the-next-failure note starts with. Exported so a reader
|
|
172
|
-
* that only has to recognize one (the backlog index's ` [blocked: waiting for the next failure]`
|
|
173
|
-
* mark) can match on it instead of parsing the date or the wording out of the full template. */
|
|
174
|
-
export const WAITING_FOR_FAILURE_PREFIX = `**Waiting for the next failure `;
|
|
175
|
-
/** The markdown note the bugfix role writes under an open bug it cannot reproduce once it has made
|
|
176
|
-
* the failure explain itself, so bugfix stops spending runs without new evidence. Same convention
|
|
177
|
-
* as NEEDS_REPLAN_NOTE: a note a later fresh run reads. It lifts when whoever records the next
|
|
178
|
-
* occurrence deletes the line, or by `tumwater bug --release <n>`. */
|
|
179
|
-
export const WAITING_FOR_FAILURE_NOTE = `${WAITING_FOR_FAILURE_PREFIX}<YYYY-MM-DD> by bugfix: <what the next failure's message will show>**`;
|
|
155
|
+
/** The literal marker the plan role writes as its own line under the PLANS.md `## Planned` entry
|
|
156
|
+
* that declares this project's deterministic check, and the feature role reads as that entry's
|
|
157
|
+
* identity. It is the one decision point between the two no-check notes (gate-notes.ts
|
|
158
|
+
* buildNoCheckNote): no marker → the plan note (write the declaration entry); marker → the
|
|
159
|
+
* feature note (implement it). Defined once so the marker the plan note names, the one
|
|
160
|
+
* declarationPlanEntry recognizes (src/backlog/backlog.ts), and the catalog texts interpolate stay
|
|
161
|
+
* identical. */
|
|
162
|
+
export const CHECK_DECLARATION_MARKER = `**Declares the project check**`;
|
|
180
163
|
/** How a role with no backlog (organize, clean, dedupe, perf, security, robustness, improve) finds
|
|
181
164
|
* its one task, as numbered steps. Written against the observed failure: with nothing to point
|
|
182
165
|
* at, a local model reads the codebase file by file — thirty whole-file reads, ~300 KB of tool
|
|
@@ -74,6 +74,23 @@ function inspectBodyLines(p, now) {
|
|
|
74
74
|
}
|
|
75
75
|
lines.push("");
|
|
76
76
|
}
|
|
77
|
+
// The feedback observer's per-install filing ledger (role-view.ts's filings,
|
|
78
|
+
// feedback-filings.ts's filingRows): each issue this install filed, newest-first, its number,
|
|
79
|
+
// title, and age — the operator twin of the ledger that is otherwise the observer's only
|
|
80
|
+
// memory. Present only for feedback, so every other role's render stays byte-identical; an
|
|
81
|
+
// enabled-but-quiet observer reads as "none filed yet" rather than a truncated report.
|
|
82
|
+
if (p.filings) {
|
|
83
|
+
lines.push("## Filings", "");
|
|
84
|
+
if (p.filings.length === 0) {
|
|
85
|
+
lines.push("- none filed yet");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
for (const f of p.filings) {
|
|
89
|
+
lines.push(`- #${f.issue} ${f.title} — ${formatAge(now - f.filedAt)} ago`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
lines.push("");
|
|
93
|
+
}
|
|
77
94
|
lines.push(...fencedOrNone("Next run's prompt", p.nextPrompt, "nothing to do this run"));
|
|
78
95
|
// run-prompt.ts dequeues exactly one queued prompt per run, so with more than one
|
|
79
96
|
// waiting the block above shows only the oldest — say so, so the count line and the
|
|
@@ -306,6 +323,8 @@ export function renderRoleMarkdown(p, now = Date.now()) {
|
|
|
306
323
|
let cap = `- Daily cap: ${usd(dc.spentUsd)} of ${usd(dc.capUsd)} spent today (${dc.over ? "holding now — no new runs until local midnight" : "not holding"})`;
|
|
307
324
|
if (dc.roleIds.length > 1)
|
|
308
325
|
cap += ` · shared by ${dc.roleIds.join(", ")}`;
|
|
326
|
+
if (p.dailyCapHitAt !== undefined)
|
|
327
|
+
cap += ` · ~cap at ${clockHm(p.dailyCapHitAt)}`;
|
|
309
328
|
lines.push(cap);
|
|
310
329
|
}
|
|
311
330
|
// The role's schedule (role-view.ts's nextRunAt/backoffSeconds/lastRunEndedAt, read from
|
|
@@ -15,22 +15,23 @@ import { pausedRoles } from "../team/team-state.js";
|
|
|
15
15
|
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
16
16
|
import { baseRoleOf, runnerIds, runnerPaused } from "./runner-ids.js";
|
|
17
17
|
import { roleCapVerdicts } from "../gates/role-cap-gates.js";
|
|
18
|
+
import { projectCapHit } from "../budget/budget.js";
|
|
18
19
|
import { yieldMultiplierFor } from "../scheduling/backoff.js";
|
|
19
20
|
import { quietHoursStatus } from "../scheduling/quiet-hours.js";
|
|
20
21
|
import { assembleRunPrompt } from "../run/run-prompt.js";
|
|
21
22
|
import { configForRole, resolvedModelFields, reviewedAfterMerge, reviewRunConfig, roleSeamTier, seamConcurrencyKey } from "../config/config-views.js";
|
|
22
23
|
import { fallbackPair, modelFallbackView } from "../config/config-fallbacks.js";
|
|
23
|
-
import { fallbackModelPair, piModelsPath,
|
|
24
|
+
import { fallbackModelPair, piModelsPath, seamContextWindowForRole } from "../pi/pi-models.js";
|
|
24
25
|
import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
25
26
|
import { formatModelSelector } from "../config/model-selector.js";
|
|
26
27
|
import { roleImageInfo } from "../agent/image-input.js";
|
|
27
28
|
import { readPiListedModels } from "../pi/pi-listed-models.js";
|
|
28
|
-
import { readImageProbeCache } from "../doctor/image-probe.js";
|
|
29
29
|
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
30
30
|
import { readClaudeAccount } from "../agent/claude/claude-account.js";
|
|
31
31
|
import { readAccount, windowEntries } from "../agent/subscription-account.js";
|
|
32
32
|
import { CONFIG_BASENAME, configPath, roleNotesPath } from "../paths.js";
|
|
33
33
|
import { coverageRows, readQaCoverage } from "../run/qa-coverage.js";
|
|
34
|
+
import { filingRows, readFeedbackFilings } from "../run/feedback-filings.js";
|
|
34
35
|
import { readTextOrNull, statOrNull } from "../files/files.js";
|
|
35
36
|
import { eventRole, eventUsage, readEvents } from "../events/event-read.js";
|
|
36
37
|
import { finiteNumber, objectOrEmpty } from "../files/json-object.js";
|
|
@@ -210,14 +211,19 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
210
211
|
const backendOverride = typeof backendOverrideRaw === "string" ? backendOverrideRaw : undefined;
|
|
211
212
|
const capUsd = cfg.maxDailyCostUsdPerRole?.[base];
|
|
212
213
|
let dailyCap;
|
|
214
|
+
let dailyCapHitAt;
|
|
213
215
|
if (role !== DIRECTOR_ROLE && typeof capUsd === "number" && Number.isFinite(capUsd) && capUsd > 0) {
|
|
214
216
|
const ids = runnerIds(cfg).filter((id) => baseRoleOf(id) === base);
|
|
215
217
|
if (!ids.includes(role))
|
|
216
218
|
ids.push(role);
|
|
217
219
|
const runners = ids.map((id) => ({ role: id, state: id === role ? state : loadRunnerState(root, id) }));
|
|
218
220
|
const verdict = roleCapVerdicts(runners, cfg.maxDailyCostUsdPerRole, now.getTime()).get(base);
|
|
219
|
-
if (verdict)
|
|
221
|
+
if (verdict) {
|
|
220
222
|
dailyCap = { capUsd, spentUsd: verdict.spentUsd, over: verdict.over, roleIds: verdict.roleIds };
|
|
223
|
+
const hitAt = projectCapHit({ spentUsd: verdict.spentUsd, capUsd }, now.getTime());
|
|
224
|
+
if (hitAt !== null)
|
|
225
|
+
dailyCapHitAt = hitAt;
|
|
226
|
+
}
|
|
221
227
|
}
|
|
222
228
|
// The role's notes, read for display. The same degradation as every other read here: a
|
|
223
229
|
// missing, empty, or unreadable file is simply "no note yet". The director has none.
|
|
@@ -245,7 +251,7 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
245
251
|
}
|
|
246
252
|
// The resolved primary seam's declared context window. A run currently on the model
|
|
247
253
|
// fallback may use a different window, which is the fallback's own documented state.
|
|
248
|
-
const contextWindow =
|
|
254
|
+
const contextWindow = seamContextWindowForRole(cfg, role, root, modelsPath);
|
|
249
255
|
return {
|
|
250
256
|
id: role,
|
|
251
257
|
title: resolved.title,
|
|
@@ -272,7 +278,7 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
272
278
|
...(account !== null ? { billing: account.billing, billingBackend } : {}),
|
|
273
279
|
...(billingWindow !== undefined ? { billingWindow } : {}),
|
|
274
280
|
chainStep,
|
|
275
|
-
...roleImageInfo(cfg, role, modelsPath,
|
|
281
|
+
...roleImageInfo(cfg, role, modelsPath, readPiListedModels(root)),
|
|
276
282
|
minRunIntervalSeconds: effective.minRunIntervalSeconds,
|
|
277
283
|
...(minRunIntervalOverride !== undefined ? { minRunIntervalOverride } : {}),
|
|
278
284
|
...(providerOverride !== undefined ? { providerOverride } : {}),
|
|
@@ -306,6 +312,13 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
306
312
|
// payload and Markdown render stay byte-identical. The ledger read degrades to empty
|
|
307
313
|
// coverage on a missing or torn file exactly as readQaCoverage already does.
|
|
308
314
|
...(role === "qa" ? { flowCoverage: coverageRows(readQaCoverage(root), now.getTime()) } : {}),
|
|
315
|
+
// The feedback observer's per-install filing ledger (plans/feedback-role.md 2/2), the twin
|
|
316
|
+
// of qa's flowCoverage block above: each issue this install filed, newest-first via the one
|
|
317
|
+
// filingRows ordering the prompt block and the operator read share. Present only for the
|
|
318
|
+
// canonical feedback id, so every other role's --json payload and Markdown render stay
|
|
319
|
+
// byte-identical. The ledger read degrades to an empty list on a missing or torn file
|
|
320
|
+
// exactly as readFeedbackFilings already does.
|
|
321
|
+
...(role === "feedback" ? { filings: filingRows(readFeedbackFilings(root)) } : {}),
|
|
309
322
|
inboxCount: queuedRolePromptCount(root, role),
|
|
310
323
|
// The resolved primary seam's declared context window in tokens, or null when it cannot
|
|
311
324
|
// be known (a non-pi seam, an unresolvable provider/model pair, or a model with no
|
|
@@ -332,5 +345,6 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
332
345
|
...(recentOutcomes.length > 0 ? { recentOutcomes } : {}),
|
|
333
346
|
...(usage.tokens !== 0 || usage.costUsd !== 0 || usage.notionalCostUsd !== 0 ? { usage } : {}),
|
|
334
347
|
...(dailyCap ? { dailyCap } : {}),
|
|
348
|
+
...(dailyCapHitAt !== undefined ? { dailyCapHitAt } : {}),
|
|
335
349
|
};
|
|
336
350
|
}
|
package/dist/src/roles/roles.js
CHANGED
|
@@ -30,9 +30,8 @@ const WORK_ROLES = new Set(["feature", "bugfix", "plan"]);
|
|
|
30
30
|
* event log for `selfcheck` and `feedback`) is not a function of whether main moved. */
|
|
31
31
|
export const OBSERVER_ROLES = new Set(["qa", "selfcheck", "feedback", "ci"]);
|
|
32
32
|
/** The built-in maintenance roles that author code on their own cadence — the eight members
|
|
33
|
-
* DEFERRABLE_ROLES
|
|
34
|
-
*
|
|
35
|
-
* here and a new maintenance role cannot be added to one set and missed in the other. */
|
|
33
|
+
* DEFERRABLE_ROLES charts. The list lives once here so a new maintenance role cannot be added
|
|
34
|
+
* to one list and missed in the other. */
|
|
36
35
|
const CODE_MAINTENANCE_ROLES = [
|
|
37
36
|
"organize",
|
|
38
37
|
"coverage",
|
|
@@ -74,25 +73,6 @@ export function yieldScaledRole(role) {
|
|
|
74
73
|
export function roleTier(role) {
|
|
75
74
|
return WORK_ROLES.has(baseRoleOf(role)) ? 0 : 1;
|
|
76
75
|
}
|
|
77
|
-
/** The roles blocked from starting an authoring run while main's own build/test suite is known
|
|
78
|
-
* red with no failing test named (the red-main baseline check; a red that names its failing tests
|
|
79
|
-
* blocks nothing, and these roles get a `<main-known-red>` note instead): every role whose diff
|
|
80
|
-
* can carry non-exempt (code) changes — on a red main such a diff is rejected deterministically
|
|
81
|
-
* by the gate's pre-check, so spending an authoring run on it is pure waste. Exempt: director
|
|
82
|
-
* (human prompts outrank autonomous gates, like the budget gate), ci (the designated healer
|
|
83
|
-
* — its run executes the suite per "leave the project working" and can fix a red main through the
|
|
84
|
-
* existing pre-merge gate; blocking it would leave only the ci role able to unblock the team), and
|
|
85
|
-
* plan/readme/steward/qa/selfcheck/feedback (markdown-only charter — their diffs are exempt
|
|
86
|
-
* from the build pre-check via review.exemptPaths, so a red main does not block them). */
|
|
87
|
-
export const BASELINE_BLOCKED_ROLES = new Set([
|
|
88
|
-
"feature",
|
|
89
|
-
...CODE_MAINTENANCE_ROLES,
|
|
90
|
-
]);
|
|
91
|
-
/** Is this role id blocked from authoring on a red main? True when its base role is in
|
|
92
|
-
* BASELINE_BLOCKED_ROLES, so `feature-2` is blocked exactly as `feature` is. */
|
|
93
|
-
export function baselineBlocked(role) {
|
|
94
|
-
return BASELINE_BLOCKED_ROLES.has(baseRoleOf(role));
|
|
95
|
-
}
|
|
96
76
|
/** Work ratio 4/4: which commit tier a role's base role merges into, for the work vs maintenance
|
|
97
77
|
* split the report and the "Merged today" tile show and the maintenance allowance (Work ratio
|
|
98
78
|
* 1a/4, src/gates/maintenance-quota.ts) counts. The one home of the split. `work` is the
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* under .tumwater/state/ and degrades to "no data" on any missing or malformed file: an
|
|
9
9
|
* observer must never fail a run on bookkeeping (the same policy as qa-coverage.ts). A stale
|
|
10
10
|
* verdict that stays stale past `CI_PARITY_STALE_MS` is a stall — the full suite has silently
|
|
11
|
-
* stopped covering new tips — surfaced as
|
|
12
|
-
*
|
|
11
|
+
* stopped covering new tips — surfaced as the doctor `ci parity` warn through the one
|
|
12
|
+
* `ciParityStalled` predicate. */
|
|
13
13
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
14
14
|
import { ciParityPath } from "../paths.js";
|
|
15
15
|
import { isJsonObject } from "../files/json-object.js";
|
|
@@ -20,14 +20,13 @@ import * as fs from "node:fs";
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
/** A stale verdict that stays stale past this window is a stall: the `ci` role — now the
|
|
22
22
|
* full suite's own runner — has silently stopped covering new tips (disabled, paused, wedged,
|
|
23
|
-
* or failing before it records). The doctor `ci parity` warn
|
|
24
|
-
*
|
|
23
|
+
* or failing before it records). The doctor `ci parity` warn reads this one threshold, so the
|
|
24
|
+
* row and the ledger cannot disagree on when stale is a problem. */
|
|
25
25
|
export const CI_PARITY_STALE_MS = 3 * DAY_MS;
|
|
26
26
|
/** True only when a parity record exists, reads `stale` (a `failed` record already warns and
|
|
27
27
|
* a `current` one is verified for the tip), and has not covered a tip within the stall
|
|
28
28
|
* window. Parameter is structural — not `CiParityStatus` — so the status-polls → ci-parity
|
|
29
|
-
* import edge stays one-way. This is the single stall predicate
|
|
30
|
-
* the alert. */
|
|
29
|
+
* import edge stays one-way. This is the single stall predicate the doctor row reads. */
|
|
31
30
|
export function ciParityStalled(parity, now) {
|
|
32
31
|
return !!parity && parity.state === "stale" && now - parity.lastRunAt > CI_PARITY_STALE_MS;
|
|
33
32
|
}
|
|
@@ -14,6 +14,13 @@ import { formatAge } from "../text/datetime.js";
|
|
|
14
14
|
* caps the block's growth and the prompt's size; older filings are dropped (the charter still
|
|
15
15
|
* dedupes against the live `gh issue list`). */
|
|
16
16
|
export const FEEDBACK_FILINGS_MAX = 100;
|
|
17
|
+
/** Collect the ledger into `FilingRow`s newest-first by `filedAt` — a stable sort, so two
|
|
18
|
+
* filings sharing a timestamp keep their ledger (insertion) order. A ledger stored by
|
|
19
|
+
* `recordFiled` is already newest-first, so this reorders nothing on the normal path; it
|
|
20
|
+
* guarantees a hand-edited or torn file that is not already ordered still reads consistently. */
|
|
21
|
+
export function filingRows(filings) {
|
|
22
|
+
return [...filings].sort((a, b) => b.filedAt - a.filedAt);
|
|
23
|
+
}
|
|
17
24
|
function isFiling(value) {
|
|
18
25
|
if (!isJsonObject(value))
|
|
19
26
|
return false;
|
|
@@ -46,6 +53,8 @@ export function recordFiled(root, issue, title, now = Date.now()) {
|
|
|
46
53
|
export function renderFilingsBlock(filings, now = Date.now()) {
|
|
47
54
|
if (filings.length === 0)
|
|
48
55
|
return undefined;
|
|
49
|
-
|
|
56
|
+
// Render through filingRows, the one ordering the prompt block and the operator read share,
|
|
57
|
+
// so a ledger not already newest-first still reads that way in both places.
|
|
58
|
+
const lines = filingRows(filings).map((f) => ` #${f.issue} ${f.title} (${formatAge(now - f.filedAt)})`);
|
|
50
59
|
return ["Previously filed from this install:", ...lines].join("\n");
|
|
51
60
|
}
|
|
@@ -26,15 +26,15 @@ export const QA_FLOWS = [
|
|
|
26
26
|
/** A coverage ledger whose newest exercise is older than this window is a stall: the `qa`
|
|
27
27
|
* role — the team's only flow-level observer, the one role that exercises the product's own
|
|
28
28
|
* flows like a user — has silently stopped recording any exercise (disabled, paused, wedged,
|
|
29
|
-
* or failing before it records). The doctor `qa coverage` warn
|
|
30
|
-
*
|
|
29
|
+
* or failing before it records). The doctor `qa coverage` warn fires through this one
|
|
30
|
+
* threshold, mirroring `ciParityStalled`'s `CI_PARITY_STALE_MS`. */
|
|
31
31
|
export const QA_COVERAGE_STALE_MS = 3 * DAY_MS;
|
|
32
32
|
/** True only when the ledger has at least one well-formed entry and every entry's `lastRunAt`
|
|
33
33
|
* is older than the stall window (`now - max(lastRunAt) > QA_COVERAGE_STALE_MS`). An empty
|
|
34
34
|
* ledger — a missing file, or a project that never ran qa — is not a stall: "no record yet" is
|
|
35
35
|
* not "stopped recording", the same rule `ciParityStalled` applies to a missing parity record.
|
|
36
|
-
* This is the one stall predicate the doctor row (`checkQaCoverage`)
|
|
37
|
-
*
|
|
36
|
+
* This is the one stall predicate the doctor's `qa coverage` row (`checkQaCoverage`) reads, so
|
|
37
|
+
* the row and the ledger cannot disagree on when a quiet qa is a problem. */
|
|
38
38
|
export function qaCoverageStalled(coverage, now) {
|
|
39
39
|
let newest = 0;
|
|
40
40
|
let has = false;
|
|
@@ -36,6 +36,16 @@ export function clearSupersededRejection(s) {
|
|
|
36
36
|
* quiet team). Small on purpose — at 3 the error ladder has already doubled twice and
|
|
37
37
|
* the failure is clearly not transient. */
|
|
38
38
|
export const ERROR_STREAK_WARN = 3;
|
|
39
|
+
/** Whether a role's consecutive-error count is at or past the warn bar (ERROR_STREAK_WARN) —
|
|
40
|
+
* the "failing" predicate the status phase cell (status-model.ts's onErrorStreak), the status
|
|
41
|
+
* poll's slot-demand read (status-roles.ts), and the doctor's error-streaks check
|
|
42
|
+
* (doctor-holds.ts) each test, so the threshold comparison cannot drift between them. The
|
|
43
|
+
* `=== ERROR_STREAK_WARN` crossing edges (run-finalize.ts, merge-slot.ts) are a different
|
|
44
|
+
* comparison (the single run that reached the bar, not the at-or-past state) and stay
|
|
45
|
+
* inline. */
|
|
46
|
+
export function errorStreakAtWarn(s) {
|
|
47
|
+
return (s.consecutiveErrors ?? 0) >= ERROR_STREAK_WARN;
|
|
48
|
+
}
|
|
39
49
|
/** Consecutive failed runs after which the error-streak circuit breaker (src/gates/streak-gate.ts)
|
|
40
50
|
* auto-pauses the role through the per-role pause marker — the same act-on-it escalation the
|
|
41
51
|
* budget gate and the pause gates perform, applied to a role failing on its own cause. Ten,
|
|
@@ -47,8 +57,10 @@ export const ERROR_STREAK_BREAKER = 10;
|
|
|
47
57
|
/** Resumes granted to one context-ceiling cut-off streak before the role stops resuming the
|
|
48
58
|
* task and falls back to a fresh run: a task that outruns the ceiling on every attempt (even
|
|
49
59
|
* from a freshly compacted context) is too big to converge, and each cycle costs an hour-plus
|
|
50
|
-
* of model time on local hardware. The streak itself keeps counting (RunnerState.cutOffStreak)
|
|
51
|
-
|
|
60
|
+
* of model time on local hardware. The streak itself keeps counting (RunnerState.cutOffStreak)
|
|
61
|
+
* so one warning per episode can be raised (run-finalize.ts) and the resting cell can read
|
|
62
|
+
* ` · cut off ×N` (status-model.ts rolePhase). */
|
|
63
|
+
export const CUT_OFF_RESUME_LIMIT = 3;
|
|
52
64
|
/** Resumes granted to one quiet-kill streak before the role abandons the starved pi session
|
|
53
65
|
* and takes a fresh run on the idle ladder. A session the backend defers past the quiet
|
|
54
66
|
* watchdog every attempt would otherwise re-send the same input immediately and forever,
|
|
@@ -14,6 +14,9 @@ import { bucketMergeEvents, resolveQueuedResult } from "../history/history-queue
|
|
|
14
14
|
import { eventUsage, readEvents, runSpanMs, runStartMap } from "../events/event-read.js";
|
|
15
15
|
import { outcomeText, usageText } from "../events/event-format.js";
|
|
16
16
|
import { baseRoleOf } from "../roles/runner-ids.js";
|
|
17
|
+
import { loadConfigSafe, defaultConfig } from "../config/config.js";
|
|
18
|
+
import { seamContextWindowForRole } from "../pi/pi-models.js";
|
|
19
|
+
import { nonNegativeNumber } from "../files/json-object.js";
|
|
17
20
|
/** The commit a run pinned: the `commit` of the first `merge_queued`/`merge_done` event in its
|
|
18
21
|
* block carrying a non-empty one, else null. The one rule both the header's sha and --stat use. */
|
|
19
22
|
export function runCommitSha(d) {
|
|
@@ -58,7 +61,7 @@ export function readRunDetail(root, role, runNumber) {
|
|
|
58
61
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
59
62
|
const e = events[i];
|
|
60
63
|
if (e.type === "run_end" && e.run === runNumber)
|
|
61
|
-
return detailFrom(role, runNumber, all, events, [e], e, null);
|
|
64
|
+
return detailFrom(root, role, runNumber, all, events, [e], e, null);
|
|
62
65
|
}
|
|
63
66
|
return null;
|
|
64
67
|
}
|
|
@@ -89,13 +92,19 @@ export function readRunDetail(root, role, runNumber) {
|
|
|
89
92
|
break;
|
|
90
93
|
block.push(e);
|
|
91
94
|
}
|
|
92
|
-
return detailFrom(role, runNumber, all, events, block, end, start);
|
|
95
|
+
return detailFrom(root, role, runNumber, all, events, block, end, start);
|
|
93
96
|
}
|
|
94
97
|
/** Fill the payload around a collected block: usage from the run_end via eventUsage/usageText,
|
|
95
98
|
* duration via runSpanMs against the role's own start map (the shared pairing helper, so this
|
|
96
99
|
* view and the history rows cannot drift on what a span is), events oldest-first. */
|
|
97
|
-
function detailFrom(role, runNumber, all, scoped, block, end, start) {
|
|
100
|
+
function detailFrom(root, role, runNumber, all, scoped, block, end, start) {
|
|
98
101
|
const usageNums = end === null ? { tokens: 0, costUsd: 0 } : eventUsage(end);
|
|
102
|
+
// The run_end's prompt-token total and its cache-read subset, corrupt-field-guarded reads
|
|
103
|
+
// (nonNegativeNumber, the same guard peakContextTokens applies) with the cache subset
|
|
104
|
+
// clamped to the total so the rendered share never exceeds 100%; 0 on an in-flight run.
|
|
105
|
+
const promptTokens = end === null ? 0 : nonNegativeNumber(end.promptTokens, 0);
|
|
106
|
+
const cacheReadTokens = end === null ? 0 : Math.min(nonNegativeNumber(end.cacheReadTokens, 0), promptTokens);
|
|
107
|
+
const preEditPromptTokens = end === null ? 0 : Math.min(nonNegativeNumber(end.preEditPromptTokens, 0), promptTokens);
|
|
99
108
|
let result = end === null ? null : outcomeText(end);
|
|
100
109
|
let merge = null;
|
|
101
110
|
if (end !== null && result === "queued") {
|
|
@@ -108,6 +117,7 @@ function detailFrom(role, runNumber, all, scoped, block, end, start) {
|
|
|
108
117
|
merge = resolved.outcome;
|
|
109
118
|
}
|
|
110
119
|
}
|
|
120
|
+
const mergedEvent = mergedEventOf(result, merge, scoped, block);
|
|
111
121
|
const detail = {
|
|
112
122
|
role,
|
|
113
123
|
run: runNumber,
|
|
@@ -119,9 +129,30 @@ function detailFrom(role, runNumber, all, scoped, block, end, start) {
|
|
|
119
129
|
tokens: usageNums.tokens,
|
|
120
130
|
costUsd: usageNums.costUsd,
|
|
121
131
|
usage: end === null ? "" : usageText(end),
|
|
132
|
+
// The peak is a corrupt-field-guarded read of the run_end (nonNegativeNumber, the same
|
|
133
|
+
// guard promptStatsFor applies); the window comes from the shared seamContextWindowForRole —
|
|
134
|
+
// same seam the role view and status table print — with loadConfigSafe falling back to
|
|
135
|
+
// defaults, so a broken tumwater.json yields a null window rather than failing this read-only
|
|
136
|
+
// view.
|
|
137
|
+
peakContextTokens: end === null ? 0 : nonNegativeNumber(end.peakContextTokens, 0),
|
|
138
|
+
// The run's prompt-token total and its cache-read subset, spread so a prompt-free or
|
|
139
|
+
// in-flight run keeps today's exact payload (no prompt/cache keys) and a cache subset > 0.
|
|
140
|
+
...(promptTokens > 0 ? { promptTokens } : {}),
|
|
141
|
+
...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
|
|
142
|
+
...(preEditPromptTokens > 0 ? { preEditPromptTokens } : {}),
|
|
143
|
+
// The run_end's ceiling-truncation flag, spread so an in-flight, older, or normal run
|
|
144
|
+
// keeps today's exact payload (no cutOff key).
|
|
145
|
+
...(end !== null && end.cutOff === true ? { cutOff: true } : {}),
|
|
146
|
+
contextWindow: seamContextWindowForRole(loadConfigSafe(root).config ?? defaultConfig(), role, root),
|
|
122
147
|
events: block,
|
|
123
148
|
merge,
|
|
124
149
|
mergedSha: mergedShaOf(result, merge, scoped, block),
|
|
150
|
+
// The end-of-run → on-main landing latency (the landing `merged` event's ts minus the run
|
|
151
|
+
// end's), spread so a run without both an end and a landing event — or a landing before its
|
|
152
|
+
// end, or a negative difference — keeps today's exact payload (no key).
|
|
153
|
+
...(end !== null && mergedEvent !== null && mergedEvent.ts - end.ts >= 0
|
|
154
|
+
? { mergeAfterRunMs: mergedEvent.ts - end.ts }
|
|
155
|
+
: {}),
|
|
125
156
|
reverted: null,
|
|
126
157
|
};
|
|
127
158
|
detail.reverted = revertedOf(detail, all);
|
|
@@ -153,23 +184,32 @@ function revertedOf(d, all) {
|
|
|
153
184
|
}
|
|
154
185
|
return null;
|
|
155
186
|
}
|
|
156
|
-
/** The
|
|
157
|
-
function
|
|
187
|
+
/** The newest `merged` event in `pool` (oldest-first) inside [from, to], or null when none. */
|
|
188
|
+
function newestMergedEvent(pool, from, to) {
|
|
158
189
|
for (let i = pool.length - 1; i >= 0; i--) {
|
|
159
190
|
const e = pool[i];
|
|
160
191
|
if (e.type === "merged" && e.ts >= from && e.ts <= to && typeof e.commit === "string" && e.commit !== "") {
|
|
161
|
-
return e
|
|
192
|
+
return e;
|
|
162
193
|
}
|
|
163
194
|
}
|
|
164
195
|
return null;
|
|
165
196
|
}
|
|
166
|
-
/** The
|
|
167
|
-
|
|
197
|
+
/** The landing `merged` event for a detail, the one behind its `mergedSha` and its
|
|
198
|
+
* `mergeAfterRunMs` span: the newest in the block for a run that merged directly (result
|
|
199
|
+
* `changed`, no queued resolve) or the newest inside the merge's own window (`merge_done` ts
|
|
200
|
+
* minus its `durationMs`, up to its ts) for a queued run; null for every other run and when
|
|
201
|
+
* the event rotated out or the merge was a no-op fast-forward (which logs none). */
|
|
202
|
+
function mergedEventOf(result, merge, scoped, block) {
|
|
168
203
|
if (merge === null) {
|
|
169
|
-
return result === "changed" ?
|
|
204
|
+
return result === "changed" ? newestMergedEvent(block, -Infinity, Infinity) : null;
|
|
170
205
|
}
|
|
171
206
|
if (merge.type !== "merge_done" || typeof merge.durationMs !== "number" || !Number.isFinite(merge.durationMs)) {
|
|
172
207
|
return null;
|
|
173
208
|
}
|
|
174
|
-
return
|
|
209
|
+
return newestMergedEvent(scoped, merge.ts - merge.durationMs, merge.ts);
|
|
210
|
+
}
|
|
211
|
+
/** The landed sha for a detail: see RunDetail.mergedSha. */
|
|
212
|
+
function mergedShaOf(result, merge, scoped, block) {
|
|
213
|
+
const e = mergedEventOf(result, merge, scoped, block);
|
|
214
|
+
return e === null ? null : e.commit;
|
|
175
215
|
}
|
|
@@ -3,7 +3,7 @@ import { formatEvent } from "../events/event-format.js";
|
|
|
3
3
|
import { eventTypeLabel } from "../events/event-labels.js";
|
|
4
4
|
import { roleLabel } from "../roles/role-labels.js";
|
|
5
5
|
import { shortSpanPhrase } from "../text/phrases.js";
|
|
6
|
-
import { shortSha } from "../text/format.js";
|
|
6
|
+
import { shortSha, contextShareLabel, compactTokens } from "../text/format.js";
|
|
7
7
|
import { piLogPath } from "../paths.js";
|
|
8
8
|
import { readRunTranscript } from "../ui/transcript-run.js";
|
|
9
9
|
import { gitTry } from "../git/git-run.js";
|
|
@@ -47,10 +47,35 @@ export function renderRunDetail(d) {
|
|
|
47
47
|
summary.push(d.startTs === null ? "unpaired" : "in flight");
|
|
48
48
|
else
|
|
49
49
|
summary.push(d.result ? eventTypeLabel(d.result) : "?");
|
|
50
|
+
// The ceiling-truncation marker, straight after the result cell. It only rides a
|
|
51
|
+
// completed end (cutOff is set from the run_end), so it can never follow in flight/unpaired.
|
|
52
|
+
if (d.cutOff === true)
|
|
53
|
+
summary.push("cut off at the context ceiling");
|
|
50
54
|
if (d.durationMs !== null)
|
|
51
55
|
summary.push(shortSpanPhrase(d.durationMs));
|
|
52
56
|
if (d.usage !== "")
|
|
53
57
|
summary.push(d.usage);
|
|
58
|
+
// The run's prompt-token total and the share the backend served from cache, the input
|
|
59
|
+
// side a cost-only header hides. The `${n}% cached` wording and rounding are the usage
|
|
60
|
+
// report's `Prompt tokens by role` share; a run with prompt tokens but no cache reads
|
|
61
|
+
// shows no parenthetical, and a run that reported neither keeps today's header
|
|
62
|
+
// byte-identical (the omit-when-zero convention).
|
|
63
|
+
const prompt = d.promptTokens ?? 0;
|
|
64
|
+
const cached = Math.min(d.cacheReadTokens ?? 0, prompt);
|
|
65
|
+
if (prompt > 0)
|
|
66
|
+
summary.push(cached > 0 ? `prompt ${compactTokens(prompt)} tok (${Math.round((100 * cached) / prompt)}% cached)` : `prompt ${compactTokens(prompt)} tok`);
|
|
67
|
+
// The run's pre-edit prompt-token share, the `… before first edit` signal a cost-only
|
|
68
|
+
// header hides: how much of the prompt budget went out before the run made its first
|
|
69
|
+
// edit. Clamped to the total (run-detail-data.ts), written after the prompt cell and
|
|
70
|
+
// before the peak-context cell; a run that reported no pre-edit reading — and an
|
|
71
|
+
// in-flight run — keeps today's header byte-identical.
|
|
72
|
+
const preEdit = Math.min(d.preEditPromptTokens ?? 0, prompt);
|
|
73
|
+
if (preEdit > 0)
|
|
74
|
+
summary.push(`pre-edit ${compactTokens(preEdit)} tok`);
|
|
75
|
+
// The run's peak context against the seam's declared window, the one header every surface
|
|
76
|
+
// renders. Omitted when the run reported no peak (byte-identical to today's header).
|
|
77
|
+
if ((d.peakContextTokens ?? 0) > 0)
|
|
78
|
+
summary.push(`peak ctx ${contextShareLabel(d.peakContextTokens ?? 0, d.contextWindow)}`);
|
|
54
79
|
if (d.model !== null)
|
|
55
80
|
summary.push(d.model);
|
|
56
81
|
const sha = runCommitSha(d);
|
|
@@ -59,6 +84,12 @@ export function renderRunDetail(d) {
|
|
|
59
84
|
header += ` · commit ${shortSha(sha)}`;
|
|
60
85
|
if (d.mergedSha !== null && d.mergedSha !== sha)
|
|
61
86
|
header += ` · merged as ${shortSha(d.mergedSha)}`;
|
|
87
|
+
// The end-of-run → on-main landing latency, the per-change twin of the day report's `Merge
|
|
88
|
+
// wait median`. shortSpanPhrase is the duration cell's renderer, so both spans read alike; a
|
|
89
|
+
// run without the field (rejected/failed merge, in flight, landing rotated out, no-op
|
|
90
|
+
// fast-forward) keeps today's header byte-identical.
|
|
91
|
+
if (d.mergeAfterRunMs !== undefined)
|
|
92
|
+
header += ` · merged ${shortSpanPhrase(d.mergeAfterRunMs)} after the run`;
|
|
62
93
|
if (d.reverted !== null) {
|
|
63
94
|
header += ` · reverted ${shortSha(d.reverted.sha)} (by operator)`; // vocabulary-ok: the deliberate-revert writer this header names
|
|
64
95
|
}
|