tumwater 0.1.2 → 0.2.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 +74 -108
- package/dist/build-info.json +2 -2
- package/dist/src/agent/agent-args.js +24 -0
- package/dist/src/agent/agent-backend.js +2 -2
- package/dist/src/agent/backend-traits.js +60 -0
- package/dist/src/agent/claude/claude-account.js +61 -8
- package/dist/src/agent/claude/claude-args.js +12 -15
- package/dist/src/agent/claude/claude-backend.js +51 -41
- package/dist/src/agent/claude/claude-env.js +7 -3
- package/dist/src/agent/claude/claude-stream.js +7 -16
- package/dist/src/agent/image-input.js +107 -0
- package/dist/src/agent/notes-shim.js +41 -0
- package/dist/src/agent/opencode/opencode-args.js +61 -0
- package/dist/src/agent/opencode/opencode-backend.js +255 -0
- package/dist/src/agent/opencode/opencode-catalog.js +184 -0
- package/dist/src/agent/opencode/opencode-config.js +188 -0
- package/dist/src/agent/opencode/opencode-env.js +36 -0
- package/dist/src/agent/opencode/opencode-session-id.js +37 -0
- package/dist/src/agent/opencode/opencode-stream.js +278 -0
- package/dist/src/agent/role-notes-write.js +16 -32
- package/dist/src/agent/run-agent.js +95 -18
- package/dist/src/agent/session-ledger.js +57 -1
- package/dist/src/backlog/backlog-archive.js +296 -0
- package/dist/src/backlog/backlog-cli.js +573 -0
- package/dist/src/backlog/backlog-commit.js +31 -0
- package/dist/src/backlog/backlog-eligibility.js +357 -33
- package/dist/src/backlog/backlog-md.js +98 -18
- package/dist/src/backlog/backlog-render.js +62 -11
- package/dist/src/backlog/backlog-structure.js +261 -58
- package/dist/src/backlog/backlog-write.js +197 -319
- package/dist/src/backlog/backlog.js +113 -16
- package/dist/src/backlog/question-answer.js +339 -0
- package/dist/src/baseline/main-baseline.js +75 -88
- package/dist/src/baseline/main-red.js +38 -30
- package/dist/src/brief/brief-edit.js +43 -0
- package/dist/src/{brief.js → brief/brief.js} +66 -10
- package/dist/src/budget/budget.js +32 -15
- package/dist/src/budget/fallback-breaker.js +8 -7
- package/dist/src/build/build-check-detect.js +8 -7
- package/dist/src/build/build-check-events.js +68 -22
- package/dist/src/build/build-check-report.js +22 -23
- package/dist/src/build/build-check-scoped.js +51 -62
- package/dist/src/build/build-check.js +15 -14
- package/dist/src/build/build-info.js +2 -1
- package/dist/src/build/build-stage.js +23 -22
- package/dist/src/build/dep-install.js +22 -21
- package/dist/src/build/host-sleep.js +1 -1
- package/dist/src/change/change-data.js +25 -14
- package/dist/src/change/change-render.js +11 -10
- package/dist/src/cli/brief-command.js +58 -0
- package/dist/src/cli/cli-args.js +25 -5
- package/dist/src/cli/cli-command-args.js +46 -14
- package/dist/src/cli/cli-editor.js +64 -0
- package/dist/src/cli/cli-flag-specs.js +54 -16
- package/dist/src/cli/cli-marker-commands.js +13 -10
- package/dist/src/cli/cli-output.js +9 -0
- package/dist/src/cli/cli-query-commands.js +458 -49
- package/dist/src/cli/cli-repo-gate.js +1 -1
- package/dist/src/cli/cli-run.js +208 -59
- package/dist/src/cli/completion.js +207 -0
- package/dist/src/cli/config-commands.js +14 -11
- package/dist/src/cli/help.js +305 -110
- package/dist/src/cli/log-commands.js +127 -21
- package/dist/src/cli/notify-command.js +33 -18
- package/dist/src/cli/question-commands.js +29 -224
- package/dist/src/cli.js +121 -40
- package/dist/src/collections.js +66 -16
- package/dist/src/concurrency/check-permit.js +16 -17
- package/dist/src/concurrency/lock.js +6 -5
- package/dist/src/concurrency/provider-lease.js +69 -0
- package/dist/src/concurrency/semaphore.js +258 -36
- package/dist/src/config/config-aliases.js +106 -0
- package/dist/src/config/config-editable-keys.js +3 -3
- package/dist/src/config/config-fallbacks.js +379 -0
- package/dist/src/config/config-field-checks.js +19 -10
- package/dist/src/config/config-keys.js +160 -0
- package/dist/src/config/config-live.js +64 -12
- package/dist/src/config/config-schema.js +34 -22
- package/dist/src/config/config-validation.js +132 -48
- package/dist/src/config/config-views.js +119 -337
- package/dist/src/config/config-write.js +138 -177
- package/dist/src/config/config.js +60 -49
- package/dist/src/config/model-selector.js +28 -2
- package/dist/src/doctor/doctor-backlog.js +546 -49
- package/dist/src/doctor/doctor-checks.js +534 -56
- package/dist/src/doctor/doctor-holds.js +193 -53
- package/dist/src/doctor/doctor-install.js +30 -0
- package/dist/src/doctor/doctor-launch-services.js +2 -2
- package/dist/src/doctor/doctor-model-checks.js +549 -157
- package/dist/src/doctor/doctor-notify.js +27 -0
- package/dist/src/doctor/doctor-orphans.js +10 -9
- package/dist/src/doctor/doctor-render.js +36 -9
- package/dist/src/doctor/doctor.js +80 -22
- package/dist/src/doctor/image-probe.js +244 -0
- package/dist/src/doctor/probe.js +64 -0
- package/dist/src/events/event-format.js +226 -102
- package/dist/src/events/event-labels.js +116 -0
- package/dist/src/events/event-legacy.js +140 -0
- package/dist/src/events/event-read.js +58 -22
- package/dist/src/events/event-window.js +27 -7
- package/dist/src/events/events.js +18 -14
- package/dist/src/events/notify.js +137 -38
- package/dist/src/failure/failure-cluster.js +12 -9
- package/dist/src/failure/failure-data.js +96 -48
- package/dist/src/failure/failure-render.js +69 -58
- package/dist/src/failure/failure-state-change.js +33 -26
- package/dist/src/failure/time-spend.js +79 -59
- package/dist/src/files/file-queue.js +16 -4
- package/dist/src/files/files.js +18 -6
- package/dist/src/files/json-object.js +18 -3
- package/dist/src/files/tail.js +1 -1
- package/dist/src/gates/bootstrap-gates.js +5 -4
- package/dist/src/gates/budget-gates.js +250 -34
- package/dist/src/gates/disk-gate.js +18 -18
- package/dist/src/gates/gate-polls.js +117 -69
- package/dist/src/gates/maintenance-quota.js +29 -29
- package/dist/src/gates/pause-gates.js +11 -11
- package/dist/src/gates/readiness.js +2 -1
- package/dist/src/gates/role-cap-gates.js +16 -21
- package/dist/src/gates/startup-gate.js +75 -31
- package/dist/src/gates/streak-gate.js +18 -16
- package/dist/src/git/commit-message.js +12 -5
- package/dist/src/git/git-diff.js +16 -16
- package/dist/src/git/git-run.js +8 -8
- package/dist/src/git/git.js +86 -55
- package/dist/src/git/slots-state.js +14 -5
- package/dist/src/git/worktree-pool.js +25 -18
- package/dist/src/git/worktree-use.js +15 -14
- package/dist/src/git/worktree.js +18 -18
- package/dist/src/gui/gui-args.js +10 -14
- package/dist/src/gui/gui-backlog-commands.js +273 -0
- package/dist/src/gui/gui-command.js +1 -1
- package/dist/src/gui/gui-config-commands.js +440 -0
- package/dist/src/gui/gui-endpoint-commands.js +16 -669
- package/dist/src/gui/gui-endpoint-common.js +49 -0
- package/dist/src/gui/gui-endpoints.js +116 -56
- package/dist/src/gui/gui-operator-commands.js +145 -0
- package/dist/src/gui/gui-prompt-commands.js +234 -0
- package/dist/src/gui/gui-server.js +28 -8
- package/dist/src/gui/gui-team-commands.js +164 -0
- package/dist/src/gui/http-body.js +17 -1
- package/dist/src/history/history-data.js +194 -98
- package/dist/src/history/history-render.js +76 -0
- package/dist/src/history/history.js +37 -58
- package/dist/src/inbox/inbox-attachments.js +75 -14
- package/dist/src/inbox/inbox-cancel.js +7 -5
- package/dist/src/inbox/inbox-edit.js +1 -1
- package/dist/src/inbox/inbox-submit.js +44 -4
- package/dist/src/inbox/inbox-top.js +83 -0
- package/dist/src/inbox/inbox.js +8 -2
- package/dist/src/inbox/pending-prompt.js +1 -1
- package/dist/src/inbox/prompt-commands.js +170 -80
- package/dist/src/init/init-templates.js +16 -14
- package/dist/src/init/init.js +12 -11
- package/dist/src/loop/leftover.js +28 -27
- package/dist/src/loop/loop-fallback.js +2 -2
- package/dist/src/loop/loop-pi.js +106 -52
- package/dist/src/loop/loop-recovery.js +15 -15
- package/dist/src/loop/loop-state.js +139 -13
- package/dist/src/loop/loop-tick.js +194 -86
- package/dist/src/loop/loop.js +72 -36
- package/dist/src/loop/model-fallback.js +3 -2
- package/dist/src/loop/revision.js +16 -7
- package/dist/src/merge/backlog-conflicts.js +155 -0
- package/dist/src/{landing/landing-batch.js → merge/merge-batch.js} +90 -89
- package/dist/src/{landing/landing-check-failures.js → merge/merge-check-failures.js} +87 -41
- package/dist/src/{landing/landing-core.js → merge/merge-core.js} +119 -95
- package/dist/src/{landing/landing-diff.js → merge/merge-diff.js} +1 -1
- package/dist/src/merge/merge-drain.js +212 -0
- package/dist/src/{landing/landing-git.js → merge/merge-git.js} +94 -21
- package/dist/src/merge/merge-pipeline.js +99 -0
- package/dist/src/{landing/landing-questions.js → merge/merge-questions.js} +9 -6
- package/dist/src/merge/merge-queue-render.js +75 -0
- package/dist/src/{landing/landing-queue.js → merge/merge-queue.js} +95 -36
- package/dist/src/merge/merge-slot.js +340 -0
- package/dist/src/{landing/landing-stack.js → merge/merge-stack.js} +66 -62
- package/dist/src/{landing/landing-merge.js → merge/merge-to-main.js} +72 -62
- package/dist/src/merge/merge-verify.js +212 -0
- package/dist/src/notes-cli.js +97 -0
- package/dist/src/operator/operator-commands.js +154 -47
- package/dist/src/operator/operator-intent.js +87 -49
- package/dist/src/operator/operator-requests.js +52 -52
- package/dist/src/operator/retire.js +62 -27
- package/dist/src/operator/revert.js +71 -0
- package/dist/src/orchestrator/orchestrator-launch.js +74 -14
- package/dist/src/orchestrator/orchestrator-scheduling.js +129 -44
- package/dist/src/orchestrator/orchestrator.js +158 -100
- package/dist/src/orchestrator/retention.js +29 -12
- package/dist/src/orchestrator/startup-migration-steps.js +51 -0
- package/dist/src/orchestrator/startup-migrations.js +40 -0
- package/dist/src/paths.js +128 -37
- package/dist/src/pi/pi-args.js +26 -3
- package/dist/src/pi/pi-backend.js +15 -23
- package/dist/src/pi/pi-bin.js +18 -12
- package/dist/src/pi/pi-event-line.js +17 -1
- package/dist/src/pi/pi-failure.js +87 -0
- package/dist/src/pi/pi-models.js +126 -38
- package/dist/src/pi/pi-run-result.js +1 -1
- package/dist/src/pi/pi-settings.js +86 -0
- package/dist/src/pi/pi-stream.js +131 -107
- package/dist/src/pi/pi-watchdogs.js +47 -43
- package/dist/src/pi-extension/bounded-output.js +20 -5
- package/dist/src/pi-extension/context-budget.js +3 -1
- package/dist/src/pi-extension/context-shake.js +54 -21
- package/dist/src/pi-extension/context-window.js +37 -0
- package/dist/src/pi-extension/full-output.js +2 -2
- package/dist/src/pi-extension/strip-thinking.js +80 -0
- package/dist/src/pi-extension/trim-system-prompt.js +38 -0
- package/dist/src/process/launch-services.js +11 -10
- package/dist/src/process/process-table.js +1 -1
- package/dist/src/process/process.js +7 -7
- package/dist/src/process/run-marker.js +29 -6
- package/dist/src/process/supervisor.js +47 -25
- package/dist/src/prompt/dialect.js +30 -10
- package/dist/src/prompt/gate-prompts.js +170 -67
- package/dist/src/prompt/principles.js +1 -0
- package/dist/src/prompt/prompt-followup.js +35 -20
- package/dist/src/prompt/prompt.js +249 -91
- package/dist/src/redeploy/redeploy-policy.js +10 -10
- package/dist/src/redeploy/redeploy.js +6 -4
- package/dist/src/redeploy/redeployer.js +63 -55
- package/dist/src/redeploy/self-reload.js +1 -1
- package/dist/src/report/report-data.js +264 -255
- package/dist/src/report/report-fold.js +353 -0
- package/dist/src/report/report-render.js +270 -33
- package/dist/src/request-timeouts.js +1 -1
- package/dist/src/review/known-flakes.js +34 -14
- package/dist/src/review/protected-edits.js +33 -0
- package/dist/src/review/review-followup.js +16 -4
- package/dist/src/review/review-precheck.js +14 -13
- package/dist/src/review/review-verdict.js +14 -0
- package/dist/src/review/review.js +81 -39
- package/dist/src/review/secret-leaks.js +127 -0
- package/dist/src/review/suite-rerun.js +4 -3
- package/dist/src/roles/legacy-role-ids.js +21 -0
- package/dist/src/roles/legacy-role-migration.js +62 -0
- package/dist/src/roles/loop-ids.js +19 -2
- package/dist/src/roles/role-catalog.js +125 -135
- package/dist/src/roles/role-guidance.js +64 -31
- package/dist/src/roles/role-labels.js +10 -0
- package/dist/src/roles/role-render.js +194 -18
- package/dist/src/roles/role-view.js +216 -17
- package/dist/src/roles/roles.js +18 -15
- package/dist/src/scheduling/backoff.js +29 -19
- package/dist/src/scheduling/claims.js +25 -9
- package/dist/src/scheduling/once-round.js +11 -11
- package/dist/src/scheduling/quiet-hours.js +25 -32
- package/dist/src/scheduling/scheduling.js +43 -24
- package/dist/src/scheduling/{work-landed-cache.js → work-merged-cache.js} +14 -14
- package/dist/src/status/status-budget.js +115 -0
- package/dist/src/status/status-data.js +77 -184
- package/dist/src/status/status-inbox.js +47 -0
- package/dist/src/status/status-install.js +27 -0
- package/dist/src/status/status-polls.js +68 -33
- package/dist/src/status/status-roles.js +105 -0
- package/dist/src/{fleet → team}/error-storm.js +10 -10
- package/dist/src/{fleet → team}/failure-spread.js +8 -8
- package/dist/src/{fleet → team}/orchestrator-info.js +86 -18
- package/dist/src/{fleet → team}/reclaim.js +5 -4
- package/dist/src/{fleet/fleet-hold.js → team/team-hold.js} +55 -21
- package/dist/src/{fleet/fleet-polls.js → team/team-polls.js} +116 -65
- package/dist/src/{fleet/fleet-state.js → team/team-state.js} +62 -37
- package/dist/src/text/datetime.js +42 -2
- package/dist/src/text/format.js +10 -1
- package/dist/src/text/markdown.js +1 -1
- package/dist/src/text/phrases.js +35 -34
- package/dist/src/text/suggest.js +3 -2
- package/dist/src/text/text.js +2 -2
- package/dist/src/tick/qa-coverage.js +2 -1
- package/dist/src/tick/stage-check.js +147 -26
- package/dist/src/tick/telemetry-digest.js +10 -9
- package/dist/src/tick/tick-apply.js +101 -72
- package/dist/src/tick/tick-detail-data.js +32 -18
- package/dist/src/tick/tick-detail.js +62 -21
- package/dist/src/tick/tick-finalize.js +29 -22
- package/dist/src/tick/tick-outcome.js +2 -1
- package/dist/src/tick/tick-prompt.js +94 -26
- package/dist/src/tick/tick-resume.js +29 -13
- package/dist/src/tick/tick-stage.js +91 -47
- package/dist/src/tick/tick-timing.js +60 -59
- package/dist/src/tick/tick-usage.js +53 -23
- package/dist/src/tick/tick-verdict.js +76 -10
- package/dist/src/ui/badges.js +118 -67
- package/dist/src/ui/gui/gui-client-boot.js +209 -30
- package/dist/src/ui/gui/gui-client-composer.js +102 -23
- package/dist/src/ui/gui/gui-client-drawer.js +330 -75
- package/dist/src/ui/gui/gui-client-history.js +96 -38
- package/dist/src/ui/gui/gui-client-labels.js +24 -0
- package/dist/src/ui/gui/gui-client-loops.js +94 -52
- package/dist/src/ui/gui/gui-client-markdown.js +1 -0
- package/dist/src/ui/gui/gui-client-model.js +63 -43
- package/dist/src/ui/gui/gui-client-operator.js +129 -39
- package/dist/src/ui/gui/gui-client-pending.js +36 -32
- package/dist/src/ui/gui/gui-client-report.js +339 -46
- package/dist/src/ui/gui/gui-client-settings.js +148 -14
- package/dist/src/ui/gui/gui-client-sound.js +2 -1
- package/dist/src/ui/gui/{gui-client-fleet.js → gui-client-team.js} +196 -72
- package/dist/src/ui/gui/gui-client.js +26 -20
- package/dist/src/ui/gui/gui-page.js +19 -19
- package/dist/src/ui/gui/gui-styles.js +12 -4
- package/dist/src/ui/progress-data.js +5 -3
- package/dist/src/ui/status-model.js +184 -122
- package/dist/src/ui/status-payload.js +119 -32
- package/dist/src/ui/status-render.js +122 -52
- package/dist/src/ui/team-alerts.js +341 -0
- package/dist/src/ui/tick-progress-model.js +14 -14
- package/dist/src/ui/tone.js +11 -8
- package/dist/src/ui/transcript-tail.js +1 -0
- package/dist/src/ui/transcript.js +23 -16
- package/dist/src/ui/tui/tui-app.js +1 -0
- package/dist/src/ui/tui/tui-backlog.js +85 -11
- package/dist/src/ui/tui/tui-editor-answer.js +97 -0
- package/dist/src/ui/tui/tui-editor-attach.js +107 -0
- package/dist/src/ui/tui/tui-editor-brief.js +107 -0
- package/dist/src/ui/tui/tui-editor-budget.js +155 -0
- package/dist/src/ui/tui/tui-editor-ctx.js +1 -0
- package/dist/src/ui/tui/tui-editor-entry.js +230 -0
- package/dist/src/ui/tui/tui-editor-interval.js +122 -0
- package/dist/src/ui/tui/tui-editor-notes.js +114 -0
- package/dist/src/ui/tui/tui-editor-pause.js +266 -0
- package/dist/src/ui/tui/tui-editor-prompt.js +95 -0
- package/dist/src/ui/tui/tui-editor-queued.js +98 -0
- package/dist/src/ui/tui/tui-editor-quiet.js +106 -0
- package/dist/src/ui/tui/tui-editor-retire.js +83 -0
- package/dist/src/ui/tui/tui-editor-search.js +73 -0
- package/dist/src/ui/tui/tui-editor-session.js +97 -0
- package/dist/src/ui/tui/tui-editors.js +152 -0
- package/dist/src/ui/tui/tui-frame.js +67 -55
- package/dist/src/ui/tui/tui-history.js +12 -7
- package/dist/src/ui/tui/tui-input.js +49 -44
- package/dist/src/ui/tui/tui-keymap.js +1 -0
- package/dist/src/ui/tui/tui-keys.js +91 -1704
- package/dist/src/ui/tui/tui-pane-activity.js +99 -0
- package/dist/src/ui/tui/tui-pane-backlog.js +224 -0
- package/dist/src/ui/tui/tui-pane-brief.js +30 -0
- package/dist/src/ui/tui/tui-pane-ctx.js +11 -0
- package/dist/src/ui/tui/tui-pane-health.js +124 -0
- package/dist/src/ui/tui/tui-pane-history.js +316 -0
- package/dist/src/ui/tui/tui-pane-pending.js +74 -0
- package/dist/src/ui/tui/tui-pane-queued.js +143 -0
- package/dist/src/ui/tui/tui-pane-report.js +122 -0
- package/dist/src/ui/tui/tui-pane-session.js +101 -0
- package/dist/src/ui/tui/tui-pane-transcript.js +150 -0
- package/dist/src/ui/tui/tui-pane.js +66 -28
- package/dist/src/ui/tui/tui-panes.js +294 -0
- package/dist/src/ui/tui/tui.js +45 -25
- package/dist/src/verdict/fix-claim.js +141 -38
- package/dist/src/verdict/no-change.js +19 -0
- package/dist/src/verdict/refusal.js +92 -8
- package/dist/src/verdict/reply-contract.js +17 -5
- package/dist/src/version.js +3 -2
- package/package.json +4 -2
- package/dist/src/agent/mcp/role-notes-server.js +0 -160
- package/dist/src/landing/backlog-conflicts.js +0 -116
- package/dist/src/landing/landing-drain.js +0 -204
- package/dist/src/landing/landing-pipeline.js +0 -96
- package/dist/src/landing/landing-queue-render.js +0 -31
- package/dist/src/landing/landing-slot.js +0 -246
- package/dist/src/landing/landing-vetting.js +0 -196
- package/dist/src/pi-extension/role-notes.js +0 -40
- package/dist/src/ui/fleet-alerts.js +0 -174
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* claude's `role_notes` surface (plans/agent-backends.md "What the pi extensions did, per
|
|
3
|
-
* backend"): a dependency-free stdio MCP server shipped in dist. claude does not load pi's
|
|
4
|
-
* bundled extensions, so the claude adapter passes this server via `--mcp-config` whenever the
|
|
5
|
-
* run carries a notebook, and the model calls `mcp__tumwater__role_notes`. It is MCP rather
|
|
6
|
-
* than "edit this file" because the notebook lives under `.tumwater/`, which the prompts
|
|
7
|
-
* forbid touching, and because every future agent CLI speaks MCP.
|
|
8
|
-
*
|
|
9
|
-
* The write path is shared with the pi extension (src/agent/role-notes-write.ts), so the 4 KB
|
|
10
|
-
* cap, the parameter schema, and the atomic write are the same on both backends. Only the
|
|
11
|
-
* JSON-RPC framing lives here.
|
|
12
|
-
*
|
|
13
|
-
* Transport is the MCP stdio convention: one JSON-RPC 2.0 message per line on stdin/stdout,
|
|
14
|
-
* and nothing else on stdout (logs would corrupt the stream). The server exits when stdin
|
|
15
|
-
* closes — exactly the lifetime claude gives a configured MCP server.
|
|
16
|
-
*/
|
|
17
|
-
import { pathToFileURL } from "node:url";
|
|
18
|
-
import { errorMessage } from "../../text/text.js";
|
|
19
|
-
import { ROLE_NOTES_DESCRIPTION, ROLE_NOTES_PARAMETERS, saveRoleNote } from "../role-notes-write.js";
|
|
20
|
-
const SERVER_NAME = "tumwater";
|
|
21
|
-
const SERVER_VERSION = "0.1.0";
|
|
22
|
-
const DEFAULT_PROTOCOL_VERSION = "2024-11-05";
|
|
23
|
-
const ROLE_NOTES_TOOL = "role_notes";
|
|
24
|
-
/** Handle one JSON-RPC line and return the response line to write, or null for a
|
|
25
|
-
* notification (no `id`). `notesPath` is the server's own TUMWATER_NOTES_PATH. */
|
|
26
|
-
function handleMcpLine(line, notesPath) {
|
|
27
|
-
let msg;
|
|
28
|
-
try {
|
|
29
|
-
msg = JSON.parse(line);
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
return errorResponse(null, -32700, "Parse error");
|
|
33
|
-
}
|
|
34
|
-
if (msg === null || typeof msg !== "object" || Array.isArray(msg)) {
|
|
35
|
-
return errorResponse(null, -32600, "Invalid Request");
|
|
36
|
-
}
|
|
37
|
-
const method = msg.method;
|
|
38
|
-
// A message with no method is a response to a request the server never sends; the CLIENT
|
|
39
|
-
// does not send one, so ignoring it is correct. A notification (no id) is answered with
|
|
40
|
-
// nothing in every case.
|
|
41
|
-
if (typeof method !== "string")
|
|
42
|
-
return null;
|
|
43
|
-
const id = msg.id;
|
|
44
|
-
const isNotification = id === undefined || id === null;
|
|
45
|
-
const result = (value) => ({ jsonrpc: "2.0", id, result: value });
|
|
46
|
-
const respond = (value) => (isNotification ? null : result(value));
|
|
47
|
-
switch (method) {
|
|
48
|
-
case "initialize": {
|
|
49
|
-
const params = (msg.params ?? {});
|
|
50
|
-
const requested = typeof params.protocolVersion === "string" ? params.protocolVersion : DEFAULT_PROTOCOL_VERSION;
|
|
51
|
-
return respond({
|
|
52
|
-
protocolVersion: requested,
|
|
53
|
-
capabilities: { tools: {} },
|
|
54
|
-
serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
case "notifications/initialized":
|
|
58
|
-
case "notifications/cancelled":
|
|
59
|
-
return null;
|
|
60
|
-
case "ping":
|
|
61
|
-
return respond({});
|
|
62
|
-
case "tools/list":
|
|
63
|
-
return respond({
|
|
64
|
-
tools: [
|
|
65
|
-
{
|
|
66
|
-
name: ROLE_NOTES_TOOL,
|
|
67
|
-
description: ROLE_NOTES_DESCRIPTION,
|
|
68
|
-
inputSchema: ROLE_NOTES_PARAMETERS,
|
|
69
|
-
},
|
|
70
|
-
],
|
|
71
|
-
});
|
|
72
|
-
case "tools/call": {
|
|
73
|
-
const params = (msg.params ?? {});
|
|
74
|
-
if (params.name !== ROLE_NOTES_TOOL) {
|
|
75
|
-
return respond(toolError(`unknown tool ${JSON.stringify(params.name)}; this server exposes ${ROLE_NOTES_TOOL}`));
|
|
76
|
-
}
|
|
77
|
-
if (notesPath === undefined) {
|
|
78
|
-
return respond(toolError("no notebook path is configured for this run"));
|
|
79
|
-
}
|
|
80
|
-
const args = (params.arguments ?? {});
|
|
81
|
-
try {
|
|
82
|
-
return respond({ content: [{ type: "text", text: saveRoleNote(notesPath, args.text) }] });
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
// Per MCP, a tool's own failure (bad input, a write error) is a successful JSON-RPC
|
|
86
|
-
// response carrying isError, so the model sees a tool error and can retry — while a
|
|
87
|
-
// JSON-RPC error would read as a transport failure of the whole server.
|
|
88
|
-
return respond(toolError(errorMessage(err)));
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
default:
|
|
92
|
-
return isNotification ? null : errorResponse(id, -32601, `Method not found: ${method}`);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
function toolError(text) {
|
|
96
|
-
return { content: [{ type: "text", text }], isError: true };
|
|
97
|
-
}
|
|
98
|
-
function errorResponse(id, code, message) {
|
|
99
|
-
return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
|
|
100
|
-
}
|
|
101
|
-
/** The largest buffered stdin line before the server treats the stream as malformed. This
|
|
102
|
-
* server only ever receives small control frames (the tool it exposes caps its `text` at 4 KB),
|
|
103
|
-
* so a message this size is not a legitimate request. Past it the buffered line is discarded
|
|
104
|
-
* and resynchronized on the next newline (the same policy the agent runner's
|
|
105
|
-
* MAX_NATIVE_LINE_CHARS applies to a byte stream with no newline), so a client that streams
|
|
106
|
-
* bytes without ever terminating a line cannot grow the buffer until the process is OOM-killed. */
|
|
107
|
-
export const MAX_MCP_LINE_CHARS = 1024 * 1024;
|
|
108
|
-
/** Wire the handler to stdin/stdout and exit on EOF. */
|
|
109
|
-
function runRoleNotesServer() {
|
|
110
|
-
const notesPath = process.env.TUMWATER_NOTES_PATH;
|
|
111
|
-
let buffer = "";
|
|
112
|
-
// Set when a buffered line passed MAX_MCP_LINE_CHARS without a newline: the rest of that
|
|
113
|
-
// line is then skipped through its newline so the stream resynchronizes on the next frame.
|
|
114
|
-
let overflow = false;
|
|
115
|
-
const flush = (line) => {
|
|
116
|
-
if (line.trim() === "")
|
|
117
|
-
return;
|
|
118
|
-
const response = handleMcpLine(line, notesPath);
|
|
119
|
-
if (response !== null)
|
|
120
|
-
process.stdout.write(JSON.stringify(response) + "\n");
|
|
121
|
-
};
|
|
122
|
-
/** Answer an over-cap frame with the JSON-RPC parse error its unparseable line would have
|
|
123
|
-
* produced, so the client sees a malformed request rather than a dead server. */
|
|
124
|
-
const discardOversized = () => {
|
|
125
|
-
buffer = "";
|
|
126
|
-
overflow = true;
|
|
127
|
-
const message = `Parse error: frame exceeded ${MAX_MCP_LINE_CHARS / (1024 * 1024)} MiB without a newline`;
|
|
128
|
-
process.stdout.write(JSON.stringify(errorResponse(null, -32700, message)) + "\n");
|
|
129
|
-
};
|
|
130
|
-
process.stdin.setEncoding("utf8");
|
|
131
|
-
process.stdin.on("data", (chunk) => {
|
|
132
|
-
buffer += chunk;
|
|
133
|
-
if (overflow) {
|
|
134
|
-
const nl = buffer.indexOf("\n");
|
|
135
|
-
if (nl < 0) {
|
|
136
|
-
buffer = ""; // Still inside the discarded frame; keep the buffer bounded.
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
overflow = false;
|
|
140
|
-
buffer = buffer.slice(nl + 1);
|
|
141
|
-
}
|
|
142
|
-
let nl;
|
|
143
|
-
while ((nl = buffer.indexOf("\n")) >= 0) {
|
|
144
|
-
flush(buffer.slice(0, nl));
|
|
145
|
-
buffer = buffer.slice(nl + 1);
|
|
146
|
-
}
|
|
147
|
-
if (buffer.length > MAX_MCP_LINE_CHARS)
|
|
148
|
-
discardOversized();
|
|
149
|
-
});
|
|
150
|
-
process.stdin.on("end", () => {
|
|
151
|
-
// A discarded over-cap frame has no complete line to flush.
|
|
152
|
-
if (!overflow && buffer !== "")
|
|
153
|
-
flush(buffer);
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
// Only run the transport when executed as the server; importing this module (the tests) must
|
|
157
|
-
// not attach stdin handlers. import.meta.url is the compiled file claude spawns by path.
|
|
158
|
-
if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
159
|
-
runRoleNotesServer();
|
|
160
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/** Deterministic first pass over a conflicted rebase for the backlog markdown files
|
|
2
|
-
* (plans/parallel-work-instances.md, "Insert-only backlog conflicts", part 3/7).
|
|
3
|
-
*
|
|
4
|
-
* The move guidance pastes every finished entry as the first one under `## Done` or
|
|
5
|
-
* `## Fixed`, so two landings off the same base insert different entries at the same
|
|
6
|
-
* location. Git's diff3 conflict for that shape has an empty base section and both sides
|
|
7
|
-
* non-empty; keeping both inserted sides adds no authored bytes, so no model run is
|
|
8
|
-
* needed. Anything else — the same entry edited on both sides, a code file, a deleted
|
|
9
|
-
* side — goes to the pi resolver untouched. */
|
|
10
|
-
import fs from "node:fs";
|
|
11
|
-
import path from "node:path";
|
|
12
|
-
import { BACKLOG_FILES } from "../backlog/backlog-md.js";
|
|
13
|
-
import { writeTextAtomic } from "../files/files.js";
|
|
14
|
-
import { git } from "../git/git-run.js";
|
|
15
|
-
/** Rewrite every insert-only conflict hunk in `text` and return the result, or null when any
|
|
16
|
-
* hunk is not insert-only (or the markers are malformed) — the caller then leaves the file
|
|
17
|
-
* for the model resolver. A hunk is insert-only when its base section is empty and both sides
|
|
18
|
-
* carry lines; it is replaced by the change's (theirs) lines, a blank separator when neither
|
|
19
|
-
* side already supplies one, then main's (ours) lines, so the later landing stays first. */
|
|
20
|
-
export function resolveInsertOnlyText(text) {
|
|
21
|
-
const lines = text.split("\n");
|
|
22
|
-
const hunks = parseHunks(lines);
|
|
23
|
-
if (hunks === null)
|
|
24
|
-
return null;
|
|
25
|
-
if (hunks.length === 0)
|
|
26
|
-
return null;
|
|
27
|
-
for (const hunk of hunks) {
|
|
28
|
-
if (hunk.base.length !== 0 || hunk.ours.length === 0 || hunk.theirs.length === 0)
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
// Replace from the end so earlier indices stay valid.
|
|
32
|
-
for (const hunk of [...hunks].sort((a, b) => b.start - a.start)) {
|
|
33
|
-
const separated = hunk.theirs[hunk.theirs.length - 1] === "" || hunk.ours[0] === "";
|
|
34
|
-
const replacement = separated
|
|
35
|
-
? [...hunk.theirs, ...hunk.ours]
|
|
36
|
-
: [...hunk.theirs, "", ...hunk.ours];
|
|
37
|
-
lines.splice(hunk.start, hunk.end - hunk.start + 1, ...replacement);
|
|
38
|
-
}
|
|
39
|
-
return lines.join("\n");
|
|
40
|
-
}
|
|
41
|
-
/** Split `lines` into conflict hunks. Returns null when a marker sequence is malformed (an
|
|
42
|
-
* unterminated hunk, or a missing `|||||||`/`=======`), so the caller can bail out. */
|
|
43
|
-
function parseHunks(lines) {
|
|
44
|
-
const hunks = [];
|
|
45
|
-
let i = 0;
|
|
46
|
-
while (i < lines.length) {
|
|
47
|
-
if (!lines[i].startsWith("<<<<<<<")) {
|
|
48
|
-
i++;
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
const start = i;
|
|
52
|
-
const ours = [];
|
|
53
|
-
const base = [];
|
|
54
|
-
const theirs = [];
|
|
55
|
-
// Consume each side up to its marker (the `<<<<<<<` line itself is skipped by starting
|
|
56
|
-
// at i + 1); a missing marker means the diff3 hunk is malformed and the parse aborts.
|
|
57
|
-
const afterOurs = collectUntilMarker(lines, i + 1, "|||||||", ours);
|
|
58
|
-
if (afterOurs === null)
|
|
59
|
-
return null; // no base marker
|
|
60
|
-
const afterBase = collectUntilMarker(lines, afterOurs, "=======", base);
|
|
61
|
-
if (afterBase === null)
|
|
62
|
-
return null; // no separator
|
|
63
|
-
const afterTheirs = collectUntilMarker(lines, afterBase, ">>>>>>>", theirs);
|
|
64
|
-
if (afterTheirs === null)
|
|
65
|
-
return null; // no end marker
|
|
66
|
-
hunks.push({ start, end: afterTheirs - 1, ours, base, theirs });
|
|
67
|
-
i = afterTheirs;
|
|
68
|
-
}
|
|
69
|
-
return hunks;
|
|
70
|
-
}
|
|
71
|
-
/** Append lines from `start` into `into` until one starts with `marker` (the marker itself is
|
|
72
|
-
* consumed, not kept), and return the index just past it. Returns null when no line from
|
|
73
|
-
* `start` on carries the marker — the malformed-hunk signal parseHunks turns into a bail-out.
|
|
74
|
-
* The one home of the drain-until-marker step shared by parseHunks's three sides, so the
|
|
75
|
-
* marker-consumption rule cannot drift between `|||||||`, `=======`, and `>>>>>>>`. */
|
|
76
|
-
function collectUntilMarker(lines, start, marker, into) {
|
|
77
|
-
let i = start;
|
|
78
|
-
while (i < lines.length && !lines[i].startsWith(marker))
|
|
79
|
-
into.push(lines[i++]);
|
|
80
|
-
return i >= lines.length ? null : i + 1;
|
|
81
|
-
}
|
|
82
|
-
/** Settle the insert-only conflicts among `files` in `wt` (plans/parallel-work-instances.md,
|
|
83
|
-
* part 3/7): every conflicted repo-root PLANS.md, BUGS.md or QUESTIONS.md whose hunks are all
|
|
84
|
-
* insert-only is rewritten without markers and staged. Returns the files still conflicted —
|
|
85
|
-
* every file that was not fully settled, including non-backlog files and files with any other
|
|
86
|
-
* hunk — so the caller runs the model resolver only over those. */
|
|
87
|
-
export async function resolveBacklogInsertConflicts(wt, files) {
|
|
88
|
-
const remaining = [];
|
|
89
|
-
for (const file of files) {
|
|
90
|
-
if (path.dirname(file) !== "." || !BACKLOG_FILES.has(file)) {
|
|
91
|
-
remaining.push(file);
|
|
92
|
-
continue;
|
|
93
|
-
}
|
|
94
|
-
const p = path.join(wt, file);
|
|
95
|
-
let text;
|
|
96
|
-
try {
|
|
97
|
-
text = fs.readFileSync(p, "utf8");
|
|
98
|
-
}
|
|
99
|
-
catch {
|
|
100
|
-
remaining.push(file); // deleted side or unreadable path — leave it to the resolver
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const resolved = resolveInsertOnlyText(text);
|
|
104
|
-
if (resolved === null) {
|
|
105
|
-
remaining.push(file);
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
// Atomic (tmp + rename): a landing process killed mid-write (SIGKILL, the timeout
|
|
109
|
-
// group-kill) must leave either the conflicted file or the fully resolved one, never a
|
|
110
|
-
// truncated backlog — the CLI's backlog writers (backlog-write.ts, question-commands.ts)
|
|
111
|
-
// already write these unbounded-history files through this helper.
|
|
112
|
-
writeTextAtomic(p, resolved);
|
|
113
|
-
await git(wt, "add", "--", file);
|
|
114
|
-
}
|
|
115
|
-
return remaining;
|
|
116
|
-
}
|
|
@@ -1,204 +0,0 @@
|
|
|
1
|
-
import { landVetted } from "./landing-batch.js";
|
|
2
|
-
import { removeLandingChange, resolveWaitStage, setLandingChangeStatus } from "./landing-slot.js";
|
|
3
|
-
import { dropLanding, queuedLandingFiles } from "./landing-queue.js";
|
|
4
|
-
import { errorMessage } from "../text/text.js";
|
|
5
|
-
import { warnEventBestEffort } from "../events/events.js";
|
|
6
|
-
import { saveLoopState } from "../loop/loop-state.js";
|
|
7
|
-
import { abortOnShutdown, discardPinnedRefs, guardLandingTask, settleLandingOutcome, } from "./landing-pipeline.js";
|
|
8
|
-
import { drainVetting, LANDING_TIER } from "./landing-vetting.js";
|
|
9
|
-
/** The tier the merge slot's conflict-resolution runs wait at: ahead of any vet parked for a
|
|
10
|
-
* permit, because the merge is the one serial step every queued change waits on. (Nothing that
|
|
11
|
-
* holds a permit ever waits on the merge: no vet takes the merge lock, and the resolver runs
|
|
12
|
-
* outside it — landing-merge.ts's mergeToMain — so this cannot deadlock.) */
|
|
13
|
-
const MERGE_TIER = LANDING_TIER - 1;
|
|
14
|
-
/** One poll of the land queue (the scheduler's WHEN: after the gates, never while a restart or
|
|
15
|
-
* a fleet hold is pending): start a vet for every queued change the pipeline does not already
|
|
16
|
-
* hold (drainVetting, in landing-vetting.ts), and start the merge when the slot is free and
|
|
17
|
-
* something is vetted (drainMerge). A queued landing is COMMITTED work awaiting completion, not
|
|
18
|
-
* a new tick, so the budget and user-pause gates deliberately do not hold it (pausing it would
|
|
19
|
-
* leave main behind while the interlock blocks that role's next tick forever). Starts tasks and
|
|
20
|
-
* returns; never awaits them — authors keep ticking behind them, which is the entire point. */
|
|
21
|
-
export async function drainLandings(ctx, p) {
|
|
22
|
-
await drainVetting(ctx, p);
|
|
23
|
-
drainMerge(ctx, p);
|
|
24
|
-
}
|
|
25
|
-
/** Settle every vetted change `tumwater abort --role` flagged (consumeAbortRequests over
|
|
26
|
-
* abortableLandings): it has no task to stop, so it ends here exactly as an aborted landing
|
|
27
|
-
* does — "aborted" written back, entry dropped, pin discarded (the deliberate stop throws the
|
|
28
|
-
* committed work away). The scheduler runs it every poll right after consuming the requests,
|
|
29
|
-
* held or not: a restart hold that skipped it would hand the flagged change, pin intact, to
|
|
30
|
-
* the next generation to land.
|
|
31
|
-
*
|
|
32
|
-
* Unlike a vet or a merge, this settles in the poll body with nothing to guard it: a throw out
|
|
33
|
-
* of the outcome write — an unwritable state file — would end the fleet's run. It must also
|
|
34
|
-
* never leave the deliberately aborted change queued (an abandoned queue file would be
|
|
35
|
-
* re-vetted and landed next poll, against the operator's intent) or keep its pin. So a failed
|
|
36
|
-
* write is reported and the abort honored by hand: queue file and marker dropped, pin
|
|
37
|
-
* discarded. */
|
|
38
|
-
export async function settleAbortedVetted(root, p) {
|
|
39
|
-
for (const [role, v] of [...p.vetted]) {
|
|
40
|
-
if (!v.userAborted)
|
|
41
|
-
continue;
|
|
42
|
-
p.vetted.delete(role);
|
|
43
|
-
try {
|
|
44
|
-
await settleLandingOutcome(root, v.entry, v.author.state, "aborted", Date.now() - v.startedAt, v.usage, v.file);
|
|
45
|
-
}
|
|
46
|
-
catch (err) {
|
|
47
|
-
warnEventBestEffort(root, "harness", `the aborted landing for ${role} could not be settled: ${errorMessage(err)}`);
|
|
48
|
-
dropLanding(v.file);
|
|
49
|
-
removeLandingChange(root, role);
|
|
50
|
-
}
|
|
51
|
-
await discardPinnedRefs(root, [role]);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
/** Start the merge when the slot is free and something is vetted: every vetted change, up to
|
|
55
|
-
* landBatchMax, in queue order among the vetted — an unvetted entry ahead of them in the queue
|
|
56
|
-
* (a slow review) does not hold them back. A vetted change whose queue entry is gone is
|
|
57
|
-
* forgotten (nothing in-process drops one; defensive). */
|
|
58
|
-
export function drainMerge(ctx, p) {
|
|
59
|
-
if (p.merge !== null || p.vetted.size === 0)
|
|
60
|
-
return;
|
|
61
|
-
const queued = queuedLandingFiles(ctx.root);
|
|
62
|
-
const files = new Set(queued.map((q) => q.file));
|
|
63
|
-
for (const [role, v] of [...p.vetted]) {
|
|
64
|
-
if (files.has(v.file))
|
|
65
|
-
continue;
|
|
66
|
-
p.vetted.delete(role);
|
|
67
|
-
removeLandingChange(ctx.root, role);
|
|
68
|
-
}
|
|
69
|
-
const picks = queued.flatMap((q) => {
|
|
70
|
-
const v = p.vetted.get(q.entry.role);
|
|
71
|
-
return v?.file === q.file ? [v] : [];
|
|
72
|
-
});
|
|
73
|
-
picks.splice(ctx.liveConfig.landBatchMax);
|
|
74
|
-
if (picks.length === 0)
|
|
75
|
-
return;
|
|
76
|
-
for (const v of picks)
|
|
77
|
-
p.vetted.delete(v.entry.role);
|
|
78
|
-
p.merge = startMerge(ctx, p, picks);
|
|
79
|
-
}
|
|
80
|
-
/** The merge task — main's one writer in the pipeline: land `picks` through landVetted on
|
|
81
|
-
* main's current tip — one change through landApprovedChange, two or more as one stack with one
|
|
82
|
-
* scope-`batch` check, one fast-forward, and 3d's largest-passing-prefix bisect — with no second
|
|
83
|
-
* review. A vetted change whose main moved since its vet is re-checked on the tree that lands
|
|
84
|
-
* (the in-lock re-check, or the stack's check), so an approval that outlived its base never
|
|
85
|
-
* lands unverified. Each defined result is written back and its entry dropped; an unattempted
|
|
86
|
-
* change (behind a bisect's attributed one) goes back to `vetted` for the next merge. A plumbing
|
|
87
|
-
* throw keeps every entry queued, but un-vetted: each is vetted afresh from its pin next poll. A
|
|
88
|
-
* user abort of any merged role stops the whole stack, and every change it had not landed ends
|
|
89
|
-
* "aborted" with its pin discarded; a shutdown keeps the pins. The task takes no permit of its
|
|
90
|
-
* own — its only model run is mergeToMain's conflict resolver, which takes a shared permit at
|
|
91
|
-
* MERGE_TIER for the run's length, outside the merge lock. */
|
|
92
|
-
function startMerge(ctx, p, picks) {
|
|
93
|
-
const { root, mainBranch, signal, roleConfig, semaphore } = ctx;
|
|
94
|
-
const merge = {
|
|
95
|
-
promise: Promise.resolve(), // Replaced below; the placeholder satisfies the type.
|
|
96
|
-
controller: new AbortController(),
|
|
97
|
-
roles: picks.map((v) => v.entry.role),
|
|
98
|
-
userAborted: false,
|
|
99
|
-
};
|
|
100
|
-
const detachShutdown = abortOnShutdown(signal, merge.controller);
|
|
101
|
-
const wiring = new Map(picks.map((v) => [
|
|
102
|
-
v.entry.role,
|
|
103
|
-
{
|
|
104
|
-
state: v.author.state,
|
|
105
|
-
foldUsage: v.foldUsage,
|
|
106
|
-
runGatePi: (opts) => v.author.runGatePi(opts),
|
|
107
|
-
runPi: async (w, prompt, s, cfg) => {
|
|
108
|
-
// A parked resolver names its wait (resolve-wait, then resolving once granted) the way
|
|
109
|
-
// a parked check does (withCheckPermit): on 2026-10-12 one waited 2.5 min for a permit
|
|
110
|
-
// with the merge queue behind it, reading as "merging". tryAcquire is acquire's own
|
|
111
|
-
// fast path for this tier (MERGE_TIER ignores the maintenance reserve).
|
|
112
|
-
if (!semaphore.tryAcquire()) {
|
|
113
|
-
const hooks = resolveWaitStage(root, v.entry.role);
|
|
114
|
-
hooks.waiting();
|
|
115
|
-
await semaphore.acquire(MERGE_TIER);
|
|
116
|
-
hooks.granted();
|
|
117
|
-
}
|
|
118
|
-
try {
|
|
119
|
-
return await v.author.runLandingPi(w, prompt, s, cfg);
|
|
120
|
-
}
|
|
121
|
-
finally {
|
|
122
|
-
semaphore.release();
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
]));
|
|
127
|
-
const task = (async () => {
|
|
128
|
-
let results;
|
|
129
|
-
let threw = false;
|
|
130
|
-
// A result reaches settleLandingOutcome the moment landVetted finalizes it, not after the
|
|
131
|
-
// whole batch; this set lets the end-of-batch loop skip what is already written back.
|
|
132
|
-
const settled = new Set();
|
|
133
|
-
const settleOne = async (s, result) => {
|
|
134
|
-
const v = picks[s];
|
|
135
|
-
settled.add(s);
|
|
136
|
-
await settleLandingOutcome(root, v.entry, v.author.state, result, Date.now() - v.startedAt, v.usage, v.file);
|
|
137
|
-
};
|
|
138
|
-
try {
|
|
139
|
-
results = await landVetted({
|
|
140
|
-
root,
|
|
141
|
-
mainBranch,
|
|
142
|
-
config: roleConfig,
|
|
143
|
-
signal: () => merge.controller.signal,
|
|
144
|
-
onChangeStatus: (role, status) => setLandingChangeStatus(root, role, status),
|
|
145
|
-
onResult: settleOne,
|
|
146
|
-
runGatePi: (opts) => picks[0].author.runGatePi(opts),
|
|
147
|
-
}, picks.map((v) => ({
|
|
148
|
-
role: v.entry.role,
|
|
149
|
-
sha: v.sha,
|
|
150
|
-
tick: v.entry.tick,
|
|
151
|
-
summary: v.entry.summary,
|
|
152
|
-
body: v.entry.body,
|
|
153
|
-
highFriction: v.entry.highFriction,
|
|
154
|
-
...(v.entry.revisionRound !== undefined ? { revisionRound: v.entry.revisionRound } : {}),
|
|
155
|
-
...(v.entry.priorReview !== undefined ? { priorReview: v.entry.priorReview } : {}),
|
|
156
|
-
...(v.verifiedHead !== undefined ? { verifiedHead: v.verifiedHead } : {}),
|
|
157
|
-
})), (role) => wiring.get(role));
|
|
158
|
-
}
|
|
159
|
-
catch (err) {
|
|
160
|
-
threw = true;
|
|
161
|
-
results = picks.map(() => undefined);
|
|
162
|
-
const head = picks[0].author;
|
|
163
|
-
head.state.lastError = errorMessage(err);
|
|
164
|
-
// The catch exists to keep every entry queued and un-vetted when landVetted throws. A
|
|
165
|
-
// throw out of this write — an unwritable state file — must not skip the settle loop
|
|
166
|
-
// and the finally that frees the merge slot, or the pipeline wedges with p.merge set
|
|
167
|
-
// forever; the write is bookkeeping a later save retries, so warn and keep going.
|
|
168
|
-
try {
|
|
169
|
-
saveLoopState(root, head.state);
|
|
170
|
-
}
|
|
171
|
-
catch (writeErr) {
|
|
172
|
-
warnEventBestEffort(root, "harness", `the merge error for ${head.state.role} could not be saved: ${errorMessage(writeErr)}`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
try {
|
|
176
|
-
for (let s = 0; s < picks.length; s++) {
|
|
177
|
-
const v = picks[s];
|
|
178
|
-
const { role } = v.entry;
|
|
179
|
-
if (settled.has(s))
|
|
180
|
-
continue; // already written back through onResult
|
|
181
|
-
const result = results[s] ?? (merge.userAborted ? "aborted" : undefined);
|
|
182
|
-
if (result === undefined && !threw) {
|
|
183
|
-
p.vetted.set(role, v);
|
|
184
|
-
setLandingChangeStatus(root, role, "vetted");
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (result !== undefined) {
|
|
188
|
-
await settleOne(s, result);
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
removeLandingChange(root, role);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
finally {
|
|
196
|
-
detachShutdown();
|
|
197
|
-
if (merge.userAborted)
|
|
198
|
-
await discardPinnedRefs(root, merge.roles);
|
|
199
|
-
p.merge = null;
|
|
200
|
-
}
|
|
201
|
-
})();
|
|
202
|
-
merge.promise = guardLandingTask(root, merge.roles.join(", "), task);
|
|
203
|
-
return merge;
|
|
204
|
-
}
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { runOnAbort } from "../process/process.js";
|
|
2
|
-
import { warnEventBestEffort } from "../events/events.js";
|
|
3
|
-
import { errorMessage } from "../text/text.js";
|
|
4
|
-
import { deleteRef } from "../git/git.js";
|
|
5
|
-
import { removeLandingChange, writeLandingOutcome } from "./landing-slot.js";
|
|
6
|
-
import { landingRefName, rejectedRefName } from "../paths.js";
|
|
7
|
-
/** A fresh pipeline: nothing in flight anywhere — no vet running, no approved change waiting,
|
|
8
|
-
* no merge holding main. The orchestrator builds one per fleet start (orchestrator.ts). */
|
|
9
|
-
export function newLandingPipeline() {
|
|
10
|
-
return { vetting: new Map(), vetted: new Map(), merge: null };
|
|
11
|
-
}
|
|
12
|
-
/** Every landing task in flight — each vet holding its permit, and the merge — for the shutdown
|
|
13
|
-
* and restart waits. A parked vet is not one of them: it has started nothing, and a shutdown
|
|
14
|
-
* (its controller) or a closed start gate (at its grant) settles it without a run. */
|
|
15
|
-
export function landingTasks(p) {
|
|
16
|
-
return [...[...p.vetting.values()].filter((v) => !v.parked), ...(p.merge ? [p.merge] : [])];
|
|
17
|
-
}
|
|
18
|
-
/** Everything `tumwater abort --role` can reach: every task in flight, plus each vetted change
|
|
19
|
-
* waiting with no task of its own. A parked vet is out of reach, as a plainly queued entry
|
|
20
|
-
* always was: its change has not started landing. */
|
|
21
|
-
export function abortableLandings(p) {
|
|
22
|
-
return [...landingTasks(p), ...p.vetted.values()];
|
|
23
|
-
}
|
|
24
|
-
/** Wire harness shutdown to one task's own controller — through the shared register/abort
|
|
25
|
-
* race helper, so an already-fired signal aborts at once. Returns the detach function. The
|
|
26
|
-
* fleet shutdown signal outlives every landing, so each task that settles while the signal is
|
|
27
|
-
* still live must detach in its own finally; without that, the one long-lived signal
|
|
28
|
-
* accumulates a listener (and the retained controller behind it) for every landing the fleet
|
|
29
|
-
* ever runs, growing without bound until the shutdown it was waiting for. */
|
|
30
|
-
export function abortOnShutdown(signal, controller) {
|
|
31
|
-
return runOnAbort(signal, () => controller.abort());
|
|
32
|
-
}
|
|
33
|
-
/** Wrap a landing task's stored promise so it can never reject: the tasks are fire-and-forget
|
|
34
|
-
* during normal operation (only the shutdown/restart drain awaits them), and an unexpected
|
|
35
|
-
* throw out of their bookkeeping — an unwritable state file, event feed, or marker while
|
|
36
|
-
* settling an outcome — would otherwise surface as an unhandled rejection and take the
|
|
37
|
-
* orchestrator down. The throw is reported best-effort; the task's own finally has already
|
|
38
|
-
* released its slot and controller, and the surviving queue entry and pin recover on the next
|
|
39
|
-
* poll. */
|
|
40
|
-
export function guardLandingTask(root, role, promise) {
|
|
41
|
-
return promise.catch((err) => {
|
|
42
|
-
warnEventBestEffort(root, "harness", `landing task for ${role} could not settle: ${errorMessage(err)}`);
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
/** Take a permit at `tier` unless `signal` fires first: resolves to its release, or to null
|
|
46
|
-
* when the signal won — a grant that arrives after that is handed straight back (a hop, never
|
|
47
|
-
* a leak), so a parked vet that a shutdown settles holds nothing. */
|
|
48
|
-
export async function acquireUnlessAborted(semaphore, tier, signal) {
|
|
49
|
-
if (signal.aborted)
|
|
50
|
-
return null;
|
|
51
|
-
const granted = semaphore.acquire(tier).then(() => true);
|
|
52
|
-
const stopped = new Promise((resolve) => signal.addEventListener("abort", () => resolve(false), { once: true }));
|
|
53
|
-
if (await Promise.race([granted, stopped]))
|
|
54
|
-
return () => semaphore.release();
|
|
55
|
-
void granted.then(() => semaphore.release());
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
/** The roles the pipeline holds right now — vetting (parked or not), vetted, or merging. */
|
|
59
|
-
export function liveRoles(p) {
|
|
60
|
-
return new Set([...p.vetting.keys(), ...p.vetted.keys(), ...(p.merge?.roles ?? [])]);
|
|
61
|
-
}
|
|
62
|
-
/** Discard the pinned landing refs of every role in a deliberately-aborted landing.
|
|
63
|
-
* `tumwater abort --role` throws the committed work away, and the pin is what would otherwise
|
|
64
|
-
* recover it, so the ref must go; a shutdown abort leaves `userAborted` unset and every ref
|
|
65
|
-
* survives for recovery. Shared by every task's settling code so their discard semantics
|
|
66
|
-
* cannot drift. A ref that is already gone is not an error. */
|
|
67
|
-
export async function discardPinnedRefs(root, roles) {
|
|
68
|
-
for (const role of roles) {
|
|
69
|
-
try {
|
|
70
|
-
await deleteRef(root, landingRefName(role));
|
|
71
|
-
}
|
|
72
|
-
catch {
|
|
73
|
-
/* already gone */
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/** Settle a landing queue entry that reached a terminal outcome — the one home of the
|
|
78
|
-
* write-plus-drop pairing every settled landing goes through: the result lands in the role's
|
|
79
|
-
* state and the landed/land_failed event (writeLandingOutcome), then the change's marker record
|
|
80
|
-
* is dropped (removeLandingChange — landing-slot leaves the marker to the caller). Shared by
|
|
81
|
-
* settleAbortedVetted, the vet's final-verdict branch, and the merge's per-change write-back, so
|
|
82
|
-
* no settled entry can keep a stale marker or lose its outcome write. The pinned-ref discard
|
|
83
|
-
* stays with the callers: only a user abort throws work away, and the merge discards its whole
|
|
84
|
-
* stack at once rather than change by change. */
|
|
85
|
-
export async function settleLandingOutcome(root, entry, state, result, durationMs, usage, file) {
|
|
86
|
-
writeLandingOutcome(root, entry, state, result, durationMs, usage, file);
|
|
87
|
-
// A revision that lands ends the rejected change's life: drop the rejected ref (plans/revise-
|
|
88
|
-
// rejected.md). A rejection repoints or deletes it in landing-core.ts, so this is the only
|
|
89
|
-
// ref cleanup a landed revision needs here. Guarded on the entry's revisionRound, not just a
|
|
90
|
-
// `changed` result: a fresh change can land while a different rejection's revision is still
|
|
91
|
-
// pending, and its landing must leave that pending revision's ref alone.
|
|
92
|
-
if (result === "changed" && entry.revisionRound !== undefined) {
|
|
93
|
-
await deleteRef(root, rejectedRefName(entry.role));
|
|
94
|
-
}
|
|
95
|
-
removeLandingChange(root, entry.role);
|
|
96
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/** The renderer behind `tumwater queue` — the operator-facing text for the land queue view the
|
|
2
|
-
* reader gathers (landing-queue.ts's readLandingQueue, beside it): one line per queued landing
|
|
3
|
-
* in execution order, plus the count of queue files the reader skipped. Pure function of the
|
|
4
|
-
* collected view and a `now` instant — no I/O, no clock reads — so the age it prints can be
|
|
5
|
-
* pinned by a test. */
|
|
6
|
-
import { humanSeconds, secondsSince } from "../text/datetime.js";
|
|
7
|
-
import { shortSha } from "../text/format.js";
|
|
8
|
-
import { plural } from "../text/phrases.js";
|
|
9
|
-
/** Render the queue as the text `tumwater queue` prints: a `land queue (<depth>):` header then
|
|
10
|
-
* one 1-based line per readable entry, oldest first, carrying the loop, tick number, short
|
|
11
|
-
* pinned sha, age, and summary. An empty queue (no entries and nothing skipped) prints
|
|
12
|
-
* `land queue empty`; a non-zero skip count appends one `(+N unreadable queue file(s) skipped)`
|
|
13
|
-
* line so a torn or foreign file the reader dropped is visible rather than silent. */
|
|
14
|
-
export function renderLandingQueue(view, now) {
|
|
15
|
-
const depth = view.entries.length + view.unreadable;
|
|
16
|
-
const lines = [];
|
|
17
|
-
if (depth === 0) {
|
|
18
|
-
lines.push("land queue empty");
|
|
19
|
-
}
|
|
20
|
-
else {
|
|
21
|
-
lines.push(`land queue (${depth}):`);
|
|
22
|
-
view.entries.forEach((e, i) => {
|
|
23
|
-
const age = humanSeconds(secondsSince(e.enqueuedAt, now));
|
|
24
|
-
lines.push(` ${i + 1}. ${e.role} tick #${e.tick} · ${shortSha(e.sha)} · ${age} ago · ${e.summary}`);
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
if (view.unreadable > 0) {
|
|
28
|
-
lines.push(` (+${plural(view.unreadable, "unreadable queue file")} skipped)`);
|
|
29
|
-
}
|
|
30
|
-
return lines.join("\n");
|
|
31
|
-
}
|