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
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* and nothing can get stuck"): the verdict is recomputed from the runner state every poll, no
|
|
8
8
|
* marker is written, and local midnight lifts it by itself.
|
|
9
9
|
*
|
|
10
|
-
* Deliberate differences from the streak gate's shape (src/gates/streak-
|
|
10
|
+
* Deliberate differences from the streak gate's shape (src/gates/streak-gates.ts), stated in
|
|
11
11
|
* the plan so no implementer re-litigates them:
|
|
12
12
|
* - This gate writes NOTHING to the shared per-role pause marker (src/team/team-state.ts's
|
|
13
13
|
* pauseRole). The marker is anonymous — after a restart across midnight the harness could
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* module owns the only event emission — the orchestrator's poll owns only the wiring. The
|
|
25
25
|
* in-memory `prev` and `warned` sets are the whole cross-poll memory: a restart with a
|
|
26
26
|
* still-over-cap role re-logs one `role_cap_paused` on the first poll, and a role already past
|
|
27
|
-
* 80% re-logs one `role_cap_warning` (the durable-cause honest report the streak-
|
|
27
|
+
* 80% re-logs one `role_cap_warning` (the durable-cause honest report the streak-gates doc
|
|
28
28
|
* accepts). */
|
|
29
29
|
import { groupBy } from "../collections.js";
|
|
30
30
|
import { budgetWarning, dailyCost, todayStamp } from "../budget/budget.js";
|
|
@@ -50,7 +50,7 @@ export function roleCapPaused(state, cap, now = Date.now()) {
|
|
|
50
50
|
* ids so a caller can log the base (the gate) or flatten the ids (the observer). */
|
|
51
51
|
export function roleCapVerdicts(runners, caps, now) {
|
|
52
52
|
// Group by base role via the shared Map-of-arrays step (collections.groupBy, which
|
|
53
|
-
// time-spend, history-data, and team-polls also fold), then drop the exempt director's
|
|
53
|
+
// failure-time-spend, history-data, and team-polls also fold), then drop the exempt director's
|
|
54
54
|
// group so it never holds a verdict; first-appearance order is preserved.
|
|
55
55
|
const groups = groupBy(runners, (r) => baseRoleOf(r.role));
|
|
56
56
|
groups.delete(DIRECTOR_ROLE);
|
|
@@ -71,6 +71,45 @@ export function roleCapVerdicts(runners, caps, now) {
|
|
|
71
71
|
export function newRoleCapGateState() {
|
|
72
72
|
return { prev: new Set(), warned: new Set() };
|
|
73
73
|
}
|
|
74
|
+
/** One base role's pause/resume edge, logged by the poll's loop: reads whether the role was
|
|
75
|
+
* already judged over its cap at the last poll (`state.prev`) and on ENTER logs one
|
|
76
|
+
* `role_cap_paused` (role, spentUsd, capUsd); on EXIT logs one `role_cap_resumed` (role). */
|
|
77
|
+
function logCapCrossing(root, base, verdict, cap, state) {
|
|
78
|
+
const was = state.prev.has(base);
|
|
79
|
+
if (verdict.over && !was) {
|
|
80
|
+
logEventBestEffort(root, {
|
|
81
|
+
source: TUMWATER_SOURCE,
|
|
82
|
+
type: "role_cap_paused",
|
|
83
|
+
role: base,
|
|
84
|
+
spentUsd: verdict.spentUsd,
|
|
85
|
+
capUsd: cap,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
else if (!verdict.over && was) {
|
|
89
|
+
logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_cap_resumed", role: base });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** The 80% early warning (PLANS.md 2026-10-09), the per-role twin of the team's
|
|
93
|
+
* budget_warning and edge-triggered with the same reset-on-below: crossing the threshold
|
|
94
|
+
* while still under the cap logs one `role_cap_warning`, a dip back under re-arms it. Warn
|
|
95
|
+
* only while the cap is open — a poll that jumps straight to the cap already logs its own
|
|
96
|
+
* `role_cap_paused`, so the warning never doubles the page on the poll the cap is reached. */
|
|
97
|
+
function logCapWarning(root, base, verdict, cap, state) {
|
|
98
|
+
const warn = !verdict.over && cap !== undefined && budgetWarning({ spentUsd: verdict.spentUsd, capUsd: cap });
|
|
99
|
+
if (warn && !state.warned.has(base)) {
|
|
100
|
+
logEventBestEffort(root, {
|
|
101
|
+
source: TUMWATER_SOURCE,
|
|
102
|
+
type: "role_cap_warning",
|
|
103
|
+
role: base,
|
|
104
|
+
spentUsd: verdict.spentUsd,
|
|
105
|
+
capUsd: cap,
|
|
106
|
+
});
|
|
107
|
+
state.warned.add(base);
|
|
108
|
+
}
|
|
109
|
+
else if (!warn) {
|
|
110
|
+
state.warned.delete(base);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
74
113
|
/** Step the gate by one orchestrator poll. Per runner (the director exempt, like every
|
|
75
114
|
* autonomous gate): judge `roleCapPaused` against its configured cap; on ENTER log one
|
|
76
115
|
* `role_cap_paused` carrying the role, its spend, and its cap; on EXIT log one
|
|
@@ -86,39 +125,9 @@ export function pollRoleCapGate(root, state, runners, caps, now) {
|
|
|
86
125
|
const paused = new Set();
|
|
87
126
|
const overBases = new Set();
|
|
88
127
|
for (const [base, verdict] of verdicts) {
|
|
89
|
-
const was = state.prev.has(base);
|
|
90
|
-
if (verdict.over && !was) {
|
|
91
|
-
logEventBestEffort(root, {
|
|
92
|
-
source: TUMWATER_SOURCE,
|
|
93
|
-
type: "role_cap_paused",
|
|
94
|
-
role: base,
|
|
95
|
-
spentUsd: verdict.spentUsd,
|
|
96
|
-
capUsd: caps?.[base],
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
else if (!verdict.over && was) {
|
|
100
|
-
logEventBestEffort(root, { source: TUMWATER_SOURCE, type: "role_cap_resumed", role: base });
|
|
101
|
-
}
|
|
102
|
-
// The 80% early warning (PLANS.md 2026-10-09), the per-role twin of the team's
|
|
103
|
-
// budget_warning and edge-triggered with the same reset-on-below: crossing the threshold
|
|
104
|
-
// while still under the cap logs one role_cap_warning, a dip back under re-arms it. Warn
|
|
105
|
-
// only while the cap is open — a poll that jumps straight to the cap already logs its own
|
|
106
|
-
// role_cap_paused, so the warning never doubles the page on the poll the cap is reached.
|
|
107
128
|
const cap = caps?.[base];
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
logEventBestEffort(root, {
|
|
111
|
-
source: TUMWATER_SOURCE,
|
|
112
|
-
type: "role_cap_warning",
|
|
113
|
-
role: base,
|
|
114
|
-
spentUsd: verdict.spentUsd,
|
|
115
|
-
capUsd: cap,
|
|
116
|
-
});
|
|
117
|
-
state.warned.add(base);
|
|
118
|
-
}
|
|
119
|
-
else if (!warn) {
|
|
120
|
-
state.warned.delete(base);
|
|
121
|
-
}
|
|
129
|
+
logCapCrossing(root, base, verdict, cap, state);
|
|
130
|
+
logCapWarning(root, base, verdict, cap, state);
|
|
122
131
|
if (verdict.over) {
|
|
123
132
|
overBases.add(base);
|
|
124
133
|
for (const id of verdict.roleIds)
|
|
@@ -9,15 +9,16 @@ import { DETACHED_HEAD_MESSAGE, NOT_A_REPO_MESSAGE, NOT_INITIALIZED_MESSAGE, NO_
|
|
|
9
9
|
import { owningTeamRoot } from "../paths.js";
|
|
10
10
|
import { backendMissingMessage, findBackendBinary, resolveBackendBin } from "../pi/pi-bin.js";
|
|
11
11
|
import { seedClaudeAccount } from "../agent/claude/claude-account.js";
|
|
12
|
-
import { seedCodexAccount } from "../agent/codex/codex-account.js";
|
|
12
|
+
import { codexApiKeyRefusalMessage, seedCodexAccount } from "../agent/codex/codex-account.js";
|
|
13
|
+
import { distinctOrdered } from "../collections.js";
|
|
13
14
|
import { opencodeCatalogStale, readOpencodeCache, refreshOpencodeCatalog, } from "../agent/opencode/opencode-catalog.js";
|
|
14
15
|
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
15
16
|
import { HOUR_MS } from "../text/datetime.js";
|
|
16
17
|
import { errorMessage } from "../text/text.js";
|
|
17
18
|
import { backendById } from "../agent/agent-backend.js";
|
|
18
19
|
// Register the built-in pi and claude adapters into the registry this module reads (see
|
|
19
|
-
// unregisteredBackendInUse) — importing run
|
|
20
|
-
import "../agent/run
|
|
20
|
+
// unregisteredBackendInUse) — importing agent-run.ts is the one place they register.
|
|
21
|
+
import "../agent/agent-run.js";
|
|
21
22
|
/** The repo-level preconditions every repo-bound command shares (requireReadyRepo in
|
|
22
23
|
* cli/cli-repo-gate.ts): git
|
|
23
24
|
* on PATH, a repository, tumwater.json present, at least one commit. The first unmet one's
|
|
@@ -58,7 +59,7 @@ opencodeSeed = {}) {
|
|
|
58
59
|
// A backend an enabled seam routes to that this build has no registered adapter for cannot
|
|
59
60
|
// run its jobs: refuse to start rather than letting the run fall through to pi with a model
|
|
60
61
|
// id pi cannot serve (plans/opencode-backend.md part 2/7). Read the registry through
|
|
61
|
-
// run
|
|
62
|
+
// agent-run.ts, whose module scope registers the built-in pi and claude adapters.
|
|
62
63
|
const unregistered = unregisteredBackendInUse(config);
|
|
63
64
|
if (unregistered !== undefined) {
|
|
64
65
|
return { problem: `backend ${unregistered} is configured but this build has no adapter for it` };
|
|
@@ -88,7 +89,7 @@ opencodeSeed = {}) {
|
|
|
88
89
|
const codexAccount = seedOnce(root, config, "codex", codexSeedAttempted, () => seedCodexAccount(root, config));
|
|
89
90
|
if (codexAccount?.billing === "api") {
|
|
90
91
|
return {
|
|
91
|
-
problem: "
|
|
92
|
+
problem: codexApiKeyRefusalMessage("<home>"),
|
|
92
93
|
};
|
|
93
94
|
}
|
|
94
95
|
// Seed the opencode models catalog (part 6b/7) before the first run: the poll surfaces price
|
|
@@ -189,16 +190,16 @@ export function opencodeProviders(config) {
|
|
|
189
190
|
return [...providers];
|
|
190
191
|
}
|
|
191
192
|
/** Every agent backend an enabled seam resolves to — the distinct agents of the one
|
|
192
|
-
* seamsInUse list (startup-
|
|
193
|
+
* seamsInUse list (startup-gates's consumer of it), so a claude or pi clause that exists only
|
|
193
194
|
* as a fallback, the resolver, or the director is checked exactly like a primary seam.
|
|
194
195
|
* One list the startup gate and doctor's binary check share, so a route the harness would
|
|
195
196
|
* spawn is checked on both surfaces. */
|
|
196
197
|
export function backendsInUse(config) {
|
|
197
|
-
return
|
|
198
|
+
return distinctOrdered(seamsInUse(config).map((s) => s.agent ?? "pi"));
|
|
198
199
|
}
|
|
199
200
|
/** The first backend an enabled seam routes to that this build has no registered adapter for,
|
|
200
201
|
* or undefined when every seam's backend is registered. The built-in pi and claude adapters
|
|
201
|
-
* are registered by importing src/agent/run
|
|
202
|
+
* are registered by importing src/agent/agent-run.ts (see the import above); a seam backend
|
|
202
203
|
* that is not in the registry is one this build cannot run. Doctor's binary check asks the
|
|
203
204
|
* same question, so the startup gate and doctor cannot disagree about which backend a build
|
|
204
205
|
* can serve. opencode's and codex's adapters now register like pi's and claude's, so only a
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** The error-streak circuit breaker (PLANS.md 2026-09-30): a role whose runs keep failing
|
|
2
2
|
* consecutively is warned at ERROR_STREAK_WARN (src/run/run-apply.ts) and named by the team-wide
|
|
3
|
-
* storm alarms (src/team/error-storm.ts, src/team/failure-spread.ts), but both
|
|
4
|
-
* role keeps running on the error ladder's 600 s max backoff forever,
|
|
5
|
-
* spend on a role that cannot succeed. Past ERROR_STREAK_BREAKER
|
|
6
|
-
* pauses the role through the same per-role pause marker
|
|
3
|
+
* storm alarms (src/team/team-error-storm.ts, src/team/team-failure-spread.ts), but both
|
|
4
|
+
* only talk — the role keeps running on the error ladder's 600 s max backoff forever,
|
|
5
|
+
* burning a model slot and spend on a role that cannot succeed. Past ERROR_STREAK_BREAKER
|
|
6
|
+
* consecutive failed runs this gate pauses the role through the same per-role pause marker
|
|
7
|
+
* the operator's `tumwater pause --role
|
|
7
8
|
* <id>` writes (src/team/team-state.ts's pauseRole — the marker, lock, and idempotence come
|
|
8
9
|
* free), so the scheduler's existing pausedRolesSet skip blocks new runs with zero scheduler
|
|
9
10
|
* changes. The director is NOT exempt: a failing director cannot process prompts anyway, and one
|
|
@@ -44,6 +45,73 @@ import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
|
44
45
|
export function newStreakGateState() {
|
|
45
46
|
return { acked: new Map() };
|
|
46
47
|
}
|
|
48
|
+
/** Seed a fresh in-run ack cache from the persisted excused streak, so a restart keeps the
|
|
49
|
+
* grace the breaker (and a later operator resume) relies on instead of re-pausing a role
|
|
50
|
+
* whose frozen streak never ran down (BUGS.md 2026-10-23). */
|
|
51
|
+
function seedStreakAck(state, role, s) {
|
|
52
|
+
if (!state.acked.has(role) && s.streakAcked !== undefined) {
|
|
53
|
+
state.acked.set(role, s.streakAcked);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Ack while paused: the marker is the ack carrier. The streak is frozen while a role is
|
|
57
|
+
* paused (no new runs start), so recording it here is exactly the level the resume excuses. */
|
|
58
|
+
function ackPausedStreak(root, role, streak, s, state) {
|
|
59
|
+
if ((state.acked.get(role) ?? 0) !== streak) {
|
|
60
|
+
state.acked.set(role, streak);
|
|
61
|
+
s.streakAcked = streak;
|
|
62
|
+
saveRunnerStateBestEffort(root, s);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** A streak that reset through the role's own success ends the excused episode: the next one
|
|
66
|
+
* must climb a fresh ERROR_STREAK_BREAKER of its own, so drop the durable ack too. */
|
|
67
|
+
function clearResetStreak(root, role, s, state) {
|
|
68
|
+
if (state.acked.get(role) !== undefined) {
|
|
69
|
+
state.acked.delete(role);
|
|
70
|
+
delete s.streakAcked;
|
|
71
|
+
saveRunnerStateBestEffort(root, s);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Ack at trip time, before the pause is observed: the bar for the next trip is
|
|
75
|
+
* ERROR_STREAK_BREAKER more failures from this level, whether or not any poll sees the role
|
|
76
|
+
* paused in between (an operator resume racing the next poll must not re-trip). The durable
|
|
77
|
+
* copy is written only once the pause is confirmed below — a failed or crashed pauseRole must
|
|
78
|
+
* not leave a permanent excused ack with no marker behind it. Returns the pause key when the
|
|
79
|
+
* pause was written, undefined when pauseRole failed. */
|
|
80
|
+
function tripStreakPause(root, role, streak, s, state, pauseKey) {
|
|
81
|
+
state.acked.set(role, streak);
|
|
82
|
+
if (!pauseRole(root, pauseKey(role)))
|
|
83
|
+
return undefined;
|
|
84
|
+
s.streakAcked = streak;
|
|
85
|
+
saveRunnerStateBestEffort(root, s);
|
|
86
|
+
logEventBestEffort(root, {
|
|
87
|
+
source: TUMWATER_SOURCE,
|
|
88
|
+
type: "role_streak_paused",
|
|
89
|
+
role: pauseKey(role),
|
|
90
|
+
streak,
|
|
91
|
+
...(s.lastError ? { lastError: s.lastError } : {}),
|
|
92
|
+
});
|
|
93
|
+
return pauseKey(role);
|
|
94
|
+
}
|
|
95
|
+
/** One runner's turn in the poll: seed the in-run ack, ack while paused, clear on a reset,
|
|
96
|
+
* and trip past the bar. Returns the pause key when this poll tripped the role, undefined
|
|
97
|
+
* otherwise. The order is forced and unchanged: seed, then paused, then reset, then the bar,
|
|
98
|
+
* then trip. */
|
|
99
|
+
function pollStreakRunner(root, role, s, state, pausedRoles, pauseKey) {
|
|
100
|
+
const streak = s.consecutiveErrors ?? 0;
|
|
101
|
+
seedStreakAck(state, role, s);
|
|
102
|
+
if (runnerPaused(pausedRoles, role)) {
|
|
103
|
+
ackPausedStreak(root, role, streak, s, state);
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
if (streak === 0) {
|
|
107
|
+
clearResetStreak(root, role, s, state);
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const ack = state.acked.get(role) ?? 0;
|
|
111
|
+
if (streak < ERROR_STREAK_BREAKER + ack)
|
|
112
|
+
return undefined;
|
|
113
|
+
return tripStreakPause(root, role, streak, s, state, pauseKey);
|
|
114
|
+
}
|
|
47
115
|
/** Step the gate by one orchestrator poll. For each runner whose consecutive error streak
|
|
48
116
|
* has reached ERROR_STREAK_BREAKER plus what its ack already excused, and whose role is not
|
|
49
117
|
* in `pausedRoles`: write the per-role pause marker and log one `role_streak_paused` event
|
|
@@ -59,54 +127,9 @@ export function newStreakGateState() {
|
|
|
59
127
|
export function pollStreakGate(root, state, runners, pausedRoles, pauseKey = (r) => r) {
|
|
60
128
|
const tripped = [];
|
|
61
129
|
for (const { role, state: s } of runners) {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// whose frozen streak never ran down (BUGS.md 2026-10-23).
|
|
66
|
-
if (!state.acked.has(role) && s.streakAcked !== undefined) {
|
|
67
|
-
state.acked.set(role, s.streakAcked);
|
|
68
|
-
}
|
|
69
|
-
// Ack while paused: the marker is the ack carrier. The streak is frozen while a role is
|
|
70
|
-
// paused (no new runs start), so recording it here is exactly the level the resume excuses.
|
|
71
|
-
if (runnerPaused(pausedRoles, role)) {
|
|
72
|
-
if ((state.acked.get(role) ?? 0) !== streak) {
|
|
73
|
-
state.acked.set(role, streak);
|
|
74
|
-
s.streakAcked = streak;
|
|
75
|
-
saveRunnerStateBestEffort(root, s);
|
|
76
|
-
}
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (streak === 0) {
|
|
80
|
-
// A streak that reset through the role's own success ends the excused episode: the
|
|
81
|
-
// next one must climb a fresh ERROR_STREAK_BREAKER of its own, so drop the durable ack too.
|
|
82
|
-
if (state.acked.get(role) !== undefined) {
|
|
83
|
-
state.acked.delete(role);
|
|
84
|
-
delete s.streakAcked;
|
|
85
|
-
saveRunnerStateBestEffort(root, s);
|
|
86
|
-
}
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
const ack = state.acked.get(role) ?? 0;
|
|
90
|
-
if (streak < ERROR_STREAK_BREAKER + ack)
|
|
91
|
-
continue;
|
|
92
|
-
// Ack at trip time, before the pause is observed: the bar for the next trip is
|
|
93
|
-
// ERROR_STREAK_BREAKER more failures from this level, whether or not any poll sees the
|
|
94
|
-
// role paused in between (an operator resume racing the next poll must not re-trip). The
|
|
95
|
-
// durable copy is written only once the pause is confirmed below — a failed or crashed
|
|
96
|
-
// pauseRole must not leave a permanent excused ack with no marker behind it.
|
|
97
|
-
state.acked.set(role, streak);
|
|
98
|
-
if (pauseRole(root, pauseKey(role))) {
|
|
99
|
-
s.streakAcked = streak;
|
|
100
|
-
saveRunnerStateBestEffort(root, s);
|
|
101
|
-
logEventBestEffort(root, {
|
|
102
|
-
source: TUMWATER_SOURCE,
|
|
103
|
-
type: "role_streak_paused",
|
|
104
|
-
role: pauseKey(role),
|
|
105
|
-
streak,
|
|
106
|
-
...(s.lastError ? { lastError: s.lastError } : {}),
|
|
107
|
-
});
|
|
108
|
-
tripped.push(pauseKey(role));
|
|
109
|
-
}
|
|
130
|
+
const key = pollStreakRunner(root, role, s, state, pausedRoles, pauseKey);
|
|
131
|
+
if (key !== undefined)
|
|
132
|
+
tripped.push(key);
|
|
110
133
|
}
|
|
111
134
|
return tripped;
|
|
112
135
|
}
|
package/dist/src/git/git-diff.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
import { changeBaseRev, gitLines } from "./git.js";
|
|
6
6
|
import { git, gitTry } from "./git-run.js";
|
|
7
7
|
import { cutSplitsSurrogatePair } from "../text/text.js";
|
|
8
|
+
/** The one cap on a fetched change diff, shared by the repo-relative patch (aheadOfMainDiff,
|
|
9
|
+
* revisionInterdiff) and the view half's uncommitted patch (change-data.ts) so neither side
|
|
10
|
+
* of a reviewed change can print unbounded text and one bound is all they agree on. */
|
|
11
|
+
export const DIFF_MAX_BYTES = 200_000;
|
|
8
12
|
/** Git's C-quoting short escapes (its `sq_lookup` table) and the byte each decodes to. The
|
|
9
13
|
* mapping was a nine-case switch that repeated `bytes += …; break;` per entry; keeping it as
|
|
10
14
|
* data lets the decoder below handle the octal and literal fallbacks in one place. */
|
|
@@ -153,7 +157,7 @@ function splitDiffByFile(diff) {
|
|
|
153
157
|
* see, and a binary file's short "Binary files differ" stub naturally ranks below any real text
|
|
154
158
|
* change. A failed `git diff` throws: the reviewer handed an empty diff judged no change at
|
|
155
159
|
* all. A view catches it itself. */
|
|
156
|
-
export async function aheadOfMainDiff(wt, mainBranch, maxBytes =
|
|
160
|
+
export async function aheadOfMainDiff(wt, mainBranch, maxBytes = DIFF_MAX_BYTES, pathspec = []) {
|
|
157
161
|
const range = `${mainBranch}...HEAD`;
|
|
158
162
|
const full = await git(wt, "diff", range, ...pathspec);
|
|
159
163
|
if (full.length <= maxBytes)
|
|
@@ -181,7 +185,7 @@ export async function aheadOfMainDiff(wt, mainBranch, maxBytes = 200_000, pathsp
|
|
|
181
185
|
* the two versions even when the change is tiny: at the default 60% a two-line addition can
|
|
182
186
|
* fail to pair and render as two unrelated commits (`<`/`>`), which is exactly the case a
|
|
183
187
|
* small revision is. */
|
|
184
|
-
export async function revisionInterdiff(wt, mainBranch, priorSha, head, maxBytes =
|
|
188
|
+
export async function revisionInterdiff(wt, mainBranch, priorSha, head, maxBytes = DIFF_MAX_BYTES) {
|
|
185
189
|
try {
|
|
186
190
|
const priorBase = await changeBaseRev(wt, mainBranch, priorSha);
|
|
187
191
|
const headBase = await changeBaseRev(wt, mainBranch, head);
|
package/dist/src/git/git.js
CHANGED
|
@@ -64,40 +64,57 @@ const GIT_REDIRECT_ENV = ["GIT_DIR", "GIT_COMMON_DIR", "GIT_WORK_TREE", "GIT_NAM
|
|
|
64
64
|
/** Refs git keeps per worktree (in the worktree's own git dir, not the common one). They are
|
|
65
65
|
* left to git. */
|
|
66
66
|
const PER_WORKTREE_REF = /^refs\/(?:bisect|worktree|rewritten)\//;
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const gitdir = resolveGitDir(dir);
|
|
76
|
-
if (gitdir === undefined)
|
|
77
|
-
return undefined;
|
|
78
|
-
let commondir = gitdir;
|
|
67
|
+
/** Whether an environment variable makes git look somewhere other than the files `dir` holds. */
|
|
68
|
+
function gitRedirectsSet() {
|
|
69
|
+
return GIT_REDIRECT_ENV.some((name) => process.env[name]);
|
|
70
|
+
}
|
|
71
|
+
/** The shared refs/objects dir a gitdir points at via its `commondir` file: the gitdir itself
|
|
72
|
+
* when there is no file (a primary checkout), the resolved pointer when there is, and
|
|
73
|
+
* undefined when the file is blank or unreadable for any reason but absence. */
|
|
74
|
+
function resolveCommondir(gitdir) {
|
|
79
75
|
try {
|
|
80
76
|
const pointer = fs.readFileSync(path.join(gitdir, "commondir"), "utf8").trim();
|
|
81
77
|
if (pointer === "")
|
|
82
78
|
return undefined;
|
|
83
|
-
|
|
79
|
+
return path.resolve(gitdir, pointer);
|
|
84
80
|
}
|
|
85
81
|
catch (err) {
|
|
86
82
|
if (errCode(err) !== "ENOENT")
|
|
87
83
|
return undefined; // A primary checkout has none.
|
|
84
|
+
return gitdir;
|
|
88
85
|
}
|
|
86
|
+
}
|
|
87
|
+
/** Whether the files-backend trust checks hold: this process's user owns both dirs, the
|
|
88
|
+
* objects dir and HEAD are present, and no reftable store shadows them. */
|
|
89
|
+
function filesBackendTrusted(dir, gitdir, commondir, uid) {
|
|
89
90
|
try {
|
|
90
91
|
if (fs.statSync(dir).uid !== uid || fs.statSync(gitdir).uid !== uid)
|
|
91
|
-
return
|
|
92
|
+
return false;
|
|
92
93
|
if (!fs.statSync(path.join(commondir, "objects")).isDirectory())
|
|
93
|
-
return
|
|
94
|
+
return false;
|
|
94
95
|
if (!fs.statSync(path.join(gitdir, "HEAD")).isFile())
|
|
95
|
-
return
|
|
96
|
+
return false;
|
|
96
97
|
}
|
|
97
98
|
catch {
|
|
98
|
-
return
|
|
99
|
+
return false;
|
|
99
100
|
}
|
|
100
|
-
|
|
101
|
+
return !fs.existsSync(path.join(commondir, "reftable"));
|
|
102
|
+
}
|
|
103
|
+
/** `dir`'s git dirs when its `.git` resolves from files to a files-backend repository this
|
|
104
|
+
* process's user owns, or undefined when git should decide. */
|
|
105
|
+
function repoFiles(dir) {
|
|
106
|
+
if (gitRedirectsSet())
|
|
107
|
+
return undefined;
|
|
108
|
+
const uid = process.getuid?.();
|
|
109
|
+
if (uid === undefined)
|
|
110
|
+
return undefined;
|
|
111
|
+
const gitdir = resolveGitDir(dir);
|
|
112
|
+
if (gitdir === undefined)
|
|
113
|
+
return undefined;
|
|
114
|
+
const commondir = resolveCommondir(gitdir);
|
|
115
|
+
if (commondir === undefined)
|
|
116
|
+
return undefined;
|
|
117
|
+
if (!filesBackendTrusted(dir, gitdir, commondir, uid))
|
|
101
118
|
return undefined;
|
|
102
119
|
return { gitdir, commondir };
|
|
103
120
|
}
|
|
@@ -147,35 +164,33 @@ function storedRef(commondir, ref) {
|
|
|
147
164
|
}
|
|
148
165
|
return shaFromPackedRefs(packed, ` ${ref}`);
|
|
149
166
|
}
|
|
150
|
-
/** The
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (repo === undefined)
|
|
158
|
-
return undefined;
|
|
159
|
-
if (ref === "HEAD") {
|
|
160
|
-
let head;
|
|
161
|
-
try {
|
|
162
|
-
head = fs.readFileSync(path.join(repo.gitdir, "HEAD"), "utf8").trim();
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
return undefined;
|
|
166
|
-
}
|
|
167
|
-
if (isSha(head))
|
|
168
|
-
return head;
|
|
169
|
-
const target = head.startsWith("ref: ") ? head.slice("ref: ".length).trim() : "";
|
|
170
|
-
if (!target.startsWith("refs/heads/") || !isPlainRefName(target))
|
|
171
|
-
return undefined;
|
|
172
|
-
return storedRef(repo.commondir, target) ?? undefined;
|
|
167
|
+
/** The HEAD read: the detached SHA held in `<repo.gitdir>/HEAD`, else the SHA its `ref: `
|
|
168
|
+
* branch symref (a plain `refs/heads/…` name) resolves to, or undefined when the files do not
|
|
169
|
+
* settle it. */
|
|
170
|
+
function refShaFromHead(repo) {
|
|
171
|
+
let head;
|
|
172
|
+
try {
|
|
173
|
+
head = fs.readFileSync(path.join(repo.gitdir, "HEAD"), "utf8").trim();
|
|
173
174
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return undefined;
|
|
177
|
-
return storedRef(repo.commondir, ref) ?? undefined;
|
|
175
|
+
catch {
|
|
176
|
+
return undefined;
|
|
178
177
|
}
|
|
178
|
+
if (isSha(head))
|
|
179
|
+
return head;
|
|
180
|
+
const target = head.startsWith("ref: ") ? head.slice("ref: ".length).trim() : "";
|
|
181
|
+
if (!target.startsWith("refs/heads/") || !isPlainRefName(target))
|
|
182
|
+
return undefined;
|
|
183
|
+
return storedRef(repo.commondir, target) ?? undefined;
|
|
184
|
+
}
|
|
185
|
+
/** A full `refs/…` name: its plain-name and per-worktree guards, then the stored read. */
|
|
186
|
+
function refShaFromFullRefName(repo, ref) {
|
|
187
|
+
if (!isPlainRefName(ref) || PER_WORKTREE_REF.test(ref))
|
|
188
|
+
return undefined;
|
|
189
|
+
return storedRef(repo.commondir, ref) ?? undefined;
|
|
190
|
+
}
|
|
191
|
+
/** A short branch name: the DWIM-order guards (`<gitdir>/<name>`, `refs/<name>` and
|
|
192
|
+
* `refs/tags/<name>` all outrank the branch), then `refs/heads/<name>`. */
|
|
193
|
+
function refShaFromShortName(repo, ref) {
|
|
179
194
|
// A full object name means the object to git, ahead of any same-named ref.
|
|
180
195
|
if (isSha(ref) || !isPlainRefName(`refs/heads/${ref}`))
|
|
181
196
|
return undefined;
|
|
@@ -189,6 +204,21 @@ export function refShaFromFiles(dir, ref) {
|
|
|
189
204
|
}
|
|
190
205
|
return storedRef(repo.commondir, `refs/heads/${ref}`) ?? undefined;
|
|
191
206
|
}
|
|
207
|
+
/** The SHA `git rev-parse --verify <ref>` would print in `dir`, read from files, or undefined
|
|
208
|
+
* when the files do not settle it (see the section comment): the caller then spawns git. `ref`
|
|
209
|
+
* is HEAD (followed when it is a branch symref or a detached SHA), a full `refs/…` name, or a
|
|
210
|
+
* short branch name, taken only when git's DWIM order could not resolve it to anything else
|
|
211
|
+
* first (`<gitdir>/<name>`, `refs/<name>`, `refs/tags/<name>` all absent). */
|
|
212
|
+
export function refShaFromFiles(dir, ref) {
|
|
213
|
+
const repo = repoFiles(dir);
|
|
214
|
+
if (repo === undefined)
|
|
215
|
+
return undefined;
|
|
216
|
+
if (ref === "HEAD")
|
|
217
|
+
return refShaFromHead(repo);
|
|
218
|
+
if (ref.startsWith("refs/"))
|
|
219
|
+
return refShaFromFullRefName(repo, ref);
|
|
220
|
+
return refShaFromShortName(repo, ref);
|
|
221
|
+
}
|
|
192
222
|
/** False when the files settle that `root`'s repository has no linked-worktree registrations
|
|
193
223
|
* (its `<commondir>/worktrees` admin dir is absent or empty), so `git worktree prune` has
|
|
194
224
|
* nothing to do. True when there are some, and undefined when git should decide. */
|
|
@@ -19,7 +19,6 @@ import { beginWorktreeUse } from "./worktree-use.js";
|
|
|
19
19
|
import { ensureDetachedWorktree, removeWorktreeDir } from "./worktree.js";
|
|
20
20
|
import { logEventBestEffort, warnEventBestEffort, tryOrWarn, TUMWATER_SOURCE } from "../events/events.js";
|
|
21
21
|
import { errorMessage } from "../text/text.js";
|
|
22
|
-
import { dropProviderLeaseKey } from "../concurrency/provider-lease.js";
|
|
23
22
|
/** In-process waiters per root, woken when any slot is released. Keyed by resolved root so two
|
|
24
23
|
* roots never wake each other. */
|
|
25
24
|
const waiters = new Map();
|
|
@@ -192,11 +191,9 @@ async function acquire(root, role, purpose, signal, slotBudget) {
|
|
|
192
191
|
logSlotWait(root, role, purpose, Date.now() - startedAt);
|
|
193
192
|
return chosen;
|
|
194
193
|
}
|
|
195
|
-
// The pool-slot wait
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
// re-key waiter is waiting to drain; the next run re-takes the key as an ordinary re-key.
|
|
199
|
-
await dropProviderLeaseKey();
|
|
194
|
+
// The pool-slot wait needs no permit dropped: a run or verify holds only the one team
|
|
195
|
+
// permit (no provider key any more), so it parks on the busy pool like any other slot
|
|
196
|
+
// waiter.
|
|
200
197
|
const waiter = registerWaiter(root);
|
|
201
198
|
try {
|
|
202
199
|
await raceAbort(waiter.promise, signal);
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
* The dashboard's GET settings/admin handlers (src/gui/gui-server.ts routes to them):
|
|
3
3
|
* prompt-text, doctor, config, retire-list, and brief. Each answers its request and touches no
|
|
4
4
|
* socket beyond its own `res`; the domain work lives one layer down (doctor/doctor.js,
|
|
5
|
-
* config/config.js, operator/retire.js), so this module only adapts HTTP onto it.
|
|
6
|
-
* into gui/gui-endpoints.ts beside the other per-family GET endpoint modules.
|
|
5
|
+
* config/config.js, operator/operator-retire.js), so this module only adapts HTTP onto it.
|
|
6
|
+
* it is barrelled into gui/gui-endpoints.ts beside the other per-family GET endpoint modules.
|
|
7
7
|
*/
|
|
8
8
|
import { runDoctor } from "../doctor/doctor.js";
|
|
9
9
|
import { knownRoleIds, loadConfigSafe } from "../config/config.js";
|
|
10
10
|
import { runnerEnabled } from "../roles/runner-ids.js";
|
|
11
|
-
import { artifactPhrase, previewRetire } from "../operator/retire.js";
|
|
11
|
+
import { artifactPhrase, previewRetire } from "../operator/operator-retire.js";
|
|
12
12
|
import { EDITABLE_CONFIG_KEYS } from "../config/config-editable-keys.js";
|
|
13
13
|
import { readBrief } from "../brief/brief.js";
|
|
14
14
|
import { readQueuedPromptBody } from "../inbox/inbox-edit.js";
|
package/dist/src/gui/gui-args.js
CHANGED
|
@@ -80,6 +80,18 @@ export function rejectBadQueueFileName(res, name) {
|
|
|
80
80
|
badRequest(res, problem);
|
|
81
81
|
return true;
|
|
82
82
|
}
|
|
83
|
+
/** Pull a required `sha` commit id out of a POST body by the rule the History revert, the
|
|
84
|
+
* merge-queue cancel/top, and the post-review accept endpoints share: a value that is not a
|
|
85
|
+
* 4-64 hex-digit commit id (so never a flag or a ref name) is refused as a 400 naming the
|
|
86
|
+
* value. Returns the validated commit id, or null once the 400 is sent — the body-sha twin of
|
|
87
|
+
* requireStringField / requirePausedFlag, so the four operators' validation and 400 wording
|
|
88
|
+
* cannot drift. */
|
|
89
|
+
export function requireSha(res, sha) {
|
|
90
|
+
if (typeof sha === "string" && /^[0-9a-f]{4,64}$/i.test(sha))
|
|
91
|
+
return sha;
|
|
92
|
+
badRequest(res, `"sha" must be a hex commit id of 4-64 digits${gotSuffix(sha)}`);
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
83
95
|
/** Read an integer query parameter with the GUI's established discipline for an explicit
|
|
84
96
|
* count: absent → `fallback` (or, when no fallback is given, the parameter is required and
|
|
85
97
|
* its absence is its own 400 — "<name> required"); present but not a plain decimal integer
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* question-answer, backlog-file, backlog-cancel, backlog-release, backlog-top, and
|
|
5
5
|
* backlog-edit — the `checkedBacklogKind` / `checkedEntryPositionAndTitle` validators they share
|
|
6
6
|
* ride with them. Each handler adapts
|
|
7
|
-
* HTTP onto the domain work one layer down (backlog-write, question-answer, backlog-commit)
|
|
7
|
+
* HTTP onto the domain work one layer down (backlog-write, backlog-question-answer, backlog-commit)
|
|
8
8
|
* and touches no socket beyond its own `res`.
|
|
9
9
|
*/
|
|
10
10
|
import { backlogHolds, cancelledClaimNote, stillHeldNote } from "../backlog/backlog-eligibility.js";
|
|
@@ -15,7 +15,7 @@ import { commitOperatorBacklogEdit } from "../backlog/backlog-commit.js";
|
|
|
15
15
|
import { stampedSubject } from "../git/commit-message.js";
|
|
16
16
|
import { fileEntryWithBody, normalizeEntryBody, OPERATOR_FILE_KINDS, tryCancelEntry, tryEditEntry, bodyStructureProblem } from "../backlog/backlog-write.js";
|
|
17
17
|
import { tryMoveEntryToTop, tryReleaseEntry } from "../backlog/backlog-release.js";
|
|
18
|
-
import { tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE } from "../backlog/question-answer.js";
|
|
18
|
+
import { tryAnswerQuestion, UNCLOSED_FENCE_MESSAGE } from "../backlog/backlog-question-answer.js";
|
|
19
19
|
import { promptLengthProblem } from "../inbox/inbox-submit.js";
|
|
20
20
|
import { requestWake, submitRolePromptAndWake, wakeQuestionAsker } from "../operator/operator-intent.js";
|
|
21
21
|
import { badRequest, okGone, sendJson } from "./gui-http-body.js";
|
|
@@ -202,8 +202,8 @@ async function respondBacklogWrite(res, root, kind, position, outcome, verb, sta
|
|
|
202
202
|
* `tumwater plan|bug --release <n>` through the non-exiting tryReleaseEntry. Body as
|
|
203
203
|
* backlog-cancel's. A released entry that is not still held is newly eligible, so the kind's
|
|
204
204
|
* role is woken (a held one wakes no role and carries `heldNote`); `gone` and `moved` answer
|
|
205
|
-
* okGone, and an entry with
|
|
206
|
-
*
|
|
205
|
+
* okGone, and an entry with no Refused or Needs review note answers status "not_refused" with no
|
|
206
|
+
* write and no wake. */
|
|
207
207
|
export async function handleBacklogRelease(req, res, root) {
|
|
208
208
|
const body = await readPostBody(req, res, '{"kind": "bug", "position": 1, "title": "<heading>"}');
|
|
209
209
|
if (!body)
|