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
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { publishInfoFields } from "../team/orchestrator-info.js";
|
|
2
|
-
import {
|
|
1
|
+
import { publishInfoFields } from "../team/team-orchestrator-info.js";
|
|
2
|
+
import { runnerEnabled } from "../roles/runner-ids.js";
|
|
3
3
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
4
4
|
import { reviewHoldKey, pollDeferralInputs, pollRunnerReasons } from "./orchestrator-scheduling.js";
|
|
5
|
-
import { seamConcurrencyKey } from "../config/config-views.js";
|
|
6
5
|
import { launchDueRuns } from "./orchestrator-launch.js";
|
|
7
6
|
import { branchHead, currentBranch } from "../git/git.js";
|
|
8
7
|
import { queuedMergeFiles, queuedMergeRoles } from "../merge/merge-queue.js";
|
|
@@ -15,11 +14,21 @@ import { checkPermitSnapshot } from "../concurrency/check-permit.js";
|
|
|
15
14
|
import { slotPoolSnapshot } from "../git/worktree-pool.js";
|
|
16
15
|
import { redeployTarget, watchVerifiesMain } from "../baseline/main-baseline.js";
|
|
17
16
|
import { p75RunDurationMs } from "../run/run-timing.js";
|
|
17
|
+
import { reapAttributableOrphans } from "../doctor/doctor-orphans.js";
|
|
18
18
|
import { pollTeamGates } from "../gates/gate-polls.js";
|
|
19
19
|
import { heldProviders, windowHeldProviders } from "../team/team-hold.js";
|
|
20
20
|
import { workReserveCount } from "../scheduling/claims.js";
|
|
21
21
|
export function newPollState() {
|
|
22
|
-
return { restart: false, holdForRestart: false, diskHeld: false, warnedBranchDivergence: false, lastReserve: 0 };
|
|
22
|
+
return { restart: false, holdForRestart: false, diskHeld: false, warnedBranchDivergence: false, lastReserve: 0, lastOrphanReapAt: 0 };
|
|
23
|
+
}
|
|
24
|
+
/** How often the orchestrator's poll loop stands down the orphan reap — a once-per-interval
|
|
25
|
+
* throttle so a slow process-table scan never runs on the busy per-poll hot path. */
|
|
26
|
+
export const ORPHAN_REAP_INTERVAL_MS = 15 * 60_000;
|
|
27
|
+
/** Whether the orphan reap is due now, given the last poll it ran: true at and after
|
|
28
|
+
* `now >= last + intervalMs`. Purely a clock comparison so tests pin it from literal
|
|
29
|
+
* numbers. A fresh state's `last` of 0 is always due against a real `now`. */
|
|
30
|
+
export function dueForOrphanReap(last, now, intervalMs = ORPHAN_REAP_INTERVAL_MS) {
|
|
31
|
+
return now - last >= intervalMs;
|
|
23
32
|
}
|
|
24
33
|
/** The start gate every reserved run passes the moment its permit is granted
|
|
25
34
|
* (runTimedRoleRun's `held`): closed while a restart is pending, and after one is decided so
|
|
@@ -29,20 +38,13 @@ function runStartHeld(state) {
|
|
|
29
38
|
return state.holdForRestart || state.restart || state.diskHeld;
|
|
30
39
|
}
|
|
31
40
|
/** The one start-gate rule for merges: a restart/disk hold's `held` counts against every role,
|
|
32
|
-
* while a hold on the reviewer's provider (`reviewHeld`) parks only the entries a
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
export function mergeStartHeld(held, reviewHeld,
|
|
38
|
-
return held || (reviewHeld &&
|
|
39
|
-
}
|
|
40
|
-
/** The base roles whose review runs after merging — the config's `review.afterMerge` list, mapped
|
|
41
|
-
* through `baseRoleOf` so a listed base id stands for all its parallel instances. The one build of
|
|
42
|
-
* the set pollMergeDrain's start gate and pollSchedule's launch gate both exempt from a
|
|
43
|
-
* reviewer-hold, so the two passes cannot read the standing differently. */
|
|
44
|
-
function afterMergeRoleIds(config) {
|
|
45
|
-
return new Set((config.review.afterMerge ?? []).map(baseRoleOf));
|
|
41
|
+
* while a hold on the reviewer's provider (`reviewHeld`) parks only the entries a caller that
|
|
42
|
+
* names no role admits — the post-merge review drain. A role named (every merge entry) is never
|
|
43
|
+
* held by the reviewer: no role start or landing waits on the reviewer. Shared by
|
|
44
|
+
* pollMergeDrain's startHeld and the merge tests, so the production wiring and its coverage
|
|
45
|
+
* cannot drift. */
|
|
46
|
+
export function mergeStartHeld(held, reviewHeld, role) {
|
|
47
|
+
return held || (reviewHeld && role === undefined);
|
|
46
48
|
}
|
|
47
49
|
/** Live-reload, notify/retention/launchservices, CLI request markers, abort settling and the
|
|
48
50
|
* branch-divergence watch. Writes state.warnedBranchDivergence. */
|
|
@@ -52,7 +54,7 @@ export async function pollRequestsAndWatch(ctx, state) {
|
|
|
52
54
|
// src/config/config-live.ts owns the last-known-good retention and the edge-triggered
|
|
53
55
|
// warnings/events around it.
|
|
54
56
|
const liveConfig = ctx.liveReload.poll();
|
|
55
|
-
// The notify command rides the same last-known-good reload (src/events/notify.ts): a live
|
|
57
|
+
// The notify command rides the same last-known-good reload (src/events/event-notify.ts): a live
|
|
56
58
|
// `config set notify` edit takes effect on the next poll, no restart.
|
|
57
59
|
ctx.notifier.update(liveConfig);
|
|
58
60
|
// Live session retention (the last restart-only setting): the edge-triggered
|
|
@@ -60,6 +62,15 @@ export async function pollRequestsAndWatch(ctx, state) {
|
|
|
60
62
|
// src/orchestrator/retention.ts. The live config (last-known-good while the file is broken or
|
|
61
63
|
// missing) drives the check, like the budget gate.
|
|
62
64
|
ctx.retentionPruner.poll(root, liveConfig.sessionRetentionDays);
|
|
65
|
+
// The orphan reap (src/doctor/doctor-orphans.ts): at most once per interval, kill the
|
|
66
|
+
// orphans the doctor row already reports that are old enough to be certain a live run no
|
|
67
|
+
// longer owns them — the automatic counterpart to the human click `doctor
|
|
68
|
+
// --kill-orphans`/dashboard/TUI makes. The time is stamped before the await, so a slow
|
|
69
|
+
// scan cannot overlap the next poll's reap (and this await anyway holds the poll step).
|
|
70
|
+
if (dueForOrphanReap(state.lastOrphanReapAt, Date.now())) {
|
|
71
|
+
state.lastOrphanReapAt = Date.now();
|
|
72
|
+
await reapAttributableOrphans(root);
|
|
73
|
+
}
|
|
63
74
|
// A missing or stale opencode catalog is refetched at most once an hour while opencode is routed.
|
|
64
75
|
ctx.opencodeCatalogRefresh.poll(liveConfig);
|
|
65
76
|
// launchservicesd's Mach-port budget (src/process/launch-services.ts owns it):
|
|
@@ -119,9 +130,8 @@ export function pollGatesAndHolds(ctx, state, { liveConfig, now }) {
|
|
|
119
130
|
state.diskHeld = gates.diskHeld;
|
|
120
131
|
// Publish the permit pool's live state (plans/provider-concurrency.md §8): observers
|
|
121
132
|
// (status, TUI, GUI) read one file instead of coupling to this Semaphore, and the team
|
|
122
|
-
// line shows
|
|
123
|
-
//
|
|
124
|
-
// renders exactly as before. The check-permit pool rides along, so status can show checks
|
|
133
|
+
// line shows the team total. Written only when it changes, like budget and disk. The
|
|
134
|
+
// check-permit pool rides along, so status can show checks
|
|
125
135
|
// running and waiting while merges stall. The workspace pool's live state rides along too:
|
|
126
136
|
// one small slots.json read per poll, and publishInfoFields rewrites the info file only
|
|
127
137
|
// when a value changed.
|
|
@@ -131,17 +141,15 @@ export function pollGatesAndHolds(ctx, state, { liveConfig, now }) {
|
|
|
131
141
|
["slotPool", slotPoolSnapshot(root)],
|
|
132
142
|
]);
|
|
133
143
|
// The failure hold, keyed per provider (PLANS.md 2026-10-05): a role is held when ITS
|
|
134
|
-
// run model's provider stands under a hold
|
|
135
|
-
//
|
|
136
|
-
// merge without a reviewer — while a role on `review.afterMerge` merges without one, so it is
|
|
137
|
-
// exempt, and the merge queue reads the same scoped reviewHeld verdict). Key presence in the
|
|
144
|
+
// run model's provider stands under a hold. A hold on the reviewer's provider parks only
|
|
145
|
+
// the post-merge review drain, never a role start, verify or landing. Key presence in the
|
|
138
146
|
// map is relapse memory, not a hold: heldProviders() reads only standing holds (`until`
|
|
139
147
|
// non-null), so a lifted hold never keeps blocking.
|
|
140
148
|
const teamHeldProviders = heldProviders(gateStates.teamHold);
|
|
141
149
|
const teamHeldNow = () => heldProviders(gateStates.teamHold);
|
|
142
150
|
// The reviewer's seam is read from the config merges actually review under — the poll's
|
|
143
151
|
// roleConfig, which a used-up claude window or the cap has already moved to a fallback —
|
|
144
|
-
// so a reviewer that left claude no longer
|
|
152
|
+
// so a reviewer that left claude no longer parks the post-merge review drain on the
|
|
145
153
|
// claude hold.
|
|
146
154
|
const reviewSeamKey = reviewHoldKey(gates.roleConfig);
|
|
147
155
|
return {
|
|
@@ -149,8 +157,7 @@ export function pollGatesAndHolds(ctx, state, { liveConfig, now }) {
|
|
|
149
157
|
teamHeldProviders,
|
|
150
158
|
teamHeldNow,
|
|
151
159
|
reviewHeldNow: () => liveConfig.review.enabled && teamHeldNow().has(reviewSeamKey),
|
|
152
|
-
|
|
153
|
-
roleProviders: new Map(runners.map((r) => [r.role, r.runProvider(now)])),
|
|
160
|
+
roleProviders: new Map(runners.map((r) => [r.role, r.runProvider()])),
|
|
154
161
|
};
|
|
155
162
|
}
|
|
156
163
|
/** Self-redeploy (src/redeploy/redeployer.ts): with main's head in hand, let the state
|
|
@@ -180,6 +187,8 @@ export async function pollRedeploy(ctx, state, { liveConfig, mainHead, now }) {
|
|
|
180
187
|
directorInFlight: ctx.directorInFlight.size,
|
|
181
188
|
roleRunP75Ms: p75RunDurationMs(ctx.roleRunDurationsMs),
|
|
182
189
|
}, liveConfig.autoRestart, now);
|
|
190
|
+
// The tip beside the target: a tip no passing check covers yet is published as waitingForCheck.
|
|
191
|
+
await redeploy.noteTip(mainHead);
|
|
183
192
|
publishInfoFields(ctx.root, ctx.info, [["build", redeploy.status(now)]]);
|
|
184
193
|
if (action === "restart") {
|
|
185
194
|
state.restart = true;
|
|
@@ -201,16 +210,12 @@ export async function pollRedeploy(ctx, state, { liveConfig, mainHead, now }) {
|
|
|
201
210
|
* merge slot, and the dedupe against main). A restart or disk hold stops the whole drain, as it
|
|
202
211
|
* starts no run; what is already in flight carries on, and a verify parked for its permit
|
|
203
212
|
* meets the same start gate as a parked run when the permit comes. A hold on the reviewer's
|
|
204
|
-
* provider parks
|
|
205
|
-
*
|
|
213
|
+
* provider parks nothing in the drain — no merge entry is held by the reviewer (its hold parks
|
|
214
|
+
* only the post-merge review drain, whose no-role start gate reads it). */
|
|
206
215
|
export async function pollMergeDrain(ctx, state, { liveConfig, mainHead }, gates) {
|
|
207
216
|
if (state.holdForRestart || state.diskHeld)
|
|
208
217
|
return;
|
|
209
218
|
const { root, mainBranch } = ctx;
|
|
210
|
-
// The roles whose review runs after merging (plans post-merge-review role): while the
|
|
211
|
-
// reviewer is held, their approved/queued changes keep draining, so the per-entry start gate
|
|
212
|
-
// exempts them from the reviewer-hold leg below.
|
|
213
|
-
const afterMergeRoles = afterMergeRoleIds(liveConfig);
|
|
214
219
|
// A `main_red` pin the pipeline kept goes back on the queue once main's head has moved.
|
|
215
220
|
for (const r of ctx.runners)
|
|
216
221
|
await requeueMainRedPin(root, mainBranch, r.state, mainHead);
|
|
@@ -222,9 +227,8 @@ export async function pollMergeDrain(ctx, state, { liveConfig, mainHead }, gates
|
|
|
222
227
|
runners: ctx.runners,
|
|
223
228
|
liveConfig,
|
|
224
229
|
roleConfig: gates.roleConfig,
|
|
225
|
-
directorConfig:
|
|
226
|
-
|
|
227
|
-
startHeld: (role) => mergeStartHeld(runStartHeld(state), gates.reviewHeldNow(), afterMergeRoles, role),
|
|
230
|
+
directorConfig: liveConfig,
|
|
231
|
+
startHeld: (role) => mergeStartHeld(runStartHeld(state), gates.reviewHeldNow(), role),
|
|
228
232
|
}, ctx.merges);
|
|
229
233
|
}
|
|
230
234
|
/** The scheduling pass: the deferral inputs feed pollRunnerReasons, whose reasons map
|
|
@@ -232,10 +236,6 @@ export async function pollMergeDrain(ctx, state, { liveConfig, mainHead }, gates
|
|
|
232
236
|
export async function pollSchedule(ctx, state, { liveConfig, mainHead, now }, gates) {
|
|
233
237
|
const { root, runners, gateStates } = ctx;
|
|
234
238
|
const inputs = pollDeferralInputs(root, runners, liveConfig, gateStates, gates, ctx.breakerNow());
|
|
235
|
-
// The roles whose review runs after merging: while the reviewer is held their runs stay
|
|
236
|
-
// launchable (the pre-merge gate skips their review), so the permit-time start gate exempts
|
|
237
|
-
// them from the reviewer-hold leg below, exactly as the scheduling pass and merge drain do.
|
|
238
|
-
const afterMergeRoles = afterMergeRoleIds(liveConfig);
|
|
239
239
|
const reasons = await pollRunnerReasons({
|
|
240
240
|
root,
|
|
241
241
|
runners,
|
|
@@ -252,14 +252,11 @@ export async function pollSchedule(ctx, state, { liveConfig, mainHead, now }, ga
|
|
|
252
252
|
bootstrapHeld: gates.bootstrapHeld,
|
|
253
253
|
budgetPausedRoles: gates.budgetPausedRoles,
|
|
254
254
|
probeRoles: inputs.probeRoles,
|
|
255
|
-
reviewStrongPaused: inputs.reviewStrongPaused,
|
|
256
255
|
roleQuietHeld: gates.roleQuietHeld,
|
|
257
256
|
holdForRestart: state.holdForRestart,
|
|
258
257
|
diskHeld: state.diskHeld,
|
|
259
258
|
heldProviders: gates.teamHeldProviders,
|
|
260
259
|
windowHeldProviders: windowHeldProviders(gateStates.teamHold),
|
|
261
|
-
reviewHeld: gates.reviewHeld,
|
|
262
|
-
afterMergeRoles,
|
|
263
260
|
roleProviders: gates.roleProviders,
|
|
264
261
|
openBugsNow: inputs.openBugsNow,
|
|
265
262
|
workBacklogOpen: inputs.workBacklogOpen,
|
|
@@ -280,52 +277,30 @@ export async function pollSchedule(ctx, state, { liveConfig, mainHead, now }, ga
|
|
|
280
277
|
probePair: inputs.probePair,
|
|
281
278
|
probeRoles: inputs.probeRoles,
|
|
282
279
|
budgetActive: gates.budgetActive,
|
|
283
|
-
windowMovedRoles: gates.windowMovedRoles,
|
|
284
280
|
semaphore: ctx.semaphore,
|
|
285
|
-
piDefault: ctx.piDefault,
|
|
286
281
|
rolePermitHolders: ctx.rolePermitHolders,
|
|
287
282
|
roleInFlight: ctx.roleInFlight,
|
|
288
283
|
directorInFlight: ctx.directorInFlight,
|
|
289
284
|
roleRunDurationsMs: ctx.roleRunDurationsMs,
|
|
290
285
|
startHeld: (role) => runStartHeld(state) ||
|
|
291
286
|
(role !== DIRECTOR_ROLE
|
|
292
|
-
? (
|
|
293
|
-
gates.teamHeldNow().has(gates.roleProviders.get(role))
|
|
287
|
+
? gates.teamHeldNow().has(gates.roleProviders.get(role))
|
|
294
288
|
: windowHeldProviders(gateStates.teamHold).has(gates.roleProviders.get(role))),
|
|
295
289
|
});
|
|
296
290
|
}
|
|
297
291
|
/** Priority headroom, then once mode's exit; returns whether the loop should stop. Writes
|
|
298
292
|
* state.lastReserve and, in once mode, fires internalStop. */
|
|
299
|
-
export function pollReserveAndExit(ctx, state, { liveConfig
|
|
293
|
+
export function pollReserveAndExit(ctx, state, { liveConfig }) {
|
|
300
294
|
const { root, runners, semaphore } = ctx;
|
|
301
295
|
// Priority headroom (plans/parallel-work-instances.md part 7/7): withhold a few permits
|
|
302
296
|
// from maintenance while a work role has work to take, so a due work run never waits out
|
|
303
297
|
// a whole maintenance run. Sized after the launch pass, so a runner launched this poll
|
|
304
298
|
// already reads as running and reserves nothing for itself.
|
|
305
|
-
const poolSnapshot = semaphore.snapshot();
|
|
306
|
-
// The provider-headroom predicate receives the structural ClaimRunner; the config that
|
|
307
|
-
// names the provider lives on the real RoleRunners, so look the runner up by role.
|
|
308
|
-
const runnerByRole = new Map(runners.map((r) => [r.role, r]));
|
|
309
299
|
const reserve = workReserveCount(root, runners, {
|
|
310
300
|
maxConcurrent: liveConfig.maxConcurrent,
|
|
311
301
|
enabled: new Set(runners.filter((r) => runnerEnabled(r.config, r.role)).map((r) => r.role)),
|
|
312
302
|
queuedMergeRoles: queuedMergeRoles(root),
|
|
313
303
|
instancesOf: (base) => liveConfig.roles[base]?.instances ?? 1,
|
|
314
|
-
// Per-provider headroom (plans/provider-concurrency.md §6): an idle work role reserves a
|
|
315
|
-
// permit only when its own provider could use it — capped and full, it would only idle
|
|
316
|
-
// the slot another provider's maintenance run could take. Read from the pool's live
|
|
317
|
-
// snapshot; an uncapped key (or the team-total-only case) always has headroom.
|
|
318
|
-
providerHasHeadroom: (r) => {
|
|
319
|
-
const runner = runnerByRole.get(r.role);
|
|
320
|
-
const cfg = runner && runner.role !== DIRECTOR_ROLE ? runner.runConfig(now) : null;
|
|
321
|
-
const key = cfg ? seamConcurrencyKey(cfg, ctx.piDefault) : undefined;
|
|
322
|
-
if (key === undefined)
|
|
323
|
-
return true;
|
|
324
|
-
const k = poolSnapshot.byKey[key];
|
|
325
|
-
if (k?.limit === undefined)
|
|
326
|
-
return true;
|
|
327
|
-
return k.inUse < k.limit;
|
|
328
|
-
},
|
|
329
304
|
});
|
|
330
305
|
semaphore.setReserve(reserve);
|
|
331
306
|
if ((state.lastReserve === 0) !== (reserve === 0)) {
|
|
@@ -48,8 +48,8 @@ export async function pollRunnerReasons(ctx) {
|
|
|
48
48
|
}
|
|
49
49
|
/** The team-hold key of the reviewer's seam, read from the config merges actually review under
|
|
50
50
|
* (the poll's roleConfig, which a used-up claude window or the cap has moved to a fallback), so
|
|
51
|
-
* a reviewer that left claude no longer
|
|
52
|
-
* hold. */
|
|
51
|
+
* a reviewer that left claude no longer parks the no-role post-merge review drain on the claude
|
|
52
|
+
* hold — no named entry (start, verify or landing) waits on the reviewer. */
|
|
53
53
|
export function reviewHoldKey(roleConfig) {
|
|
54
54
|
return seamHoldKey(reviewRunConfig(roleConfig));
|
|
55
55
|
}
|
|
@@ -65,11 +65,8 @@ export function reviewHoldKey(roleConfig) {
|
|
|
65
65
|
* exactly why it is being probed). Only those roles pierce the budget hold, and the launch
|
|
66
66
|
* pass admits exactly one of them as the probe — a probe blind to the pair's tier would lift
|
|
67
67
|
* the pause for every role, and a probe with no eligible runner must not hijack the poll's
|
|
68
|
-
* other launches. The
|
|
69
|
-
*
|
|
70
|
-
* held (a `review.afterMerge` role is exempt — its review runs after merging) — the per-tier
|
|
71
|
-
* pause set alone would let small/default roles author changes no reviewer could judge. The
|
|
72
|
-
* probe of the strong pair pierces this hold for exactly the probed tier's roles too. */
|
|
68
|
+
* other launches. The probe of a demoted pair pierces the hold for
|
|
69
|
+
* exactly the probed tier's roles too. */
|
|
73
70
|
export function pollDeferralInputs(root, runners, liveConfig, gateStates, gates, breakerNowMs) {
|
|
74
71
|
const openBugsNow = openBugs(root).length > 0;
|
|
75
72
|
const workBacklogOpen = eligibleEntries(root, "feature").length > 0 || eligibleEntries(root, "bugfix").length > 0;
|
|
@@ -85,6 +82,5 @@ export function pollDeferralInputs(root, runners, liveConfig, gateStates, gates,
|
|
|
85
82
|
probeRoles.add(r.role);
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
|
-
|
|
89
|
-
return { openBugsNow, workBacklogOpen, plansFull, probePair, probeRoles, reviewStrongPaused };
|
|
85
|
+
return { openBugsNow, workBacklogOpen, plansFull, probePair, probeRoles };
|
|
90
86
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readOrchestratorInfo, writeOrchestratorInfo } from "../team/orchestrator-info.js";
|
|
1
|
+
import { readOrchestratorInfo, writeOrchestratorInfo } from "../team/team-orchestrator-info.js";
|
|
2
2
|
import { runnerIds } from "../roles/runner-ids.js";
|
|
3
3
|
import { newLiveConfigReload } from "../config/config-live.js";
|
|
4
4
|
import { FALLBACK_BREAKER_POLICY, } from "../budget/fallback-breaker.js";
|
|
@@ -10,20 +10,18 @@ import { mergeTasks, newMergePipeline } from "../merge/merge-pipeline.js";
|
|
|
10
10
|
import { bestEffortEventLogger, logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
11
11
|
import { removeQuiet } from "../files/files.js";
|
|
12
12
|
import { OnceRound } from "../scheduling/once-round.js";
|
|
13
|
-
import { newNotifier } from "../events/notify.js";
|
|
13
|
+
import { newNotifier } from "../events/event-notify.js";
|
|
14
14
|
import { piModelsPath } from "../pi/pi-models.js";
|
|
15
|
-
import { piSettingsPath, readPiDefaultModel } from "../pi/pi-settings.js";
|
|
16
15
|
import { Semaphore } from "../concurrency/semaphore.js";
|
|
17
16
|
import { orchestratorStatePath } from "../paths.js";
|
|
18
17
|
import { createMainWatch } from "../baseline/main-watch.js";
|
|
19
18
|
import { queueCulpritHeal } from "../baseline/main-red.js";
|
|
20
19
|
import { RetentionPruner } from "./retention.js";
|
|
21
|
-
import { newOpencodeCatalogRefresh } from "../gates/startup-
|
|
20
|
+
import { newOpencodeCatalogRefresh } from "../gates/startup-gates.js";
|
|
22
21
|
import { WorkMergedCache } from "../scheduling/work-merged-cache.js";
|
|
23
22
|
import { drainInFlightWork, HANDOFF_MERGE_WINDOW_MS, sleepInterruptible, } from "../run/run-timing.js";
|
|
24
23
|
import { newTeamGateStates } from "../gates/gate-polls.js";
|
|
25
|
-
import {
|
|
26
|
-
import { ReclaimController } from "../team/reclaim.js";
|
|
24
|
+
import { ReclaimController } from "../team/team-reclaim.js";
|
|
27
25
|
import { reapPreviousTeamOrphans } from "../doctor/doctor-orphans.js";
|
|
28
26
|
import { runStartupMigrations } from "./startup-migration-steps.js";
|
|
29
27
|
import { archiveAtTeamStart } from "../backlog/backlog-archive.js";
|
|
@@ -34,9 +32,6 @@ export async function runOrchestrator(opts) {
|
|
|
34
32
|
const { root, config, mainBranch, signal: externalSignal } = opts;
|
|
35
33
|
const pollMs = opts.pollMs ?? POLL_MS;
|
|
36
34
|
const modelsPath = opts.modelsPath ?? piModelsPath();
|
|
37
|
-
// pi's saved default model — the same stat-cached read status-roles.ts makes — used as the
|
|
38
|
-
// fallback provider key for a pi seam that names no model (plans/provider-concurrency.md §2).
|
|
39
|
-
const piDefault = readPiDefaultModel(piSettingsPath(modelsPath), root);
|
|
40
35
|
const breakerPolicy = opts.fallbackBreakerPolicy ?? FALLBACK_BREAKER_POLICY;
|
|
41
36
|
const breakerNow = opts.breakerNow ?? Date.now;
|
|
42
37
|
// Parallel work instances (part 5b/7): the runner list is every runner id the config enables
|
|
@@ -77,10 +72,6 @@ export async function runOrchestrator(opts) {
|
|
|
77
72
|
// resolver — merge-drain.ts) all hold a permit; see its MergePipelineContext for why a
|
|
78
73
|
// merge is not exempt.
|
|
79
74
|
const semaphore = new Semaphore(Math.max(1, config.maxConcurrent));
|
|
80
|
-
// Seed the pool's per-provider caps from the startup config (plans/provider-concurrency.md
|
|
81
|
-
// §7): a role run's lease is bounded by its provider key from the first launch onward, not
|
|
82
|
-
// just after a live edit. A config without the key passes an empty map, the no-change case.
|
|
83
|
-
semaphore.setKeyCaps(config.maxConcurrentPerProvider ?? {});
|
|
84
75
|
const infoFile = orchestratorStatePath(root);
|
|
85
76
|
const info = { pid: process.pid, startedAt: Date.now(), roles: enabled };
|
|
86
77
|
if (redeploy)
|
|
@@ -174,9 +165,6 @@ export async function runOrchestrator(opts) {
|
|
|
174
165
|
// the start gate reads gateStates.teamHold at permit time, so its closures always see
|
|
175
166
|
// the latest poll's hold verdict rather than the poll that created them.
|
|
176
167
|
const gateStates = newTeamGateStates(config);
|
|
177
|
-
// A restart must not re-announce a window that is still open.
|
|
178
|
-
gateStates.claudeWindowWarned = loggedWindowKeys(root, "subscription_window_warning", Date.now());
|
|
179
|
-
gateStates.claudeWindowReserved = loggedWindowKeys(root, "subscription_window_reserved", Date.now());
|
|
180
168
|
// The live config the last successful reload produced (last-known-good while the file is
|
|
181
169
|
// broken or missing). The reload bookkeeping itself lives in src/config/config-live.ts.
|
|
182
170
|
const liveReload = newLiveConfigReload({ root, config, mainBranch, runnerSignal: roleSignal, runners, semaphore, roleFilter: opts.roleFilter });
|
|
@@ -194,10 +182,10 @@ export async function runOrchestrator(opts) {
|
|
|
194
182
|
// logs exactly once — on the transition in, never while merely not-due and never on exit.
|
|
195
183
|
// In-memory only: a restart mid-episode can re-log at most one event.
|
|
196
184
|
const deferredDue = new Map();
|
|
197
|
-
// The operator notify hook (src/events/notify.ts owns the whole concern): one
|
|
198
|
-
// command run on notable events (budget_paused, role_streak_paused,
|
|
199
|
-
// restart_blocked). Subscribed here so it sees every event this
|
|
200
|
-
// disposed beside the orchestrator_stop event below.
|
|
185
|
+
// The operator notify hook (src/events/event-notify.ts owns the whole concern): one
|
|
186
|
+
// configured shell command run on notable events (budget_paused, role_streak_paused,
|
|
187
|
+
// merge_failed, restart_blocked). Subscribed here so it sees every event this
|
|
188
|
+
// process logs from start; disposed beside the orchestrator_stop event below.
|
|
201
189
|
const notifier = newNotifier(root);
|
|
202
190
|
// Best-effort event logging in the poll loop (bestEffortEventLogger, events.ts): this loop
|
|
203
191
|
// and its finally run with no catch, so an unwritable feed (ENOSPC, EACCES, the path replaced
|
|
@@ -221,7 +209,6 @@ export async function runOrchestrator(opts) {
|
|
|
221
209
|
once,
|
|
222
210
|
info,
|
|
223
211
|
modelsPath,
|
|
224
|
-
piDefault,
|
|
225
212
|
redeploy,
|
|
226
213
|
launchServicesWatch,
|
|
227
214
|
reclaim,
|
|
@@ -19,52 +19,73 @@ import { noteSkip } from "./scheduling-scope.js";
|
|
|
19
19
|
* runner's claim/heldElsewhere state, the claim group's held set, and once-mode settlement on
|
|
20
20
|
* the skip paths. */
|
|
21
21
|
export function admitRunner(scope, runner, reason) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
// still runs), skips its scheduled and main-moved runs instead of paying a model run that
|
|
36
|
-
// can only end in
|
|
37
|
-
// nothing-to-do. Startup, inbox and resume runs, a fresh operator wake, and a runner with a
|
|
38
|
-
// claim or outstanding work still run. nextRunAt and backoff stay put, so the first poll
|
|
39
|
-
// after a plan turns eligible starts a run for it.
|
|
40
|
-
const standDownBase = baseRoleOf(runner.role);
|
|
41
|
-
if ((standDownBase === "feature" || (standDownBase === BUGFIX_ROLE && scope.openBugsNow)) &&
|
|
22
|
+
if (standDownForNoWork(scope, runner, reason))
|
|
23
|
+
return "skipped";
|
|
24
|
+
return claimGroupAdmission(scope, runner, reason);
|
|
25
|
+
}
|
|
26
|
+
/** The candidate half of the stand-down test: true when a feature runner, or a bugfix runner
|
|
27
|
+
* whose open bugs are non-empty (no open bugs at all is search mode, which still runs), is due
|
|
28
|
+
* a scheduled or main-moved run, is not freshly woken, and holds no claim or outstanding work —
|
|
29
|
+
* so startup, inbox and resume runs, a fresh operator wake, and a claimed or mid-entry runner
|
|
30
|
+
* all pass through. The caller stands the runner down only when this holds and no entry is
|
|
31
|
+
* eligible. */
|
|
32
|
+
function noWorkScheduledRun(scope, runner, reason) {
|
|
33
|
+
const base = baseRoleOf(runner.role);
|
|
34
|
+
return ((base === "feature" || (base === BUGFIX_ROLE && scope.openBugsNow)) &&
|
|
42
35
|
(reason === "scheduled" || reason === "main moved") &&
|
|
43
36
|
!freshlyWoken(runner.state) &&
|
|
44
37
|
runner.state.claim === undefined &&
|
|
45
|
-
!hasOutstandingWork(runner.state))
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
runner.state.heldElsewhere = undefined;
|
|
56
|
-
if (scope.once.active)
|
|
57
|
-
scope.once.settle(runner.role, "no free entry");
|
|
58
|
-
return "skipped";
|
|
59
|
-
}
|
|
38
|
+
!hasOutstandingWork(runner.state));
|
|
39
|
+
}
|
|
40
|
+
/** The eligible-entries list for a base role, computed once per poll and memoized on the
|
|
41
|
+
* scope so the stand-down and claim-group paths share it. */
|
|
42
|
+
function eligibleForBase(scope, base) {
|
|
43
|
+
let eligible = scope.eligibleMemo.get(base);
|
|
44
|
+
if (eligible === undefined) {
|
|
45
|
+
eligible =
|
|
46
|
+
scope.claimGroups.get(base)?.eligible ?? eligibleEntries(scope.root, base);
|
|
47
|
+
scope.eligibleMemo.set(base, eligible);
|
|
60
48
|
}
|
|
49
|
+
return eligible;
|
|
50
|
+
}
|
|
51
|
+
/** Stand down a runner with no eligible entry at all on its scheduled and main-moved runs: the
|
|
52
|
+
* feature charter says "If PLANS.md is empty or everything is done, there is nothing to do" and
|
|
53
|
+
* the bugfix charter says "when every open entry carries [a Refused note], there is nothing to
|
|
54
|
+
* do", so a feature runner (primary included) with no eligible plan at all, or a bugfix runner
|
|
55
|
+
* whose open bugs are all held (no open bugs at all is search mode, which still runs), skips
|
|
56
|
+
* its scheduled and main-moved runs instead of paying a model run that can only end in
|
|
57
|
+
* nothing-to-do. nextRunAt and backoff stay put, so the first poll after a plan turns eligible
|
|
58
|
+
* starts a run for it. */
|
|
59
|
+
function standDownForNoWork(scope, runner, reason) {
|
|
60
|
+
if (!noWorkScheduledRun(scope, runner, reason))
|
|
61
|
+
return false;
|
|
62
|
+
if (eligibleForBase(scope, baseRoleOf(runner.role)).length > 0)
|
|
63
|
+
return false;
|
|
64
|
+
noteSkip(scope.root, runner, "no-free-entry");
|
|
65
|
+
runner.state.heldElsewhere = undefined;
|
|
66
|
+
if (scope.once.active)
|
|
67
|
+
scope.once.settle(runner.role, "no free entry");
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
/** Parallel work instances (part 4/7): a multi-runner base role's claim assignment, after
|
|
71
|
+
* every gate has admitted this runner. A runner that already holds a claim proceeds — its
|
|
72
|
+
* revision, resume or retry is the same entry. An unclaimed runner with outstanding work
|
|
73
|
+
* (a revision) is the mid-entry runner the group hold waits on: it proceeds unassigned —
|
|
74
|
+
* holding it too would deadlock the whole group, since only its run can settle the hold.
|
|
75
|
+
* Otherwise, when the run is not an inbox/resume run, it takes the first free entry; an
|
|
76
|
+
* extra instance with none free is skipped without touching its backoff, and the primary
|
|
77
|
+
* stands down too while siblings hold every eligible entry, unless an operator wake is fresh
|
|
78
|
+
* (then it runs unassigned). */
|
|
79
|
+
function claimGroupAdmission(scope, runner, reason) {
|
|
61
80
|
const claimGroup = scope.claimGroups.get(baseRoleOf(runner.role));
|
|
62
81
|
if (claimGroup) {
|
|
63
82
|
if (runner.state.claim) {
|
|
64
83
|
// keeps its claim
|
|
84
|
+
return "admitted";
|
|
65
85
|
}
|
|
66
86
|
else if (hasOutstandingWork(runner.state)) {
|
|
67
87
|
// finishes its own unrecorded entry
|
|
88
|
+
return "admitted";
|
|
68
89
|
}
|
|
69
90
|
else if (reason !== "inbox" && reason !== "resume") {
|
|
70
91
|
// The group holds an unclaimed mid-entry sibling: assign nothing this poll, or a
|
|
@@ -74,39 +95,7 @@ export function admitRunner(scope, runner, reason) {
|
|
|
74
95
|
noteSkip(scope.root, runner, "claim-hold");
|
|
75
96
|
return "skipped";
|
|
76
97
|
}
|
|
77
|
-
|
|
78
|
-
const pick = assignNext(free);
|
|
79
|
-
// An unassigned runner is told which entries its siblings hold (cleared when none).
|
|
80
|
-
const heldTitles = claimGroup.eligible
|
|
81
|
-
.filter((e) => claimGroup.held.has(e.key))
|
|
82
|
-
.map((e) => e.title);
|
|
83
|
-
runner.state.heldElsewhere = !pick && heldTitles.length > 0 ? heldTitles : undefined;
|
|
84
|
-
if (pick) {
|
|
85
|
-
runner.state.claim = {
|
|
86
|
-
file: baseRoleOf(runner.role) === BUGFIX_ROLE ? "BUGS.md" : "PLANS.md",
|
|
87
|
-
key: pick.key,
|
|
88
|
-
title: pick.title,
|
|
89
|
-
at: scope.now,
|
|
90
|
-
source: "assigned",
|
|
91
|
-
};
|
|
92
|
-
claimGroup.held.add(pick.key);
|
|
93
|
-
logEventBestEffort(scope.root, {
|
|
94
|
-
source: runner.role,
|
|
95
|
-
type: "claim",
|
|
96
|
-
action: "assigned",
|
|
97
|
-
key: pick.key,
|
|
98
|
-
title: pick.title,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
else if (instanceIndex(runner.role) >= 2 ||
|
|
102
|
-
(heldTitles.length > 0 && !freshlyWoken(runner.state))) {
|
|
103
|
-
// An idle extra instance, or a primary whose eligible entries siblings all hold
|
|
104
|
-
// (unless an operator wake is fresh): no run, no backoff climb.
|
|
105
|
-
noteSkip(scope.root, runner, "no-free-entry");
|
|
106
|
-
if (scope.once.active)
|
|
107
|
-
scope.once.settle(runner.role, "no free entry");
|
|
108
|
-
return "skipped";
|
|
109
|
-
}
|
|
98
|
+
return assignClaim(scope, runner, claimGroup.eligible, claimGroup.held);
|
|
110
99
|
}
|
|
111
100
|
}
|
|
112
101
|
else {
|
|
@@ -114,3 +103,47 @@ export function admitRunner(scope, runner, reason) {
|
|
|
114
103
|
}
|
|
115
104
|
return "admitted";
|
|
116
105
|
}
|
|
106
|
+
/** Assign the first free eligible entry to an unclaimed admitted runner: record the claim on
|
|
107
|
+
* the runner, add it to the group's held set, and log the assignment. With no free entry, an
|
|
108
|
+
* idle extra instance, or a primary whose eligible entries siblings all hold (unless an
|
|
109
|
+
* operator wake is fresh), stands down rather than run unassigned. */
|
|
110
|
+
function assignClaim(scope, runner, eligible, held) {
|
|
111
|
+
const free = eligible.filter((e) => !held.has(e.key));
|
|
112
|
+
const pick = assignNext(free);
|
|
113
|
+
// An unassigned runner is told which entries its siblings hold (cleared when none).
|
|
114
|
+
const heldTitles = eligible.filter((e) => held.has(e.key)).map((e) => e.title);
|
|
115
|
+
runner.state.heldElsewhere = !pick && heldTitles.length > 0 ? heldTitles : undefined;
|
|
116
|
+
if (pick) {
|
|
117
|
+
recordAssignedClaim(scope, runner, pick, held);
|
|
118
|
+
return "admitted";
|
|
119
|
+
}
|
|
120
|
+
else if (instanceIndex(runner.role) >= 2 ||
|
|
121
|
+
(heldTitles.length > 0 && !freshlyWoken(runner.state))) {
|
|
122
|
+
// An idle extra instance, or a primary whose eligible entries siblings all hold
|
|
123
|
+
// (unless an operator wake is fresh): no run, no backoff climb.
|
|
124
|
+
noteSkip(scope.root, runner, "no-free-entry");
|
|
125
|
+
if (scope.once.active)
|
|
126
|
+
scope.once.settle(runner.role, "no free entry");
|
|
127
|
+
return "skipped";
|
|
128
|
+
}
|
|
129
|
+
return "admitted";
|
|
130
|
+
}
|
|
131
|
+
/** Build the runner's claim record, add the picked entry to the group's held set, and log the
|
|
132
|
+
* assignment to the events feed. */
|
|
133
|
+
function recordAssignedClaim(scope, runner, pick, held) {
|
|
134
|
+
runner.state.claim = {
|
|
135
|
+
file: baseRoleOf(runner.role) === BUGFIX_ROLE ? "BUGS.md" : "PLANS.md",
|
|
136
|
+
key: pick.key,
|
|
137
|
+
title: pick.title,
|
|
138
|
+
at: scope.now,
|
|
139
|
+
source: "assigned",
|
|
140
|
+
};
|
|
141
|
+
held.add(pick.key);
|
|
142
|
+
logEventBestEffort(scope.root, {
|
|
143
|
+
source: runner.role,
|
|
144
|
+
type: "claim",
|
|
145
|
+
action: "assigned",
|
|
146
|
+
key: pick.key,
|
|
147
|
+
title: pick.title,
|
|
148
|
+
});
|
|
149
|
+
}
|