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
package/dist/src/git/git-run.js
CHANGED
|
@@ -2,10 +2,44 @@
|
|
|
2
2
|
* identity the harness authors commits with. The query helpers built on top of it (heads,
|
|
3
3
|
* refs, branches, diffs) live in git.ts and its siblings — this module knows git's argv,
|
|
4
4
|
* not the repository's shape. */
|
|
5
|
+
/** The author every harness-authored commit carries: tumwater, always, so blame-based
|
|
6
|
+
* classification (docs/code-metrics.md "Who wrote it", keyed on author and trailer) and
|
|
7
|
+
* INV-H1's "tumwater-stamped commit" wording stay true regardless of who invokes the team. */
|
|
8
|
+
const COMMIT_AUTHOR = "tumwater <tumwater@localhost>";
|
|
9
|
+
/** The ambient git identity of whoever invokes the team — `git config user.name` and
|
|
10
|
+
* `user.email` from the repo's local config and then global/system (git's own lookup path),
|
|
11
|
+
* read through gitTry so a key that is unset (git exits nonzero) reads as blank. When the
|
|
12
|
+
* repo and machine configure none (or only one of the two), fall back to tumwater on both
|
|
13
|
+
* sides so runs still work with no global git config (the long-standing fallback). No
|
|
14
|
+
* process-global cache: identity is resolved per call against the command's `cwd`, so a
|
|
15
|
+
* repo (or a test) with its own local identity reads correctly and no cross-repo bleed is
|
|
16
|
+
* possible. */
|
|
17
|
+
export async function committerIdent(cwd) {
|
|
18
|
+
const name = (await gitTry(cwd, "config", "user.name"))?.trim() ?? "";
|
|
19
|
+
const email = (await gitTry(cwd, "config", "user.email"))?.trim() ?? "";
|
|
20
|
+
if (name !== "" && email !== "")
|
|
21
|
+
return { name, email };
|
|
22
|
+
return { name: "tumwater", email: "tumwater@localhost" };
|
|
23
|
+
}
|
|
24
|
+
/** The `--author` argv fragment every `git commit` call appends right after its `commit`
|
|
25
|
+
* subcommand, so the tumwater author is pinned: a commit would otherwise inherit the previous
|
|
26
|
+
* commit's author once committer and author stop being the same identity. `--author` is a
|
|
27
|
+
* commit option, not a global one, so it belongs after the subcommand, unlike the `-c` pairs. */
|
|
28
|
+
export function commitAuthorArgs() {
|
|
29
|
+
return ["--author", COMMIT_AUTHOR];
|
|
30
|
+
}
|
|
31
|
+
/** Committer-only identity argv: the two `-c` pairs alone. For a rebase/cherry-pick these
|
|
32
|
+
* preserve each source commit's original (tumwater) author while rewriting the committer to
|
|
33
|
+
* the account running the team; a `git commit` call adds `commitAuthorArgs()` after its
|
|
34
|
+
* subcommand to pin the author too. */
|
|
35
|
+
export async function committerIdentArgs(cwd) {
|
|
36
|
+
const { name, email } = await committerIdent(cwd);
|
|
37
|
+
return ["-c", `user.name=${name}`, "-c", `user.email=${email}`];
|
|
38
|
+
}
|
|
5
39
|
import { spawn } from "node:child_process";
|
|
6
40
|
import { findOnPath } from "../files/files.js";
|
|
7
41
|
import { realGitFromXcrun } from "./xcrun-git.js";
|
|
8
|
-
import { EXEC_MAX_BUFFER, childEnv, releaseChildHandles, signalTree } from "../process/process.js";
|
|
42
|
+
import { EXEC_MAX_BUFFER, captureCappedOutput, childEnv, releaseChildHandles, signalTree } from "../process/process.js";
|
|
9
43
|
import { KILL_GRACE_MS, armGroupDeadline } from "../process/process-group.js";
|
|
10
44
|
import { errorMessage } from "../text/text.js";
|
|
11
45
|
/** The wall-clock bound on one git subprocess. Generous enough for a large rebase or diff on a slow
|
|
@@ -15,13 +49,6 @@ import { errorMessage } from "../text/text.js";
|
|
|
15
49
|
* SIGTERM → SIGKILL escalation then runs for KILL_GRACE_MS, so one call is bounded at timeoutMs +
|
|
16
50
|
* KILL_GRACE_MS whatever the child does. */
|
|
17
51
|
export const GIT_TIMEOUT_MS = 600_000;
|
|
18
|
-
/** Identity used for harness-authored commits so ticks work without global git config. */
|
|
19
|
-
export const COMMIT_IDENT = [
|
|
20
|
-
"-c",
|
|
21
|
-
"user.name=tumwater",
|
|
22
|
-
"-c",
|
|
23
|
-
"user.email=tumwater@localhost",
|
|
24
|
-
];
|
|
25
52
|
/** The error every failed git subprocess reports: the argv, the cause text (git's stderr or the
|
|
26
53
|
* spawn error's message), and git's exit code or the spawn errno. */
|
|
27
54
|
export class GitError extends Error {
|
|
@@ -35,12 +62,26 @@ export class GitError extends Error {
|
|
|
35
62
|
/** git's exit code (a number) — or a spawn errno like "ENOENT" (a string) when the
|
|
36
63
|
* binary itself could not be started, which is what execFile puts in `err.code` then. */
|
|
37
64
|
code) {
|
|
38
|
-
|
|
65
|
+
// Quote any argv a space or quote would make ambiguous (a multi-word commit message, a
|
|
66
|
+
// path), so the reported command reads unambiguously instead of "commit -m some message"
|
|
67
|
+
// reading as a stray subcommand plus arguments. Single-token argv stays bare, keeping the
|
|
68
|
+
// pinned "git status failed (1)" shape. JSON.stringify is the codebase's shared value
|
|
69
|
+
// quoting idiom (gotSuffix, cli/error text use it for the same reason).
|
|
70
|
+
const argv = args.map((a) => (/[\s"\\]/.test(a) ? JSON.stringify(a) : a)).join(" ");
|
|
71
|
+
super(`git ${argv} failed${code !== undefined ? ` (${code})` : ""}${stderr ? `: ${stderr}` : ""}`);
|
|
39
72
|
this.args = args;
|
|
40
73
|
this.stderr = stderr;
|
|
41
74
|
this.code = code;
|
|
42
75
|
}
|
|
43
76
|
}
|
|
77
|
+
/** True when `err` is a git call whose process could not be started at all: a spawn errno such
|
|
78
|
+
* as EAGAIN or ENOMEM, which is what every git call reported while the 2026-10-21 process-table
|
|
79
|
+
* wedge lasted. That says nothing about the repository and passes with the condition, so a
|
|
80
|
+
* caller about to act on the answer (discard edits, reset a workspace) waits and retries rather
|
|
81
|
+
* than treating it as "nothing there". */
|
|
82
|
+
export function gitCouldNotStart(err) {
|
|
83
|
+
return err instanceof GitError && typeof err.code === "string";
|
|
84
|
+
}
|
|
44
85
|
/** The one error every entry point that shells out to git reports when the binary itself is
|
|
45
86
|
* missing from PATH. Without a preflight check, `isGitRepo`'s failed probe reads as "not a
|
|
46
87
|
* git repository (run `git init` first)" — pointing at the wrong fix for a machine with no
|
|
@@ -129,17 +170,25 @@ export function execGitBounded(args, opts) {
|
|
|
129
170
|
stdio: ["pipe", "pipe", "pipe"],
|
|
130
171
|
...(opts.extraEnv ? { env: childEnv(opts.extraEnv) } : {}),
|
|
131
172
|
});
|
|
132
|
-
let stdout = "";
|
|
133
|
-
let stderr = "";
|
|
134
173
|
const promise = new Promise((resolve, reject) => {
|
|
135
174
|
let settled = false;
|
|
136
|
-
let stdoutSize = 0;
|
|
137
|
-
let stderrSize = 0;
|
|
138
175
|
const deadline = armGroupDeadline(child, {
|
|
139
176
|
timeoutMs,
|
|
140
177
|
killGraceMs,
|
|
141
178
|
onTimedOut: () => finish(new GitTimeoutError(timeoutMs)),
|
|
142
179
|
});
|
|
180
|
+
// setEncoding gives the stream a StringDecoder, so a multibyte UTF-8 character split
|
|
181
|
+
// across chunk boundaries is reassembled rather than corrupted — what execFile's utf8
|
|
182
|
+
// decoding did before this rewrite. It must precede captureCappedOutput so the receiver
|
|
183
|
+
// sees strings, not raw buffers.
|
|
184
|
+
child.stdout?.setEncoding("utf8");
|
|
185
|
+
child.stderr?.setEncoding("utf8");
|
|
186
|
+
const cap = captureCappedOutput(child, EXEC_MAX_BUFFER, (stream) => {
|
|
187
|
+
// Output past the ceiling is a hard failure, not silent truncation: killing the group
|
|
188
|
+
// stops a git that would otherwise emit unbounded output, and the error names the stream.
|
|
189
|
+
signalTree(child, "SIGKILL");
|
|
190
|
+
finish(new GitExecError(cap.stderr, "ERR_CHILD_PROCESS_STDIO_MAXBUFFER", `${stream} maxBuffer length exceeded`));
|
|
191
|
+
});
|
|
143
192
|
const finish = (err) => {
|
|
144
193
|
if (settled)
|
|
145
194
|
return;
|
|
@@ -152,32 +201,11 @@ export function execGitBounded(args, opts) {
|
|
|
152
201
|
reject(err);
|
|
153
202
|
}
|
|
154
203
|
else {
|
|
155
|
-
|
|
204
|
+
// Read cap's fields at settle time (they accumulate as data arrives) rather than
|
|
205
|
+
// destructuring a snapshot that stays empty.
|
|
206
|
+
resolve({ stdout: cap.stdout, stderr: cap.stderr });
|
|
156
207
|
}
|
|
157
208
|
};
|
|
158
|
-
const onOverflow = (stream) => {
|
|
159
|
-
signalTree(child, "SIGKILL");
|
|
160
|
-
finish(new GitExecError(stderr, "ERR_CHILD_PROCESS_STDIO_MAXBUFFER", `${stream} maxBuffer length exceeded`));
|
|
161
|
-
};
|
|
162
|
-
// setEncoding gives the stream a StringDecoder, so a multibyte UTF-8 character split
|
|
163
|
-
// across chunk boundaries is reassembled rather than corrupted — what execFile's utf8
|
|
164
|
-
// decoding did before this rewrite.
|
|
165
|
-
child.stdout?.setEncoding("utf8");
|
|
166
|
-
child.stderr?.setEncoding("utf8");
|
|
167
|
-
child.stdout?.on("data", (chunk) => {
|
|
168
|
-
stdoutSize += Buffer.byteLength(chunk);
|
|
169
|
-
if (stdoutSize > EXEC_MAX_BUFFER)
|
|
170
|
-
onOverflow("stdout");
|
|
171
|
-
else
|
|
172
|
-
stdout += chunk;
|
|
173
|
-
});
|
|
174
|
-
child.stderr?.on("data", (chunk) => {
|
|
175
|
-
stderrSize += Buffer.byteLength(chunk);
|
|
176
|
-
if (stderrSize > EXEC_MAX_BUFFER)
|
|
177
|
-
onOverflow("stderr");
|
|
178
|
-
else
|
|
179
|
-
stderr += chunk;
|
|
180
|
-
});
|
|
181
209
|
child.on("error", (err) => {
|
|
182
210
|
// Before the deadline this is the spawn failing (git missing from PATH); after it, it can
|
|
183
211
|
// only be a teardown signal that could not be delivered — the run is a timeout either
|
|
@@ -194,7 +222,7 @@ export function execGitBounded(args, opts) {
|
|
|
194
222
|
else if (code === 0)
|
|
195
223
|
finish();
|
|
196
224
|
else
|
|
197
|
-
finish(new GitExecError(stderr, code ?? undefined));
|
|
225
|
+
finish(new GitExecError(cap.stderr, code ?? undefined));
|
|
198
226
|
});
|
|
199
227
|
});
|
|
200
228
|
return Object.assign(promise, { child });
|
|
@@ -235,3 +263,16 @@ export async function gitTry(cwd, ...args) {
|
|
|
235
263
|
return null;
|
|
236
264
|
}
|
|
237
265
|
}
|
|
266
|
+
/** gitTry for a call whose failure is itself an answer the caller acts on (no merge-base, a
|
|
267
|
+
* cherry-pick that stopped), when acting on it drops work: null only when git ran and failed,
|
|
268
|
+
* and a throw when git could not start (gitCouldNotStart), which answers nothing. */
|
|
269
|
+
export async function gitTryStarted(cwd, ...args) {
|
|
270
|
+
try {
|
|
271
|
+
return await runGit(cwd, args);
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
if (gitCouldNotStart(err))
|
|
275
|
+
throw err;
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
}
|
package/dist/src/git/git.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { errCode } from "../errno.js";
|
|
2
|
+
import { isDirectory } from "../files/files.js";
|
|
1
3
|
import fs from "node:fs";
|
|
2
4
|
import path from "node:path";
|
|
3
|
-
import {
|
|
5
|
+
import { GIT_TIMEOUT_MS, commitAuthorArgs, committerIdentArgs, execGitBounded, git, gitCouldNotStart, gitTry } from "./git-run.js";
|
|
4
6
|
/** A valid object id (SHA-1 or SHA-256). */
|
|
5
7
|
function isSha(s) {
|
|
6
8
|
return /^[0-9a-f]{40,64}$/.test(s);
|
|
@@ -12,17 +14,11 @@ function isSha(s) {
|
|
|
12
14
|
* when the ref cannot be resolved from files — no repo here, `.git` is a worktree pointer
|
|
13
15
|
* file, the branch does not exist, or content fails validation — so callers can fall back to
|
|
14
16
|
* `git rev-parse`. Synchronous and microsecond-scale: this exists for poll loops that would
|
|
15
|
-
* otherwise pay a ~10ms subprocess spawn per
|
|
17
|
+
* otherwise pay a ~10ms subprocess spawn per run just to watch main. */
|
|
16
18
|
export function readBranchHead(root, branch) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
catch {
|
|
22
|
-
return null; // Not a repo (or .git missing).
|
|
23
|
-
}
|
|
24
|
-
if (!st.isDirectory())
|
|
25
|
-
return null; // Worktree pointer file — the spawn fallback knows better.
|
|
19
|
+
// Not a repo (or .git missing), or a worktree pointer file — the spawn fallback knows better.
|
|
20
|
+
if (!isDirectory(path.join(root, ".git")))
|
|
21
|
+
return null;
|
|
26
22
|
try {
|
|
27
23
|
const sha = fs.readFileSync(path.join(root, ".git", "refs", "heads", branch), "utf8").trim();
|
|
28
24
|
if (isSha(sha))
|
|
@@ -43,8 +39,8 @@ export function readBranchHead(root, branch) {
|
|
|
43
39
|
/** Resolve a branch's head without paying for a spawn when the ref files suffice:
|
|
44
40
|
* readBranchHead (microsecond-scale) first, then `git rev-parse` as the fallback that covers
|
|
45
41
|
* what file reads cannot (a worktree-pointer .git, anything unusual). Returns null when
|
|
46
|
-
* neither resolves it. Shared by the orchestrator's per-poll main watch, each
|
|
47
|
-
* end-of-
|
|
42
|
+
* neither resolves it. Shared by the orchestrator's per-poll main watch, each run's
|
|
43
|
+
* end-of-run head update, the red-main baseline check, and the stack merger, so the
|
|
48
44
|
* file-first/spawn-fallback strategy lives in one place. */
|
|
49
45
|
export async function branchHead(root, branch) {
|
|
50
46
|
return readBranchHead(root, branch) ?? (await gitTry(root, "rev-parse", branch));
|
|
@@ -87,7 +83,7 @@ function repoFiles(dir) {
|
|
|
87
83
|
commondir = path.resolve(gitdir, pointer);
|
|
88
84
|
}
|
|
89
85
|
catch (err) {
|
|
90
|
-
if (err
|
|
86
|
+
if (errCode(err) !== "ENOENT")
|
|
91
87
|
return undefined; // A primary checkout has none.
|
|
92
88
|
}
|
|
93
89
|
try {
|
|
@@ -139,7 +135,7 @@ function storedRef(commondir, ref) {
|
|
|
139
135
|
return isSha(loose) ? loose : undefined;
|
|
140
136
|
}
|
|
141
137
|
catch (err) {
|
|
142
|
-
if (err
|
|
138
|
+
if (errCode(err) !== "ENOENT")
|
|
143
139
|
return undefined;
|
|
144
140
|
}
|
|
145
141
|
let packed;
|
|
@@ -147,7 +143,7 @@ function storedRef(commondir, ref) {
|
|
|
147
143
|
packed = fs.readFileSync(path.join(commondir, "packed-refs"), "utf8");
|
|
148
144
|
}
|
|
149
145
|
catch (err) {
|
|
150
|
-
return err
|
|
146
|
+
return errCode(err) === "ENOENT" ? null : undefined;
|
|
151
147
|
}
|
|
152
148
|
return shaFromPackedRefs(packed, ` ${ref}`);
|
|
153
149
|
}
|
|
@@ -204,7 +200,7 @@ export function hasWorktreeRegistrations(root) {
|
|
|
204
200
|
return fs.readdirSync(path.join(repo.commondir, "worktrees")).length > 0;
|
|
205
201
|
}
|
|
206
202
|
catch (err) {
|
|
207
|
-
return err
|
|
203
|
+
return errCode(err) === "ENOENT" ? false : undefined;
|
|
208
204
|
}
|
|
209
205
|
}
|
|
210
206
|
/** True when the files settle that the full ref name `ref` does NOT exist in `dir`'s repository:
|
|
@@ -249,6 +245,10 @@ export async function refsUnder(root, prefix) {
|
|
|
249
245
|
return { ref, sha };
|
|
250
246
|
});
|
|
251
247
|
}
|
|
248
|
+
/** Whether `sha` names a commit present in the repository. */
|
|
249
|
+
export async function commitExists(root, sha) {
|
|
250
|
+
return (await gitTry(root, "cat-file", "-e", `${sha}^{commit}`)) !== null;
|
|
251
|
+
}
|
|
252
252
|
/** Delete `ref` only while it still names `sha` (git update-ref -d guarded by the old value). */
|
|
253
253
|
export async function deleteRefIfAt(root, ref, sha) {
|
|
254
254
|
await gitTry(root, "update-ref", "-d", ref, sha);
|
|
@@ -294,7 +294,7 @@ function toplevelFromFiles(dir) {
|
|
|
294
294
|
text = fs.readFileSync(config, "utf8");
|
|
295
295
|
}
|
|
296
296
|
catch (err) {
|
|
297
|
-
if (err
|
|
297
|
+
if (errCode(err) === "ENOENT")
|
|
298
298
|
continue;
|
|
299
299
|
return undefined;
|
|
300
300
|
}
|
|
@@ -311,12 +311,7 @@ function toplevelFromFiles(dir) {
|
|
|
311
311
|
}
|
|
312
312
|
/** True when the local branch `<branch>` exists in the repo. */
|
|
313
313
|
export async function branchExists(root, branch) {
|
|
314
|
-
|
|
315
|
-
if (refShaFromFiles(root, ref) !== undefined)
|
|
316
|
-
return true;
|
|
317
|
-
if (refAbsentFromFiles(root, ref))
|
|
318
|
-
return false;
|
|
319
|
-
return (await gitTry(root, "rev-parse", "--verify", ref)) !== null;
|
|
314
|
+
return (await refSha(root, `refs/heads/${branch}`)) !== null;
|
|
320
315
|
}
|
|
321
316
|
/** The revision a worktree's change should be judged against: the merge-base of its HEAD with
|
|
322
317
|
* `mainBranch`, falling back to main's tip when the merge-base fails (no common ancestor, or no
|
|
@@ -430,17 +425,23 @@ export async function isDirty(cwd) {
|
|
|
430
425
|
* HEAD by the time the reset runs) and untracked files via clean -fd. Returns the new commit hash,
|
|
431
426
|
* or null when nothing was stageable under those paths (the caller then decides what to do with the
|
|
432
427
|
* rest). The refusal path uses this so a refusing run's objection note is kept while its half-done
|
|
433
|
-
* code work is not.
|
|
428
|
+
* code work is not. A git that could not start throws instead (gitCouldNotStart): read as
|
|
429
|
+
* "nothing stageable", it would send the caller on to discard everything, notes included. */
|
|
434
430
|
export async function commitPathsAndDiscardRest(wt, message, paths) {
|
|
435
431
|
if (paths.length === 0)
|
|
436
432
|
return null;
|
|
437
|
-
|
|
438
|
-
|
|
433
|
+
try {
|
|
434
|
+
await git(wt, "add", "--", ...paths);
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
if (gitCouldNotStart(err))
|
|
438
|
+
throw err;
|
|
439
439
|
return null; // Pathspec matched nothing — treat as nothing stageable.
|
|
440
|
-
|
|
440
|
+
}
|
|
441
|
+
const staged = await git(wt, "diff", "--cached", "--name-only");
|
|
441
442
|
if (!staged)
|
|
442
443
|
return null; // Nothing actually changed under those paths.
|
|
443
|
-
await git(wt, ...
|
|
444
|
+
await git(wt, ...(await committerIdentArgs(wt)), "commit", ...commitAuthorArgs(), "-m", message);
|
|
444
445
|
await git(wt, "reset", "--hard", "HEAD");
|
|
445
446
|
await git(wt, "clean", "-fd");
|
|
446
447
|
return headOf(wt, "HEAD");
|
|
@@ -473,9 +474,10 @@ export async function aheadOfMain(wt, mainBranch) {
|
|
|
473
474
|
}
|
|
474
475
|
/** The patch-id of the change `head` makes on top of `base`: `git diff` over `base...head` (from
|
|
475
476
|
* their merge-base, the same range aheadOfMainDiff hands the reviewer) piped into
|
|
476
|
-
* `git patch-id`.
|
|
477
|
-
*
|
|
478
|
-
*
|
|
477
|
+
* `git patch-id`. The diff is `-U0`: the change's own lines only, no context. Two shas carrying
|
|
478
|
+
* the same change — an approved head and its rebase onto a main that moved next to a hunk —
|
|
479
|
+
* share it, since patch-id ignores hunk line numbers; a rebase that changed any added or removed
|
|
480
|
+
* line does not. `--verbatim` over `--stable`: `--stable` also
|
|
479
481
|
* strips whitespace, so a whitespace-only difference would reuse an approval. `--binary` puts
|
|
480
482
|
* binary content in the hash (without it every binary change reads "Binary files differ"), and
|
|
481
483
|
* `--no-ext-diff --no-textconv` keep user diff config out of it. The raw stdout goes to
|
|
@@ -486,7 +488,7 @@ export async function patchId(wt, base, head) {
|
|
|
486
488
|
try {
|
|
487
489
|
// Both children run through execGitBounded, so a hung diff or patch-id is SIGTERMed group-wide
|
|
488
490
|
// and SIGKILLed after the grace instead of stalling merge's approval check.
|
|
489
|
-
const { stdout: diff } = await execGitBounded(["diff", "--no-color", "--no-ext-diff", "--no-textconv", "--binary", `${base}...${head}`], { cwd: wt, timeoutMs: GIT_TIMEOUT_MS });
|
|
491
|
+
const { stdout: diff } = await execGitBounded(["diff", "--no-color", "--no-ext-diff", "--no-textconv", "--binary", "-U0", `${base}...${head}`], { cwd: wt, timeoutMs: GIT_TIMEOUT_MS });
|
|
490
492
|
if (diff === "")
|
|
491
493
|
return null;
|
|
492
494
|
const run = execGitBounded(["patch-id", "--verbatim"], { cwd: wt, timeoutMs: GIT_TIMEOUT_MS });
|
|
@@ -514,6 +516,6 @@ export async function foldCommitsSince(wt, base) {
|
|
|
514
516
|
/** Stage and commit everything in the worktree. Returns the new commit hash. */
|
|
515
517
|
export async function commitAll(wt, message) {
|
|
516
518
|
await git(wt, "add", "-A");
|
|
517
|
-
await git(wt, ...
|
|
519
|
+
await git(wt, ...(await committerIdentArgs(wt)), "commit", ...commitAuthorArgs(), "-m", message);
|
|
518
520
|
return headOf(wt, "HEAD");
|
|
519
521
|
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/** Saving what a pooled slot still holds before it is reset for another lease, and handing a
|
|
2
|
+
* role's unfinished work back to its next run. A run the harness could not clean up after
|
|
3
|
+
* (killed, or its own git calls failing, as every spawn did during the 2026-10-21 process-table
|
|
4
|
+
* wedge) can release its slot with uncommitted edits still in it. The next lease resets the slot
|
|
5
|
+
* to main, which used to fail on those edits for every role it was handed ("Your local changes …
|
|
6
|
+
* would be overwritten by checkout"), and a role branch's forced checkout dropped them without a
|
|
7
|
+
* trace. The reset now always succeeds (worktree.ts checks out with --force), so this is the step
|
|
8
|
+
* that keeps the work: the edits become a patch and a warning names the slot, the role that left
|
|
9
|
+
* them and the file.
|
|
10
|
+
*
|
|
11
|
+
* A patch whose run gave its slot up unsettled (the slot record's `unsettledRun`: an exception
|
|
12
|
+
* escaped the run, or its process died holding the lease) waits under salvagePendingDir until
|
|
13
|
+
* that role's next run, which restores it into its fresh checkout (restoreSalvagedWork) and
|
|
14
|
+
* runs as a handoff over it. A patch the role's branch has moved under still goes back: files
|
|
15
|
+
* that conflict keep their markers, and hunks git cannot place at all reach the run as text to
|
|
16
|
+
* apply by hand. Every other patch is edits a settled run chose to discard, or a verification's
|
|
17
|
+
* scratch output: those stay directly under salvageDir for the operator. Session retention
|
|
18
|
+
* prunes both kinds (src/orchestrator/retention.ts).
|
|
19
|
+
*
|
|
20
|
+
* Neither half throws work away on a git that could not start (gitCouldNotStart, the wedge's
|
|
21
|
+
* EAGAIN): the salvage leaves the slot as it is and fails the lease, and the restore leaves the
|
|
22
|
+
* patch waiting, so the next attempt finds everything where it was. */
|
|
23
|
+
import fs from "node:fs";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { warnEventBestEffort, TUMWATER_SOURCE } from "../events/events.js";
|
|
27
|
+
import { writeTextAtomic } from "../files/files.js";
|
|
28
|
+
import { conflictedFiles } from "../merge/merge-git.js";
|
|
29
|
+
import { salvageDir, salvagePendingDir } from "../paths.js";
|
|
30
|
+
import { errorMessage } from "../text/text.js";
|
|
31
|
+
import { git, gitCouldNotStart, gitTry, runGit } from "./git-run.js";
|
|
32
|
+
import { isUsableWorktree } from "./worktree.js";
|
|
33
|
+
/** The patch file's name: a sortable local timestamp, the slot's directory name and the role that
|
|
34
|
+
* last held it, so a listing reads in order and names whose work each file is. */
|
|
35
|
+
function salvageFileName(now, slotDir, leftBy) {
|
|
36
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
37
|
+
const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` +
|
|
38
|
+
`-${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
39
|
+
return `${stamp}-${path.basename(slotDir)}-${leftBy ?? "unknown"}.patch`;
|
|
40
|
+
}
|
|
41
|
+
/** Save the uncommitted changes in `dir` (modified, deleted and untracked files, ignored ones
|
|
42
|
+
* excepted) as a patch against its HEAD, and log a warning naming the slot, the role that left
|
|
43
|
+
* them and the file, before `forRole`'s lease resets it. The patch waits for `recoverFor`'s next
|
|
44
|
+
* run under salvagePendingDir when it is set, and is kept directly under salvageDir otherwise.
|
|
45
|
+
* Returns the patch path, or null when there was nothing to save or `dir` is not a worktree git
|
|
46
|
+
* can read (a slot about to be recreated). The patch is built through a temporary index, so the
|
|
47
|
+
* slot's own index is untouched. Throws only when git could not start (gitCouldNotStart), which
|
|
48
|
+
* fails the lease with the slot untouched: the lease that finds git running again saves the
|
|
49
|
+
* edits. Any other failure to save is logged as a warning that the edits are being discarded
|
|
50
|
+
* unsaved, and the reset goes ahead, since a slot that no lease can reset would fail every role
|
|
51
|
+
* the pool hands it. */
|
|
52
|
+
export async function salvageSlotLeftovers(root, dir, { leftBy, recoverFor, forRole }, now = new Date()) {
|
|
53
|
+
const slot = path.basename(dir);
|
|
54
|
+
let status;
|
|
55
|
+
try {
|
|
56
|
+
status = await git(dir, "status", "--porcelain", "--untracked-files=all");
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
if (gitCouldNotStart(err) && (await isUsableWorktree(dir)))
|
|
60
|
+
throw err;
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
if (status === "")
|
|
64
|
+
return null;
|
|
65
|
+
const files = status.split("\n").filter((line) => line.trim() !== "").length;
|
|
66
|
+
const by = leftBy ?? "an unknown role";
|
|
67
|
+
let tmp;
|
|
68
|
+
try {
|
|
69
|
+
tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tumwater-salvage-"));
|
|
70
|
+
const env = { GIT_INDEX_FILE: path.join(tmp, "index") };
|
|
71
|
+
await runGit(dir, ["read-tree", "HEAD"], env);
|
|
72
|
+
await runGit(dir, ["add", "-A"], env);
|
|
73
|
+
const patch = await runGit(dir, ["diff", "--cached", "--binary", "HEAD"], env);
|
|
74
|
+
if (patch === "")
|
|
75
|
+
return null;
|
|
76
|
+
const base = await git(dir, "rev-parse", "HEAD");
|
|
77
|
+
const home = recoverFor === null ? salvageDir(root) : salvagePendingDir(root, recoverFor);
|
|
78
|
+
const file = path.join(home, salvageFileName(now, dir, leftBy));
|
|
79
|
+
fs.mkdirSync(home, { recursive: true });
|
|
80
|
+
// Atomic: the patch is the only copy left once the lease resets the workspace, so a kill
|
|
81
|
+
// mid-write must never leave a truncated .patch that a later restore reads and partially
|
|
82
|
+
// applies. Written to a temp name and renamed, a reader (restoreSalvagedWork) sees either
|
|
83
|
+
// no file or the complete patch, never a prefix.
|
|
84
|
+
writeTextAtomic(file, `# ${files} uncommitted file(s) ${by} left in workspace ${slot}, saved before ${forRole} reused it\n` +
|
|
85
|
+
`# base ${base}; apply with: git apply --index <this file> on a checkout of that commit\n` +
|
|
86
|
+
`${patch}\n`);
|
|
87
|
+
const saved = path.relative(root, file);
|
|
88
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, recoverFor === null
|
|
89
|
+
? `workspace ${slot} still held ${files} uncommitted file(s) ${by} left behind; saved them to ${saved} and reset it for ${forRole}`
|
|
90
|
+
: `workspace ${slot} still held ${files} uncommitted file(s) from ${recoverFor}'s unfinished run; saved them to ${saved} for its next run and reset it for ${forRole}`);
|
|
91
|
+
return file;
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
if (gitCouldNotStart(err))
|
|
95
|
+
throw err;
|
|
96
|
+
warnEventBestEffort(root, TUMWATER_SOURCE, `workspace ${slot} still held ${files} uncommitted file(s) ${by} left behind, and saving them failed (${errorMessage(err)}); resetting it for ${forRole} discards them`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
if (tmp !== undefined)
|
|
101
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** The patches waiting for `role`'s next run, newest first (the timestamped names sort in time
|
|
105
|
+
* order). Empty when none wait or the directory cannot be read. */
|
|
106
|
+
function pendingPatches(root, role) {
|
|
107
|
+
const dir = salvagePendingDir(root, role);
|
|
108
|
+
let names;
|
|
109
|
+
try {
|
|
110
|
+
names = fs.readdirSync(dir);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
return names
|
|
116
|
+
.filter((name) => name.endsWith(".patch"))
|
|
117
|
+
.sort()
|
|
118
|
+
.reverse()
|
|
119
|
+
.map((name) => path.join(dir, name));
|
|
120
|
+
}
|
|
121
|
+
/** Move a pending patch out to salvageDir, where it is kept for the operator until retention
|
|
122
|
+
* prunes it, and return its new path (the old one when the move fails, so the warning that
|
|
123
|
+
* names it still points at the file). */
|
|
124
|
+
function settlePatch(root, file) {
|
|
125
|
+
const kept = path.join(salvageDir(root), path.basename(file));
|
|
126
|
+
try {
|
|
127
|
+
fs.renameSync(file, kept);
|
|
128
|
+
return kept;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return file;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/** Restore the newest patch of `role`'s unfinished work waiting under salvagePendingDir into `dir`,
|
|
135
|
+
* the fresh checkout of the role's branch its run is about to work in, as uncommitted edits. The
|
|
136
|
+
* patch applies exactly when the checkout is still at the patch's base. When the branch has moved,
|
|
137
|
+
* each file goes back on its own (placeSalvagedFile): by a three-way merge, which may leave
|
|
138
|
+
* conflict markers, else hunk by hunk, with the hunks that fit nowhere returned as text. The
|
|
139
|
+
* result is unstaged, so the run finds the worktree an interrupted run would have kept. The
|
|
140
|
+
* patch, and any older one it supersedes, then moves out to salvageDir. Returns null when
|
|
141
|
+
* nothing was pending. When git could not start or the patch could not be read, the checkout is
|
|
142
|
+
* put back as it was, the patch stays waiting for the role's next run, and a warning says so.
|
|
143
|
+
* Never throws. */
|
|
144
|
+
export async function restoreSalvagedWork(root, role, dir) {
|
|
145
|
+
const [newest, ...older] = pendingPatches(root, role);
|
|
146
|
+
if (newest === undefined)
|
|
147
|
+
return null;
|
|
148
|
+
let placed;
|
|
149
|
+
try {
|
|
150
|
+
placed = await applySalvage(dir, newest);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
await gitTry(dir, "reset", "-q", "--hard");
|
|
154
|
+
await gitTry(dir, "clean", "-fdq");
|
|
155
|
+
warnEventBestEffort(root, role, `could not restore the uncommitted work an earlier run left unfinished (${errorMessage(err)}); it stays waiting at ${path.relative(root, newest)} for the next run`);
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
for (const file of older)
|
|
159
|
+
settlePatch(root, file);
|
|
160
|
+
return { patch: settlePatch(root, newest), ...placed };
|
|
161
|
+
}
|
|
162
|
+
/** Apply `patch` into the clean checkout `dir`: exactly when it fits, else file by file. Throws
|
|
163
|
+
* only when git could not start or the patch cannot be read. */
|
|
164
|
+
async function applySalvage(dir, patch) {
|
|
165
|
+
try {
|
|
166
|
+
await git(dir, "apply", "--index", patch);
|
|
167
|
+
await git(dir, "reset", "-q");
|
|
168
|
+
return { conflicted: [], unplaced: [] };
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
if (gitCouldNotStart(err))
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
const unplaced = [];
|
|
175
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "tumwater-restore-"));
|
|
176
|
+
try {
|
|
177
|
+
const sections = fs.readFileSync(patch, "utf8").split(/^(?=diff --git )/m).filter((p) => p.startsWith("diff --git "));
|
|
178
|
+
for (const [i, section] of sections.entries()) {
|
|
179
|
+
const file = path.join(tmp, `${i}.patch`);
|
|
180
|
+
fs.writeFileSync(file, section);
|
|
181
|
+
unplaced.push(...(await placeSalvagedFile(dir, file, section)));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
// Read before the reset drops the unmerged index entries; the markers stay in the files.
|
|
188
|
+
const conflicted = await conflictedFiles(dir);
|
|
189
|
+
await git(dir, "reset", "-q");
|
|
190
|
+
return { conflicted, unplaced };
|
|
191
|
+
}
|
|
192
|
+
/** The paths one file's section of a patch touches (`git apply --numstat -z`: a rename names its
|
|
193
|
+
* new path), without decoding git's quoting by hand. */
|
|
194
|
+
async function sectionPaths(dir, file) {
|
|
195
|
+
const fields = (await git(dir, "apply", "--numstat", "-z", file)).split("\0");
|
|
196
|
+
const paths = [];
|
|
197
|
+
for (let i = 0; i < fields.length; i++) {
|
|
198
|
+
const m = /^[-\d]+\t[-\d]+\t(.*)$/s.exec(fields[i]);
|
|
199
|
+
if (m === null)
|
|
200
|
+
continue;
|
|
201
|
+
if (m[1] !== "")
|
|
202
|
+
paths.push(m[1]);
|
|
203
|
+
else if (fields[i + 2] !== undefined) {
|
|
204
|
+
paths.push(fields[i + 2]); // a rename: the empty path is followed by its old and new path
|
|
205
|
+
i += 2;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return paths;
|
|
209
|
+
}
|
|
210
|
+
/** Put one file's section of a salvaged patch back into `dir`: by a three-way merge, which leaves
|
|
211
|
+
* conflict markers where both sides changed the same lines, else by direct application hunk by
|
|
212
|
+
* hunk. Returns the hunks that fit nowhere (git's `.rej` text, read and removed so it is never
|
|
213
|
+
* committed), or the whole section when git could place none of it; empty when all of it went
|
|
214
|
+
* back. */
|
|
215
|
+
async function placeSalvagedFile(dir, file, section) {
|
|
216
|
+
const paths = await sectionPaths(dir, file).catch((err) => {
|
|
217
|
+
if (gitCouldNotStart(err))
|
|
218
|
+
throw err;
|
|
219
|
+
return [];
|
|
220
|
+
});
|
|
221
|
+
const label = paths.join(", ") || (/^diff --git a\/(.*?) b\//.exec(section)?.[1] ?? "a file");
|
|
222
|
+
if (await applied(dir, ["apply", "--3way", file]))
|
|
223
|
+
return [];
|
|
224
|
+
if (paths.length > 0 && (await git(dir, "--literal-pathspecs", "ls-files", "-u", "--", ...paths)) !== "")
|
|
225
|
+
return [];
|
|
226
|
+
// A deletion whose file the branch has already removed is done; there is nothing to hand back.
|
|
227
|
+
if (/^deleted file mode /m.test(section) && paths.length > 0 && paths.every((p) => !fs.existsSync(path.join(dir, p)))) {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
if (await applied(dir, ["apply", "--reject", file]))
|
|
231
|
+
return [];
|
|
232
|
+
const rejected = [];
|
|
233
|
+
for (const p of paths) {
|
|
234
|
+
const rej = path.join(dir, `${p}.rej`);
|
|
235
|
+
if (!fs.existsSync(rej))
|
|
236
|
+
continue;
|
|
237
|
+
rejected.push({ path: p, diff: fs.readFileSync(rej, "utf8") });
|
|
238
|
+
fs.rmSync(rej, { force: true });
|
|
239
|
+
}
|
|
240
|
+
return rejected.length > 0 ? rejected : [{ path: label, diff: section }];
|
|
241
|
+
}
|
|
242
|
+
/** Run one `git apply` and say whether it succeeded; a git that could not start throws. */
|
|
243
|
+
async function applied(dir, args) {
|
|
244
|
+
try {
|
|
245
|
+
await git(dir, ...args);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
if (gitCouldNotStart(err))
|
|
250
|
+
throw err;
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// vocabulary: plain
|
|
2
1
|
/** The persisted slot layout of the worktree pool (plans/worktree-pool.md): which
|
|
3
2
|
* `.tumwater/worktrees/_slot-<n>` checkouts exist, who leases each, and which role each is pinned
|
|
4
3
|
* for. Kept in its own module, apart from the pool's lease machinery, so the readers that run in
|
|
@@ -26,8 +25,8 @@ function withSlotsLock(root, fn) {
|
|
|
26
25
|
* entries follows readJsonFile's no-data policy for the file as a whole: the good slots stay,
|
|
27
26
|
* the reader and the operator view degrade instead of crashing. Each readable entry's known
|
|
28
27
|
* fields are defaulted by readSlotRecord (a `lease` that is not a complete claim reads as no
|
|
29
|
-
* lease, and pinnedFor/lastRole/lastReleasedAt as null), while any field it does
|
|
30
|
-
* rides along so an update's read-modify-write still round-trips it. */
|
|
28
|
+
* lease, and pinnedFor/lastRole/lastReleasedAt/unsettledRun as null), while any field it does
|
|
29
|
+
* not know rides along so an update's read-modify-write still round-trips it. */
|
|
31
30
|
export function readSlotsState(root) {
|
|
32
31
|
const state = readJsonFile(slotsStatePath(root));
|
|
33
32
|
if (state === null || !Array.isArray(state.slots))
|
|
@@ -46,7 +45,7 @@ export function readSlotsState(root) {
|
|
|
46
45
|
function isSlotLease(value) {
|
|
47
46
|
return (isJsonObject(value) &&
|
|
48
47
|
typeof value.role === "string" &&
|
|
49
|
-
(value.purpose === "run" || value.purpose === "verify" || value.purpose === "tick" || value.purpose === "vet") && // vocabulary-ok: legacy value read from old files
|
|
48
|
+
(value.purpose === "run" || value.purpose === "verify" || value.purpose === "verify-check" || value.purpose === "tick" || value.purpose === "vet") && // vocabulary-ok: legacy value read from old files
|
|
50
49
|
nonNegativeNumber(value.since, null) !== null &&
|
|
51
50
|
nonNegativeNumber(value.pid, null) !== null);
|
|
52
51
|
}
|
|
@@ -78,6 +77,7 @@ function readSlotRecord(slot) {
|
|
|
78
77
|
pinnedAt: slot.pinnedAt === undefined ? undefined : finiteNumber(slot.pinnedAt, null),
|
|
79
78
|
lastRole: typeof slot.lastRole === "string" ? canonicalRoleId(slot.lastRole) : null,
|
|
80
79
|
lastReleasedAt: finiteNumber(slot.lastReleasedAt, null),
|
|
80
|
+
unsettledRun: typeof slot.unsettledRun === "string" ? canonicalRoleId(slot.unsettledRun) : null,
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
83
|
function writeSlotsStateUnlocked(root, state) {
|
|
@@ -102,9 +102,9 @@ export function slotForDir(root, dir) {
|
|
|
102
102
|
}
|
|
103
103
|
/** The directory holding a role's checkout, resolved from other processes' point of view
|
|
104
104
|
* (plans/worktree-pool.md, "Readers find a role's checkout"): a slot listed as leased by the role
|
|
105
|
-
* for a
|
|
105
|
+
* for a run, or pinned for the role, wins; otherwise the legacy `worktreePath` when it is a
|
|
106
106
|
* usable worktree; otherwise null (the reader's "absent" state). Changes no behavior until role
|
|
107
|
-
*
|
|
107
|
+
* runs lease slots (part 4/5), because nothing writes those leases yet. */
|
|
108
108
|
export async function roleWorktreeDir(root, role) {
|
|
109
109
|
const slots = readSlotsState(root).slots;
|
|
110
110
|
const leased = slots.find((slot) => slot.lease?.role === role && slot.lease.purpose === "run");
|