tumwater 0.4.2 → 0.4.4
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 +6 -6
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-backend.js +6 -6
- package/dist/src/agent/{run-agent.js → agent-run.js} +27 -13
- package/dist/src/agent/{run-settle.js → agent-settle.js} +48 -24
- package/dist/src/agent/{run-spawn.js → agent-spawn.js} +21 -12
- package/dist/src/agent/{run-stream.js → agent-stream.js} +4 -4
- package/dist/src/agent/backend-traits.js +2 -2
- package/dist/src/agent/claude/claude-account.js +6 -27
- package/dist/src/agent/claude/claude-backend.js +3 -3
- package/dist/src/agent/claude/claude-env.js +27 -31
- package/dist/src/agent/claude/claude-stream.js +111 -54
- package/dist/src/agent/codex/codex-account.js +8 -0
- package/dist/src/agent/codex/codex-args.js +5 -5
- package/dist/src/agent/codex/codex-backend.js +2 -2
- package/dist/src/agent/codex/codex-env.js +28 -15
- package/dist/src/agent/codex/codex-hook-trust.js +39 -20
- package/dist/src/agent/codex/codex-rollout.js +93 -56
- package/dist/src/agent/codex/codex-stream.js +1 -1
- package/dist/src/agent/env-scrub.js +18 -0
- package/dist/src/agent/image-input.js +68 -11
- package/dist/src/agent/notes-shim.js +2 -2
- package/dist/src/agent/opencode/opencode-backend.js +26 -24
- package/dist/src/agent/opencode/opencode-catalog.js +85 -36
- package/dist/src/agent/opencode/opencode-config.js +32 -14
- package/dist/src/agent/opencode/opencode-env.js +12 -13
- package/dist/src/agent/opencode/opencode-stream.js +47 -25
- package/dist/src/agent/session-ledger.js +23 -27
- package/dist/src/agent/subscription-account.js +2 -83
- package/dist/src/backlog/backlog-archive.js +3 -4
- package/dist/src/backlog/backlog-cli-view.js +81 -41
- package/dist/src/backlog/backlog-cli.js +57 -26
- package/dist/src/backlog/backlog-eligibility.js +56 -122
- package/dist/src/backlog/backlog-md.js +5 -5
- package/dist/src/backlog/backlog-parts.js +65 -27
- package/dist/src/backlog/{question-answer.js → backlog-question-answer.js} +1 -1
- package/dist/src/backlog/backlog-release.js +12 -12
- package/dist/src/backlog/backlog-structure.js +114 -61
- package/dist/src/backlog/backlog.js +3 -2
- package/dist/src/baseline/main-baseline.js +148 -84
- package/dist/src/baseline/main-guardrails.js +2 -2
- package/dist/src/baseline/main-watch.js +84 -50
- package/dist/src/brief/brief.js +26 -13
- package/dist/src/budget/budget-fallback-view.js +100 -243
- package/dist/src/budget/budget-wiring.js +29 -65
- package/dist/src/budget/budget.js +4 -5
- package/dist/src/budget/fallback-breaker.js +2 -2
- package/dist/src/build/build-check-detect.js +43 -14
- package/dist/src/build/build-check-events.js +2 -2
- package/dist/src/build/build-check-report.js +10 -0
- package/dist/src/build/build-check-scoped.js +40 -18
- package/dist/src/build/build-check.js +13 -5
- package/dist/src/build/build-stage.js +6 -4
- package/dist/src/change/change-data.js +1 -4
- package/dist/src/cli/cli-command-args.js +89 -52
- package/dist/src/cli/cli-completion-command.js +75 -0
- package/dist/src/cli/{config-commands.js → cli-config-commands.js} +82 -71
- package/dist/src/cli/cli-doctor-command.js +103 -0
- package/dist/src/cli/cli-flag-specs.js +42 -22
- package/dist/src/cli/cli-init.js +36 -14
- package/dist/src/cli/{log-commands.js → cli-log-commands.js} +43 -23
- package/dist/src/cli/{notify-command.js → cli-notify-command.js} +2 -2
- package/dist/src/cli/cli-query-commands.js +2 -2
- package/dist/src/cli/{question-commands.js → cli-question-commands.js} +86 -60
- package/dist/src/cli/cli-repo-gate.js +2 -2
- package/dist/src/cli/cli-role-command.js +2 -41
- package/dist/src/cli/cli-start-summary.js +4 -3
- package/dist/src/cli/cli-start-supervisor.js +190 -0
- package/dist/src/cli/cli-start.js +94 -244
- package/dist/src/cli/completion.js +85 -49
- package/dist/src/cli/follow-view.js +20 -2
- package/dist/src/cli/help.js +6 -13
- package/dist/src/cli.js +70 -176
- package/dist/src/collections.js +39 -27
- package/dist/src/concurrency/check-slots.js +70 -0
- package/dist/src/concurrency/lock.js +23 -1
- package/dist/src/concurrency/run-permit.js +49 -0
- package/dist/src/concurrency/semaphore.js +46 -250
- package/dist/src/config/config-editable-keys.js +4 -7
- package/dist/src/config/config-fallbacks.js +81 -101
- package/dist/src/config/config-field-checks.js +66 -37
- package/dist/src/config/config-keys.js +66 -40
- package/dist/src/config/config-live.js +4 -28
- package/dist/src/config/{model-selector.js → config-model-selector.js} +4 -4
- package/dist/src/config/config-request.js +163 -102
- package/dist/src/config/config-schema.js +10 -12
- package/dist/src/config/config-selector-checks.js +1 -1
- package/dist/src/config/config-validation-agents.js +2 -2
- package/dist/src/config/config-validation-roles.js +8 -72
- package/dist/src/config/config-validation.js +4 -39
- package/dist/src/config/config-views.js +26 -50
- package/dist/src/config/config-write.js +128 -102
- package/dist/src/config/config.js +5 -14
- package/dist/src/doctor/doctor-backlog-bugs.js +7 -6
- package/dist/src/doctor/doctor-backlog-plans.js +53 -120
- package/dist/src/doctor/doctor-backlog.js +16 -14
- package/dist/src/doctor/doctor-check-names.js +3 -6
- package/dist/src/doctor/doctor-checks.js +8 -8
- package/dist/src/doctor/doctor-claude-checks.js +70 -36
- package/dist/src/doctor/doctor-codex-checks.js +6 -6
- package/dist/src/doctor/doctor-contention-checks.js +57 -117
- package/dist/src/doctor/doctor-fallback-checks.js +48 -26
- package/dist/src/doctor/doctor-holds.js +8 -47
- package/dist/src/doctor/doctor-install.js +1 -1
- package/dist/src/doctor/doctor-launch-services.js +1 -1
- package/dist/src/doctor/doctor-model-checks.js +178 -94
- package/dist/src/doctor/doctor-notify.js +2 -2
- package/dist/src/doctor/doctor-opencode-checks.js +5 -4
- package/dist/src/doctor/doctor-orphans.js +122 -21
- package/dist/src/doctor/{outcome.js → doctor-outcome.js} +7 -0
- package/dist/src/doctor/doctor-pi-checks.js +2 -2
- package/dist/src/doctor/doctor-principles.js +3 -3
- package/dist/src/doctor/doctor-project-checks.js +24 -11
- package/dist/src/doctor/doctor-role-notes.js +1 -1
- package/dist/src/doctor/doctor.js +8 -11
- package/dist/src/events/event-format.js +44 -64
- package/dist/src/events/event-labels.js +1 -5
- package/dist/src/events/event-legacy.js +3 -5
- package/dist/src/events/{notify.js → event-notify.js} +6 -6
- package/dist/src/events/event-read.js +1 -1
- package/dist/src/events/event-window.js +1 -1
- package/dist/src/events/events.js +8 -0
- package/dist/src/failure/failure-cluster.js +9 -18
- package/dist/src/failure/failure-data.js +12 -8
- package/dist/src/failure/failure-render.js +46 -36
- package/dist/src/failure/failure-state-change.js +54 -120
- package/dist/src/failure/{time-spend.js → failure-time-spend.js} +117 -84
- package/dist/src/files/files.js +30 -0
- package/dist/src/files/json-object.js +8 -8
- package/dist/src/files/stat-cache.js +12 -8
- package/dist/src/gates/budget-gates.js +85 -84
- package/dist/src/gates/gate-polls.js +32 -74
- package/dist/src/gates/pause-gates.js +16 -2
- package/dist/src/gates/readiness.js +1 -1
- package/dist/src/gates/role-cap-gates.js +44 -35
- package/dist/src/gates/{startup-gate.js → startup-gates.js} +9 -8
- package/dist/src/gates/{streak-gate.js → streak-gates.js} +75 -52
- package/dist/src/git/git-diff.js +6 -2
- package/dist/src/git/git.js +75 -45
- package/dist/src/git/worktree-pool.js +3 -6
- package/dist/src/gui/gui-admin-endpoints.js +3 -3
- package/dist/src/gui/gui-args.js +12 -0
- package/dist/src/gui/gui-backlog-commands.js +4 -4
- package/dist/src/gui/gui-backlog-endpoints.js +61 -41
- package/dist/src/gui/gui-command.js +43 -24
- package/dist/src/gui/gui-config-commands.js +2 -64
- package/dist/src/gui/gui-endpoint-commands.js +1 -1
- package/dist/src/gui/gui-endpoint-common.js +19 -1
- package/dist/src/gui/gui-history-endpoints.js +100 -52
- package/dist/src/gui/gui-operator-commands.js +43 -48
- package/dist/src/gui/gui-prompt-commands.js +25 -25
- package/dist/src/gui/gui-report-endpoints.js +1 -1
- package/dist/src/gui/gui-server.js +1 -2
- package/dist/src/gui/gui-team-commands.js +2 -1
- package/dist/src/history/history-data.js +71 -44
- package/dist/src/history/history-queued.js +49 -27
- package/dist/src/history/history.js +54 -36
- package/dist/src/inbox/prompt-commands.js +59 -42
- package/dist/src/init/init.js +88 -40
- package/dist/src/merge/merge-backlog-conflicts.js +1 -1
- package/dist/src/merge/merge-check-failures.js +12 -9
- package/dist/src/merge/merge-core.js +7 -32
- package/dist/src/merge/merge-drain.js +11 -13
- package/dist/src/merge/merge-pipeline.js +6 -7
- package/dist/src/merge/merge-post-review-drain.js +11 -14
- package/dist/src/merge/merge-queue-render.js +3 -3
- package/dist/src/merge/merge-requeue.js +1 -1
- package/dist/src/merge/merge-slot.js +0 -8
- package/dist/src/merge/merge-stack.js +17 -18
- package/dist/src/merge/merge-to-main.js +13 -58
- package/dist/src/merge/merge-verify.js +12 -21
- package/dist/src/notes-cli.js +52 -42
- package/dist/src/operator/operator-commands.js +80 -47
- package/dist/src/operator/operator-intent.js +3 -3
- package/dist/src/orchestrator/orchestrator-launch.js +31 -63
- package/dist/src/orchestrator/orchestrator-poll.js +44 -69
- package/dist/src/orchestrator/orchestrator-scheduling.js +5 -9
- package/dist/src/orchestrator/orchestrator.js +8 -21
- package/dist/src/orchestrator/scheduling-admit.js +101 -68
- package/dist/src/orchestrator/scheduling-scope.js +18 -27
- package/dist/src/orchestrator/scheduling-skip.js +10 -14
- package/dist/src/orchestrator/startup-migrations.js +1 -1
- package/dist/src/paths.js +10 -9
- package/dist/src/pi/pi-args.js +5 -2
- package/dist/src/pi/pi-backend.js +2 -2
- package/dist/src/pi/pi-bin.js +3 -3
- package/dist/src/pi/pi-event-line.js +28 -1
- package/dist/src/pi/pi-listed-models.js +13 -0
- package/dist/src/pi/pi-models.js +43 -29
- package/dist/src/pi/pi-settings.js +43 -20
- package/dist/src/pi/pi-stream.js +41 -8
- package/dist/src/pi/pi-watchdogs.js +43 -15
- package/dist/src/pi/pi.js +2 -2
- package/dist/src/pi-extension/context-shake.js +32 -20
- package/dist/src/pi-extension/kill-guard.js +69 -0
- package/dist/src/process/process-group.js +51 -26
- package/dist/src/process/process.js +20 -12
- package/dist/src/process/run-marker.js +33 -15
- package/dist/src/process/supervisor.js +40 -28
- package/dist/src/prompt/dialect.js +1 -1
- package/dist/src/prompt/gate-notes.js +2 -2
- package/dist/src/prompt/gate-prompts.js +1 -44
- package/dist/src/prompt/notes-backlog.js +66 -37
- package/dist/src/prompt/principles.js +6 -4
- package/dist/src/prompt/prompt-followup.js +12 -4
- package/dist/src/prompt/prompt.js +88 -38
- package/dist/src/redeploy/redeployer.js +21 -1
- package/dist/src/report/report-data.js +53 -37
- package/dist/src/report/report-fold.js +16 -9
- package/dist/src/report/report-render.js +21 -194
- package/dist/src/report/report-since-data.js +4 -3
- package/dist/src/report/report-since-render.js +173 -0
- package/dist/src/report/report-usage-fold.js +8 -8
- package/dist/src/report/report.js +46 -22
- package/dist/src/review/diff-rules.js +35 -21
- package/dist/src/review/post-merge-review-render.js +9 -3
- package/dist/src/review/post-merge-review.js +48 -41
- package/dist/src/review/review-precheck.js +14 -4
- package/dist/src/review/review.js +48 -170
- package/dist/src/review/secret-leaks.js +26 -16
- package/dist/src/roles/legacy-role-migration.js +6 -3
- package/dist/src/roles/role-catalog.js +11 -3
- package/dist/src/roles/role-guidance.js +16 -12
- package/dist/src/roles/role-render.js +7 -46
- package/dist/src/roles/role-view.js +10 -33
- package/dist/src/run/acceptance-greps.js +88 -45
- package/dist/src/run/ci-parity.js +19 -8
- package/dist/src/run/run-apply.js +12 -15
- package/dist/src/run/run-detail-data.js +121 -68
- package/dist/src/run/run-detail-render.js +92 -35
- package/dist/src/run/run-detail.js +72 -41
- package/dist/src/run/run-prompt.js +63 -50
- package/dist/src/run/run-resume.js +76 -29
- package/dist/src/run/run-stage.js +141 -90
- package/dist/src/run/run-timing.js +8 -2
- package/dist/src/run/run-usage.js +31 -1
- package/dist/src/run/run-verdict.js +128 -50
- package/dist/src/run/stage-check.js +60 -50
- package/dist/src/run/stage-checks-shared.js +28 -0
- package/dist/src/run/stage-name-checks.js +111 -92
- package/dist/src/run/stage-path-checks.js +69 -42
- package/dist/src/run/stage-prose-checks.js +55 -40
- package/dist/src/runner/{leftover.js → runner-leftover.js} +9 -26
- package/dist/src/runner/runner-outcome.js +7 -2
- package/dist/src/runner/runner-pi.js +9 -22
- package/dist/src/runner/runner-recovery.js +5 -13
- package/dist/src/runner/{revision.js → runner-revision.js} +1 -2
- package/dist/src/runner/runner-run.js +103 -50
- package/dist/src/runner/runner-state-load.js +18 -28
- package/dist/src/runner/runner-state.js +0 -14
- package/dist/src/runner/runner.js +41 -25
- package/dist/src/scheduling/backoff.js +8 -3
- package/dist/src/scheduling/claim-entries.js +2 -1
- package/dist/src/scheduling/claims.js +0 -4
- package/dist/src/scheduling/scheduling.js +53 -66
- package/dist/src/status/status-budget.js +4 -43
- package/dist/src/status/status-data.js +35 -39
- package/dist/src/status/status-role-notes.js +2 -2
- package/dist/src/status/status-roles.js +9 -18
- package/dist/src/team/{error-storm.js → team-error-storm.js} +1 -1
- package/dist/src/team/{failure-spread.js → team-failure-spread.js} +5 -5
- package/dist/src/team/team-hold.js +105 -70
- package/dist/src/team/{orchestrator-info.js → team-orchestrator-info.js} +55 -36
- package/dist/src/team/team-polls.js +53 -201
- package/dist/src/team/{reclaim.js → team-reclaim.js} +61 -31
- package/dist/src/team/team-state.js +4 -13
- package/dist/src/text/datetime.js +16 -7
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +29 -15
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +6 -5
- package/dist/src/ui/alert-feeds.js +46 -111
- package/dist/src/ui/badges.js +17 -88
- package/dist/src/ui/gui/gui-client-boot.js +2 -8
- package/dist/src/ui/gui/gui-client-composer.js +1 -1
- package/dist/src/ui/gui/gui-client-drawer-config.js +8 -33
- package/dist/src/ui/gui/gui-client-drawer.js +1 -1
- package/dist/src/ui/gui/gui-client-feed.js +1 -1
- package/dist/src/ui/gui/gui-client-history.js +28 -14
- package/dist/src/ui/gui/gui-client-model.js +0 -12
- package/dist/src/ui/gui/gui-client-operator.js +7 -5
- package/dist/src/ui/gui/gui-client-pending.js +7 -6
- package/dist/src/ui/gui/gui-client-report-chart.js +5 -5
- package/dist/src/ui/gui/gui-client-report.js +1 -1
- package/dist/src/ui/gui/gui-client-roles.js +0 -1
- package/dist/src/ui/gui/gui-client-settings.js +43 -84
- package/dist/src/ui/gui/gui-client-team.js +12 -6
- package/dist/src/ui/gui/gui-client.js +7 -1
- package/dist/src/ui/progress-data.js +107 -44
- package/dist/src/ui/run-progress-model.js +20 -4
- package/dist/src/ui/status-model.js +2 -24
- package/dist/src/ui/status-payload.js +11 -30
- package/dist/src/ui/status-render.js +13 -24
- package/dist/src/ui/team-alert-backlog.js +14 -204
- package/dist/src/ui/team-alert-shared.js +6 -29
- package/dist/src/ui/team-alert-status.js +9 -170
- package/dist/src/ui/team-alert-tail.js +19 -215
- package/dist/src/ui/team-alerts.js +8 -15
- package/dist/src/ui/transcript-run.js +27 -16
- package/dist/src/ui/tui/tui-backlog.js +2 -2
- package/dist/src/ui/tui/tui-editor-answer.js +12 -10
- package/dist/src/ui/tui/tui-editor-attach.js +4 -3
- package/dist/src/ui/tui/tui-editor-backends.js +4 -3
- package/dist/src/ui/tui/tui-editor-brief.js +6 -9
- package/dist/src/ui/tui/tui-editor-budget.js +4 -3
- package/dist/src/ui/tui/tui-editor-config.js +80 -68
- package/dist/src/ui/tui/tui-editor-entry.js +12 -20
- package/dist/src/ui/tui/tui-editor-pause.js +51 -57
- package/dist/src/ui/tui/tui-editor-prompt.js +9 -13
- package/dist/src/ui/tui/tui-editor-queued.js +11 -14
- package/dist/src/ui/tui/tui-editor-quiet.js +4 -3
- package/dist/src/ui/tui/tui-editor-release.js +4 -3
- package/dist/src/ui/tui/tui-editor-retire.js +7 -7
- package/dist/src/ui/tui/tui-editor-role-enabled.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-file.js +4 -4
- package/dist/src/ui/tui/tui-editor-role-key.js +4 -3
- package/dist/src/ui/tui/tui-editor-role-model.js +97 -15
- package/dist/src/ui/tui/tui-editor-search.js +4 -3
- package/dist/src/ui/tui/tui-editor-session.js +17 -4
- package/dist/src/ui/tui/tui-editors.js +5 -10
- package/dist/src/ui/tui/tui-frame.js +182 -134
- package/dist/src/ui/tui/tui-guard.js +20 -0
- package/dist/src/ui/tui/tui-input.js +167 -98
- package/dist/src/ui/tui/tui-keymap.js +51 -29
- package/dist/src/ui/tui/tui-keys.js +1 -1
- package/dist/src/ui/tui/tui-pane-activity.js +5 -4
- package/dist/src/ui/tui/tui-pane-backlog.js +74 -36
- package/dist/src/ui/tui/tui-pane-ctx.js +1 -1
- package/dist/src/ui/tui/tui-pane-health.js +252 -230
- package/dist/src/ui/tui/tui-pane-history.js +67 -74
- package/dist/src/ui/tui/tui-pane-pending.js +288 -247
- package/dist/src/ui/tui/tui-pane-queued.js +25 -27
- package/dist/src/ui/tui/tui-pane-report.js +2 -1
- package/dist/src/ui/tui/tui-pane-session.js +15 -10
- package/dist/src/ui/tui/tui-pane-transcript.js +19 -31
- package/dist/src/ui/tui/tui-pane.js +14 -11
- package/dist/src/ui/tui/tui-panes.js +38 -33
- package/dist/src/ui/tui/tui.js +4 -7
- package/dist/src/verdict/fix-claim.js +107 -56
- package/dist/src/verdict/refusal.js +1 -1
- package/dist/src/verdict/reply-contract.js +3 -4
- package/package.json +5 -5
- package/dist/src/concurrency/provider-lease.js +0 -105
- package/dist/src/review/review-followup.js +0 -87
- package/dist/src/review/review-run.js +0 -395
- package/dist/src/runner/model-fallback.js +0 -76
- package/dist/src/runner/runner-fallback.js +0 -66
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +0 -24
- package/dist/src/ui/tui/tui-editor-provider-caps.js +0 -27
- /package/dist/src/cli/{brief-command.js → cli-brief-command.js} +0 -0
- /package/dist/src/doctor/{probe.js → doctor-probe.js} +0 -0
- /package/dist/src/gates/{disk-gate.js → disk-gates.js} +0 -0
- /package/dist/src/operator/{retire.js → operator-retire.js} +0 -0
- /package/dist/src/operator/{revert.js → operator-revert.js} +0 -0
|
@@ -19,24 +19,24 @@ import { projectCapHit } from "../budget/budget.js";
|
|
|
19
19
|
import { yieldMultiplierFor } from "../scheduling/backoff.js";
|
|
20
20
|
import { quietHoursStatus } from "../scheduling/quiet-hours.js";
|
|
21
21
|
import { assembleRunPrompt } from "../run/run-prompt.js";
|
|
22
|
-
import { configForRole, resolvedModelFields,
|
|
23
|
-
import { fallbackPair
|
|
22
|
+
import { configForRole, resolvedModelFields, reviewRunConfig, roleSeamTier } from "../config/config-views.js";
|
|
23
|
+
import { fallbackPair } from "../config/config-fallbacks.js";
|
|
24
24
|
import { fallbackModelPair, piModelsPath, seamContextWindowForRole } from "../pi/pi-models.js";
|
|
25
25
|
import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
26
|
-
import { formatModelSelector } from "../config/model-selector.js";
|
|
26
|
+
import { formatModelSelector } from "../config/config-model-selector.js";
|
|
27
27
|
import { roleImageInfo } from "../agent/image-input.js";
|
|
28
28
|
import { readPiListedModels } from "../pi/pi-listed-models.js";
|
|
29
29
|
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
30
30
|
import { readClaudeAccount } from "../agent/claude/claude-account.js";
|
|
31
|
-
import { readAccount
|
|
31
|
+
import { readAccount } 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
34
|
import { filingRows, readFeedbackFilings } from "../run/feedback-filings.js";
|
|
35
|
-
import {
|
|
35
|
+
import { readRegularTextOrNull, statOrNull } from "../files/files.js";
|
|
36
36
|
import { eventRole, eventUsage, readEvents } from "../events/event-read.js";
|
|
37
37
|
import { finiteNumber, objectOrEmpty } from "../files/json-object.js";
|
|
38
38
|
import { canonicalRoleId } from "./legacy-role-ids.js";
|
|
39
|
-
import { orchestratorAlive, readOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
39
|
+
import { orchestratorAlive, readOrchestratorInfo } from "../team/team-orchestrator-info.js";
|
|
40
40
|
/** The roles.<base>.<field> value the FILE actually holds, or undefined when the file never
|
|
41
41
|
* set that field. Needed only for the seeded per-role fields: overlayDefaults fills every
|
|
42
42
|
* built-in role's minRunIntervalSeconds from config.ts's catalog, so the merged config cannot
|
|
@@ -94,7 +94,6 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
94
94
|
const account = billingBackend !== undefined ? readAccount(root, billingBackend) : null;
|
|
95
95
|
const info = readOrchestratorInfo(root);
|
|
96
96
|
const chainStep = (orchestratorAlive(root, info) && info?.chain?.[roleSeamTier(cfg, role)]) || null;
|
|
97
|
-
const billingWindow = account?.billing === "subscription" ? windowEntries(account)[0]?.type : undefined;
|
|
98
97
|
// pi's own saved default for a pi seam that resolves no model, read once beside the models
|
|
99
98
|
// catalog this collector already takes as a seam (piSettingsPath). The config's `thinking`
|
|
100
99
|
// wins over the default's because tumwater passes it to pi as `--thinking`, mirroring the
|
|
@@ -103,14 +102,6 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
103
102
|
effective.provider === undefined &&
|
|
104
103
|
effective.model === undefined;
|
|
105
104
|
const piDefault = runsPiWithoutAModel ? readPiDefaultModel(piSettingsPath(modelsPath), root) : null;
|
|
106
|
-
// The role's provider key and its cap (plans/provider-concurrency.md §8): the same pure
|
|
107
|
-
// key the orchestrator's admission resolves for this role, answered from the same
|
|
108
|
-
// piDefault seam the orchestrator carries, so `tumwater role <id>` names the exact key
|
|
109
|
-
// whose cap bounds the role. The cap is the map's entry for that key, absent when no
|
|
110
|
-
// `maxConcurrentPerProvider` map is set or the key has no entry (then the role is bounded
|
|
111
|
-
// only by the team total, and the cap is omitted).
|
|
112
|
-
const providerKey = seamConcurrencyKey(effective, piDefault);
|
|
113
|
-
const providerCap = providerKey !== undefined ? cfg.maxConcurrentPerProvider?.[providerKey] : undefined;
|
|
114
105
|
// The fallback story the operator sees: the declared pair, but the price-resolved pair when
|
|
115
106
|
// one exists (a map with no `default` entry borrows another tier's free pair) and the
|
|
116
107
|
// freeness verdict from the same read. `fallbackFree` false keeps the declared pair visible
|
|
@@ -135,9 +126,8 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
135
126
|
const accounts = (b) => (b === "claude" ? claudeFallbackAccount : null);
|
|
136
127
|
const freeFallback = declaredFallback ? fallbackModelPair(cfg, modelsPath, accounts, opencodeCatalog) : null;
|
|
137
128
|
const fallback = freeFallback ?? declaredFallback;
|
|
138
|
-
// One state read serves
|
|
129
|
+
// One state read serves the next-run preview below.
|
|
139
130
|
const state = loadRunnerState(root, role);
|
|
140
|
-
const episode = state.modelFallback;
|
|
141
131
|
// The role's recent run outcomes, read from the event log (readEvents) so the command
|
|
142
132
|
// answers with the team stopped and never touches live state. readEvents returns the log's
|
|
143
133
|
// newest events in file (chronological) order, so filtering to this role's run_end events
|
|
@@ -233,7 +223,7 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
233
223
|
let noteSavedAt = null;
|
|
234
224
|
if (role !== DIRECTOR_ROLE) {
|
|
235
225
|
const notesPath = roleNotesPath(root, role);
|
|
236
|
-
const text =
|
|
226
|
+
const text = readRegularTextOrNull(notesPath);
|
|
237
227
|
note = text !== null && text.trim() !== "" ? text : null;
|
|
238
228
|
if (note !== null)
|
|
239
229
|
noteSavedAt = statOrNull(notesPath)?.mtimeMs ?? null;
|
|
@@ -270,13 +260,9 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
270
260
|
}
|
|
271
261
|
: {}),
|
|
272
262
|
backend: effective.agent ?? "pi",
|
|
273
|
-
...(providerKey !== undefined ? { providerKey } : {}),
|
|
274
|
-
...(providerCap !== undefined ? { providerCap } : {}),
|
|
275
263
|
fallback,
|
|
276
264
|
fallbackFree: freeFallback !== null,
|
|
277
|
-
modelFallback: modelFallbackView(episode, cfg, role),
|
|
278
265
|
...(account !== null ? { billing: account.billing, billingBackend } : {}),
|
|
279
|
-
...(billingWindow !== undefined ? { billingWindow } : {}),
|
|
280
266
|
chainStep,
|
|
281
267
|
...roleImageInfo(cfg, role, modelsPath, readPiListedModels(root)),
|
|
282
268
|
minRunIntervalSeconds: effective.minRunIntervalSeconds,
|
|
@@ -290,15 +276,6 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
290
276
|
// so the merged cfg tells presence exactly. Resolved through the base role like the
|
|
291
277
|
// instructions and min-run-interval override.
|
|
292
278
|
...(cfg.roles[base]?.instances !== undefined ? { instances: cfg.roles[base].instances } : {}),
|
|
293
|
-
// Whether the review gate reviews this role's commits after they merge — the role page's
|
|
294
|
-
// `- Review: after merge` line. Present only while review is enabled and review.afterMerge
|
|
295
|
-
// lists the role's base id, so a pre-merge-reviewed role or a gate-off config leaves the
|
|
296
|
-
// payload (and its render) unchanged.
|
|
297
|
-
...(cfg.review.enabled && reviewedAfterMerge(cfg, role) ? { reviewedAfterMerge: true } : {}),
|
|
298
|
-
// Whether the review gate is on at all (review.enabled !== false) — the gate toggle's
|
|
299
|
-
// presence, distinct from reviewedAfterMerge's membership, so a before-merge-reviewed role
|
|
300
|
-
// still gets the drawer's Review control while the gate runs.
|
|
301
|
-
...(cfg.review.enabled ? { reviewEnabled: true } : {}),
|
|
302
279
|
// The roles.<base>.instructions override verbatim — resolved through the base role like
|
|
303
280
|
// the cap and quiet hours, so an instance id shows (and can edit) the override its runs
|
|
304
281
|
// carry (run-prompt reads config.roles[base]); null when unset.
|
|
@@ -334,8 +311,8 @@ export function rolePayload(root, typedRole, modelsPath = piModelsPath(), now =
|
|
|
334
311
|
preview: true,
|
|
335
312
|
})?.prompt ?? null,
|
|
336
313
|
// The role's schedule — surfaced read-only from the same persisted state rolePayload
|
|
337
|
-
// already loaded for the
|
|
338
|
-
//
|
|
314
|
+
// already loaded for the next-run preview, so no new read path exists and the collector
|
|
315
|
+
// stays one loadRunnerState per role. `lastRunEndedAt`
|
|
339
316
|
// is omitted (JSON unchanged) until the role has actually run.
|
|
340
317
|
nextRunAt: state.nextRunAt,
|
|
341
318
|
backoffSeconds: state.backoffSeconds,
|
|
@@ -39,56 +39,99 @@ export function emptyGrepCriteria(body) {
|
|
|
39
39
|
* shell syntax (an unquoted `|;&<>$()` or backtick) or leaves a quote open. Nothing runs through
|
|
40
40
|
* a shell, so `*` stays literal. */
|
|
41
41
|
export function grepArgv(cmd) {
|
|
42
|
-
const words =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
42
|
+
const words = splitShellWords(cmd);
|
|
43
|
+
return words === undefined ? undefined : grepInvocation(words);
|
|
44
|
+
}
|
|
45
|
+
/** Take one character at `i` inside a single-quoted run (`s.quote === "'"`): a `'` closes the
|
|
46
|
+
* quote, anything else stays literal. Returns the index after the character. Inside single
|
|
47
|
+
* quotes everything is literal. */
|
|
48
|
+
function takeSingleQuoted(cmd, i, s) {
|
|
49
|
+
const c = cmd[i];
|
|
50
|
+
if (c === "'")
|
|
51
|
+
s.quote = null;
|
|
52
|
+
else
|
|
53
|
+
s.cur += c;
|
|
54
|
+
return i + 1;
|
|
55
|
+
}
|
|
56
|
+
/** Take one character at `i` inside a double-quoted run (`s.quote === '"'`): a `\\`-escaped
|
|
57
|
+
* `"` or `\\` appends the escaped character and consumes two characters, a `"` closes the
|
|
58
|
+
* quote, anything else stays literal. Returns the index after the consumed character(s). Double
|
|
59
|
+
* quotes honour only `\"` and `\\`. */
|
|
60
|
+
function takeDoubleQuoted(cmd, i, s) {
|
|
61
|
+
const c = cmd[i];
|
|
62
|
+
const next = cmd[i + 1];
|
|
63
|
+
if (c === "\\" && (next === '"' || next === "\\")) {
|
|
64
|
+
s.cur += next;
|
|
65
|
+
return i + 2;
|
|
66
|
+
}
|
|
67
|
+
if (c === '"')
|
|
68
|
+
s.quote = null;
|
|
69
|
+
else
|
|
70
|
+
s.cur += c;
|
|
71
|
+
return i + 1;
|
|
72
|
+
}
|
|
73
|
+
/** Take one character at `i` outside any quote, or return `undefined` when it is refused shell
|
|
74
|
+
* syntax: an unquoted `'`/`"` opens the quote and marks a word, an unquoted `\\` appends the
|
|
75
|
+
* next character (if any) and marks a word, an unquoted `|;&<>$()` or backtick refuses the
|
|
76
|
+
* command, whitespace ends a word (pushing `cur` when one is open) and resets the buffer, and
|
|
77
|
+
* anything else appends and marks a word. Returns the index after the consumed character(s), or
|
|
78
|
+
* `undefined` for the shell refusal. */
|
|
79
|
+
function takeUnquoted(cmd, i, s) {
|
|
80
|
+
const c = cmd[i];
|
|
81
|
+
if (c === "'" || c === '"') {
|
|
82
|
+
s.quote = c;
|
|
83
|
+
s.inWord = true;
|
|
84
|
+
return i + 1;
|
|
85
|
+
}
|
|
86
|
+
if (c === "\\") {
|
|
87
|
+
if (i + 1 < cmd.length) {
|
|
88
|
+
s.cur += cmd[i + 1];
|
|
89
|
+
s.inWord = true;
|
|
90
|
+
return i + 2;
|
|
82
91
|
}
|
|
92
|
+
s.inWord = true;
|
|
93
|
+
return i + 1;
|
|
94
|
+
}
|
|
95
|
+
if (/[|;&<>$()`]/.test(c))
|
|
96
|
+
return undefined;
|
|
97
|
+
if (/\s/.test(c)) {
|
|
98
|
+
if (s.inWord)
|
|
99
|
+
s.words.push(s.cur);
|
|
100
|
+
s.cur = "";
|
|
101
|
+
s.inWord = false;
|
|
102
|
+
return i + 1;
|
|
103
|
+
}
|
|
104
|
+
s.cur += c;
|
|
105
|
+
s.inWord = true;
|
|
106
|
+
return i + 1;
|
|
107
|
+
}
|
|
108
|
+
/** Walk the whole command, dispatching each character to the quote-aware helper, and reject an
|
|
109
|
+
* unterminated quote. Returns the word list. */
|
|
110
|
+
function splitShellWords(cmd) {
|
|
111
|
+
const s = { words: [], cur: "", inWord: false, quote: null };
|
|
112
|
+
let i = 0;
|
|
113
|
+
while (i < cmd.length) {
|
|
114
|
+
if (s.quote === "'")
|
|
115
|
+
i = takeSingleQuoted(cmd, i, s);
|
|
116
|
+
else if (s.quote === '"')
|
|
117
|
+
i = takeDoubleQuoted(cmd, i, s);
|
|
83
118
|
else {
|
|
84
|
-
|
|
85
|
-
|
|
119
|
+
const next = takeUnquoted(cmd, i, s);
|
|
120
|
+
if (next === undefined)
|
|
121
|
+
return undefined;
|
|
122
|
+
i = next;
|
|
86
123
|
}
|
|
87
124
|
}
|
|
88
|
-
if (quote !== null)
|
|
125
|
+
if (s.quote !== null)
|
|
89
126
|
return undefined;
|
|
90
|
-
if (inWord)
|
|
91
|
-
words.push(cur);
|
|
127
|
+
if (s.inWord)
|
|
128
|
+
s.words.push(s.cur);
|
|
129
|
+
return s.words;
|
|
130
|
+
}
|
|
131
|
+
/** The shape pick: what invocation does this word list name? `git grep …` gives file `git` and
|
|
132
|
+
* the args after `git`; `grep …` gives file `grep` and the args after `grep`; anything else is
|
|
133
|
+
* not a grep we run. */
|
|
134
|
+
function grepInvocation(words) {
|
|
92
135
|
if (words[0] === "git" && words[1] === "grep")
|
|
93
136
|
return { file: "git", args: words.slice(1) };
|
|
94
137
|
if (words[0] === "grep")
|
|
@@ -13,6 +13,7 @@
|
|
|
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";
|
|
16
|
+
import { cachedByStat } from "../files/stat-cache.js";
|
|
16
17
|
import { currentBranchFromHeadFile, readBranchHead, targetBranch } from "../git/git.js";
|
|
17
18
|
import { DAY_MS } from "../text/datetime.js";
|
|
18
19
|
import * as crypto from "node:crypto";
|
|
@@ -54,18 +55,28 @@ export function readCiParity(root) {
|
|
|
54
55
|
export function recordParity(root, sha, result, now = Date.now()) {
|
|
55
56
|
writeJsonAtomic(ciParityPath(root), { parity: { tip: sha, lockHash: lockHashOf(root), result, lastRunAt: now } });
|
|
56
57
|
}
|
|
58
|
+
// Stat-keyed cache (stat-cache.cachedByStat) for the lock hash: the status snapshot and
|
|
59
|
+
// doctor poll it, and package-lock.json is a multi-ten-KB file that changes only on a
|
|
60
|
+
// dependency edit, so an unchanged lock serves the hash from one stat instead of re-reading
|
|
61
|
+
// and re-hashing the whole file every poll — the same stat-keyed contract every other
|
|
62
|
+
// per-poll snapshot reader uses. Any write invalidates via dev/ino/mtime/size (the standard
|
|
63
|
+
// freshness check); bounded inside cachedByStat.
|
|
64
|
+
const lockHashCache = new Map();
|
|
57
65
|
/** The hash of `package-lock.json` at `root` itself — what the current tree carries. An
|
|
58
66
|
* empty string when the file is missing (or unreadable) reads as an unmatched hash, so a
|
|
59
67
|
* swing to "no lock file" renders stale rather than pretending nothing moved. */
|
|
60
68
|
export function lockHashOf(root) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
const file = path.join(root, "package-lock.json");
|
|
70
|
+
return (cachedByStat(lockHashCache, file, file, () => {
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
text = fs.readFileSync(file, "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null; // Missing or unreadable reads as an unmatched hash (a cache miss).
|
|
77
|
+
}
|
|
78
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
79
|
+
}, (hash) => hash) ?? "");
|
|
69
80
|
}
|
|
70
81
|
/** The current main tip and lock hash, resolved synchronously from the ref files and the
|
|
71
82
|
* working tree (mirroring status-polls.ts's per-poll read: readBranchHead + targetBranch, no
|
|
@@ -46,7 +46,7 @@ export const ERROR_STREAK_WARN = 3;
|
|
|
46
46
|
export function errorStreakAtWarn(s) {
|
|
47
47
|
return (s.consecutiveErrors ?? 0) >= ERROR_STREAK_WARN;
|
|
48
48
|
}
|
|
49
|
-
/** Consecutive failed runs after which the error-streak circuit breaker (src/gates/streak-
|
|
49
|
+
/** Consecutive failed runs after which the error-streak circuit breaker (src/gates/streak-gates.ts)
|
|
50
50
|
* auto-pauses the role through the per-role pause marker — the same act-on-it escalation the
|
|
51
51
|
* budget gate and the pause gates perform, applied to a role failing on its own cause. Ten,
|
|
52
52
|
* not the warn bar: at 3 the failure is diagnosed but possibly transient, and by run 4 the
|
|
@@ -68,6 +68,13 @@ export const CUT_OFF_RESUME_LIMIT = 3;
|
|
|
68
68
|
* consumed 44% of the team over two days). Mirrors CUT_OFF_RESUME_LIMIT; the streak itself
|
|
69
69
|
* keeps counting (RunnerState.quietKillStreak) so one warning per episode can be raised. */
|
|
70
70
|
export const QUIET_KILL_RESUME_LIMIT = 3;
|
|
71
|
+
/** Bump a streak counter to `current + 1` when `hit`, and reset it to 0 otherwise — the one
|
|
72
|
+
* home of the cut-off, quiet-kill and no-change streaks' shared shape in applyRunOutcome, so
|
|
73
|
+
* the three count identically. (The consecutive-error streak does NOT share it: it resumes
|
|
74
|
+
* from a `queued`-ended run rather than resetting, so it keeps its own longer arm.) */
|
|
75
|
+
function bumpOrResetStreak(current, hit) {
|
|
76
|
+
return hit ? (current ?? 0) + 1 : 0;
|
|
77
|
+
}
|
|
71
78
|
/** Record a finished run on the role's state and schedule its next run from the outcome.
|
|
72
79
|
* Split out of RoleRunner.run() (runner.ts) so the scheduling policy — which outcomes retry
|
|
73
80
|
* promptly, which back off and on which ladder, how cut-off resumes are bounded — sits with
|
|
@@ -78,11 +85,10 @@ export const QUIET_KILL_RESUME_LIMIT = 3;
|
|
|
78
85
|
export function applyRunOutcome(s, cfg, role, outcome) {
|
|
79
86
|
s.running = false;
|
|
80
87
|
s.parkedSince = undefined;
|
|
81
|
-
s.parkedOn = undefined;
|
|
82
88
|
// The cut-off streak counts EVERY consecutive run truncated at the context ceiling, past the
|
|
83
89
|
// resume limit too: the next fresh run's prompt names how many attempts the window has eaten
|
|
84
90
|
// (buildCutOffNote), so the count must not freeze at the limit. Any other outcome resets it.
|
|
85
|
-
s.cutOffStreak =
|
|
91
|
+
s.cutOffStreak = bumpOrResetStreak(s.cutOffStreak, outcome.cutOff);
|
|
86
92
|
// The error streak counts consecutive failed runs; any other result resets it
|
|
87
93
|
// (BUGS.md 2026-09-15: 44 identical failures raised no alarm). A run whose leftover
|
|
88
94
|
// recovery re-queued a pin its last merge failed counts too, even though the run
|
|
@@ -106,7 +112,7 @@ export function applyRunOutcome(s, cfg, role, outcome) {
|
|
|
106
112
|
// The quiet-kill streak counts consecutive watchdog kills; any other result resets it.
|
|
107
113
|
// run-finalize.ts warns once when it crosses QUIET_KILL_RESUME_LIMIT, and the branch below uses
|
|
108
114
|
// it to bound how many times a starved session is resumed (BUGS.md 2026-09-18).
|
|
109
|
-
s.quietKillStreak = outcome.result === "quiet_killed"
|
|
115
|
+
s.quietKillStreak = bumpOrResetStreak(s.quietKillStreak, outcome.result === "quiet_killed");
|
|
110
116
|
// The no-change streak counts consecutive completed runs that ended `no_change` (the
|
|
111
117
|
// resting state cell's ` · no change ×N` suffix — status-model.ts rolePhase). Any other
|
|
112
118
|
// completed result resets it, so a role that merges between no-ops never shows a stale
|
|
@@ -115,7 +121,7 @@ export function applyRunOutcome(s, cfg, role, outcome) {
|
|
|
115
121
|
// `quiet_killed`/`user_aborted` all reset it and only a `no_change` run grows it.
|
|
116
122
|
// Observational only — never warns, unlike the error/quiet-kill streaks: a legitimately
|
|
117
123
|
// idle maintenance role with an empty backlog can hold a long streak and that is expected.
|
|
118
|
-
s.noChangeStreak = outcome.result === "no_change"
|
|
124
|
+
s.noChangeStreak = bumpOrResetStreak(s.noChangeStreak, outcome.result === "no_change");
|
|
119
125
|
// The yield ring records the result beside the streaks: a merge or a counted empty
|
|
120
126
|
// stretches the role's min-run gap (yieldMultiplier, read by isEligible), an error-class
|
|
121
127
|
// result is skipped entirely — no evidence either way (yield-scaled clocks, PLANS.md).
|
|
@@ -156,10 +162,6 @@ export function applyRunOutcome(s, cfg, role, outcome) {
|
|
|
156
162
|
// (`result !== "aborted"`) covers both.
|
|
157
163
|
if (outcome.result === "changed")
|
|
158
164
|
s.commits += 1;
|
|
159
|
-
// A merged change's patch-id approval has done its job: the same patch authored again
|
|
160
|
-
// later (after a revert, say) is a new change and gets its own review.
|
|
161
|
-
if (outcome.result === "changed")
|
|
162
|
-
s.approvedPatchIds = undefined;
|
|
163
165
|
// The role committed new work, so the note about its discarded change has been delivered.
|
|
164
166
|
s.conflictDiscard = undefined;
|
|
165
167
|
s.backoffSeconds = 0;
|
|
@@ -304,7 +306,6 @@ export function applyMergeOutcome(s, result, change) {
|
|
|
304
306
|
// merge, like a no_change run, leaves the prior value standing (PLANS.md "Each loop's
|
|
305
307
|
// last-landed age").
|
|
306
308
|
s.lastMergedAt = Date.now();
|
|
307
|
-
s.approvedPatchIds = undefined; // see applyRunOutcome
|
|
308
309
|
// A merge supersedes a stale rejection: a model-reviewed merge has already overwritten
|
|
309
310
|
// lastReview with its approve, but a review-exempt (or review-disabled) merge records no
|
|
310
311
|
// verdict, so a stale reject would otherwise ride every later run prompt (BUGS.md
|
|
@@ -329,11 +330,10 @@ export function applyMergeOutcome(s, result, change) {
|
|
|
329
330
|
// leave the streak unchanged and keep the fast minimum-interval retry the queued run
|
|
330
331
|
// scheduled. This merge outcome is the accumulation point (the authoring run ends
|
|
331
332
|
// `queued`, which no longer resets the streak, see applyRunOutcome) — the same field the
|
|
332
|
-
// warn, the breaker (src/gates/streak-
|
|
333
|
+
// warn, the breaker (src/gates/streak-gates.ts), and the error-storm observation
|
|
333
334
|
// (src/team/team-polls.ts) already read. No new machinery, no new constants: the warn
|
|
334
335
|
// bar and the breaker bar are ERROR_STREAK_WARN and ERROR_STREAK_BREAKER themselves.
|
|
335
336
|
if (s.revision === undefined) {
|
|
336
|
-
s.approvedPatchIds = undefined; // the chain ended: nothing queued is still approved
|
|
337
337
|
s.consecutiveErrors = (s.consecutiveErrors ?? 0) + 1;
|
|
338
338
|
const lastReasons = s.lastReview?.verdict === "reject" || s.lastReview?.verdict === "reverted" ? s.lastReview.reasons : undefined;
|
|
339
339
|
s.lastError = `review rejected: ${firstReason(lastReasons).slice(0, 200)}`;
|
|
@@ -369,9 +369,6 @@ export function applyMergeOutcome(s, result, change) {
|
|
|
369
369
|
s.mergeCheckFailures = undefined;
|
|
370
370
|
s.mergeFlakeExcused = undefined;
|
|
371
371
|
}
|
|
372
|
-
// A permanent-reviewer-config hold ends with its pin's life: merged, or rejected away.
|
|
373
|
-
if (changeGone)
|
|
374
|
-
s.mergeReviewError = undefined;
|
|
375
372
|
// A hand-back ends with its change's life too: once the resolved change merges or is rejected,
|
|
376
373
|
// a later change gets its own one-hand-back budget (PLANS.md "Robust conflict landing, part
|
|
377
374
|
// 2/2"). A merge_conflict keeps it, so the resolved change's own cap discard fires.
|
|
@@ -46,45 +46,59 @@ export function readRunDetail(root, role, runNumber) {
|
|
|
46
46
|
// same bound.
|
|
47
47
|
const all = readEvents(root, HISTORY_SCAN_MAX_EVENTS);
|
|
48
48
|
const events = all.filter((e) => e.source === role);
|
|
49
|
-
|
|
50
|
-
let startIdx = -1;
|
|
51
|
-
for (let i = events.length - 1; i >= 0; i--) {
|
|
52
|
-
const e = events[i];
|
|
53
|
-
if (e.type === "run_start" && e.run === runNumber) {
|
|
54
|
-
startIdx = i;
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
49
|
+
const startIdx = newestRunStartIndex(events, runNumber);
|
|
58
50
|
if (startIdx < 0) {
|
|
59
51
|
// The start is gone (rotation) or the run never ran. A surviving run_end for the same
|
|
60
52
|
// number still answers — unpaired, undated at the front — else there is no such run here.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return detailFrom(root, role, runNumber, all, events, [e], e, null);
|
|
65
|
-
}
|
|
53
|
+
const loneEnd = newestRunEnd(events, runNumber);
|
|
54
|
+
if (loneEnd !== null)
|
|
55
|
+
return detailFrom(root, role, runNumber, all, events, [loneEnd], loneEnd, null);
|
|
66
56
|
return null;
|
|
67
57
|
}
|
|
68
58
|
const start = events[startIdx];
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
59
|
+
const { end, nextStart } = runBlockBounds(events, startIdx, runNumber);
|
|
60
|
+
const block = collectRunBlock(events, startIdx, end, nextStart);
|
|
61
|
+
return detailFrom(root, role, runNumber, all, events, block, end, start);
|
|
62
|
+
}
|
|
63
|
+
/** The newest run_start for this role+run — the block's lower bound — or -1 when none (the
|
|
64
|
+
* start rotated out or the run never ran). A backward scan: a counter reset can repeat a run
|
|
65
|
+
* number, and the operator asking by number means the most recent one. */
|
|
66
|
+
function newestRunStartIndex(events, runNumber) {
|
|
67
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
68
|
+
const e = events[i];
|
|
69
|
+
if (e.type === "run_start" && e.run === runNumber)
|
|
70
|
+
return i;
|
|
71
|
+
}
|
|
72
|
+
return -1;
|
|
73
|
+
}
|
|
74
|
+
/** The newest surviving run_end for this role+run, or null. Read only on the no-start path:
|
|
75
|
+
* the start is gone (rotation) or the run never ran, but a surviving run_end still answers —
|
|
76
|
+
* unpaired, undated at the front — else there is no such run here. */
|
|
77
|
+
function newestRunEnd(events, runNumber) {
|
|
78
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
79
|
+
const e = events[i];
|
|
80
|
+
if (e.type === "run_end" && e.run === runNumber)
|
|
81
|
+
return e;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
/** The block's upper bound for the run opened by `startIdx`: the first run_end for this run
|
|
86
|
+
* after it ends the block; else a later run_start (this run's end lost to rotation) closes it
|
|
87
|
+
* exclusive — the events after belong to the next run, not this one. Neither afterwards reads
|
|
88
|
+
* as in flight: everything since the start belongs to the open run. */
|
|
89
|
+
function runBlockBounds(events, startIdx, runNumber) {
|
|
75
90
|
for (let i = startIdx + 1; i < events.length; i++) {
|
|
76
91
|
const e = events[i];
|
|
77
|
-
if (e.type === "run_end" && e.run === runNumber)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (e.type === "run_start") {
|
|
82
|
-
nextStart = e;
|
|
83
|
-
break;
|
|
84
|
-
}
|
|
92
|
+
if (e.type === "run_end" && e.run === runNumber)
|
|
93
|
+
return { end: e, nextStart: null };
|
|
94
|
+
if (e.type === "run_start")
|
|
95
|
+
return { end: null, nextStart: e };
|
|
85
96
|
}
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
return { end: null, nextStart: null };
|
|
98
|
+
}
|
|
99
|
+
/** The block itself: the start event plus everything this role logged up to the bound —
|
|
100
|
+
* inclusive at a paired end, exclusive at a next run's start. */
|
|
101
|
+
function collectRunBlock(events, startIdx, end, nextStart) {
|
|
88
102
|
const block = [];
|
|
89
103
|
for (let i = startIdx; i < events.length; i++) {
|
|
90
104
|
const e = events[i];
|
|
@@ -92,19 +106,12 @@ export function readRunDetail(root, role, runNumber) {
|
|
|
92
106
|
break;
|
|
93
107
|
block.push(e);
|
|
94
108
|
}
|
|
95
|
-
return
|
|
109
|
+
return block;
|
|
96
110
|
}
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
function
|
|
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);
|
|
111
|
+
/** The verdict a run reports: the `run_end`'s outcome text, resolved through its merge for a
|
|
112
|
+
* run that ended `queued` (its result becomes the merge's result and `merge` the merge_done/
|
|
113
|
+
* merge_failed event carrying it) — the same fresh claim cursor walk the history rows use. */
|
|
114
|
+
function runResultOf(end, scoped) {
|
|
108
115
|
let result = end === null ? null : outcomeText(end);
|
|
109
116
|
let merge = null;
|
|
110
117
|
if (end !== null && result === "queued") {
|
|
@@ -117,42 +124,88 @@ function detailFrom(root, role, runNumber, all, scoped, block, end, start) {
|
|
|
117
124
|
merge = resolved.outcome;
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
return { result, merge };
|
|
128
|
+
}
|
|
129
|
+
/** The token and cost totals from the run_end; both zero on an in-flight run. */
|
|
130
|
+
function runUsageOf(end) {
|
|
131
|
+
if (end === null)
|
|
132
|
+
return { tokens: 0, costUsd: 0 };
|
|
133
|
+
const { tokens, costUsd } = eventUsage(end);
|
|
134
|
+
return { tokens, costUsd };
|
|
135
|
+
}
|
|
136
|
+
/** The run's start/end timestamps and its span: `startTs`/`endTs` null when rotation cut that
|
|
137
|
+
* side, `durationMs` null whenever the pair is incomplete (no fabricated span). */
|
|
138
|
+
function runTimesOf(start, end, scoped) {
|
|
139
|
+
return {
|
|
124
140
|
startTs: start === null ? null : start.ts,
|
|
125
|
-
model: typeof start?.model === "string" && start.model.length > 0 ? start.model : null,
|
|
126
141
|
endTs: end === null ? null : end.ts,
|
|
127
142
|
durationMs: end === null ? null : runSpanMs(end, runStartMap(scoped)),
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** The model the run's `run_start` named, or null when it carried none or sits outside the scan. */
|
|
146
|
+
function runModelOf(start) {
|
|
147
|
+
return typeof start?.model === "string" && start.model.length > 0 ? start.model : null;
|
|
148
|
+
}
|
|
149
|
+
/** The run's usage as its header line; empty when the run carries neither total. */
|
|
150
|
+
function runUsageTextOf(end) {
|
|
151
|
+
return end === null ? "" : usageText(end);
|
|
152
|
+
}
|
|
153
|
+
/** The run_end's prompt-token total and its cache-read and pre-edit subsets, corrupt-field-
|
|
154
|
+
* guarded reads (nonNegativeNumber) with the subsets clamped to the total so the rendered share
|
|
155
|
+
* never exceeds 100%; all three keys absent on a prompt-free or in-flight run. */
|
|
156
|
+
function runPromptFields(end) {
|
|
157
|
+
const promptTokens = end === null ? 0 : nonNegativeNumber(end.promptTokens, 0);
|
|
158
|
+
const cacheReadTokens = end === null ? 0 : Math.min(nonNegativeNumber(end.cacheReadTokens, 0), promptTokens);
|
|
159
|
+
const preEditPromptTokens = end === null ? 0 : Math.min(nonNegativeNumber(end.preEditPromptTokens, 0), promptTokens);
|
|
160
|
+
return {
|
|
140
161
|
...(promptTokens > 0 ? { promptTokens } : {}),
|
|
141
162
|
...(cacheReadTokens > 0 ? { cacheReadTokens } : {}),
|
|
142
163
|
...(preEditPromptTokens > 0 ? { preEditPromptTokens } : {}),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** The run_end's ceiling-truncation flag, absent when the run did not cut off. */
|
|
167
|
+
function runCutOffField(end) {
|
|
168
|
+
return end !== null && end.cutOff === true ? { cutOff: true } : {};
|
|
169
|
+
}
|
|
170
|
+
/** The run's peak context, a corrupt-field-guarded read of the run_end; 0 when in flight. */
|
|
171
|
+
function runPeakContextOf(end) {
|
|
172
|
+
return end === null ? 0 : nonNegativeNumber(end.peakContextTokens, 0);
|
|
173
|
+
}
|
|
174
|
+
/** The end-of-run → on-main landing latency (the landing `merged` event's ts minus the run
|
|
175
|
+
* end's), absent without both an end and a landing event — or a landing before the end, or a
|
|
176
|
+
* negative difference. */
|
|
177
|
+
function runMergeLatencyField(end, mergedEvent) {
|
|
178
|
+
return end !== null && mergedEvent !== null && mergedEvent.ts - end.ts >= 0
|
|
179
|
+
? { mergeAfterRunMs: mergedEvent.ts - end.ts }
|
|
180
|
+
: {};
|
|
181
|
+
}
|
|
182
|
+
/** Fill the payload around a collected block: each concern resolves in its own named helper
|
|
183
|
+
* (runResultOf, runUsageOf, runTimesOf, runModelOf, runUsageTextOf, runPromptFields,
|
|
184
|
+
* runCutOffField, runPeakContextOf, runMergeLatencyField), and this assembles their results.
|
|
185
|
+
* Events stay oldest-first; usage/duration pair through the shared event-read helpers so this
|
|
186
|
+
* view and the history rows cannot drift on what a span is. */
|
|
187
|
+
function detailFrom(root, role, runNumber, all, scoped, block, end, start) {
|
|
188
|
+
const { result, merge } = runResultOf(end, scoped);
|
|
189
|
+
const mergedEvent = mergedEventOf(result, merge, scoped, block);
|
|
190
|
+
const detail = {
|
|
191
|
+
role,
|
|
192
|
+
run: runNumber,
|
|
193
|
+
...runTimesOf(start, end, scoped),
|
|
194
|
+
model: runModelOf(start),
|
|
195
|
+
...runUsageOf(end),
|
|
196
|
+
usage: runUsageTextOf(end),
|
|
197
|
+
result,
|
|
198
|
+
// The window comes from the shared seamContextWindowForRole — same seam the role view and
|
|
199
|
+
// status table print — with loadConfigSafe falling back to defaults, so a broken
|
|
200
|
+
// tumwater.json yields a null window rather than failing this read-only view.
|
|
201
|
+
peakContextTokens: runPeakContextOf(end),
|
|
202
|
+
...runPromptFields(end),
|
|
203
|
+
...runCutOffField(end),
|
|
146
204
|
contextWindow: seamContextWindowForRole(loadConfigSafe(root).config ?? defaultConfig(), role, root),
|
|
147
205
|
events: block,
|
|
148
206
|
merge,
|
|
149
207
|
mergedSha: mergedShaOf(result, merge, scoped, block),
|
|
150
|
-
|
|
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
|
-
: {}),
|
|
208
|
+
...runMergeLatencyField(end, mergedEvent),
|
|
156
209
|
reverted: null,
|
|
157
210
|
};
|
|
158
211
|
detail.reverted = revertedOf(detail, all);
|