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
|
@@ -6,15 +6,12 @@ import { logEventBestEffort, warnEventBestEffort } from "../events/events.js";
|
|
|
6
6
|
import { promptPreview } from "../inbox/inbox.js";
|
|
7
7
|
import { assembleRunPrompt } from "../run/run-prompt.js";
|
|
8
8
|
import { RunnerPi } from "./runner-pi.js";
|
|
9
|
-
import { configForRole, seamHoldKey, } from "../config/config-views.js";
|
|
10
|
-
import { fallbackRoleConfig } from "../config/config-fallbacks.js";
|
|
9
|
+
import { configForRole, seamHoldKey, withModelOverrides, } from "../config/config-views.js";
|
|
11
10
|
import { PendingPrompt } from "../inbox/pending-prompt.js";
|
|
12
|
-
import { modelFallbackActive } from "./model-fallback.js";
|
|
13
11
|
import { clearBackoff } from "../scheduling/backoff.js";
|
|
14
12
|
import { RunUsage } from "../run/run-usage.js";
|
|
15
13
|
import { runOncePhase, runPhase, } from "./runner-run.js";
|
|
16
14
|
import { finishAbortedRunPhase, finishRecoveryRunPhase, pinAndResetPhase, } from "./runner-recovery.js";
|
|
17
|
-
import { foldModelFallbackPhase } from "./runner-fallback.js";
|
|
18
15
|
/** One role runner: owns a persistent worktree + branch and runs one run at a time. */
|
|
19
16
|
export class RoleRunner {
|
|
20
17
|
root;
|
|
@@ -73,6 +70,13 @@ export class RoleRunner {
|
|
|
73
70
|
* resolve to, since different roles may run on different pairs.
|
|
74
71
|
* Transient — never persisted — and captured fresh at every run start. */
|
|
75
72
|
runPair;
|
|
73
|
+
/** Whether the budget gate currently has the team on its free fallback pairs (src/gates/
|
|
74
|
+
* budget-gates.ts onFallback, set alongside `config` by the orchestrator's poll): an
|
|
75
|
+
* image reroute may then look only among the free fallback pairs, never a priced primary
|
|
76
|
+
* (plans/image-input.md "Reroute target" "Regime"). Pure scheduler input, never whether
|
|
77
|
+
* the runner itself is mid-fallback; defaults false, so standalone-runner tests stay on the
|
|
78
|
+
* primary regime unless they set it. */
|
|
79
|
+
budgetFallbackActive = false;
|
|
76
80
|
/** The merge failure this run's leftover recovery is retrying, if any (a retriable merger
|
|
77
81
|
* outcome that kept the pin, which recovery re-queued): set in finishRecoveryRun and
|
|
78
82
|
* attached to the returned RunOutcome so applyRunOutcome can feed it into the error streak
|
|
@@ -117,7 +121,6 @@ export class RoleRunner {
|
|
|
117
121
|
this.state.resumePending = true;
|
|
118
122
|
this.state.running = false;
|
|
119
123
|
this.state.parkedSince = undefined;
|
|
120
|
-
this.state.parkedOn = undefined;
|
|
121
124
|
}
|
|
122
125
|
save() {
|
|
123
126
|
saveRunnerState(this.root, this.state);
|
|
@@ -174,22 +177,39 @@ export class RoleRunner {
|
|
|
174
177
|
runModelPair() {
|
|
175
178
|
return this.state.running ? (this.runPair ?? null) : null;
|
|
176
179
|
}
|
|
177
|
-
/**
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
/** Which pair set an image reroute may look among (plans/image-input.md "Reroute target"
|
|
181
|
+
* "Regime"): the free fallback pairs while the budget gate is on fallback; otherwise the
|
|
182
|
+
* primary tiers. One home for the rule, shared by the target-resolution sites (the verdict's
|
|
183
|
+
* reroute arm, the attachment preflight). */
|
|
184
|
+
rerouteRegime() {
|
|
185
|
+
return this.budgetFallbackActive ? "fallback" : "primary";
|
|
186
|
+
}
|
|
187
|
+
/** The config this role's NEXT run will run on: a pending image reroute's target, else the
|
|
188
|
+
* role's own resolved config (configForRole). The hold and start passes read this so a role
|
|
189
|
+
* is keyed by the provider it will use. The budget gate's fallback pair, when a cap episode
|
|
190
|
+
* is active, is installed into the live config itself, so this read already names the pair
|
|
191
|
+
* the run uses. */
|
|
192
|
+
runConfig() {
|
|
193
|
+
// A pending image-input reroute (plans/image-input.md part 4/5) outranks the role's own
|
|
194
|
+
// tier: the next run starts on the image-capable target the blind-read run recorded,
|
|
195
|
+
// rebuilt from the stored seam over the live config.
|
|
196
|
+
if (this.state.imageReroute) {
|
|
197
|
+
const r = this.state.imageReroute;
|
|
198
|
+
return withModelOverrides(this.config, r.tier, {
|
|
199
|
+
backend: r.backend,
|
|
200
|
+
provider: r.provider,
|
|
201
|
+
model: r.model,
|
|
202
|
+
thinking: r.thinking,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
185
205
|
return configForRole(this.config, this.role);
|
|
186
206
|
}
|
|
187
|
-
/** The seam key `runConfig(
|
|
207
|
+
/** The seam key `runConfig()` resolves to (config-views.ts seamHoldKey): the agent id
|
|
188
208
|
* for a non-pi backend (claude seams carry no pi provider), else the pi provider. The hold
|
|
189
209
|
* and start passes read only this, so a claude seam is gated by a `claude` hold and pi's
|
|
190
210
|
* default seams (undefined) are not. */
|
|
191
|
-
runProvider(
|
|
192
|
-
return seamHoldKey(this.runConfig(
|
|
211
|
+
runProvider() {
|
|
212
|
+
return seamHoldKey(this.runConfig());
|
|
193
213
|
}
|
|
194
214
|
/** The signal every pi run of the current run watches: harness shutdown OR a per-run user
|
|
195
215
|
* abort (Node ≥ 20's AbortSignal.any). */
|
|
@@ -258,19 +278,15 @@ export class RoleRunner {
|
|
|
258
278
|
this.usage.fold(this.state, run, false, provider, pair);
|
|
259
279
|
}
|
|
260
280
|
/** Run one full run of this role runner (the pipeline lives in src/runner/runner-run.ts):
|
|
261
|
-
* `runPhase` resolves the
|
|
281
|
+
* `runPhase` resolves the role's own config, logs run_start, runs the pipeline,
|
|
262
282
|
* and finalizes. Never throws: a failed run is an "error" result, saved and logged. */
|
|
263
283
|
async run() {
|
|
264
284
|
return runPhase(this.runContext());
|
|
265
285
|
}
|
|
266
|
-
/** Thin delegate to foldModelFallbackPhase (src/runner/runner-fallback.ts). */
|
|
267
|
-
foldModelFallback(fallbackCtx, pi) {
|
|
268
|
-
foldModelFallbackPhase(this.runContext(), fallbackCtx, pi);
|
|
269
|
-
}
|
|
270
286
|
/** The run pipeline's entry (src/runner/runner-run.ts): a thin delegate so `run` assembles
|
|
271
287
|
* phase context once and tests can still override `runOnce` to bypass the pipeline. */
|
|
272
|
-
async runOnce(cfg
|
|
273
|
-
return runOncePhase(this.runContext(), cfg
|
|
288
|
+
async runOnce(cfg) {
|
|
289
|
+
return runOncePhase(this.runContext(), cfg);
|
|
274
290
|
}
|
|
275
291
|
/** Assemble the run pipeline's host context (src/runner/runner-run.ts) from this runner's
|
|
276
292
|
* fields. `config` and `recoveryFailure` are live getters, so a mid-run change is seen as
|
|
@@ -293,13 +309,14 @@ export class RoleRunner {
|
|
|
293
309
|
warn: (message) => this.warn(message),
|
|
294
310
|
runSignal: () => this.runSignal(),
|
|
295
311
|
runPrompt: (backend) => this.runPrompt(backend),
|
|
312
|
+
rerouteRegime: () => this.rerouteRegime(),
|
|
296
313
|
get recoveryFailure() {
|
|
297
314
|
return self.recoveryFailure;
|
|
298
315
|
},
|
|
299
316
|
setRecoveryFailure: (message) => {
|
|
300
317
|
self.recoveryFailure = message;
|
|
301
318
|
},
|
|
302
|
-
runOnce: (cfg
|
|
319
|
+
runOnce: (cfg) => this.runOnce(cfg),
|
|
303
320
|
setRunPair: (pair) => {
|
|
304
321
|
self.runPair = pair;
|
|
305
322
|
},
|
|
@@ -312,7 +329,6 @@ export class RoleRunner {
|
|
|
312
329
|
},
|
|
313
330
|
finishAbortedRun: (userPrompt, wt) => this.finishAbortedRun(userPrompt, wt),
|
|
314
331
|
pinAndReset: (wt, sha) => this.pinAndReset(wt, sha),
|
|
315
|
-
foldModelFallback: (fallbackCtx, pi) => this.foldModelFallback(fallbackCtx, pi),
|
|
316
332
|
finishRecoveryRun: (recovered, userPrompt, wt, priorMergeFailure) => this.finishRecoveryRun(recovered, userPrompt, wt, priorMergeFailure),
|
|
317
333
|
};
|
|
318
334
|
}
|
|
@@ -68,6 +68,11 @@ export function pushYieldOutcome(s, result) {
|
|
|
68
68
|
s.recentOutcomes = ((s.recentOutcomes ?? "") + ch).slice(-YIELD_RING);
|
|
69
69
|
}
|
|
70
70
|
const YIELD_MAX = 8;
|
|
71
|
+
/** How many recent counted runs the reset window spans — a merge anywhere in the last 10
|
|
72
|
+
* keeps the multiplier at 1, and the same 10 empties are what start the doubling. */
|
|
73
|
+
const YIELD_RESET_RECENT = 10;
|
|
74
|
+
/** How many further empty runs double the multiplier again (10→×2, 15→×4, 20→×8). */
|
|
75
|
+
const YIELD_DOUBLE_STEP = 5;
|
|
71
76
|
/** The multiplier on a scalable role's minRunIntervalSeconds gap its recent yield earns:
|
|
72
77
|
* 1 while any of the last 10 counted runs merged, otherwise doubling per 5 further empty
|
|
73
78
|
* runs — 10 empties ×2, 15 ×4, 20 ×8 — capped at 8. `recent` is the ring's chars, oldest
|
|
@@ -76,12 +81,12 @@ const YIELD_MAX = 8;
|
|
|
76
81
|
* the ring: one merge is the evidence the role's clock should trust, and the ring's older
|
|
77
82
|
* half only matters once the merge has aged out of the recent window. */
|
|
78
83
|
export function yieldMultiplier(recent) {
|
|
79
|
-
if (recent.slice(-
|
|
84
|
+
if (recent.slice(-YIELD_RESET_RECENT).includes(YIELD_MERGE))
|
|
80
85
|
return 1;
|
|
81
86
|
const empty = recent.filter((c) => c !== YIELD_MERGE).length;
|
|
82
|
-
if (empty <
|
|
87
|
+
if (empty < YIELD_RESET_RECENT)
|
|
83
88
|
return 1;
|
|
84
|
-
return Math.min(YIELD_MAX, 2 ** (Math.floor((empty -
|
|
89
|
+
return Math.min(YIELD_MAX, 2 ** (Math.floor((empty - YIELD_RESET_RECENT) / YIELD_DOUBLE_STEP) + 1));
|
|
85
90
|
}
|
|
86
91
|
/** The yield multiplier a runner's state earns: the role's recent-yield multiplier when
|
|
87
92
|
* yieldScaledRole(role) holds, else 1 (a role outside the scalable set keeps its plain gap
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* backlog through `loadRunnerState` / `listedKeys`, never mutates. */
|
|
9
9
|
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
10
10
|
import { CLAIM_IDLE_MAX_MS, listedKeys } from "./claims.js";
|
|
11
|
+
import { HOUR_MS } from "../text/datetime.js";
|
|
11
12
|
/** The claim states among `ids` that an operator should act on, in the given order: a claim
|
|
12
13
|
* whose key is absent from its role's backlog section is `gone` (0 hours, and gone wins over
|
|
13
14
|
* idle for the same claim); otherwise a claim older than `CLAIM_IDLE_MAX_MS` is `idle` with
|
|
@@ -29,7 +30,7 @@ export function staleClaimEntries(root, ids, now = Date.now()) {
|
|
|
29
30
|
file: claim.file,
|
|
30
31
|
title: claim.title,
|
|
31
32
|
kind: "idle",
|
|
32
|
-
hours: Math.round((now - claim.at) /
|
|
33
|
+
hours: Math.round((now - claim.at) / HOUR_MS),
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -152,10 +152,6 @@ export function workReserveCount(root, runners, opts) {
|
|
|
152
152
|
continue;
|
|
153
153
|
if (r.state.running || opts.queuedMergeRoles.has(r.role))
|
|
154
154
|
continue;
|
|
155
|
-
// A work role whose provider is full (capped and at its cap) cannot use a reserved permit
|
|
156
|
-
// anyway, so it must not withhold one from another provider's maintenance run (§6).
|
|
157
|
-
if (opts.providerHasHeadroom !== undefined && !opts.providerHasHeadroom(r))
|
|
158
|
-
continue;
|
|
159
155
|
const eligible = eligibleFor(base);
|
|
160
156
|
const holdsClaim = r.state.claim !== undefined && eligible.has(r.state.claim.key);
|
|
161
157
|
const hasFreeEntry = [...eligible].some((k) => !(heldByBase.get(base)?.has(k) ?? false));
|
|
@@ -13,91 +13,78 @@ import { BUGFIX_ROLE, DEFERRABLE_ROLES, DIRECTOR_ROLE, roleTier, } from "../role
|
|
|
13
13
|
import { baseRoleOf, runnerEnabled } from "../roles/runner-ids.js";
|
|
14
14
|
import { HOUR_MS } from "../text/datetime.js";
|
|
15
15
|
import { yieldMultiplierFor } from "./backoff.js";
|
|
16
|
-
/**
|
|
17
|
-
* (
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// An interrupted run (graceful abort or crash) leaves half-finished work in its pi
|
|
37
|
-
// session and worktree: resume it promptly on restart instead of holding it for a full
|
|
38
|
-
// interval — the min gap below throttles scheduled runs, not recovery. nextRunAt still
|
|
39
|
-
// gates cut-off resumes, which deliberately wait one interval from their compacted context.
|
|
40
|
-
// A queued prompt never preempts this gate: a resuming run assembles buildResumePrompt
|
|
41
|
-
// and does not dequeue the per-role queue, so an early run would only resume sooner than
|
|
42
|
-
// its deliberate wait — the prompt is consumed by the fresh run the inbox check below
|
|
43
|
-
// makes due the moment the resume finishes.
|
|
16
|
+
/** Can the runner start at all right now? A role disabled in tumwater.json stops running
|
|
17
|
+
* immediately (live-reload; re-enabling resumes within one poll cycle because the runner and
|
|
18
|
+
* its persisted state survive), and an already-running role is never eligible. Either way the
|
|
19
|
+
* runner holds this poll — the single gate behind isEligible's two early returns. */
|
|
20
|
+
function blockedFromRunning(runner, s) {
|
|
21
|
+
return !runnerEnabled(runner.config, runner.role) || s.running === true;
|
|
22
|
+
}
|
|
23
|
+
/** The director carries the user's own requests: no min-gap, no backoff — a queued prompt
|
|
24
|
+
* runs as soon as the previous one finishes. Reaches either arm and always resolves. */
|
|
25
|
+
function directorGate(s, now, inboxCount, opts) {
|
|
26
|
+
if (inboxCount > 0)
|
|
27
|
+
return { run: true, reason: "inbox" };
|
|
28
|
+
if (opts.leftoverPending)
|
|
29
|
+
return now >= s.nextRunAt ? { run: true, reason: "leftover" } : { run: false };
|
|
30
|
+
return { run: false };
|
|
31
|
+
}
|
|
32
|
+
/** An interrupted run (graceful abort or crash) resumes promptly on restart; nextRunAt still
|
|
33
|
+
* gates cut-off resumes, which deliberately wait one interval. A queued prompt never preempts
|
|
34
|
+
* this gate. Resolves only when a resume is pending, else undefined. */
|
|
35
|
+
function resumeGate(s, now) {
|
|
44
36
|
if (s.resumePending) {
|
|
45
37
|
return now >= s.nextRunAt ? { run: true, reason: "resume" } : { run: false };
|
|
46
38
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// after the run ends.
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
/** A queued per-role prompt is due on its own existence, exactly like the director's inbox: no
|
|
42
|
+
* min-gap, no backoff, no scheduled clock. The queue is the durable record of the demand. */
|
|
43
|
+
function inboxGate(inboxCount) {
|
|
53
44
|
if (inboxCount > 0)
|
|
54
45
|
return { run: true, reason: "inbox" };
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// multiplied by the multiplier its recent results earned — a search role that keeps
|
|
62
|
-
// finding nothing slows down, and the multiplication gates the "main moved" wake below
|
|
63
|
-
// too, which is where the cost actually comes from (the gap check above runs first, so a
|
|
64
|
-
// scaled role inside its stretched gap neither runs on schedule nor on a main move).
|
|
65
|
-
// One merge in the last ten counted runs restores the plain gap; error-class results
|
|
66
|
-
// never enter the ring (backoff.ts), so a failing backend does not stretch anything;
|
|
67
|
-
// `--once`, an operator wake, and a queued prompt all bypass the gap check entirely.
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
/** The per-role interval (a slow clock) gates scheduled runs and "main moved" early wakes.
|
|
49
|
+
* Yield scaling multiplies the gap; an operator wake overrides it. Resolves with the hold when
|
|
50
|
+
* the runner is inside the (possibly stretched) gap, else undefined. */
|
|
51
|
+
function gapGate(runner, s, now, opts) {
|
|
68
52
|
const mult = yieldMultiplierFor(runner.state);
|
|
69
|
-
// The same planBacklogOpen rule (scheduleConfigForRole) runner-run.ts schedules nextRunAt
|
|
70
|
-
// from: while PLANS.md has planning room, plan runs on a zero gap; once the target is met it
|
|
71
|
-
// drops to its 3600 s default (the plans-full deferral then holds the run). Yield scaling
|
|
72
|
-
// still applies to whatever gap is chosen.
|
|
73
53
|
const minGapSeconds = scheduleConfigForRole(runner.config, runner.role, opts.planBacklogOpen === true).minRunIntervalSeconds;
|
|
74
54
|
const minGap = opts.once ? 0 : minGapSeconds * 1000 * mult;
|
|
75
55
|
const sinceLast = now - (s.lastRunEndedAt ?? 0);
|
|
76
|
-
// An operator wake newer than the gap window's opening run overrides the interval
|
|
77
|
-
// (RunnerState.wokenAt): an explicit "try again now" with an empty queue must not silently
|
|
78
|
-
// wait out the remaining slow clock it ran inside — a queued prompt carries its own
|
|
79
|
-
// due-ness in the inbox check above, so it never depends on this stamp surviving.
|
|
80
56
|
const woken = freshlyWoken(s);
|
|
81
57
|
if (sinceLast < minGap && !woken) {
|
|
82
|
-
// Held inside the stretched gap: the runner is not due this poll (a schedule clock with
|
|
83
|
-
// the plain gap, or a main move under the multiplier, would otherwise admit it). Report
|
|
84
|
-
// the yield-scaled hold so the phase cell reads "waiting out its gap" instead of the
|
|
85
|
-
// bare "due — starts on the next poll" of the no-reason fallback (RunnerState.skipReason).
|
|
86
58
|
return mult > 1 ? { run: false, skip: "yield-gap" } : { run: false };
|
|
87
59
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
/** The scheduled clock, then the "main moved" wake. Once mode overrides the clock when
|
|
63
|
+
* backoffSeconds is 0 (a productive run scheduled it); a raised backoff honors the deadline.
|
|
64
|
+
* Resolves only once a scheduled reason applies, else undefined. */
|
|
65
|
+
function scheduledGate(s, now, mainHead, opts) {
|
|
91
66
|
if (now >= s.nextRunAt || (opts.once && s.backoffSeconds === 0)) {
|
|
92
67
|
return { run: true, reason: s.runs === 0 ? "startup" : "scheduled" };
|
|
93
68
|
}
|
|
94
|
-
// The world changed under a sleeping role: main moved since its last run. The stretched
|
|
95
|
-
// gap above gates this wake too (the gap check runs first), so a role whose recent runs
|
|
96
|
-
// merged nothing does not re-check on every merge — the multiplier's whole point.
|
|
97
69
|
if (s.lastMainHead && mainHead && mainHead !== s.lastMainHead) {
|
|
98
70
|
return { run: true, reason: "main moved" };
|
|
99
71
|
}
|
|
100
|
-
return
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
/** Should this role run now? `inboxCount` is the calling role's own queued-prompt count
|
|
75
|
+
* (the director's queue for the director, the per-role queue for everyone else) — see the
|
|
76
|
+
* inbox check below. Exported for tests. */
|
|
77
|
+
export function isEligible(runner, now, mainHead, inboxCount, opts = {}) {
|
|
78
|
+
const s = runner.state;
|
|
79
|
+
if (blockedFromRunning(runner, s))
|
|
80
|
+
return { run: false };
|
|
81
|
+
if (runner.role === DIRECTOR_ROLE)
|
|
82
|
+
return directorGate(s, now, inboxCount, opts);
|
|
83
|
+
return (resumeGate(s, now) ??
|
|
84
|
+
inboxGate(inboxCount) ??
|
|
85
|
+
gapGate(runner, s, now, opts) ??
|
|
86
|
+
scheduledGate(s, now, mainHead, opts) ??
|
|
87
|
+
{ run: false });
|
|
101
88
|
}
|
|
102
89
|
/** Fair scheduling order for one poll's eligible roles: the director always leads (it runs
|
|
103
90
|
* the user's prompts), then the work tier (feature/bugfix/plan) before maintenance — a long-idle
|
|
@@ -1,46 +1,11 @@
|
|
|
1
1
|
import { budgetReached, teamDailyCost, modelPairName, projectCapHit } from "../budget/budget.js";
|
|
2
2
|
import { roleCapVerdicts } from "../gates/role-cap-gates.js";
|
|
3
|
-
import {
|
|
3
|
+
import { roleSeamTier } from "../config/config-views.js";
|
|
4
4
|
import { resolveTierFallbacks, tierFallbackLabels, tiersResolveDistinctPairs } from "../config/config-fallbacks.js";
|
|
5
|
-
import {
|
|
6
|
-
import { fallbackModelFree, piPairFree, readPiProviders, seamFree, teamModelsFree } from "../pi/pi-models.js";
|
|
5
|
+
import { fallbackModelFree, piPairFree, readPiProviders, teamModelsFree } from "../pi/pi-models.js";
|
|
7
6
|
import { readOpencodeCache } from "../agent/opencode/opencode-catalog.js";
|
|
8
|
-
import {
|
|
9
|
-
import { compareWindowEntries, readAccount, windowEntries, } from "../agent/subscription-account.js";
|
|
7
|
+
import { readAccount } from "../agent/subscription-account.js";
|
|
10
8
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
11
|
-
/** The subscription-window blocks and the team-global free-ness answer for a poll (see
|
|
12
|
-
* StatusSnapshot.subscriptionWindows / subscriptionOnly): one `SubscriptionWindowBlock` per
|
|
13
|
-
* distinct non-`pi` subscription backend the enabled seams (roles, review, resolver) route to
|
|
14
|
-
* whose `accounts(backend)` reads subscription billing with at least one window. `only` is true
|
|
15
|
-
* exactly when every paying seam is free (judged by its own backend's account or pi pair
|
|
16
|
-
* price), so the display knows whether the windows replace or merely join the daily spend. The
|
|
17
|
-
* blocks sort most-constrained-first by the same comparator windowEntries uses for one
|
|
18
|
-
* account's windows (then backend), so a team's tightest window always leads. For a claude-only
|
|
19
|
-
* team this is one claude block whose fields match the old claudeWindow block's, aside from the
|
|
20
|
-
* added backend/label. The reviewer and the strong-tier resolver are included, like
|
|
21
|
-
* teamModelsFree, since they can spend. */
|
|
22
|
-
function windowBlocks(cfg, providers, accounts, catalog) {
|
|
23
|
-
const seams = enabledRoleIds(cfg).map((role) => configForRole(cfg, role));
|
|
24
|
-
if (cfg.review.enabled)
|
|
25
|
-
seams.push(reviewConfig(cfg));
|
|
26
|
-
if (seams.length > 0)
|
|
27
|
-
seams.push(resolverConfig(cfg));
|
|
28
|
-
const blocks = [];
|
|
29
|
-
for (const backend of new Set(seams.map((s) => s.agent).filter((b) => b !== undefined && b !== "pi"))) {
|
|
30
|
-
const account = accounts(backend);
|
|
31
|
-
if (account?.billing !== "subscription")
|
|
32
|
-
continue;
|
|
33
|
-
const windows = windowEntries(account);
|
|
34
|
-
if (windows.length === 0)
|
|
35
|
-
continue;
|
|
36
|
-
const heldUntil = windows
|
|
37
|
-
.filter((w) => w.status === "rejected" && w.isUsingOverage !== true && typeof w.resetsAt === "number")
|
|
38
|
-
.reduce((max, w) => Math.max(max, w.resetsAt), 0) || null;
|
|
39
|
-
blocks.push({ backend, label: backendDisplayName(backend), windows, primary: windows[0], heldUntil });
|
|
40
|
-
}
|
|
41
|
-
blocks.sort((a, b) => compareWindowEntries(a.primary, b.primary) || a.backend.localeCompare(b.backend));
|
|
42
|
-
return { blocks, only: !seams.some((s) => !seamFree(providers, s, accounts, catalog)) };
|
|
43
|
-
}
|
|
44
9
|
/** The spend/billing block for a poll: the one materialized `spentUsd` (the scheduler's own
|
|
45
10
|
* published figure while running — BUGS.md 2026-09-30 — the persisted states' sum otherwise)
|
|
46
11
|
* that the budget object, the burn-rate projection and `free`/`fallback`/`tiers` all read;
|
|
@@ -49,7 +14,7 @@ function windowBlocks(cfg, providers, accounts, catalog) {
|
|
|
49
14
|
* (piPairFree, fallbackModelFree, teamModelsFree, resolveTierFallbacks) so it has one home:
|
|
50
15
|
* models.json is read once (readPiProviders) and each subscription backend's account once
|
|
51
16
|
* (readAccount, on the snapshot's own pinned `now`, via the per-poll memo), and every free-ness
|
|
52
|
-
* verdict
|
|
17
|
+
* verdict judges those same readings. The director is exempt from both
|
|
53
18
|
* pause sets, exactly as the gates exempt it. */
|
|
54
19
|
export function collectBudget(root, cfg, rows, roles, running, info, modelsPath, now) {
|
|
55
20
|
// The running orchestrator's published gate figures (read once above, with the pid and the
|
|
@@ -80,8 +45,6 @@ export function collectBudget(root, cfg, rows, roles, running, info, modelsPath,
|
|
|
80
45
|
const publishedDemotions = info?.fallbackDemotions ?? {};
|
|
81
46
|
const usable = (p) => piPairFree(providers, p, accounts, catalog) && !(modelPairName(p) in publishedDemotions);
|
|
82
47
|
const servingResolved = resolveTierFallbacks(cfg, usable);
|
|
83
|
-
// The subscription usage windows (part 3a/9): one block per subscription backend in use.
|
|
84
|
-
const { blocks: subscriptionWindows, only: subscriptionOnly } = windowBlocks(cfg, providers, accounts, catalog);
|
|
85
48
|
// The per-tier budget pause set (part 5c/8): the same resolution the scheduler's gate folds,
|
|
86
49
|
// over the SAME usable predicate an observer can re-derive — the price check, and the pair
|
|
87
50
|
// not among the running orchestrator's published per-pair demotions. Computed only while the
|
|
@@ -115,8 +78,6 @@ export function collectBudget(root, cfg, rows, roles, running, info, modelsPath,
|
|
|
115
78
|
? tierFallbackLabels(servingResolved)
|
|
116
79
|
: null,
|
|
117
80
|
},
|
|
118
|
-
subscriptionWindows,
|
|
119
|
-
subscriptionOnly,
|
|
120
81
|
budgetPausedRoles,
|
|
121
82
|
capPaused,
|
|
122
83
|
};
|
|
@@ -5,8 +5,8 @@ import { piModelsPath } from "../pi/pi-models.js";
|
|
|
5
5
|
import { configForStatus, queueInFlight, mainCheckForPoll, maintenanceQuotaForPoll, ciParityForPoll } from "./status-polls.js";
|
|
6
6
|
import { quietHoursStatus, roleQuietHold } from "../scheduling/quiet-hours.js";
|
|
7
7
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
8
|
-
import {
|
|
9
|
-
import { orchestratorAlive, readOrchestratorInfo, } from "../team/orchestrator-info.js";
|
|
8
|
+
import { scheduledStopAt, scheduledWakes, standingRolePause, standingTeamPause } from "../team/team-state.js";
|
|
9
|
+
import { orchestratorAlive, readOrchestratorInfo, } from "../team/team-orchestrator-info.js";
|
|
10
10
|
import { bootstrapHoldsRole, bootstrapStatus } from "../gates/bootstrap-gates.js";
|
|
11
11
|
import { readMergeQueue } from "../merge/merge-queue.js";
|
|
12
12
|
import { collectRoleRows } from "./status-roles.js";
|
|
@@ -15,7 +15,7 @@ import { collectRoleNotesOverCap } from "./status-role-notes.js";
|
|
|
15
15
|
import { collectBudget } from "./status-budget.js";
|
|
16
16
|
import { collectRootInstallDrift } from "./status-install.js";
|
|
17
17
|
import { detectBuildCheck } from "../build/build-check-detect.js";
|
|
18
|
-
import {
|
|
18
|
+
import { briefStatus } from "../brief/brief.js";
|
|
19
19
|
import { principlesBlockOverCap } from "../prompt/principles.js";
|
|
20
20
|
/** The earlier of the two optional stop deadlines (`run --for`'s and a scheduled stop's). */
|
|
21
21
|
function earliestStop(a, b) {
|
|
@@ -49,28 +49,14 @@ function snapshotInboxFields(root, inboxes) {
|
|
|
49
49
|
questions: openQuestions(root).length,
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
/** The spend/billing fields of the snapshot: the budget
|
|
53
|
-
*
|
|
54
|
-
function snapshotBudgetFields(budget,
|
|
52
|
+
/** The spend/billing fields of the snapshot: the budget and the running-only chain/provider
|
|
53
|
+
* holds. */
|
|
54
|
+
function snapshotBudgetFields(budget, running, info) {
|
|
55
55
|
return {
|
|
56
56
|
// Unconditional (never null): a disabled team still shows its spend and the badge is
|
|
57
57
|
// the affordance for SETTING a cap. models.json itself is stat-cached inside pi-models.ts,
|
|
58
58
|
// so an unchanged catalog costs one stat per poll, not a re-read plus parse.
|
|
59
59
|
budget,
|
|
60
|
-
// One window block per subscription backend in use, raw for `status --json` and the
|
|
61
|
-
// parts 3b/9–3e/9 displays; omitted when no subscription backend reported a window.
|
|
62
|
-
...(subscriptionWindows.length > 0 ? { subscriptionWindows } : {}),
|
|
63
|
-
subscriptionOnly,
|
|
64
|
-
...(claudeBlock
|
|
65
|
-
? {
|
|
66
|
-
claudeWindow: {
|
|
67
|
-
windows: claudeBlock.windows,
|
|
68
|
-
primary: claudeBlock.primary,
|
|
69
|
-
only: subscriptionOnly,
|
|
70
|
-
heldUntil: claudeBlock.heldUntil,
|
|
71
|
-
},
|
|
72
|
-
}
|
|
73
|
-
: {}),
|
|
74
60
|
...(running && info?.chain && Object.keys(info.chain).length > 0 ? { chain: info.chain } : {}),
|
|
75
61
|
// Only while running: a stale info file beside a dead pid must not speak for a stopped
|
|
76
62
|
// team, and only while a hold stands (an empty list is the same as none).
|
|
@@ -79,7 +65,7 @@ function snapshotBudgetFields(budget, subscriptionWindows, subscriptionOnly, cla
|
|
|
79
65
|
}
|
|
80
66
|
/** The pause/quiet-hours fields of the snapshot: the paused flags and their deadlines, the
|
|
81
67
|
* maintenance quota and bootstrap holds, and the per-role quiet windows. */
|
|
82
|
-
function snapshotPauseFields(teamPause, markerRoles, roles, markerSet, capPaused, maintenanceQuota, bootstrapNow, roleRows, budgetPausedRoles, cfg, root, wakes, quiet) {
|
|
68
|
+
function snapshotPauseFields(teamPause, markerRoles, markerRolesUntil, roles, markerSet, capPaused, maintenanceQuota, bootstrapNow, roleRows, budgetPausedRoles, cfg, root, wakes, quiet) {
|
|
83
69
|
return {
|
|
84
70
|
paused: teamPause !== null,
|
|
85
71
|
pausedRoles: markerRoles,
|
|
@@ -105,7 +91,7 @@ function snapshotPauseFields(teamPause, markerRoles, roles, markerSet, capPaused
|
|
|
105
91
|
.filter((l) => l.role !== DIRECTOR_ROLE && roleQuietHold(cfg.quietHoursPerRole, baseRoleOf(l.role), new Date()))
|
|
106
92
|
.map((l) => [l.role, cfg.quietHoursPerRole?.[baseRoleOf(l.role)] ?? ""])),
|
|
107
93
|
pausedUntil: teamPause?.until,
|
|
108
|
-
pausedRolesUntil:
|
|
94
|
+
pausedRolesUntil: markerRolesUntil,
|
|
109
95
|
scheduledWake: wakes[0],
|
|
110
96
|
scheduledWakes: wakes.length > 0 ? wakes : undefined,
|
|
111
97
|
pauseReason: teamPause?.reason,
|
|
@@ -116,13 +102,17 @@ function snapshotPauseFields(teamPause, markerRoles, roles, markerSet, capPaused
|
|
|
116
102
|
/** The merge-queue and doctor health fields of the snapshot (main-check, ci-parity and the
|
|
117
103
|
* build-check/brief/principles flags, plus the role notes over cap). */
|
|
118
104
|
function snapshotHealthFields(root, cfg, mergeQueue, roleNotesOverCap) {
|
|
105
|
+
// The brief's two degraded flags from ONE scan/read (brief.briefStatus): the always-on
|
|
106
|
+
// `no-brief` and `brief-truncated` amber alerts, read once per poll rather than twice
|
|
107
|
+
// re-reading the whole brief file.
|
|
108
|
+
const brief = briefStatus(root);
|
|
119
109
|
return {
|
|
120
110
|
mergeQueue,
|
|
121
111
|
mainCheck: mainCheckForPoll(root, cfg),
|
|
122
112
|
ciParity: ciParityForPoll(root, cfg),
|
|
123
113
|
noDeclaredCheck: detectBuildCheck(root, cfg) === null ? true : undefined,
|
|
124
|
-
noBrief:
|
|
125
|
-
briefTruncated:
|
|
114
|
+
noBrief: brief.missing ? true : undefined,
|
|
115
|
+
briefTruncated: brief.overCap ? true : undefined,
|
|
126
116
|
...(roleNotesOverCap.length > 0 ? { roleNotesOverCap } : {}),
|
|
127
117
|
principlesTruncated: principlesBlockOverCap(root) ? true : undefined,
|
|
128
118
|
};
|
|
@@ -166,7 +156,10 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
|
|
|
166
156
|
const bootstrapNow = bootstrapStatus(root, cfg);
|
|
167
157
|
// One row per role id (part 5b/7): an instance role shows `feature` and `feature-2`.
|
|
168
158
|
const roles = runnerIds(cfg);
|
|
169
|
-
|
|
159
|
+
// One read of the paused-roles marker per poll serves both the set and its shared deadline
|
|
160
|
+
// (`pause --role --for` wrote it): standingRolePause returns both from the same parse, so a
|
|
161
|
+
// snapshot never reads the same marker file twice (pausedRoles + pausedRolesUntil each did).
|
|
162
|
+
const { roles: markerRoles, until: markerRolesUntil } = standingRolePause(root);
|
|
170
163
|
const markerSet = new Set(markerRoles);
|
|
171
164
|
// One read of the orchestrator info file per poll: it serves both the displayed pid and the
|
|
172
165
|
// liveness check (passing it to orchestratorAlive skips its own re-read).
|
|
@@ -175,7 +168,15 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
|
|
|
175
168
|
// run or verification) or is pinned to — threaded into the roles collector so `running`,
|
|
176
169
|
// `disk` and the rows agree.
|
|
177
170
|
const slots = readSlotsState(root).slots;
|
|
178
|
-
|
|
171
|
+
// One merge-queue pass per poll (merge-queue.ts's stat cache keeps an unchanged queue at one
|
|
172
|
+
// stat per file) serves three consumers — the `mergeQueue` field below, the busy runner rows'
|
|
173
|
+
// surplus-eligibility check (collectRoleRows' `queued`), and the in-maintenance in-flight
|
|
174
|
+
// count (maintenanceQuotaForPoll) — so a snapshot never lists or re-reads the same queue
|
|
175
|
+
// twice, and the rows, the depth and the quota cannot straddle a concurrent enqueue/drop.
|
|
176
|
+
const queue = readMergeQueue(root);
|
|
177
|
+
const merges = queue.entries;
|
|
178
|
+
const queuedRoles = new Set(merges.map((e) => e.role));
|
|
179
|
+
const roleRows = collectRoleRows(root, cfg, slots, modelsPath, markerSet, queuedRoles);
|
|
179
180
|
// Every role whose notes file runs past the read bound (status-role-notes.ts): the standing
|
|
180
181
|
// twin of the doctor `role notes` row, raised as the amber role-notes-truncated alert.
|
|
181
182
|
const roleNotesOverCap = collectRoleNotesOverCap(root);
|
|
@@ -189,13 +190,11 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
|
|
|
189
190
|
// One read of the pending scheduled wakes feeds both the earliest and the full list, so they
|
|
190
191
|
// cannot disagree.
|
|
191
192
|
const wakes = scheduledWakes(root, now);
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
// still has a queue entry for what the marker names (per change, for a batch) —
|
|
195
|
-
// cross-check makes every crash ordering self-healing (a stale marker alone never displays,
|
|
193
|
+
// The merge queue read is hoisted above the rows collector (one pass serves the depth, the
|
|
194
|
+
// rows' surplus check, and the quota below); inFlight is the 4/5 marker only when a live
|
|
195
|
+
// orchestrator still has a queue entry for what the marker names (per change, for a batch) —
|
|
196
|
+
// the cross-check makes every crash ordering self-healing (a stale marker alone never displays,
|
|
196
197
|
// and needs no cleanup pass).
|
|
197
|
-
const queue = readMergeQueue(root);
|
|
198
|
-
const merges = queue.entries;
|
|
199
198
|
const mergeQueue = {
|
|
200
199
|
depth: merges.length + queue.unreadable,
|
|
201
200
|
};
|
|
@@ -225,21 +224,18 @@ export function snapshot(root, modelsPath = piModelsPath(), now = Date.now()) {
|
|
|
225
224
|
// The spend/billing block (status-budget.ts): the budget object, the per-backend
|
|
226
225
|
// subscription windows and the team-global free-ness answer, and the two per-role pause
|
|
227
226
|
// verdicts, all from one readPiProviders and one readAccount per backend in use per poll.
|
|
228
|
-
const { budget,
|
|
229
|
-
// The one-release claude spelling (see StatusSnapshot.claudeWindow), derived from the claude
|
|
230
|
-
// block so `subscriptionWindows` stays the single source of the window data.
|
|
231
|
-
const claudeBlock = subscriptionWindows.find((b) => b.backend === "claude");
|
|
227
|
+
const { budget, budgetPausedRoles, capPaused } = collectBudget(root, cfg, roleRows, roles, running, info, modelsPath, now);
|
|
232
228
|
// The maintenance allowance (plans/work-ratio.md, part 1b/4): the same pure verdict the
|
|
233
229
|
// gate folds, over the same event log, so the dashboards show the hold the scheduler
|
|
234
230
|
// enforces. `merges` is the one merge queue snapshot read above, so in-flight maintenance
|
|
235
231
|
// merges count once here and in mergeQueue.
|
|
236
|
-
const maintenanceQuota = maintenanceQuotaForPoll(root, cfg.maintenancePerWorkMerge, roleRows,
|
|
232
|
+
const maintenanceQuota = maintenanceQuotaForPoll(root, cfg.maintenancePerWorkMerge, roleRows, queuedRoles, now);
|
|
237
233
|
return {
|
|
238
234
|
...snapshotRunFields(running, info),
|
|
239
235
|
...snapshotInboxFields(root, { inbox, inboxPrompts, inboxFiles, inboxQueuedAt, inboxNotBefore, roleInbox, roleInboxPrompts }),
|
|
240
236
|
roles: roleRows,
|
|
241
|
-
...snapshotBudgetFields(budget,
|
|
242
|
-
...snapshotPauseFields(teamPause, markerRoles, roles, markerSet, capPaused, maintenanceQuota, bootstrapNow, roleRows, budgetPausedRoles, cfg, root, wakes, quiet),
|
|
237
|
+
...snapshotBudgetFields(budget, running, info),
|
|
238
|
+
...snapshotPauseFields(teamPause, markerRoles, markerRolesUntil, roles, markerSet, capPaused, maintenanceQuota, bootstrapNow, roleRows, budgetPausedRoles, cfg, root, wakes, quiet),
|
|
243
239
|
...snapshotHealthFields(root, cfg, mergeQueue, roleNotesOverCap),
|
|
244
240
|
...snapshotResourceFields(root, running, info, rootDrift),
|
|
245
241
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { readRegularTextOrNull } from "../files/files.js";
|
|
4
4
|
import { cachedByStat } from "../files/stat-cache.js";
|
|
5
5
|
import { roleNotesPath } from "../paths.js";
|
|
6
6
|
import { ROLE_NOTES_MAX_BYTES } from "../agent/role-notes-write.js";
|
|
@@ -34,7 +34,7 @@ export function readRoleNotesOverCap(root) {
|
|
|
34
34
|
continue;
|
|
35
35
|
const file = path.join(notesDir, name);
|
|
36
36
|
const chars = cachedByStat(noteLengthCache, file, file, () => {
|
|
37
|
-
const text =
|
|
37
|
+
const text = readRegularTextOrNull(file);
|
|
38
38
|
return text === null ? null : text.length;
|
|
39
39
|
}, (n) => n);
|
|
40
40
|
if (chars === null)
|