tumwater 0.2.0 → 0.3.0
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 +27 -13
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +8 -9
- package/dist/src/agent/agent-backend.js +8 -6
- package/dist/src/agent/backend-traits.js +42 -25
- package/dist/src/agent/claude/claude-account.js +71 -204
- package/dist/src/agent/claude/claude-args.js +9 -6
- package/dist/src/agent/claude/claude-backend.js +10 -10
- package/dist/src/agent/claude/claude-env.js +1 -1
- package/dist/src/agent/claude/claude-stream.js +4 -3
- package/dist/src/agent/claude/claude-write-guard.js +49 -0
- package/dist/src/agent/claude/write-guard-hook.js +13 -0
- package/dist/src/agent/codex/codex-account.js +119 -0
- package/dist/src/agent/codex/codex-args.js +181 -0
- package/dist/src/agent/codex/codex-backend.js +279 -0
- package/dist/src/agent/codex/codex-catalog.js +97 -0
- package/dist/src/agent/codex/codex-env.js +18 -0
- package/dist/src/agent/codex/codex-hook-trust.js +246 -0
- package/dist/src/agent/codex/codex-patch-paths.js +36 -0
- package/dist/src/agent/codex/codex-rollout.js +179 -0
- package/dist/src/agent/codex/codex-stream.js +402 -0
- package/dist/src/agent/codex/codex-write-guard.js +86 -0
- package/dist/src/agent/codex/write-guard-hook.js +12 -0
- package/dist/src/agent/image-input.js +29 -24
- package/dist/src/agent/notes-shim.js +11 -12
- package/dist/src/agent/opencode/opencode-args.js +0 -1
- package/dist/src/agent/opencode/opencode-backend.js +12 -9
- package/dist/src/agent/opencode/opencode-catalog.js +55 -29
- package/dist/src/agent/opencode/opencode-config.js +19 -14
- package/dist/src/agent/opencode/opencode-env.js +0 -1
- package/dist/src/agent/opencode/opencode-session-id.js +1 -2
- package/dist/src/agent/opencode/opencode-stream.js +1 -1
- package/dist/src/agent/opencode/write-guard-plugin/index.js +48 -0
- package/dist/src/agent/role-notes-write.js +5 -6
- package/dist/src/agent/run-agent.js +465 -448
- package/dist/src/agent/run-settle.js +214 -0
- package/dist/src/agent/session-ledger.js +86 -16
- package/dist/src/agent/subscription-account.js +220 -0
- package/dist/src/agent/write-guard.js +148 -0
- package/dist/src/backlog/backlog-archive.js +246 -55
- package/dist/src/backlog/backlog-cli-view.js +280 -0
- package/dist/src/backlog/backlog-cli.js +85 -274
- package/dist/src/backlog/backlog-commit.js +4 -5
- package/dist/src/backlog/backlog-eligibility.js +175 -242
- package/dist/src/backlog/backlog-index.js +149 -0
- package/dist/src/backlog/backlog-md.js +54 -6
- package/dist/src/backlog/backlog-parts.js +254 -0
- package/dist/src/backlog/backlog-release.js +117 -0
- package/dist/src/backlog/backlog-render.js +27 -0
- package/dist/src/backlog/backlog-structure.js +99 -156
- package/dist/src/backlog/backlog-write.js +40 -127
- package/dist/src/backlog/backlog.js +21 -6
- package/dist/src/backlog/question-answer.js +21 -15
- package/dist/src/baseline/main-baseline.js +89 -20
- package/dist/src/baseline/main-culprit.js +94 -0
- package/dist/src/baseline/main-guardrails.js +209 -0
- package/dist/src/baseline/main-red.js +139 -32
- package/dist/src/baseline/main-revert.js +133 -0
- package/dist/src/baseline/main-watch.js +184 -0
- package/dist/src/brief/brief-edit.js +9 -11
- package/dist/src/brief/brief.js +31 -51
- package/dist/src/budget/budget.js +30 -20
- package/dist/src/budget/fallback-breaker.js +26 -27
- package/dist/src/build/build-check-detect.js +23 -15
- package/dist/src/build/build-check-events.js +25 -6
- package/dist/src/build/build-check-report.js +49 -9
- package/dist/src/build/build-check-scoped.js +18 -11
- package/dist/src/build/build-check.js +2 -1
- package/dist/src/build/build-info.js +2 -3
- package/dist/src/build/build-stage.js +9 -14
- package/dist/src/build/dep-install.js +1 -2
- package/dist/src/change/change-data.js +123 -31
- package/dist/src/change/change-render.js +46 -5
- package/dist/src/cli/brief-command.js +3 -4
- package/dist/src/cli/cli-args.js +118 -33
- package/dist/src/cli/cli-command-args.js +46 -18
- package/dist/src/cli/cli-editor.js +2 -1
- package/dist/src/cli/cli-flag-specs.js +150 -45
- package/dist/src/cli/cli-init.js +51 -0
- package/dist/src/cli/cli-marker-commands.js +4 -4
- package/dist/src/cli/cli-query-commands.js +308 -590
- package/dist/src/cli/cli-repo-gate.js +11 -1
- package/dist/src/cli/cli-role-command.js +420 -0
- package/dist/src/cli/cli-start-summary.js +160 -0
- package/dist/src/cli/{cli-run.js → cli-start.js} +165 -237
- package/dist/src/cli/completion.js +149 -22
- package/dist/src/cli/config-commands.js +41 -18
- package/dist/src/cli/follow-view.js +75 -0
- package/dist/src/cli/help.js +189 -73
- package/dist/src/cli/log-commands.js +148 -82
- package/dist/src/cli/question-commands.js +205 -10
- package/dist/src/cli.js +106 -43
- package/dist/src/collections.js +26 -10
- package/dist/src/concurrency/check-permit.js +20 -3
- package/dist/src/concurrency/lock.js +17 -13
- package/dist/src/concurrency/provider-lease.js +44 -8
- package/dist/src/concurrency/semaphore.js +102 -26
- package/dist/src/config/config-aliases.js +0 -1
- package/dist/src/config/config-editable-keys.js +13 -1
- package/dist/src/config/config-fallbacks.js +93 -56
- package/dist/src/config/config-field-checks.js +40 -10
- package/dist/src/config/config-keys.js +30 -8
- package/dist/src/config/config-live.js +63 -24
- package/dist/src/config/config-request.js +137 -0
- package/dist/src/config/config-schema.js +31 -24
- package/dist/src/config/config-selector-checks.js +123 -0
- package/dist/src/config/config-validation-agents.js +80 -0
- package/dist/src/config/config-validation-model.js +116 -0
- package/dist/src/config/config-validation.js +141 -199
- package/dist/src/config/config-views.js +127 -53
- package/dist/src/config/config-write-core.js +135 -0
- package/dist/src/config/config-write.js +131 -264
- package/dist/src/config/config.js +48 -38
- package/dist/src/config/model-selector.js +34 -34
- package/dist/src/doctor/doctor-backlog-bugs.js +287 -0
- package/dist/src/doctor/doctor-backlog-plans.js +263 -0
- package/dist/src/doctor/doctor-backlog.js +100 -496
- package/dist/src/doctor/doctor-check-names.js +81 -0
- package/dist/src/doctor/doctor-checks.js +52 -579
- package/dist/src/doctor/doctor-claude-checks.js +145 -0
- package/dist/src/doctor/doctor-codex-checks.js +279 -0
- package/dist/src/doctor/doctor-contention-checks.js +380 -0
- package/dist/src/doctor/doctor-holds.js +82 -66
- package/dist/src/doctor/doctor-install.js +2 -1
- package/dist/src/doctor/doctor-model-checks.js +236 -402
- package/dist/src/doctor/doctor-notify.js +3 -4
- package/dist/src/doctor/doctor-opencode-checks.js +162 -0
- package/dist/src/doctor/doctor-orphans.js +182 -63
- package/dist/src/doctor/doctor-project-checks.js +303 -0
- package/dist/src/doctor/doctor.js +121 -77
- package/dist/src/doctor/image-probe.js +21 -8
- package/dist/src/doctor/outcome.js +3 -2
- package/dist/src/doctor/probe.js +7 -11
- package/dist/src/events/event-format.js +181 -86
- package/dist/src/events/event-labels.js +12 -5
- package/dist/src/events/event-legacy.js +39 -15
- package/dist/src/events/event-read.js +39 -39
- package/dist/src/events/event-tally.js +16 -0
- package/dist/src/events/event-window.js +60 -24
- package/dist/src/events/events.js +30 -13
- package/dist/src/events/notify.js +7 -7
- package/dist/src/failure/failure-cluster.js +12 -12
- package/dist/src/failure/failure-data.js +56 -56
- package/dist/src/failure/failure-render.js +31 -23
- package/dist/src/failure/failure-state-change.js +17 -12
- package/dist/src/failure/time-spend.js +99 -35
- package/dist/src/files/files.js +111 -35
- package/dist/src/files/json-files.js +3 -3
- package/dist/src/files/json-object.js +28 -7
- package/dist/src/files/stat-cache.js +2 -2
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +4 -5
- package/dist/src/gates/budget-gates.js +177 -84
- package/dist/src/gates/disk-gate.js +1 -1
- package/dist/src/gates/gate-polls.js +108 -58
- package/dist/src/gates/maintenance-quota.js +10 -13
- package/dist/src/gates/pause-gates.js +28 -10
- package/dist/src/gates/readiness.js +7 -3
- package/dist/src/gates/role-cap-gates.js +8 -8
- package/dist/src/gates/startup-gate.js +94 -32
- package/dist/src/gates/streak-gate.js +54 -30
- package/dist/src/git/commit-message.js +77 -46
- package/dist/src/git/git-diff.js +16 -10
- package/dist/src/git/git-run.js +79 -38
- package/dist/src/git/git.js +36 -34
- package/dist/src/git/slot-salvage.js +253 -0
- package/dist/src/git/slots-state.js +6 -6
- package/dist/src/git/worktree-pool.js +132 -38
- package/dist/src/git/worktree-use.js +4 -11
- package/dist/src/git/worktree.js +94 -37
- package/dist/src/gui/gui-admin-endpoints.js +96 -0
- package/dist/src/gui/gui-args.js +79 -8
- package/dist/src/gui/gui-backlog-commands.js +42 -18
- package/dist/src/gui/gui-backlog-endpoints.js +155 -0
- package/dist/src/gui/gui-command.js +8 -13
- package/dist/src/gui/gui-config-commands.js +94 -72
- package/dist/src/gui/gui-endpoint-commands.js +4 -3
- package/dist/src/gui/gui-endpoint-common.js +4 -4
- package/dist/src/gui/gui-endpoints.js +11 -380
- package/dist/src/gui/gui-history-endpoints.js +301 -0
- package/dist/src/gui/gui-operator-commands.js +108 -13
- package/dist/src/gui/gui-prompt-commands.js +11 -14
- package/dist/src/gui/gui-report-endpoints.js +90 -0
- package/dist/src/gui/gui-server.js +11 -4
- package/dist/src/gui/gui-team-commands.js +7 -7
- package/dist/src/gui/http-body.js +1 -1
- package/dist/src/history/history-data.js +122 -234
- package/dist/src/history/history-queued.js +147 -0
- package/dist/src/history/history-render.js +42 -22
- package/dist/src/history/history.js +193 -49
- package/dist/src/inbox/inbox-attachments.js +29 -30
- package/dist/src/inbox/inbox-cancel.js +10 -11
- package/dist/src/inbox/inbox-edit.js +9 -9
- package/dist/src/inbox/inbox-submit.js +18 -18
- package/dist/src/inbox/inbox-top.js +25 -9
- package/dist/src/inbox/inbox.js +42 -43
- package/dist/src/inbox/pending-prompt.js +26 -26
- package/dist/src/inbox/prompt-commands.js +62 -49
- package/dist/src/inbox/prompt-not-before.js +2 -2
- package/dist/src/init/init-templates.js +1 -2
- package/dist/src/init/init.js +28 -23
- package/dist/src/merge/backlog-conflicts.js +215 -35
- package/dist/src/merge/merge-batch.js +14 -8
- package/dist/src/merge/merge-cancel.js +53 -0
- package/dist/src/merge/merge-check-failures.js +44 -22
- package/dist/src/merge/merge-core.js +81 -32
- package/dist/src/merge/merge-diff.js +5 -4
- package/dist/src/merge/merge-drain.js +150 -11
- package/dist/src/merge/merge-git.js +177 -38
- package/dist/src/merge/merge-pipeline.js +15 -3
- package/dist/src/merge/merge-post-review.js +90 -0
- package/dist/src/merge/merge-queue-render.js +49 -11
- package/dist/src/merge/merge-queue.js +31 -15
- package/dist/src/merge/merge-requeue.js +102 -0
- package/dist/src/merge/merge-slot.js +61 -16
- package/dist/src/merge/merge-stack.js +33 -11
- package/dist/src/merge/merge-to-main.js +86 -27
- package/dist/src/merge/merge-top.js +69 -0
- package/dist/src/merge/merge-verify.js +12 -10
- package/dist/src/notes-cli.js +30 -15
- package/dist/src/operator/operator-commands.js +192 -81
- package/dist/src/operator/operator-intent.js +69 -42
- package/dist/src/operator/operator-requests.js +35 -40
- package/dist/src/operator/retire.js +5 -6
- package/dist/src/operator/revert.js +142 -34
- package/dist/src/orchestrator/orchestrator-launch.js +60 -52
- package/dist/src/orchestrator/orchestrator-poll.js +320 -0
- package/dist/src/orchestrator/orchestrator-scheduling.js +68 -369
- package/dist/src/orchestrator/orchestrator.js +128 -369
- package/dist/src/orchestrator/retention.js +42 -8
- package/dist/src/orchestrator/scheduling-admit.js +116 -0
- package/dist/src/orchestrator/scheduling-scope.js +201 -0
- package/dist/src/orchestrator/scheduling-skip.js +185 -0
- package/dist/src/orchestrator/startup-migration-steps.js +8 -7
- package/dist/src/orchestrator/startup-migrations.js +11 -7
- package/dist/src/paths.js +116 -49
- package/dist/src/pi/command-shape.js +12 -1
- package/dist/src/pi/pi-args.js +6 -4
- package/dist/src/pi/pi-backend.js +6 -6
- package/dist/src/pi/pi-bin.js +54 -63
- package/dist/src/pi/pi-listed-models.js +53 -0
- package/dist/src/pi/pi-models.js +63 -30
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +1 -1
- package/dist/src/pi/pi-stream.js +6 -6
- package/dist/src/pi/pi-watchdogs.js +18 -19
- package/dist/src/pi-extension/bounded-output.js +5 -3
- package/dist/src/pi-extension/context-budget.js +2 -2
- package/dist/src/pi-extension/context-shake.js +1 -1
- package/dist/src/pi-extension/context-window.js +0 -1
- package/dist/src/pi-extension/full-output.js +3 -10
- package/dist/src/pi-extension/strip-thinking.js +0 -1
- package/dist/src/pi-extension/trim-system-prompt.js +3 -4
- package/dist/src/pi-extension/write-guard.js +37 -0
- package/dist/src/process/launch-services.js +1 -2
- package/dist/src/process/node-bin.js +16 -0
- package/dist/src/process/process-group.js +8 -18
- package/dist/src/process/process.js +41 -2
- package/dist/src/process/run-marker.js +18 -6
- package/dist/src/process/supervisor.js +22 -21
- package/dist/src/prompt/dialect.js +17 -2
- package/dist/src/prompt/gate-notes.js +203 -0
- package/dist/src/prompt/gate-prompts.js +121 -211
- package/dist/src/prompt/notes-backlog.js +181 -0
- package/dist/src/prompt/principles.js +1 -2
- package/dist/src/prompt/prompt-followup.js +19 -14
- package/dist/src/prompt/prompt.js +129 -204
- package/dist/src/redeploy/redeploy-policy.js +24 -19
- package/dist/src/redeploy/redeploy-probes.js +1 -1
- package/dist/src/redeploy/redeploy.js +2 -3
- package/dist/src/redeploy/redeployer.js +20 -20
- package/dist/src/redeploy/self-reload.js +2 -1
- package/dist/src/report/report-data.js +161 -77
- package/dist/src/report/report-fold.js +129 -52
- package/dist/src/report/report-main-red.js +55 -0
- package/dist/src/report/report-render.js +133 -64
- package/dist/src/report/report-types.js +1 -0
- package/dist/src/report/report.js +26 -9
- package/dist/src/request-timeouts.js +2 -2
- package/dist/src/review/known-flakes.js +100 -1
- package/dist/src/review/post-merge-review.js +106 -0
- package/dist/src/review/protected-edits.js +4 -5
- package/dist/src/review/review-followup.js +8 -8
- package/dist/src/review/review-precheck.js +66 -23
- package/dist/src/review/review-run.js +395 -0
- package/dist/src/review/review-verdict.js +2 -2
- package/dist/src/review/review.js +155 -360
- package/dist/src/review/secret-leaks.js +36 -16
- package/dist/src/review/suite-rerun.js +0 -1
- package/dist/src/roles/legacy-role-migration.js +80 -7
- package/dist/src/roles/role-catalog.js +69 -12
- package/dist/src/roles/role-guidance.js +55 -20
- package/dist/src/roles/role-labels.js +1 -2
- package/dist/src/roles/role-render.js +67 -50
- package/dist/src/roles/role-view.js +59 -47
- package/dist/src/roles/roles.js +31 -31
- package/dist/src/roles/runner-ids.js +97 -0
- package/dist/src/run/acceptance-greps.js +119 -0
- package/dist/src/run/feedback-filings.js +51 -0
- package/dist/src/{tick → run}/qa-coverage.js +9 -27
- package/dist/src/{tick/tick-apply.js → run/run-apply.js} +95 -94
- package/dist/src/run/run-detail-data.js +179 -0
- package/dist/src/run/run-detail-render.js +98 -0
- package/dist/src/run/run-detail.js +168 -0
- package/dist/src/{tick/tick-finalize.js → run/run-finalize.js} +72 -60
- package/dist/src/{tick/tick-outcome.js → run/run-outcome.js} +6 -7
- package/dist/src/{tick/tick-prompt.js → run/run-prompt.js} +108 -73
- package/dist/src/{tick/tick-resume.js → run/run-resume.js} +55 -26
- package/dist/src/{tick/tick-stage.js → run/run-stage.js} +108 -50
- package/dist/src/{tick/tick-timing.js → run/run-timing.js} +43 -41
- package/dist/src/{tick/tick-usage.js → run/run-usage.js} +45 -37
- package/dist/src/{tick/tick-verdict.js → run/run-verdict.js} +91 -56
- package/dist/src/run/selfcheck-digest.js +40 -0
- package/dist/src/run/stage-backlog-checks.js +249 -0
- package/dist/src/run/stage-check.js +172 -0
- package/dist/src/run/stage-path-checks.js +730 -0
- package/dist/src/{loop → runner}/leftover.js +80 -25
- package/dist/src/{loop → runner}/model-fallback.js +12 -12
- package/dist/src/runner/revision.js +202 -0
- package/dist/src/{loop/loop-fallback.js → runner/runner-fallback.js} +8 -8
- package/dist/src/runner/runner-outcome.js +107 -0
- package/dist/src/{loop/loop-pi.js → runner/runner-pi.js} +140 -131
- package/dist/src/{loop/loop-recovery.js → runner/runner-recovery.js} +21 -15
- package/dist/src/{loop/loop-tick.js → runner/runner-run.js} +218 -240
- package/dist/src/{loop/loop-state.js → runner/runner-state-load.js} +61 -114
- package/dist/src/runner/runner-state.js +102 -0
- package/dist/src/{loop/loop.js → runner/runner.js} +123 -120
- package/dist/src/scheduling/backoff.js +38 -38
- package/dist/src/scheduling/claims.js +35 -22
- package/dist/src/scheduling/once-round.js +22 -22
- package/dist/src/scheduling/quiet-hours.js +5 -6
- package/dist/src/scheduling/scheduling.js +74 -64
- package/dist/src/scheduling/work-merged-cache.js +2 -2
- package/dist/src/status/status-budget.js +55 -47
- package/dist/src/status/status-data.js +43 -16
- package/dist/src/status/status-inbox.js +2 -2
- package/dist/src/status/status-install.js +0 -1
- package/dist/src/status/status-polls.js +28 -28
- package/dist/src/status/status-roles.js +39 -13
- package/dist/src/team/error-storm.js +8 -8
- package/dist/src/team/failure-spread.js +2 -2
- package/dist/src/team/orchestrator-info.js +15 -9
- package/dist/src/team/reclaim.js +5 -6
- package/dist/src/team/team-hold.js +7 -7
- package/dist/src/team/team-polls.js +91 -50
- package/dist/src/team/team-state.js +8 -2
- package/dist/src/text/datetime.js +63 -17
- package/dist/src/text/format.js +48 -11
- package/dist/src/text/markdown.js +8 -5
- package/dist/src/text/phrases.js +57 -23
- package/dist/src/text/suggest.js +1 -1
- package/dist/src/text/text.js +67 -13
- package/dist/src/ui/badges.js +65 -37
- package/dist/src/ui/gui/gui-client-backlog.js +292 -0
- package/dist/src/ui/gui/gui-client-boot.js +32 -26
- package/dist/src/ui/gui/gui-client-composer.js +8 -9
- package/dist/src/ui/gui/gui-client-drawer-config.js +212 -0
- package/dist/src/ui/gui/gui-client-drawer.js +126 -285
- package/dist/src/ui/gui/gui-client-feed.js +271 -0
- package/dist/src/ui/gui/gui-client-history.js +434 -67
- package/dist/src/ui/gui/gui-client-labels.js +1 -2
- package/dist/src/ui/gui/gui-client-markdown.js +1 -2
- package/dist/src/ui/gui/gui-client-model.js +27 -36
- package/dist/src/ui/gui/gui-client-operator.js +55 -51
- package/dist/src/ui/gui/gui-client-pending.js +227 -23
- package/dist/src/ui/gui/gui-client-report-chart.js +464 -0
- package/dist/src/ui/gui/gui-client-report.js +87 -427
- package/dist/src/ui/gui/{gui-client-loops.js → gui-client-roles.js} +43 -44
- package/dist/src/ui/gui/gui-client-settings.js +56 -4
- package/dist/src/ui/gui/gui-client-team.js +25 -465
- package/dist/src/ui/gui/gui-client.js +27 -25
- package/dist/src/ui/gui/gui-icons.js +0 -2
- package/dist/src/ui/gui/gui-page.js +9 -7
- package/dist/src/ui/gui/gui-styles.js +34 -34
- package/dist/src/ui/progress-data.js +9 -11
- package/dist/src/ui/{tick-progress-model.js → run-progress-model.js} +31 -31
- package/dist/src/ui/status-model.js +133 -125
- package/dist/src/ui/status-payload.js +136 -49
- package/dist/src/ui/status-render.js +101 -82
- package/dist/src/ui/team-alerts.js +248 -57
- package/dist/src/ui/tone.js +5 -6
- package/dist/src/ui/transcript-run.js +58 -0
- package/dist/src/ui/transcript-tail.js +28 -20
- package/dist/src/ui/transcript.js +63 -23
- package/dist/src/ui/tui/tui-app.js +0 -1
- package/dist/src/ui/tui/tui-backlog.js +51 -14
- package/dist/src/ui/tui/tui-editor-answer.js +27 -31
- package/dist/src/ui/tui/tui-editor-attach.js +3 -5
- package/dist/src/ui/tui/tui-editor-backends.js +132 -0
- package/dist/src/ui/tui/tui-editor-brief.js +10 -12
- package/dist/src/ui/tui/tui-editor-budget.js +35 -31
- package/dist/src/ui/tui/tui-editor-claude-reserve.js +24 -0
- package/dist/src/ui/tui/tui-editor-concurrency.js +24 -0
- package/dist/src/ui/tui/tui-editor-config.js +86 -0
- package/dist/src/ui/tui/tui-editor-entry.js +36 -47
- package/dist/src/ui/tui/tui-editor-fallback.js +31 -0
- package/dist/src/ui/tui/tui-editor-instances.js +37 -0
- package/dist/src/ui/tui/tui-editor-instructions.js +136 -0
- package/dist/src/ui/tui/tui-editor-interval.js +11 -9
- package/dist/src/ui/tui/tui-editor-model.js +29 -0
- package/dist/src/ui/tui/tui-editor-notes.js +3 -5
- package/dist/src/ui/tui/tui-editor-notify.js +25 -0
- package/dist/src/ui/tui/tui-editor-pause.js +48 -59
- package/dist/src/ui/tui/tui-editor-prompt.js +11 -16
- package/dist/src/ui/tui/tui-editor-provider-caps.js +27 -0
- package/dist/src/ui/tui/tui-editor-queued.js +5 -9
- package/dist/src/ui/tui/tui-editor-quiet.js +99 -35
- package/dist/src/ui/tui/tui-editor-retire.js +4 -6
- package/dist/src/ui/tui/tui-editor-role-key.js +76 -0
- package/dist/src/ui/tui/tui-editor-role-model.js +28 -0
- package/dist/src/ui/tui/tui-editor-search.js +45 -33
- package/dist/src/ui/tui/tui-editor-session.js +60 -34
- package/dist/src/ui/tui/tui-editor-slots.js +23 -0
- package/dist/src/ui/tui/tui-editors.js +35 -2
- package/dist/src/ui/tui/tui-frame.js +56 -11
- package/dist/src/ui/tui/tui-history.js +8 -9
- package/dist/src/ui/tui/tui-input.js +13 -11
- package/dist/src/ui/tui/tui-keymap.js +0 -1
- package/dist/src/ui/tui/tui-keys.js +24 -9
- package/dist/src/ui/tui/tui-pane-activity.js +14 -11
- package/dist/src/ui/tui/tui-pane-backlog.js +171 -126
- package/dist/src/ui/tui/tui-pane-brief.js +0 -1
- package/dist/src/ui/tui/tui-pane-ctx.js +2 -2
- package/dist/src/ui/tui/tui-pane-health.js +58 -8
- package/dist/src/ui/tui/tui-pane-history.js +402 -82
- package/dist/src/ui/tui/tui-pane-pending.js +308 -13
- package/dist/src/ui/tui/tui-pane-queued.js +71 -55
- package/dist/src/ui/tui/tui-pane-report.js +46 -14
- package/dist/src/ui/tui/tui-pane-session.js +25 -6
- package/dist/src/ui/tui/tui-pane-transcript.js +111 -37
- package/dist/src/ui/tui/tui-pane.js +80 -23
- package/dist/src/ui/tui/tui-panes.js +100 -30
- package/dist/src/ui/tui/tui.js +14 -14
- package/dist/src/verdict/fix-claim.js +82 -32
- package/dist/src/verdict/no-change.js +4 -4
- package/dist/src/verdict/refusal.js +40 -16
- package/dist/src/verdict/reply-contract.js +42 -31
- package/dist/src/version.js +0 -1
- package/package.json +2 -1
- package/dist/src/loop/revision.js +0 -70
- package/dist/src/roles/loop-ids.js +0 -77
- package/dist/src/tick/stage-check.js +0 -419
- package/dist/src/tick/telemetry-digest.js +0 -25
- package/dist/src/tick/tick-detail-data.js +0 -108
- package/dist/src/tick/tick-detail.js +0 -163
- package/dist/src/ui/gui/gui-client-sound.js +0 -53
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The worktree pool's lease machinery (plans/worktree-pool.md, "Leases"; part 2a/5): a fixed set
|
|
3
|
-
* of `.tumwater/worktrees/_slot-<n>` checkouts that
|
|
2
|
+
* of `.tumwater/worktrees/_slot-<n>` checkouts that runs and verifies lease and switch commits in
|
|
4
3
|
* place, instead of keeping one `_land-<role>` checkout per role. The persisted layout and its
|
|
5
4
|
* cross-process lock live in slots-state.ts, which other processes (`tumwater diff`, `retire`) also
|
|
6
5
|
* read; this module owns choosing a slot, holding it through the disk-floor registry, and handing
|
|
7
|
-
* it back.
|
|
6
|
+
* it back. Since part 4/5 landed, a role's run (runner-run.ts) and a verify (merge-batch.ts's
|
|
7
|
+
* verifyRequest) both acquire one through leaseSlot.
|
|
8
8
|
*
|
|
9
9
|
* The lease decision is synchronous: `updateSlotsState` writes slots.json under the slots lock, and
|
|
10
|
-
* one Node process runs every
|
|
10
|
+
* one Node process runs every run and merge, so two concurrent `leaseSlot` calls claim distinct
|
|
11
11
|
* free slots and a call with none free registers an in-process waiter. A release wakes the waiters,
|
|
12
12
|
* which re-decide; `signal` aborts a waiter. */
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { loadConfig, slotCount } from "../config/config.js";
|
|
15
15
|
import { worktreesDir, slotWorktreePath } from "../paths.js";
|
|
16
|
-
import {
|
|
16
|
+
import { loadRunnerState } from "../runner/runner-state-load.js";
|
|
17
17
|
import { DIRECTOR_ROLE } from "../roles/roles.js";
|
|
18
18
|
import { readSlotsState, slotForDir, updateSlotsState } from "./slots-state.js";
|
|
19
|
+
import { salvageSlotLeftovers } from "./slot-salvage.js";
|
|
19
20
|
import { beginWorktreeUse } from "./worktree-use.js";
|
|
20
21
|
import { ensureDetachedWorktree, removeWorktreeDir } from "./worktree.js";
|
|
21
|
-
import { logEventBestEffort, warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
22
|
+
import { logEventBestEffort, warnEventBestEffort, tryOrWarn, TUMWATER_SOURCE } from "../events/events.js";
|
|
22
23
|
import { errorMessage } from "../text/text.js";
|
|
23
24
|
import { subdirectoryNames } from "../files/files.js";
|
|
24
25
|
import { dropProviderLeaseKey } from "../concurrency/provider-lease.js";
|
|
@@ -84,11 +85,16 @@ function raceAbort(wait, signal) {
|
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
/** Clear leases owned by any process other than this one: their runs are dead (design "Leases").
|
|
87
|
-
* Pins are kept, and the freed slots become candidates for the current claim.
|
|
88
|
+
* Pins are kept, and the freed slots become candidates for the current claim. A dead lease never
|
|
89
|
+
* released, so its role is recorded as the slot's last holder here, and a dead run as unsettled:
|
|
90
|
+
* whatever it left in the slot is that role's unfinished work (slot-salvage.ts). */
|
|
88
91
|
function clearDeadLeases(state, pid) {
|
|
89
92
|
for (const slot of state.slots) {
|
|
90
|
-
if (slot.lease
|
|
91
|
-
|
|
93
|
+
if (slot.lease === null || slot.lease.pid === pid)
|
|
94
|
+
continue;
|
|
95
|
+
slot.lastRole = slot.lease.role;
|
|
96
|
+
slot.unsettledRun = slot.lease.purpose === "run" ? slot.lease.role : null;
|
|
97
|
+
slot.lease = null;
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
function claim(slot, role, purpose, now, pid) {
|
|
@@ -107,7 +113,9 @@ function mostRecentlyReleased(slots) {
|
|
|
107
113
|
function chooseSlot(root, state, role, purpose, count, now, pid) {
|
|
108
114
|
clearDeadLeases(state, pid);
|
|
109
115
|
const free = state.slots.filter((slot) => slot.lease === null);
|
|
110
|
-
|
|
116
|
+
// A check slot is a scratch checkout: it never takes a slot pinned for any role, the leasing
|
|
117
|
+
// role's own included (that pin holds the author's resumable work).
|
|
118
|
+
const eligible = free.filter((slot) => purpose === "verify-check" ? slot.pinnedFor === null : slot.pinnedFor === null || slot.pinnedFor === role);
|
|
111
119
|
const pinned = eligible.find((slot) => slot.pinnedFor === role);
|
|
112
120
|
if (pinned !== undefined)
|
|
113
121
|
return claim(pinned, role, purpose, now, pid);
|
|
@@ -159,15 +167,23 @@ function logSlotWait(root, role, purpose, waitedMs) {
|
|
|
159
167
|
}
|
|
160
168
|
function tryClaim(root, role, purpose, count) {
|
|
161
169
|
let chosen = null;
|
|
170
|
+
const key = rootKey(root);
|
|
171
|
+
const unsaved = unsavedReleases.get(key);
|
|
162
172
|
updateSlotsState(root, (state) => {
|
|
173
|
+
for (const slot of state.slots) {
|
|
174
|
+
if (unsaved?.has(slot.dir) && slot.lease?.pid === process.pid)
|
|
175
|
+
slot.lease = null;
|
|
176
|
+
}
|
|
163
177
|
chosen = chooseSlot(root, state, role, purpose, count, Date.now(), process.pid);
|
|
164
178
|
});
|
|
179
|
+
// The write landed, so the reclaim of every unsaved release is on disk.
|
|
180
|
+
unsavedReleases.delete(key);
|
|
165
181
|
return chosen;
|
|
166
182
|
}
|
|
167
183
|
async function acquire(root, role, purpose, signal, slotBudget) {
|
|
168
184
|
// The caller's live budget when given; else liveCount's tolerance — a broken or missing
|
|
169
185
|
// tumwater.json degrades to the default budget rather than erroring the lease with a raw
|
|
170
|
-
// load failure (2026-10-08: a mid-run broken config errored every
|
|
186
|
+
// load failure (2026-10-08: a mid-run broken config errored every run via this read even
|
|
171
187
|
// while the orchestrator held last-known-good).
|
|
172
188
|
const count = slotBudget ?? liveCount(root);
|
|
173
189
|
const startedAt = Date.now();
|
|
@@ -181,7 +197,7 @@ async function acquire(root, role, purpose, signal, slotBudget) {
|
|
|
181
197
|
}
|
|
182
198
|
// The pool-slot wait rule (plans/provider-concurrency.md §5 "No deadlock"): a lease never holds
|
|
183
199
|
// a provider key while it waits for a pool slot. Drop the calling lease's key (which never
|
|
184
|
-
// waits) before parking here, so this
|
|
200
|
+
// waits) before parking here, so this run or verify holds no provider slot whose in-use a
|
|
185
201
|
// re-key waiter is waiting to drain; the next run re-takes the key as an ordinary re-key.
|
|
186
202
|
await dropProviderLeaseKey();
|
|
187
203
|
const waiter = registerWaiter(root);
|
|
@@ -194,6 +210,15 @@ async function acquire(root, role, purpose, signal, slotBudget) {
|
|
|
194
210
|
}
|
|
195
211
|
}
|
|
196
212
|
}
|
|
213
|
+
/** The pool's live state for observers (status, TUI, dashboard): slots leased by this process out
|
|
214
|
+
* of the configured budget, and the leases waiting for one. A lease held by another pid is dead
|
|
215
|
+
* (see `SlotLease`), so it is not counted. `wakeWaiters` empties the waiter set before woken
|
|
216
|
+
* waiters re-register, so `waiting` can read low for one turn; observers poll, so the next poll
|
|
217
|
+
* corrects it. */
|
|
218
|
+
export function slotPoolSnapshot(root) {
|
|
219
|
+
const leased = readSlotsState(root).slots.filter((s) => s.lease !== null && s.lease.pid === process.pid).length;
|
|
220
|
+
return { leased, limit: liveCount(root), waiting: waiters.get(rootKey(root))?.size ?? 0 };
|
|
221
|
+
}
|
|
197
222
|
/** The live slot budget, or an unbounded one when the config is unreadable: an invalid config
|
|
198
223
|
* must not stop a held slot from being freed. */
|
|
199
224
|
function liveCount(root) {
|
|
@@ -210,12 +235,33 @@ function liveCount(root) {
|
|
|
210
235
|
function isCanonicalSlotDir(dir) {
|
|
211
236
|
return /^_slot-\d+$/.test(path.basename(dir));
|
|
212
237
|
}
|
|
213
|
-
/**
|
|
214
|
-
*
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
*
|
|
218
|
-
|
|
238
|
+
/** Slot dirs per root whose release write to slots.json failed in this process; the next claim
|
|
239
|
+
* clears their stale lease. */
|
|
240
|
+
const unsavedReleases = new Map();
|
|
241
|
+
/** `releaseSlot` that never throws: a failed slots.json write (ENOSPC, EACCES, a lock timeout)
|
|
242
|
+
* warns and remembers the slot so the next `tryClaim` here reclaims it, instead of masking the
|
|
243
|
+
* caller's own outcome and leaving the slot leased by this live pid. The pin and `lastRole` the
|
|
244
|
+
* failed release would have recorded are not replayed; a lost pin only means the next resume
|
|
245
|
+
* gets a fresh checkout. */
|
|
246
|
+
function freeSlot(root, dir, role, count, pin, unsettledRun = null) {
|
|
247
|
+
try {
|
|
248
|
+
return releaseSlot(root, dir, role, count, pin, unsettledRun);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
const key = rootKey(root);
|
|
252
|
+
const dirs = unsavedReleases.get(key) ?? new Set();
|
|
253
|
+
dirs.add(dir);
|
|
254
|
+
unsavedReleases.set(key, dirs);
|
|
255
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `could not free slot ${path.basename(dir)}: ${errorMessage(err)}; the next lease reclaims it`);
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/** Free `dir` in slots.json, record `lastRole`/`lastReleasedAt`/`unsettledRun`, and return the
|
|
260
|
+
* idle unpinned slot dirs to remove because the live slot budget shrank below them
|
|
261
|
+
* (oldest-released first). `pin` keeps the slot pinned for `role`; otherwise a pin this role held
|
|
262
|
+
* is cleared. A non-canonical (migrated legacy) slot is dropped the moment it is unpinned, so the
|
|
263
|
+
* one resume it was kept for runs in it and the dir is removed on that first release. */
|
|
264
|
+
function releaseSlot(root, dir, role, count, pin, unsettledRun) {
|
|
219
265
|
const toRemove = [];
|
|
220
266
|
updateSlotsState(root, (state) => {
|
|
221
267
|
const slot = state.slots.find((s) => s.dir === dir);
|
|
@@ -223,6 +269,7 @@ function releaseSlot(root, dir, role, count, pin) {
|
|
|
223
269
|
slot.lease = null;
|
|
224
270
|
slot.lastRole = role;
|
|
225
271
|
slot.lastReleasedAt = Date.now();
|
|
272
|
+
slot.unsettledRun = unsettledRun;
|
|
226
273
|
if (pin) {
|
|
227
274
|
if (slot.pinnedFor !== role)
|
|
228
275
|
slot.pinnedAt = Date.now();
|
|
@@ -270,15 +317,15 @@ export async function removeDroppedSlots(root, dirs, remove = removeWorktreeDir)
|
|
|
270
317
|
}
|
|
271
318
|
}
|
|
272
319
|
/** One-time startup migration (plans/worktree-pool.md "Legacy role worktrees", part 4c/5):
|
|
273
|
-
* retire the pre-pool `.tumwater/worktrees/<role>` checkouts now that
|
|
320
|
+
* retire the pre-pool `.tumwater/worktrees/<role>` checkouts now that runs lease pooled slots.
|
|
274
321
|
* A resumable enabled role keeps its legacy dir as a pinned slot so its pending resume runs in
|
|
275
322
|
* the same cwd; every other legacy dir is removed (its branch keeps any commit), and pins for
|
|
276
|
-
* roles outside `configured` are cleared. `configured` is every
|
|
277
|
-
* enable (`
|
|
323
|
+
* roles outside `configured` are cleared. `configured` is every role id the config's roles
|
|
324
|
+
* enable (`runnerIds`), never a `--once --role` filter: a scoped run must leave the other roles'
|
|
278
325
|
* legacy dirs and pins alone, or it silently loses their pending resumes. It runs after
|
|
279
326
|
* `runOrchestrator` constructs every
|
|
280
|
-
* `
|
|
281
|
-
* from the on-disk `running` flag only in memory (it does not save it), so `
|
|
327
|
+
* `RoleRunner` but before any run can call `leaseSlot`: the constructor infers `resumePending`
|
|
328
|
+
* from the on-disk `running` flag only in memory (it does not save it), so `loadRunnerState` here
|
|
282
329
|
* still reports the on-disk flags. *Removing* a dir is best-effort and logged: the slot records
|
|
283
330
|
* are already updated, so a leaked directory is only a leak the next prune collects. */
|
|
284
331
|
export async function retireLegacyRoleWorktrees(root, configured) {
|
|
@@ -291,7 +338,7 @@ export async function retireLegacyRoleWorktrees(root, configured) {
|
|
|
291
338
|
updateSlotsState(root, (state) => {
|
|
292
339
|
for (const role of names) {
|
|
293
340
|
const legacyDir = path.join(dir, role);
|
|
294
|
-
const legacy =
|
|
341
|
+
const legacy = loadRunnerState(root, role);
|
|
295
342
|
const resumable = legacy.resumePending === true || legacy.running === true;
|
|
296
343
|
const ownRecord = state.slots.find((slot) => slot.dir === legacyDir);
|
|
297
344
|
if (resumable && configuredSet.has(role)) {
|
|
@@ -354,35 +401,81 @@ export async function retireLegacyRoleWorktrees(root, configured) {
|
|
|
354
401
|
}
|
|
355
402
|
}
|
|
356
403
|
}
|
|
404
|
+
/** Startup pass for runs the previous process died in (crash, kill -9, power loss): such a run's
|
|
405
|
+
* lease is still in slots.json with the dead pid, and its RoleRunner reads the persisted
|
|
406
|
+
* `running` flag as a resume that must run in the same checkout, the cwd its session used. Pin
|
|
407
|
+
* each such slot for its role, as the graceful shutdown's abort pin would have, unless the role
|
|
408
|
+
* already holds a pin; the next claim then clears the dead lease (marking the run unsettled) and
|
|
409
|
+
* the resume leases the pinned slot with its edits in place. Only configured roles count, as in
|
|
410
|
+
* retireLegacyRoleWorktrees, so a removed role never leaks a pin. Like that pass, it runs before
|
|
411
|
+
* any run can lease, while loadRunnerState still reports the on-disk flags. */
|
|
412
|
+
export function pinCrashedRuns(root, configured) {
|
|
413
|
+
const configuredSet = new Set(configured);
|
|
414
|
+
updateSlotsState(root, (state) => {
|
|
415
|
+
for (const slot of state.slots) {
|
|
416
|
+
const lease = slot.lease;
|
|
417
|
+
if (lease === null || lease.pid === process.pid || lease.purpose !== "run")
|
|
418
|
+
continue;
|
|
419
|
+
if (!configuredSet.has(lease.role) || state.slots.some((s) => s.pinnedFor === lease.role))
|
|
420
|
+
continue;
|
|
421
|
+
const runner = loadRunnerState(root, lease.role);
|
|
422
|
+
if (runner.running !== true && runner.resumePending !== true)
|
|
423
|
+
continue;
|
|
424
|
+
slot.pinnedFor = lease.role;
|
|
425
|
+
slot.pinnedAt = Date.now();
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
357
429
|
/** Lease a pooled slot for `role`, prepared at `ref`. Resolves once a slot is held and reset to
|
|
358
|
-
* `ref
|
|
430
|
+
* `ref`, any uncommitted edits its last holder left saved first (salvageSlotLeftovers), as work
|
|
431
|
+
* to hand back when that holder was a run that never settled them; rejects on a `signal` abort
|
|
432
|
+
* while waiting. The returned `release` is idempotent. */
|
|
359
433
|
export async function leaseSlot(root, options) {
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
434
|
+
const dir = await acquire(root, options.role, options.purpose, options.signal, options.slotBudget);
|
|
435
|
+
return prepareLeased(root, dir, options);
|
|
436
|
+
}
|
|
437
|
+
/** Lease a pooled slot only when one is free right now: resolves null instead of waiting, so a
|
|
438
|
+
* caller that can do without a second checkout (a verify's check slot) never queues behind
|
|
439
|
+
* the pool. Otherwise as leaseSlot. */
|
|
440
|
+
export async function tryLeaseSlot(root, options) {
|
|
441
|
+
const dir = tryClaim(root, options.role, options.purpose, options.slotBudget ?? liveCount(root));
|
|
442
|
+
return dir === null ? null : prepareLeased(root, dir, options);
|
|
443
|
+
}
|
|
444
|
+
/** Prepare the slot `dir` this process just claimed at `options.ref` and hand back its handle. */
|
|
445
|
+
async function prepareLeased(root, dir, options) {
|
|
446
|
+
const { role, purpose, ref, keep } = options;
|
|
447
|
+
const record = slotForDir(root, dir);
|
|
448
|
+
// A run that gave the slot up unsettled is the one its leftovers belong to, even when a
|
|
449
|
+
// failed prepare has since recorded another role as the last holder.
|
|
450
|
+
const unsettledRun = record?.unsettledRun ?? null;
|
|
451
|
+
const preserved = keep === true && record?.pinnedFor === role;
|
|
363
452
|
const useRelease = await beginWorktreeUse(root, dir);
|
|
364
453
|
// The use registry is LRU bookkeeping; freeing the slot is the real work of a release, so a
|
|
365
454
|
// failed registry write here (ENOSPC, a lock timeout) must not escape: the dead-lease reaper
|
|
366
455
|
// only clears another process's lease, so the slot would stay leased by this live pid and the
|
|
367
456
|
// pool would shrink by one every time. Warn so the failure stays visible, then free anyway.
|
|
368
457
|
const releaseUse = () => {
|
|
369
|
-
|
|
370
|
-
useRelease();
|
|
371
|
-
}
|
|
372
|
-
catch (err) {
|
|
373
|
-
warnEventBestEffort(root, TUMWATER_SOURCE, `could not record use release for ${path.basename(dir)}: ${errorMessage(err)}`);
|
|
374
|
-
}
|
|
458
|
+
tryOrWarn(root, TUMWATER_SOURCE, `record use release for ${path.basename(dir)}`, useRelease);
|
|
375
459
|
};
|
|
376
460
|
if (!preserved) {
|
|
377
461
|
try {
|
|
462
|
+
// A run the harness could not clean up after may have left uncommitted edits here; save
|
|
463
|
+
// them before the reset discards them, for that role's next run when it left them
|
|
464
|
+
// unsettled (slot-salvage.ts).
|
|
465
|
+
await salvageSlotLeftovers(root, dir, {
|
|
466
|
+
leftBy: unsettledRun ?? record?.lastRole ?? null,
|
|
467
|
+
recoverFor: unsettledRun,
|
|
468
|
+
forRole: role,
|
|
469
|
+
});
|
|
378
470
|
await ensureDetachedWorktree(root, dir, ref);
|
|
379
471
|
}
|
|
380
472
|
catch (err) {
|
|
381
473
|
releaseUse();
|
|
382
474
|
// Free the claim without shrinking. releaseSlot retires a migrated legacy slot on this
|
|
383
475
|
// unpinned release, so hand its removal to removeDroppedSlots; a canonical slot survives
|
|
384
|
-
// for reuse on the next lease.
|
|
385
|
-
|
|
476
|
+
// for reuse on the next lease. This lease never used the slot, so an unsettled run's
|
|
477
|
+
// claim on what is left there stands.
|
|
478
|
+
const toRemove = freeSlot(root, dir, role, Number.MAX_SAFE_INTEGER, false, unsettledRun);
|
|
386
479
|
wakeWaiters(root);
|
|
387
480
|
void removeDroppedSlots(root, toRemove);
|
|
388
481
|
throw err;
|
|
@@ -392,12 +485,13 @@ export async function leaseSlot(root, options) {
|
|
|
392
485
|
return {
|
|
393
486
|
dir,
|
|
394
487
|
preserved,
|
|
395
|
-
release: (
|
|
488
|
+
release: (how) => {
|
|
396
489
|
if (released)
|
|
397
490
|
return;
|
|
398
491
|
released = true;
|
|
399
492
|
releaseUse();
|
|
400
|
-
const
|
|
493
|
+
const unsettled = how?.unsettled === true && purpose === "run" ? role : null;
|
|
494
|
+
const toRemove = freeSlot(root, dir, role, liveCount(root), how?.pin === true, unsettled);
|
|
401
495
|
wakeWaiters(root);
|
|
402
496
|
// Best-effort: a slot whose directory is already gone is unregistered by the prune.
|
|
403
497
|
void removeDroppedSlots(root, toRemove);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The worktree-use registry (plans/disk-floor.md, "Reclaiming build outputs", part 2/4): which
|
|
3
2
|
* harness worktrees are in use right now, and when each was last used. The orchestrator is the one
|
|
4
|
-
* process that runs
|
|
3
|
+
* process that launches runs and merges, so a plain in-process map is enough for the "in use" half;
|
|
5
4
|
* the "last used" half is persisted to `.tumwater/state/worktree-use.json` so least-recently-used
|
|
6
5
|
* reclaim ordering survives a restart.
|
|
7
6
|
*
|
|
@@ -13,8 +12,7 @@ import path from "node:path";
|
|
|
13
12
|
import { readJsonFile, writeJsonAtomic } from "../files/json-files.js";
|
|
14
13
|
import { finiteNumber, isJsonObject } from "../files/json-object.js";
|
|
15
14
|
import { withStateLock } from "../concurrency/lock.js";
|
|
16
|
-
import {
|
|
17
|
-
import { errorMessage } from "../text/text.js";
|
|
15
|
+
import { tryOrWarn, TUMWATER_SOURCE } from "../events/events.js";
|
|
18
16
|
import { worktreeUseLockPath, worktreeUsePath } from "../paths.js";
|
|
19
17
|
/** In-process state, keyed by the resolved directory so two roots' same-named worktrees never
|
|
20
18
|
* collide. */
|
|
@@ -89,15 +87,10 @@ export function updateWorktreeUse(root, change) {
|
|
|
89
87
|
* volume is full — is warned and dropped rather than thrown. Both release paths use it:
|
|
90
88
|
* `beginWorktreeUse`'s release and `releaseReclaim`, the latter in a `finally` after a reclaim.
|
|
91
89
|
* Their bookkeeping must not become the caller's failure — a throw there would mask a successful
|
|
92
|
-
*
|
|
90
|
+
* run, merge, or clean with a registry error. The in-process `uses`/`reclaiming` state is updated
|
|
93
91
|
* before this runs, so only reclaim ordering degrades. */
|
|
94
92
|
function updateWorktreeUseBestEffort(root, change) {
|
|
95
|
-
|
|
96
|
-
updateWorktreeUse(root, change);
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
warnEventBestEffort(root, TUMWATER_SOURCE, `could not update the worktree-use registry: ${errorMessage(err)}`);
|
|
100
|
-
}
|
|
93
|
+
tryOrWarn(root, TUMWATER_SOURCE, "update the worktree-use registry", () => updateWorktreeUse(root, change));
|
|
101
94
|
}
|
|
102
95
|
/** True while a `useWorktree` body holds this worktree. */
|
|
103
96
|
export function isWorktreeInUse(dir) {
|
package/dist/src/git/worktree.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { GIT_TIMEOUT_MS, git, gitTry } from "./git-run.js";
|
|
4
|
-
import { branchExists, hasWorktreeRegistrations, isGitRepo, resolveGitDir } from "./git.js";
|
|
5
|
-
import {
|
|
4
|
+
import { branchExists, commitExists, deleteRef, hasWorktreeRegistrations, isGitRepo, refShaFromFiles, refsUnder, resolveGitDir } from "./git.js";
|
|
5
|
+
import { logEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
6
|
+
import { isDirectory, pruneOldDirectory, removeTree, subdirectoryNames } from "../files/files.js";
|
|
6
7
|
import { KILL_GRACE_MS } from "../process/process-group.js";
|
|
7
8
|
import { branchName, worktreePath, worktreesDir } from "../paths.js";
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
import { shortSha } from "../text/format.js";
|
|
10
|
+
/** Persistent-worktree lifecycle for the harness: role worktrees (one per role, reset to main
|
|
11
|
+
* on every fresh run) and the detached mirror worktree redeploy.ts verifies and compiles.
|
|
10
12
|
* The generic git plumbing these build on lives in git/git.ts; this module owns creating,
|
|
11
13
|
* self-healing, resetting, and abort-syncing those worktrees. */
|
|
12
14
|
/** True when `dir` exists and is still a usable git worktree — its .git pointer file present
|
|
13
15
|
* and resolvable via rev-parse. The shared usability probe of both ensure* functions below;
|
|
14
16
|
* a directory that fails it (pointer lost, or admin-side registration under <root>/.git/
|
|
15
17
|
* worktrees/ pruned by outside git maintenance) is rebuilt from scratch instead of failing
|
|
16
|
-
* every
|
|
18
|
+
* every run. Also exported for read-only viewers that must classify a worktree as absent
|
|
17
19
|
* rather than fail (change/change-data.ts's collectRoleChange), so the probe's exact
|
|
18
20
|
* conditions live in one place. */
|
|
19
21
|
export async function isUsableWorktree(dir) {
|
|
@@ -30,7 +32,7 @@ async function pruneWorktrees(root) {
|
|
|
30
32
|
/** Clear the way for `worktree add` at `dir`: prune stale registrations first (a registration
|
|
31
33
|
* whose directory was deleted still blocks the add), then remove a leftover unusable directory
|
|
32
34
|
* and prune again so no registration points at it. The removed content is harness scratch only
|
|
33
|
-
* — role worktrees are reset to main on every fresh
|
|
35
|
+
* — role worktrees are reset to main on every fresh run, and branch commits survive in
|
|
34
36
|
* refs/heads either way. Call only inside serializeSetup: a prune deletes any registration
|
|
35
37
|
* another `worktree add` is still creating. */
|
|
36
38
|
async function clearStaleWorktree(root, dir) {
|
|
@@ -75,8 +77,8 @@ async function serializeSetup(root, setup) {
|
|
|
75
77
|
}
|
|
76
78
|
/** Ensure a persistent worktree + branch exists for a role. Returns the worktree path. Self-heals
|
|
77
79
|
* when the directory exists but is no longer a usable worktree: it removes and re-adds the
|
|
78
|
-
* directory instead of failing every
|
|
79
|
-
* (plans/worktree-pool.md, "Role
|
|
80
|
+
* directory instead of failing every run (see clearStaleWorktree). Since the worktree pool merged
|
|
81
|
+
* (plans/worktree-pool.md, "Role runs lease slots"), this serves the director only; every other
|
|
80
82
|
* role leases a pooled `_slot-<n>` via leaseSlot instead. */
|
|
81
83
|
export async function ensureWorktree(root, role, mainBranch) {
|
|
82
84
|
const wt = worktreePath(root, role);
|
|
@@ -130,9 +132,9 @@ export async function removeWorktreeDir(root, dir) {
|
|
|
130
132
|
export async function removeWorktree(root, role) {
|
|
131
133
|
await removeWorktreeDir(root, worktreePath(root, role));
|
|
132
134
|
}
|
|
133
|
-
/** Remove the `_land-<role>` checkouts left by a pre-pool build: the pool's
|
|
135
|
+
/** Remove the `_land-<role>` checkouts left by a pre-pool build: the pool's runs keep their
|
|
134
136
|
* commit on the role branch and a queued merge re-verifies from its pinned ref, so the checkout
|
|
135
|
-
* holds nothing to keep. Called once at orchestrator startup, before any
|
|
137
|
+
* holds nothing to keep. Called once at orchestrator startup, before any run; each removal goes
|
|
136
138
|
* through removeWorktreeDir (force `worktree remove`, with the shared dead-registration fallback),
|
|
137
139
|
* then one prune registers the vanished checkouts. */
|
|
138
140
|
export async function removeLegacyLandWorktrees(root) {
|
|
@@ -144,6 +146,22 @@ export async function removeLegacyLandWorktrees(root) {
|
|
|
144
146
|
if (names.length > 0)
|
|
145
147
|
await gitTry(root, "worktree", "prune");
|
|
146
148
|
}
|
|
149
|
+
/** Delete every `refs/tumwater/` ref whose commit is missing, loose or packed, logging a warning
|
|
150
|
+
* for each, and return the dropped names in refsUnder order: a dangling pin makes `git gc` exit
|
|
151
|
+
* 128 until someone deletes it. Uses the unguarded deleteRef, as checkoutDetached does, so
|
|
152
|
+
* the delete does not depend on the sha read. Runs at team start, before any run, while nothing
|
|
153
|
+
* else writes pins; refs outside `refs/tumwater/` are never touched. */
|
|
154
|
+
export async function dropDanglingTumwaterPins(root) {
|
|
155
|
+
const dropped = [];
|
|
156
|
+
for (const { ref, sha } of await refsUnder(root, "refs/tumwater/")) {
|
|
157
|
+
if (await commitExists(root, sha))
|
|
158
|
+
continue;
|
|
159
|
+
logDroppedRef(root, ref, "at team start");
|
|
160
|
+
await deleteRef(root, ref);
|
|
161
|
+
dropped.push(ref);
|
|
162
|
+
}
|
|
163
|
+
return dropped;
|
|
164
|
+
}
|
|
147
165
|
/** Cheap file-existence shape probe for removeWorktreeDir: resolveGitDir needs the `.git` pointer,
|
|
148
166
|
* and probing it for a directory that does not exist would only produce a caught throw — the
|
|
149
167
|
* existence check keeps the common absent case allocation-free. Not a usability check; that
|
|
@@ -156,7 +174,7 @@ function isUsableWorktreeSyncPath(dir) {
|
|
|
156
174
|
* exactly `ref`'s tree. A stale index.lock left by a killed git is cleared first, or the
|
|
157
175
|
* checkout/reset below would fail on it forever. Used by redeploy.ts as the pristine copy of main
|
|
158
176
|
* it verifies and compiles — the primary checkout may be dirty or on another branch, a role
|
|
159
|
-
* worktree is never pristine while its
|
|
177
|
+
* worktree is never pristine while its role works. */
|
|
160
178
|
export async function ensureDetachedWorktree(root, dir, ref) {
|
|
161
179
|
if (!(await isUsableWorktree(dir))) {
|
|
162
180
|
const created = await serializeSetup(root, async () => {
|
|
@@ -169,7 +187,7 @@ export async function ensureDetachedWorktree(root, dir, ref) {
|
|
|
169
187
|
if (created)
|
|
170
188
|
return dir;
|
|
171
189
|
}
|
|
172
|
-
await hardResetWorktree(dir, ref,
|
|
190
|
+
await hardResetWorktree(dir, ref, root);
|
|
173
191
|
return dir;
|
|
174
192
|
}
|
|
175
193
|
/** True when no merge and no rebase is in progress in `wt`, checked from the state files
|
|
@@ -177,19 +195,14 @@ export async function ensureDetachedWorktree(root, dir, ref) {
|
|
|
177
195
|
* rebase — instead of spawning two aborts that can only fail. Returns false ("run the
|
|
178
196
|
* aborts anyway") whenever either is running OR the gitdir cannot be resolved from files:
|
|
179
197
|
* false is always safe, it just means "do exactly what the old spawn-based check did".
|
|
180
|
-
* Synchronous and microsecond-scale: this exists because abortSync runs on every fresh
|
|
198
|
+
* Synchronous and microsecond-scale: this exists because abortSync runs on every fresh run
|
|
181
199
|
* and the common case used to cost two ~10ms subprocess spawns that were guaranteed no-ops. */
|
|
182
200
|
function syncStateClear(wt) {
|
|
183
201
|
const gitdir = resolveGitDir(wt);
|
|
184
202
|
if (gitdir === undefined)
|
|
185
203
|
return false; // Not a repo we can inspect — fall back to the spawns.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
return false; // Pointer target gone: uncertain.
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
204
|
+
if (!isDirectory(gitdir))
|
|
205
|
+
return false; // Pointer target gone: uncertain.
|
|
193
206
|
try {
|
|
194
207
|
fs.statSync(path.join(gitdir, "MERGE_HEAD"));
|
|
195
208
|
return false;
|
|
@@ -198,13 +211,8 @@ function syncStateClear(wt) {
|
|
|
198
211
|
// No merge in progress — rebase state next.
|
|
199
212
|
}
|
|
200
213
|
for (const dir of ["rebase-merge", "rebase-apply"]) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
return false;
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
// This backend's rebase state is absent.
|
|
207
|
-
}
|
|
214
|
+
if (isDirectory(path.join(gitdir, dir)))
|
|
215
|
+
return false;
|
|
208
216
|
}
|
|
209
217
|
return true;
|
|
210
218
|
}
|
|
@@ -225,7 +233,7 @@ const STALE_GIT_LOCK_MS = GIT_TIMEOUT_MS + KILL_GRACE_MS;
|
|
|
225
233
|
/** Remove a stale `index.lock` from `wt`'s gitdir. A git killed mid-write — our own group
|
|
226
234
|
* deadline's SIGKILL after a SIGTERM it could not handle, a crashed harness, an operator kill —
|
|
227
235
|
* leaves index.lock behind, and every later `git reset --hard` then fails "Unable to create
|
|
228
|
-
* '…/index.lock': File exists" with no self-heal, wedging the
|
|
236
|
+
* '…/index.lock': File exists" with no self-heal, wedging the role on every run. A *fresh*
|
|
229
237
|
* lock is left alone: it may belong to a git still running, so this clears wreckage, never a
|
|
230
238
|
* live writer. */
|
|
231
239
|
function clearStaleIndexLock(wt) {
|
|
@@ -241,23 +249,72 @@ function clearStaleIndexLock(wt) {
|
|
|
241
249
|
// No lock file (the common case), or an unreadable gitdir: nothing to clear.
|
|
242
250
|
}
|
|
243
251
|
}
|
|
252
|
+
/** `git checkout --detach --force ref`, tolerating a dangling tumwater pin. `--force` because every
|
|
253
|
+
* caller hard-resets to `ref` right after: without it, uncommitted edits a killed run left in the
|
|
254
|
+
* worktree failed the checkout ("Your local changes … would be overwritten") on every later reset,
|
|
255
|
+
* so a pooled slot holding them failed each role it was handed (2026-10-21). A pooled slot's
|
|
256
|
+
* edits are saved first (slot-salvage.ts); the harness's own scratch worktrees hold nothing to
|
|
257
|
+
* keep. Leaving a detached HEAD makes
|
|
258
|
+
* git walk every ref to report orphaned commits, so one `refs/tumwater/…` pin (merge, rejected,
|
|
259
|
+
* legacy landing) whose object is gone fails the checkout "fatal: bad object <ref>" (128) and
|
|
260
|
+
* errors the role's run. The pin is unusable either way, so the named ref is deleted and the
|
|
261
|
+
* checkout retried, after a `warning` event records its name, sha and file age as evidence; any
|
|
262
|
+
* other failure, or a bad ref outside refs/tumwater/, still throws. */
|
|
263
|
+
async function checkoutDetached(root, wt, ref) {
|
|
264
|
+
for (;;) {
|
|
265
|
+
try {
|
|
266
|
+
await git(wt, "checkout", "--detach", "--force", ref);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
catch (err) {
|
|
270
|
+
const dangling = /fatal: bad object (refs\/tumwater\/\S+)/.exec(String(err instanceof Error ? err.message : err));
|
|
271
|
+
if (dangling === null)
|
|
272
|
+
throw err;
|
|
273
|
+
const bad = dangling[1];
|
|
274
|
+
logDroppedRef(root, bad, "so a worktree checkout could proceed");
|
|
275
|
+
await git(wt, "update-ref", "-d", bad);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Record a dangling pin about to be deleted: its ref, the sha it named and, for a loose ref
|
|
280
|
+
* file, when it was last written (omitted when the ref was packed). */
|
|
281
|
+
function logDroppedRef(root, ref, why) {
|
|
282
|
+
const sha = refShaFromFiles(root, ref);
|
|
283
|
+
const gitDir = resolveGitDir(root);
|
|
284
|
+
let refWrittenAt;
|
|
285
|
+
try {
|
|
286
|
+
if (gitDir !== undefined)
|
|
287
|
+
refWrittenAt = new Date(fs.statSync(path.join(gitDir, ref)).mtimeMs).toISOString();
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// Packed ref, or the file is already gone: no age to report.
|
|
291
|
+
}
|
|
292
|
+
logEventBestEffort(root, {
|
|
293
|
+
source: TUMWATER_SOURCE,
|
|
294
|
+
type: "warning",
|
|
295
|
+
message: `dropped dangling ref ${ref} (${sha ? shortSha(sha) : "unknown sha"}, object missing) ${why}`,
|
|
296
|
+
ref,
|
|
297
|
+
sha,
|
|
298
|
+
...(refWrittenAt === undefined ? {} : { refWrittenAt }),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
244
301
|
/** Hard-reset a worktree to `ref`, self-healing the two wedges a killed git leaves behind: a
|
|
245
302
|
* stale index.lock is cleared first (or the reset fails "Unable to create '…/index.lock'"
|
|
246
|
-
* forever), and an interrupted merge or rebase is aborted (or the
|
|
247
|
-
* already rebasing"). With `
|
|
248
|
-
* ensureDetachedWorktree's repair path for a worktree left on a
|
|
249
|
-
* leaves its branch checked out. Untracked files are
|
|
250
|
-
* `dist/` build dir is reaped by age). */
|
|
251
|
-
async function hardResetWorktree(wt, ref,
|
|
303
|
+
* forever), and an interrupted merge or rebase is aborted (or the run wedges on "you are
|
|
304
|
+
* already rebasing"). With `detachRoot` (the repository root), the worktree is checked out
|
|
305
|
+
* detached at `ref` first — ensureDetachedWorktree's repair path for a worktree left on a
|
|
306
|
+
* branch; resetWorktreeToMain leaves its branch checked out. Untracked files are
|
|
307
|
+
* dropped (ignored files survive; the stale `dist/` build dir is reaped by age). */
|
|
308
|
+
async function hardResetWorktree(wt, ref, detachRoot) {
|
|
252
309
|
clearStaleIndexLock(wt);
|
|
253
310
|
await abortSync(wt);
|
|
254
|
-
if (
|
|
255
|
-
await
|
|
311
|
+
if (detachRoot !== undefined)
|
|
312
|
+
await checkoutDetached(detachRoot, wt, ref);
|
|
256
313
|
await git(wt, "reset", "--hard", ref);
|
|
257
314
|
await git(wt, "clean", "-fd");
|
|
258
315
|
pruneStaleBuildDir(wt);
|
|
259
316
|
}
|
|
260
|
-
/** Hard-reset a worktree's branch to main — the fresh-
|
|
317
|
+
/** Hard-reset a worktree's branch to main — the fresh-run reset. */
|
|
261
318
|
export async function resetWorktreeToMain(wt, mainBranch) {
|
|
262
319
|
await hardResetWorktree(wt, mainBranch);
|
|
263
320
|
}
|