@adhdev/daemon-core 0.9.82-rc.49 → 0.9.82-rc.490
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/dist/boot/daemon-lifecycle.d.ts +9 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/build-info.d.ts +37 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +68 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +220 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +137 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
- package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +69 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
- package/dist/commands/chat-commands-read.d.ts +23 -0
- package/dist/commands/chat-commands-scope.d.ts +39 -0
- package/dist/commands/chat-commands-shared.d.ts +33 -0
- package/dist/commands/chat-commands-write.d.ts +14 -0
- package/dist/commands/chat-commands.d.ts +10 -23
- package/dist/commands/cli-manager.d.ts +71 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +64 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +48 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/mesh-restart.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +152 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router-aggregate-status.d.ts +24 -0
- package/dist/commands/router-mesh-session-owner.d.ts +59 -0
- package/dist/commands/router-refine.d.ts +151 -0
- package/dist/commands/router-worktree-cleanup.d.ts +133 -0
- package/dist/commands/router.d.ts +203 -15
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +11 -0
- package/dist/config/config.d.ts +19 -0
- package/dist/config/mesh-config.d.ts +65 -0
- package/dist/config/mesh-json-config.d.ts +199 -0
- package/dist/config/registry-resolver.d.ts +54 -0
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/config/state-store.d.ts +30 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/cli-detector.d.ts +17 -0
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +26 -2
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +80 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/git-worktree.d.ts +71 -1
- package/dist/git/index.d.ts +3 -1
- package/dist/index.d.ts +70 -17
- package/dist/index.js +53532 -19073
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +53283 -18989
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/logging/debug-config.d.ts +16 -0
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +81 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +256 -0
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +203 -0
- package/dist/mesh/mesh-clone-grace.d.ts +8 -0
- package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
- package/dist/mesh/mesh-event-classify.d.ts +7 -0
- package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
- package/dist/mesh/mesh-events-pending.d.ts +258 -0
- package/dist/mesh/mesh-events-stale.d.ts +61 -0
- package/dist/mesh/mesh-events-utils.d.ts +75 -0
- package/dist/mesh/mesh-events.d.ts +6 -49
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +21 -1
- package/dist/mesh/mesh-init.d.ts +128 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
- package/dist/mesh/mesh-ledger.d.ts +139 -1
- package/dist/mesh/mesh-magi-status.d.ts +63 -0
- package/dist/mesh/mesh-missions.d.ts +281 -0
- package/dist/mesh/mesh-node-identity.d.ts +310 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
- package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
- package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
- package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-gates.d.ts +457 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +567 -0
- package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
- package/dist/mesh/mesh-task-inflight.d.ts +46 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
- package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +392 -1
- package/dist/mesh/preview-freshness.d.ts +28 -0
- package/dist/mesh/refine-config.d.ts +97 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +46 -0
- package/dist/providers/chat-message-normalization.d.ts +58 -1
- package/dist/providers/cli-provider-effect-format.d.ts +30 -0
- package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
- package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
- package/dist/providers/cli-provider-instance-types.d.ts +45 -0
- package/dist/providers/cli-provider-instance.d.ts +426 -30
- package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
- package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
- package/dist/providers/contracts.d.ts +196 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +79 -0
- package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/constants.d.ts +12 -0
- package/dist/providers/native-history/dispatcher.d.ts +44 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +30 -0
- package/dist/providers/provider-instance.d.ts +31 -1
- package/dist/providers/provider-loader.d.ts +42 -5
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +98 -0
- package/dist/providers/spec/cli-adapter.d.ts +223 -0
- package/dist/providers/spec/evaluator.d.ts +45 -0
- package/dist/providers/spec/fsm-driver.d.ts +449 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +202 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +244 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +616 -12
- package/dist/runtime-defaults.d.ts +2 -0
- package/dist/session-host/managed-host.d.ts +64 -0
- package/dist/session-host/runtime-surface.d.ts +10 -16
- package/dist/sessions/registry.d.ts +26 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +110 -55
- package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +27 -0
- package/dist/system/hash.d.ts +8 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +20 -6
- package/package.json +11 -4
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +80 -14
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +3 -1
- package/src/cli-adapter-types.ts +68 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1282 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +1048 -1143
- package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/src/cli-adapters/provider-cli-parse.ts +19 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +121 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/pty-write-chunking.ts +106 -0
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +204 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/chat-commands-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2788 -0
- package/src/commands/chat-commands-scope.ts +54 -0
- package/src/commands/chat-commands-shared.ts +114 -0
- package/src/commands/chat-commands-write.ts +891 -0
- package/src/commands/chat-commands.ts +19 -1841
- package/src/commands/cli-manager.d.ts +2 -0
- package/src/commands/cli-manager.ts +724 -27
- package/src/commands/handler.ts +865 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
- package/src/commands/high-family/mesh-events.ts +102 -0
- package/src/commands/high-family/mesh-status.ts +857 -0
- package/src/commands/high-family/types.ts +80 -0
- package/src/commands/low-family/coordinator-prompt.ts +125 -0
- package/src/commands/low-family/daemon-lifecycle.ts +120 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +87 -0
- package/src/commands/low-family/notification.ts +116 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +282 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +39 -0
- package/src/commands/med-family/cli-agent.ts +270 -0
- package/src/commands/med-family/fast-forward.ts +230 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +37 -0
- package/src/commands/med-family/mesh-crud.ts +1268 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +167 -0
- package/src/commands/med-family/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +153 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router-aggregate-status.ts +209 -0
- package/src/commands/router-mesh-session-owner.ts +114 -0
- package/src/commands/router-refine.ts +1794 -0
- package/src/commands/router-worktree-cleanup.ts +870 -0
- package/src/commands/router.ts +567 -4149
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +517 -24
- package/src/config/config.ts +30 -0
- package/src/config/mesh-config.ts +300 -24
- package/src/config/mesh-json-config.ts +376 -0
- package/src/config/recent-activity.ts +8 -2
- package/src/config/registry-resolver.ts +100 -0
- package/src/config/repo-settings.ts +111 -0
- package/src/config/state-store.ts +55 -0
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/daemon/dev-server.ts +0 -541
- package/src/detection/cli-detector.ts +94 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +112 -16
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +822 -48
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +261 -4
- package/src/git/index.ts +17 -0
- package/src/index.ts +190 -13
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/command-log.ts +7 -5
- package/src/logging/debug-config.ts +25 -0
- package/src/logging/debug-trace.ts +7 -2
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +341 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +510 -0
- package/src/mesh/coordinator-prompt.ts +627 -34
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +706 -0
- package/src/mesh/mesh-clone-grace.ts +68 -0
- package/src/mesh/mesh-completion-synthesis.ts +416 -0
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-delivery-policy.ts +284 -0
- package/src/mesh/mesh-event-classify.ts +78 -0
- package/src/mesh/mesh-event-forwarding.ts +2030 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +32 -0
- package/src/mesh/mesh-events-pending.ts +1548 -0
- package/src/mesh/mesh-events-stale.ts +452 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +43 -1035
- package/src/mesh/mesh-fast-forward.ts +856 -0
- package/src/mesh/mesh-host-ownership.ts +41 -3
- package/src/mesh/mesh-init.ts +350 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
- package/src/mesh/mesh-ledger.ts +897 -102
- package/src/mesh/mesh-magi-status.ts +223 -0
- package/src/mesh/mesh-missions.ts +695 -0
- package/src/mesh/mesh-node-identity.ts +2065 -0
- package/src/mesh/mesh-queue-assignment.ts +2498 -0
- package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
- package/src/mesh/mesh-reconcile-config.ts +66 -0
- package/src/mesh/mesh-reconcile-identity.ts +103 -0
- package/src/mesh/mesh-reconcile-loop.ts +1696 -0
- package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1792 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-remote-event-pull.ts +279 -0
- package/src/mesh/mesh-review-inbox.ts +308 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +2292 -0
- package/src/mesh/mesh-scheduling-runtime.ts +198 -0
- package/src/mesh/mesh-task-inflight.ts +70 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +1489 -145
- package/src/mesh/preview-freshness.ts +163 -0
- package/src/mesh/refine-config.ts +129 -12
- package/src/mesh/worktree-bootstrap-config.ts +599 -0
- package/src/providers/acp-provider-instance.ts +43 -10
- package/src/providers/approval-utils.d.ts +5 -0
- package/src/providers/approval-utils.ts +100 -6
- package/src/providers/chat-message-normalization.ts +126 -4
- package/src/providers/cli-provider-effect-format.ts +53 -0
- package/src/providers/cli-provider-history-dedup.ts +75 -0
- package/src/providers/cli-provider-input-prompt.ts +133 -0
- package/src/providers/cli-provider-instance-types.ts +131 -0
- package/src/providers/cli-provider-instance.ts +2771 -499
- package/src/providers/cli-provider-status-helpers.ts +123 -0
- package/src/providers/cli-provider-transcript-merge.ts +114 -0
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +207 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/manual-attendance.ts +105 -0
- package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +479 -0
- package/src/providers/native-history/constants.ts +19 -0
- package/src/providers/native-history/dispatcher.ts +617 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +71 -0
- package/src/providers/provider-instance.ts +24 -1
- package/src/providers/provider-loader.ts +700 -55
- package/src/providers/provider-schema.ts +93 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +538 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +246 -0
- package/src/providers/spec/cli-adapter.ts +1064 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1528 -0
- package/src/providers/spec/fsm-evaluator.ts +290 -0
- package/src/providers/spec/fsm-loader.ts +128 -0
- package/src/providers/spec/fsm-types.ts +301 -0
- package/src/providers/spec/native-history-executor.ts +1241 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +284 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +861 -14
- package/src/runtime-defaults.ts +39 -0
- package/src/session-host/managed-host.ts +218 -0
- package/src/session-host/runtime-surface.ts +20 -80
- package/src/sessions/registry.ts +44 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +148 -55
- package/src/status/builders.ts +69 -6
- package/src/status/chat-tail-hot-sessions.ts +117 -2
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +100 -27
- package/src/system/hash.ts +23 -0
- package/src/system/host-memory.ts +29 -12
- package/src/system/load-better-sqlite3.ts +68 -0
- package/src/types.ts +26 -6
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -0,0 +1,1794 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refinery job orchestration — extracted from router.ts (behavior-preserving code move).
|
|
3
|
+
*
|
|
4
|
+
* These functions were `DaemonCommandRouter` methods; they now take the router
|
|
5
|
+
* instance as `self`. The class keeps thin delegating wrappers for the entry
|
|
6
|
+
* points referenced elsewhere (startMeshRefineJob / batchRefineMeshNodes /
|
|
7
|
+
* startMeshRefineBatchJob bound into MedFamilyContext; resumePendingRefineJobsOnStartup
|
|
8
|
+
* called by the daemon boot lifecycle). No stage order, event, log string, error
|
|
9
|
+
* message, or result shape was changed — only physical location + `this.` → `self.`.
|
|
10
|
+
*/
|
|
11
|
+
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import type { DaemonCommandRouter, CommandRouterResult } from './router.js';
|
|
13
|
+
import { LOG } from '../logging/logger.js';
|
|
14
|
+
import { createInteractionId } from '../logging/debug-trace.js';
|
|
15
|
+
import { meshNodeIdMatches } from '@adhdev/mesh-shared';
|
|
16
|
+
import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
17
|
+
import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
|
|
18
|
+
import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
|
|
19
|
+
import { DEFAULT_MESH_POLICY } from '../repo-mesh-types.js';
|
|
20
|
+
import { classifyChangedPackages } from '../git/git-status.js';
|
|
21
|
+
import type { ChangedPackageClassification } from '../git/git-status.js';
|
|
22
|
+
import { readStringValue } from '../mesh/mesh-node-identity.js';
|
|
23
|
+
import {
|
|
24
|
+
alignRefinerySubmodulesAfterMerge,
|
|
25
|
+
buildMeshRefineValidationPlan,
|
|
26
|
+
buildSubmodulePublishRequiredNextStep,
|
|
27
|
+
MeshRefineAsyncJobStatus,
|
|
28
|
+
MeshRefineBatchJobHandle,
|
|
29
|
+
MeshRefineBatchJobStatus,
|
|
30
|
+
MeshRefineBatchTerminalJob,
|
|
31
|
+
MeshRefineJobHandle,
|
|
32
|
+
MeshRefineTerminalJob,
|
|
33
|
+
RefineContext,
|
|
34
|
+
RefineExecFileAsync,
|
|
35
|
+
RefineStageOutcome,
|
|
36
|
+
recordMeshRefineStage,
|
|
37
|
+
resolveRefineryAutoPublishSubmoduleMainCommits,
|
|
38
|
+
runMeshRefineEffectiveDiffGate,
|
|
39
|
+
runMeshRefinePatchEquivalenceGate,
|
|
40
|
+
runMeshRefineSubmoduleReachabilityGate,
|
|
41
|
+
runMeshRefineValidationGate,
|
|
42
|
+
truncateValidationOutput,
|
|
43
|
+
} from '../mesh/mesh-refine-gates.js';
|
|
44
|
+
|
|
45
|
+
export function buildRefineJobKey(self: DaemonCommandRouter, meshId: string, nodeId: string): string {
|
|
46
|
+
return `${meshId}:${nodeId}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function buildRefineJobHandle(self: DaemonCommandRouter, args: {
|
|
50
|
+
meshId: string;
|
|
51
|
+
nodeId: string;
|
|
52
|
+
node?: any;
|
|
53
|
+
status?: MeshRefineAsyncJobStatus;
|
|
54
|
+
startedAt?: string;
|
|
55
|
+
completedAt?: string;
|
|
56
|
+
jobId?: string;
|
|
57
|
+
interactionId?: string;
|
|
58
|
+
retryOfJobId?: string;
|
|
59
|
+
coordinatorDaemonId?: string;
|
|
60
|
+
}): MeshRefineJobHandle {
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
async: true,
|
|
64
|
+
status: args.status || 'accepted',
|
|
65
|
+
jobId: args.jobId || `refine_${createInteractionId()}`,
|
|
66
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
67
|
+
meshId: args.meshId,
|
|
68
|
+
nodeId: args.nodeId,
|
|
69
|
+
targetNodeId: args.nodeId,
|
|
70
|
+
targetDaemonId: readStringValue(args.node?.daemonId),
|
|
71
|
+
workspace: readStringValue(args.node?.workspace),
|
|
72
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
73
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
74
|
+
...(args.retryOfJobId ? { retryOfJobId: args.retryOfJobId } : {}),
|
|
75
|
+
...(args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {}),
|
|
76
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
77
|
+
evidence: {
|
|
78
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
79
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
80
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Slim the terminal-stage refine result down to the fields a coordinator needs to
|
|
87
|
+
* decide next-step, dropping the heavy per-command / per-entry detail.
|
|
88
|
+
*
|
|
89
|
+
* The full `CommandRouterResult` (with `validationSummary.commandsRun[]` carrying
|
|
90
|
+
* per-command stdout/stderr, `rejectedCommands`, `suggestions`, `suggestedConfig`,
|
|
91
|
+
* the full `patchEquivalence`, and `submoduleReachability.entries[]`/`.unreachable[]`)
|
|
92
|
+
* routinely exceeds 70KB and overflows the coordinator token limit when it rides on a
|
|
93
|
+
* `mesh_wait_events` payload. The full detail is still persisted verbatim to the ledger
|
|
94
|
+
* (`appendRefineJobLedger`) and `terminalRefineJobs`, so slimming only the EVENT loses
|
|
95
|
+
* nothing — the coordinator can pull the full record on demand via
|
|
96
|
+
* `evidence.ledgerCommand` / `taskHistoryKind`.
|
|
97
|
+
*/
|
|
98
|
+
export function slimRefineEventResult(result: Record<string, unknown>): Record<string, unknown> {
|
|
99
|
+
const slim: Record<string, unknown> = {};
|
|
100
|
+
// Top-level scalars the coordinator branches on.
|
|
101
|
+
for (const key of [
|
|
102
|
+
'success', 'code', 'error', 'convergenceStatus', 'blockedReason',
|
|
103
|
+
'branch', 'into', 'terminalKind', 'nextStep', 'finalBranchConvergenceState',
|
|
104
|
+
] as const) {
|
|
105
|
+
if (result[key] !== undefined) slim[key] = result[key];
|
|
106
|
+
}
|
|
107
|
+
// Mapped subset of the unreachable-submodule commits (path + autoPublishAllowed),
|
|
108
|
+
// not the full commit records.
|
|
109
|
+
if (Array.isArray(result.unreachableSubmoduleCommits)) {
|
|
110
|
+
slim.unreachableSubmoduleCommits = (result.unreachableSubmoduleCommits as Array<Record<string, unknown>>)
|
|
111
|
+
.map(e => ({ path: e?.path, autoPublishAllowed: e?.autoPublishAllowed }));
|
|
112
|
+
}
|
|
113
|
+
// Reduced validation summary — status + failure classification + config source
|
|
114
|
+
// + a count of commands run (drop the full commandsRun/rejectedCommands/
|
|
115
|
+
// suggestions/suggestedConfig detail).
|
|
116
|
+
if (result.validationSummary && typeof result.validationSummary === 'object') {
|
|
117
|
+
const vs = result.validationSummary as Record<string, unknown>;
|
|
118
|
+
slim.validationSummary = {
|
|
119
|
+
status: vs.status,
|
|
120
|
+
failureCode: vs.failureCode,
|
|
121
|
+
configSource: vs.configSource,
|
|
122
|
+
configSourceType: vs.configSourceType,
|
|
123
|
+
commandsRunCount: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : undefined,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
// Reduce patch-equivalence to just its verdict.
|
|
127
|
+
if (result.patchEquivalence && typeof result.patchEquivalence === 'object') {
|
|
128
|
+
const pe = result.patchEquivalence as Record<string, unknown>;
|
|
129
|
+
slim.patchEquivalence = { status: pe.status, equivalent: pe.equivalent };
|
|
130
|
+
}
|
|
131
|
+
// Reduce submodule reachability to counts; drop the full entries/unreachable arrays.
|
|
132
|
+
if (result.submoduleReachability && typeof result.submoduleReachability === 'object') {
|
|
133
|
+
const sr = result.submoduleReachability as Record<string, unknown>;
|
|
134
|
+
slim.submoduleReachability = {
|
|
135
|
+
checked: Array.isArray(sr.entries) ? sr.entries.length : undefined,
|
|
136
|
+
unreachable: Array.isArray(sr.unreachable) ? sr.unreachable.length : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return slim;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function queueRefineJobEvent(self: DaemonCommandRouter, event: 'refine:accepted' | 'refine:completed' | 'refine:failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): void {
|
|
143
|
+
const slimResult = result ? slimRefineEventResult(result) : undefined;
|
|
144
|
+
const metadataEvent = {
|
|
145
|
+
source: 'refine_mesh_node_async_job',
|
|
146
|
+
jobId: handle.jobId,
|
|
147
|
+
interactionId: handle.interactionId,
|
|
148
|
+
meshId: handle.meshId,
|
|
149
|
+
nodeId: handle.targetNodeId,
|
|
150
|
+
targetDaemonId: handle.targetDaemonId,
|
|
151
|
+
workspace: handle.workspace,
|
|
152
|
+
status: handle.status,
|
|
153
|
+
startedAt: handle.startedAt,
|
|
154
|
+
completedAt: handle.completedAt,
|
|
155
|
+
retryOfJobId: handle.retryOfJobId,
|
|
156
|
+
...(slimResult ? { result: slimResult } : {}),
|
|
157
|
+
};
|
|
158
|
+
const eventPayload = {
|
|
159
|
+
event,
|
|
160
|
+
meshId: handle.meshId,
|
|
161
|
+
nodeLabel: handle.targetNodeId,
|
|
162
|
+
nodeId: handle.targetNodeId,
|
|
163
|
+
workspace: handle.workspace,
|
|
164
|
+
metadataEvent,
|
|
165
|
+
queuedAt: Date.now(),
|
|
166
|
+
...(handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}),
|
|
167
|
+
};
|
|
168
|
+
if (typeof self.deps.instanceManager?.getByCategory === 'function') {
|
|
169
|
+
const forwarded = handleMeshForwardEvent(
|
|
170
|
+
{ instanceManager: self.deps.instanceManager } as any,
|
|
171
|
+
{
|
|
172
|
+
event,
|
|
173
|
+
meshId: handle.meshId,
|
|
174
|
+
nodeId: handle.targetNodeId,
|
|
175
|
+
workspace: handle.workspace,
|
|
176
|
+
jobId: handle.jobId,
|
|
177
|
+
interactionId: handle.interactionId,
|
|
178
|
+
status: handle.status,
|
|
179
|
+
targetDaemonId: handle.targetDaemonId,
|
|
180
|
+
startedAt: handle.startedAt,
|
|
181
|
+
completedAt: handle.completedAt,
|
|
182
|
+
retryOfJobId: handle.retryOfJobId,
|
|
183
|
+
...(slimResult ? { result: slimResult } : {}),
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
if (forwarded?.success === true) return;
|
|
187
|
+
LOG.warn('Mesh', `[Refinery] Failed to forward async refine event ${event}: ${forwarded?.error || 'unknown error'}`);
|
|
188
|
+
}
|
|
189
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export async function appendRefineJobLedger(self: DaemonCommandRouter, kind: 'task_dispatched' | 'task_completed' | 'task_failed', handle: MeshRefineJobHandle, result?: Record<string, unknown>): Promise<void> {
|
|
193
|
+
try {
|
|
194
|
+
const { appendLedgerEntry, buildLedgerOriginatingCoordinatorStamp } = await import('../mesh/mesh-ledger.js');
|
|
195
|
+
// B2a: on dispatch, stamp the originating coordinator so a later completion
|
|
196
|
+
// emit can restore `dispatchedBy` and route the terminal event back (unicast).
|
|
197
|
+
// Refine jobs carry only a coordinator DAEMON id (no session), which is enough
|
|
198
|
+
// to route to the daemon-level coordinator. Absent → omitted (v1 entry).
|
|
199
|
+
const originatingStamp = kind === 'task_dispatched'
|
|
200
|
+
? buildLedgerOriginatingCoordinatorStamp({ coordinatorDaemonId: handle.targetCoordinatorDaemonId })
|
|
201
|
+
: undefined;
|
|
202
|
+
appendLedgerEntry(handle.meshId, {
|
|
203
|
+
kind,
|
|
204
|
+
nodeId: handle.targetNodeId,
|
|
205
|
+
payload: {
|
|
206
|
+
source: 'refine_mesh_node_async_job',
|
|
207
|
+
refineJob: {
|
|
208
|
+
jobId: handle.jobId,
|
|
209
|
+
interactionId: handle.interactionId,
|
|
210
|
+
status: handle.status,
|
|
211
|
+
meshId: handle.meshId,
|
|
212
|
+
nodeId: handle.targetNodeId,
|
|
213
|
+
targetDaemonId: handle.targetDaemonId,
|
|
214
|
+
targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
|
|
215
|
+
workspace: handle.workspace,
|
|
216
|
+
startedAt: handle.startedAt,
|
|
217
|
+
completedAt: handle.completedAt,
|
|
218
|
+
retryOfJobId: handle.retryOfJobId,
|
|
219
|
+
},
|
|
220
|
+
async: true,
|
|
221
|
+
retryOfJobId: handle.retryOfJobId,
|
|
222
|
+
...(originatingStamp ? { originatingCoordinator: originatingStamp } : {}),
|
|
223
|
+
...(result ? {
|
|
224
|
+
success: result.success === true,
|
|
225
|
+
result,
|
|
226
|
+
finalBranchConvergenceState: result.finalBranchConvergenceState,
|
|
227
|
+
...(result.blockerContext ? { blockerContext: result.blockerContext } : {}),
|
|
228
|
+
} : {}),
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
} catch (e: any) {
|
|
232
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine ledger entry: ${e?.message || e}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* On daemon restart, scan all mesh ledgers for refine jobs that were dispatched
|
|
238
|
+
* but never completed/failed (i.e. the daemon died mid-job). Re-queue each one
|
|
239
|
+
* so the job runs to completion automatically without coordinator intervention.
|
|
240
|
+
*/
|
|
241
|
+
export async function resumePendingRefineJobsOnStartup(self: DaemonCommandRouter): Promise<void> {
|
|
242
|
+
try {
|
|
243
|
+
const { listMeshes } = await import('../config/mesh-config.js');
|
|
244
|
+
const { readLedgerEntries } = await import('../mesh/mesh-ledger.js');
|
|
245
|
+
const meshIds: string[] = listMeshes().map(m => m.id).filter(Boolean) as string[];
|
|
246
|
+
for (const meshId of meshIds) {
|
|
247
|
+
const entries = readLedgerEntries(meshId, { kind: ['task_dispatched', 'task_completed', 'task_failed'] });
|
|
248
|
+
// Build set of nodeIds that already have a terminal entry.
|
|
249
|
+
const terminal = new Set<string>();
|
|
250
|
+
for (const e of entries) {
|
|
251
|
+
if ((e.kind === 'task_completed' || e.kind === 'task_failed') && e.nodeId) {
|
|
252
|
+
const jobId = (e.payload as any)?.refineJob?.jobId;
|
|
253
|
+
if (jobId) terminal.add(`${e.nodeId}:${jobId}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Re-dispatch dispatched jobs with no matching terminal entry.
|
|
257
|
+
for (const e of entries) {
|
|
258
|
+
if (e.kind !== 'task_dispatched' || !e.nodeId) continue;
|
|
259
|
+
const source = (e.payload as any)?.source;
|
|
260
|
+
if (source !== 'refine_mesh_node_async_job') continue;
|
|
261
|
+
const jobId = (e.payload as any)?.refineJob?.jobId;
|
|
262
|
+
if (!jobId || terminal.has(`${e.nodeId}:${jobId}`)) continue;
|
|
263
|
+
const key = buildRefineJobKey(self, meshId, e.nodeId);
|
|
264
|
+
if (self.runningRefineJobs.has(key)) continue;
|
|
265
|
+
const coordinatorDaemonId = (e.payload as any)?.refineJob?.targetCoordinatorDaemonId;
|
|
266
|
+
LOG.info('Mesh', `[Refinery] Auto-resuming interrupted refine job for node ${e.nodeId} (jobId=${jobId})`);
|
|
267
|
+
void startMeshRefineJob(self, meshId, e.nodeId, {
|
|
268
|
+
coordinatorDaemonId,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch (e: any) {
|
|
273
|
+
LOG.warn('Mesh', `[Refinery] resumePendingRefineJobsOnStartup failed: ${e?.message || e}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Synchronous refinery for a single worktree node — the gate pipeline that
|
|
279
|
+
* validates, preflights (patch-equivalence / submodule-reachability /
|
|
280
|
+
* no-op), merges, aligns submodules, cleans up the worktree node and
|
|
281
|
+
* (optionally) pushes. The body is a flat sequence of stage methods; each
|
|
282
|
+
* stage either returns a terminal CommandRouterResult (gate failure or a
|
|
283
|
+
* successful already-merged short-circuit) or `continue` with the extended
|
|
284
|
+
* context. Behavior — stage order, every early-exit, and every result shape —
|
|
285
|
+
* is identical to the previous single inlined body.
|
|
286
|
+
*/
|
|
287
|
+
export async function executeMeshRefineNodeSynchronously(self: DaemonCommandRouter, meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
288
|
+
const refineStages: Array<Record<string, unknown>> = [];
|
|
289
|
+
try {
|
|
290
|
+
const resolved = await refineResolveRefsStage(self, meshId, nodeId, args, refineStages);
|
|
291
|
+
if (resolved.kind === 'terminal') return resolved.result;
|
|
292
|
+
const ctx = resolved.ctx;
|
|
293
|
+
|
|
294
|
+
const validation = await refineValidationStage(self, ctx);
|
|
295
|
+
if (validation.kind === 'terminal') return validation.result;
|
|
296
|
+
|
|
297
|
+
const patchEquivalence = await refinePatchEquivalenceStage(self, ctx);
|
|
298
|
+
if (patchEquivalence.kind === 'terminal') return patchEquivalence.result;
|
|
299
|
+
|
|
300
|
+
const submoduleReachability = await refineSubmoduleReachabilityStage(self, ctx);
|
|
301
|
+
if (submoduleReachability.kind === 'terminal') return submoduleReachability.result;
|
|
302
|
+
|
|
303
|
+
const effectiveDiff = await refineEffectiveDiffStage(self, ctx);
|
|
304
|
+
if (effectiveDiff.kind === 'terminal') return effectiveDiff.result;
|
|
305
|
+
|
|
306
|
+
const merge = await refineMergeAndFinalizeStage(self, ctx);
|
|
307
|
+
return (merge as { kind: 'terminal'; result: CommandRouterResult }).result;
|
|
308
|
+
} catch (e: any) {
|
|
309
|
+
return { success: false, error: e.message, refineStages };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* resolve_refs stage: resolve the mesh / worktree node / source node /
|
|
315
|
+
* repoRoot, then the worktree branch, base branch, fetched base head and
|
|
316
|
+
* branch head. Seeds the RefineContext consumed by every later stage.
|
|
317
|
+
*/
|
|
318
|
+
export async function refineResolveRefsStage(self: DaemonCommandRouter,
|
|
319
|
+
meshId: string,
|
|
320
|
+
nodeId: string,
|
|
321
|
+
args: any,
|
|
322
|
+
refineStages: Array<Record<string, unknown>>,
|
|
323
|
+
): Promise<RefineStageOutcome> {
|
|
324
|
+
// preferInline: same as startMeshRefineJob — inline-cache-only clone nodes must resolve.
|
|
325
|
+
const meshRecord = await self.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
326
|
+
const mesh = meshRecord?.mesh;
|
|
327
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
328
|
+
if (!node) return { kind: 'terminal', result: { success: false, error: `Node '${nodeId}' not found in mesh`, refineStages } };
|
|
329
|
+
|
|
330
|
+
if (!node.isLocalWorktree || !node.workspace) {
|
|
331
|
+
return { kind: 'terminal', result: { success: false, error: `Refinery requires a local worktree node`, refineStages } };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const sourceNode = node.clonedFromNodeId
|
|
335
|
+
? mesh?.nodes.find((n: any) => meshNodeIdMatches(n, node.clonedFromNodeId))
|
|
336
|
+
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
337
|
+
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
338
|
+
if (!repoRoot) return { kind: 'terminal', result: { success: false, error: 'Source node repoRoot not found', refineStages } };
|
|
339
|
+
|
|
340
|
+
const { execFile } = await import('node:child_process');
|
|
341
|
+
const { promisify } = await import('node:util');
|
|
342
|
+
const execFileAsync = promisify(execFile) as unknown as RefineExecFileAsync;
|
|
343
|
+
|
|
344
|
+
const resolveStarted = Date.now();
|
|
345
|
+
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
346
|
+
const branch = branchStdout.trim();
|
|
347
|
+
if (!branch) return { kind: 'terminal', result: { success: false, error: 'Could not determine branch of the worktree node', refineStages } };
|
|
348
|
+
|
|
349
|
+
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
350
|
+
const baseBranch = baseBranchStdout.trim();
|
|
351
|
+
|
|
352
|
+
// Fetch origin so baseHead reflects the latest pushed state, not a stale local HEAD.
|
|
353
|
+
// This prevents patch_equivalence failures when sequential Refines push to origin/main
|
|
354
|
+
// but the local main checkout hasn't been fast-forwarded yet.
|
|
355
|
+
let fetchWarning: string | undefined;
|
|
356
|
+
try {
|
|
357
|
+
await execFileAsync('git', ['fetch', 'origin', baseBranch], { cwd: repoRoot, encoding: 'utf8' });
|
|
358
|
+
} catch (e: any) {
|
|
359
|
+
fetchWarning = `git fetch origin ${baseBranch} failed (proceeding with local HEAD): ${e?.message}`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Prefer origin/<baseBranch> as the authoritative base; fall back to local HEAD if fetch failed.
|
|
363
|
+
let baseHeadRaw: string;
|
|
364
|
+
try {
|
|
365
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', `origin/${baseBranch}`], { cwd: repoRoot, encoding: 'utf8' });
|
|
366
|
+
baseHeadRaw = stdout.trim();
|
|
367
|
+
} catch {
|
|
368
|
+
const { stdout: localHead } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
369
|
+
baseHeadRaw = localHead.trim();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const { stdout: branchHeadStdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
373
|
+
const baseHead = baseHeadRaw;
|
|
374
|
+
const branchHead = branchHeadStdout.trim();
|
|
375
|
+
|
|
376
|
+
// Coarse daemon-vs-web change-impact for baseHead..branchHead, computed
|
|
377
|
+
// against the worktree so the same policy (.adhdev/change-impact.*) as the
|
|
378
|
+
// stale-build detector applies. Threaded onto ctx so the validation gate
|
|
379
|
+
// can scope its command set: a web-only branch skips daemon-scoped commands.
|
|
380
|
+
// FAIL-OPEN: any classification error leaves changeImpact undefined → the
|
|
381
|
+
// gate runs the full command set (never skip on uncertainty).
|
|
382
|
+
let changeImpact: ChangedPackageClassification | undefined;
|
|
383
|
+
try {
|
|
384
|
+
changeImpact = await classifyChangedPackages(node.workspace, baseHead, branchHead);
|
|
385
|
+
} catch {
|
|
386
|
+
changeImpact = undefined;
|
|
387
|
+
}
|
|
388
|
+
recordMeshRefineStage(refineStages, 'resolve_refs', 'passed', resolveStarted, {
|
|
389
|
+
branch, baseBranch, baseHead, branchHead,
|
|
390
|
+
...(changeImpact ? { changeImpact } : {}),
|
|
391
|
+
...(fetchWarning ? { fetchWarning } : {}),
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
kind: 'continue',
|
|
396
|
+
ctx: {
|
|
397
|
+
meshId,
|
|
398
|
+
nodeId,
|
|
399
|
+
args,
|
|
400
|
+
refineStages,
|
|
401
|
+
execFileAsync,
|
|
402
|
+
mesh,
|
|
403
|
+
node,
|
|
404
|
+
sourceNode,
|
|
405
|
+
repoRoot,
|
|
406
|
+
branch,
|
|
407
|
+
baseBranch,
|
|
408
|
+
baseHead,
|
|
409
|
+
branchHead,
|
|
410
|
+
changeImpact,
|
|
411
|
+
validationSummary: undefined as any,
|
|
412
|
+
patchEquivalence: undefined as any,
|
|
413
|
+
submoduleReachability: undefined as any,
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* validation stage: run the refinery validation gate (typecheck / test /
|
|
420
|
+
* lint / build per node config) and block on failure or when no allowlisted
|
|
421
|
+
* command was available. On pass, stores the summary on the context.
|
|
422
|
+
*/
|
|
423
|
+
export async function refineValidationStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
424
|
+
const { mesh, node, branch, baseBranch, refineStages } = ctx;
|
|
425
|
+
const validationStarted = Date.now();
|
|
426
|
+
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace, {
|
|
427
|
+
// (a) Scope the validation command set by coarse change-impact (resolved
|
|
428
|
+
// in resolve_refs). Undefined → gate runs the full command set (fail-open).
|
|
429
|
+
changeImpact: ctx.changeImpact,
|
|
430
|
+
// M2-2: consume the node's persisted bootstrap state; persist re-runs.
|
|
431
|
+
persistedBootstrapState: (node as any).worktreeBootstrap as WorktreeBootstrapState | undefined,
|
|
432
|
+
onBootstrapStateChange: (state) => {
|
|
433
|
+
(node as any).worktreeBootstrap = state;
|
|
434
|
+
void import('../config/mesh-config.js')
|
|
435
|
+
.then(({ updateNode }) => updateNode(mesh.id, node.id, { worktreeBootstrap: state } as any))
|
|
436
|
+
.catch(() => { /* persistence is best-effort */ });
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
ctx.validationSummary = validationSummary;
|
|
440
|
+
recordMeshRefineStage(
|
|
441
|
+
refineStages,
|
|
442
|
+
'validation',
|
|
443
|
+
validationSummary.status === 'passed' ? 'passed' : validationSummary.status === 'failed' ? 'failed' : 'skipped',
|
|
444
|
+
validationStarted,
|
|
445
|
+
{ validationStatus: validationSummary.status, commandsRun: validationSummary.commandsRun.length },
|
|
446
|
+
);
|
|
447
|
+
if (validationSummary.status === 'failed') {
|
|
448
|
+
const firstFailedCmd = Array.isArray(validationSummary.commandsRun)
|
|
449
|
+
? (validationSummary.commandsRun as Array<Record<string, unknown>>).find(c => c.success === false)
|
|
450
|
+
: undefined;
|
|
451
|
+
const buildValidationFailedError = (): string => {
|
|
452
|
+
const base = validationSummary.failureCode === 'missing_dependencies'
|
|
453
|
+
? 'Refinery validation dependencies are missing for a change-affected package; merge/refine was not attempted. '
|
|
454
|
+
+ 'To make this self-service, either (1) configure .adhdev/worktree_bootstrap.json (or validation.bootstrapCommands in .adhdev/refine.json) so Refinery installs deps before validation, '
|
|
455
|
+
+ 'or (2) converge the branch via the documented manual fast-forward-only bypass (rebase onto the fetched base, verify strict ancestry, then push ff-only) instead of the refine gate.'
|
|
456
|
+
: validationSummary.failureCode === 'dependency_bootstrap_failed'
|
|
457
|
+
? 'Refinery dependency/bootstrap command failed; merge/refine was not attempted.'
|
|
458
|
+
: validationSummary.failureCode === 'spawn_resolution_failed'
|
|
459
|
+
? (validationSummary.spawnResolutionError
|
|
460
|
+
|| 'Refinery validation command could not be spawned (executable not found); merge/refine was not attempted.')
|
|
461
|
+
: 'Refinery validation gate failed; merge/refine was not attempted.';
|
|
462
|
+
if (!firstFailedCmd) return base;
|
|
463
|
+
const cmdName = typeof firstFailedCmd.displayCommand === 'string' ? firstFailedCmd.displayCommand
|
|
464
|
+
: typeof firstFailedCmd.command === 'string'
|
|
465
|
+
? [firstFailedCmd.command, ...(Array.isArray(firstFailedCmd.args) ? firstFailedCmd.args : [])].join(' ').trim()
|
|
466
|
+
: typeof firstFailedCmd.cmd === 'string' ? firstFailedCmd.cmd : '';
|
|
467
|
+
const rawOutput = [firstFailedCmd.stdout, firstFailedCmd.stderr, firstFailedCmd.output]
|
|
468
|
+
.filter(s => typeof s === 'string' && s.length > 0)
|
|
469
|
+
.join('\n');
|
|
470
|
+
const tail = rawOutput.length > 800 ? rawOutput.slice(-800) : rawOutput;
|
|
471
|
+
return [
|
|
472
|
+
base,
|
|
473
|
+
cmdName ? `First failing command: ${cmdName}` : '',
|
|
474
|
+
tail ? `Output (tail):\n${tail}` : '',
|
|
475
|
+
].filter(Boolean).join('\n');
|
|
476
|
+
};
|
|
477
|
+
return { kind: 'terminal', result: {
|
|
478
|
+
success: false,
|
|
479
|
+
code: validationSummary.failureCode || 'validation_failed',
|
|
480
|
+
convergenceStatus: 'blocked_review',
|
|
481
|
+
error: buildValidationFailedError(),
|
|
482
|
+
branch,
|
|
483
|
+
into: baseBranch,
|
|
484
|
+
validationSummary,
|
|
485
|
+
refineStages,
|
|
486
|
+
finalBranchConvergenceState: {
|
|
487
|
+
branch,
|
|
488
|
+
baseBranch,
|
|
489
|
+
merged: false,
|
|
490
|
+
removed: false,
|
|
491
|
+
validation: 'failed',
|
|
492
|
+
status: 'blocked_review',
|
|
493
|
+
},
|
|
494
|
+
} };
|
|
495
|
+
}
|
|
496
|
+
if (validationSummary.status === 'skipped') {
|
|
497
|
+
return { kind: 'terminal', result: {
|
|
498
|
+
success: false,
|
|
499
|
+
code: 'validation_unavailable',
|
|
500
|
+
convergenceStatus: 'blocked_review',
|
|
501
|
+
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
502
|
+
branch,
|
|
503
|
+
into: baseBranch,
|
|
504
|
+
validationSummary,
|
|
505
|
+
refineStages,
|
|
506
|
+
finalBranchConvergenceState: {
|
|
507
|
+
branch,
|
|
508
|
+
baseBranch,
|
|
509
|
+
merged: false,
|
|
510
|
+
removed: false,
|
|
511
|
+
validation: 'unavailable',
|
|
512
|
+
status: 'blocked_review',
|
|
513
|
+
},
|
|
514
|
+
} };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return { kind: 'continue', ctx };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* patch_equivalence stage: preflight that the worktree branch's cumulative
|
|
522
|
+
* patch is equivalent to base+branch. On a "behind base" branch, auto-rebase
|
|
523
|
+
* once and re-check; on an empty merge-tree with real branch changes, treat as
|
|
524
|
+
* already-merged-via-another-path and short-circuit to cleanup. Mutates the
|
|
525
|
+
* context's branchHead (after rebase) and patchEquivalence (rebased gate).
|
|
526
|
+
*/
|
|
527
|
+
export async function refinePatchEquivalenceStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
528
|
+
const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, validationSummary, refineStages, execFileAsync } = ctx;
|
|
529
|
+
let branchHead = ctx.branchHead;
|
|
530
|
+
const patchEquivalenceStarted = Date.now();
|
|
531
|
+
let patchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
532
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence', patchEquivalence.status, patchEquivalenceStarted, {
|
|
533
|
+
equivalent: patchEquivalence.equivalent,
|
|
534
|
+
expectedPatchId: patchEquivalence.expectedPatchId,
|
|
535
|
+
actualPatchId: patchEquivalence.actualPatchId,
|
|
536
|
+
error: patchEquivalence.error,
|
|
537
|
+
actionableHint: patchEquivalence.actionableHint,
|
|
538
|
+
});
|
|
539
|
+
if (!patchEquivalence.equivalent) {
|
|
540
|
+
// Auto-rebase: if branch is simply behind base, attempt rebase automatically before failing.
|
|
541
|
+
let didAutoRebase = false;
|
|
542
|
+
let isBehindBase = false;
|
|
543
|
+
try {
|
|
544
|
+
execFileSync('git', ['merge-base', '--is-ancestor', branchHead, baseHead], {
|
|
545
|
+
cwd: node.workspace,
|
|
546
|
+
stdio: 'ignore',
|
|
547
|
+
});
|
|
548
|
+
isBehindBase = true;
|
|
549
|
+
} catch { /* non-zero exit means branchHead is not an ancestor of baseHead */ }
|
|
550
|
+
|
|
551
|
+
if (isBehindBase) {
|
|
552
|
+
const autoRebaseStarted = Date.now();
|
|
553
|
+
try {
|
|
554
|
+
execFileSync('git', ['rebase', baseHead], {
|
|
555
|
+
cwd: node.workspace,
|
|
556
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
557
|
+
});
|
|
558
|
+
const { stdout: rebasedHeadStdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: node.workspace, encoding: 'utf8' });
|
|
559
|
+
branchHead = rebasedHeadStdout.trim();
|
|
560
|
+
const rebasedPatchEquivalence = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
561
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', rebasedPatchEquivalence.status, autoRebaseStarted, {
|
|
562
|
+
equivalent: rebasedPatchEquivalence.equivalent,
|
|
563
|
+
expectedPatchId: rebasedPatchEquivalence.expectedPatchId,
|
|
564
|
+
actualPatchId: rebasedPatchEquivalence.actualPatchId,
|
|
565
|
+
error: rebasedPatchEquivalence.error,
|
|
566
|
+
rebasedBranchHead: branchHead,
|
|
567
|
+
});
|
|
568
|
+
if (rebasedPatchEquivalence.equivalent) {
|
|
569
|
+
patchEquivalence = rebasedPatchEquivalence;
|
|
570
|
+
didAutoRebase = true;
|
|
571
|
+
} else {
|
|
572
|
+
return { kind: 'terminal', result: {
|
|
573
|
+
success: false,
|
|
574
|
+
code: 'needs_rebase',
|
|
575
|
+
convergenceStatus: 'blocked_review',
|
|
576
|
+
error: 'Branch was rebased onto base but patch equivalence still failed; manual intervention required.',
|
|
577
|
+
branch,
|
|
578
|
+
into: baseBranch,
|
|
579
|
+
validationSummary,
|
|
580
|
+
patchEquivalence: rebasedPatchEquivalence,
|
|
581
|
+
refineStages,
|
|
582
|
+
finalBranchConvergenceState: {
|
|
583
|
+
branch,
|
|
584
|
+
baseBranch,
|
|
585
|
+
merged: false,
|
|
586
|
+
removed: false,
|
|
587
|
+
validation: 'passed',
|
|
588
|
+
patchEquivalence: 'failed',
|
|
589
|
+
status: 'blocked_review',
|
|
590
|
+
},
|
|
591
|
+
} };
|
|
592
|
+
}
|
|
593
|
+
} catch (rebaseErr: any) {
|
|
594
|
+
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
595
|
+
recordMeshRefineStage(refineStages, 'patch_equivalence_after_auto_rebase', 'failed', autoRebaseStarted, {
|
|
596
|
+
error: rebaseErr?.message || String(rebaseErr),
|
|
597
|
+
});
|
|
598
|
+
return { kind: 'terminal', result: {
|
|
599
|
+
success: false,
|
|
600
|
+
code: 'needs_rebase_with_conflicts',
|
|
601
|
+
convergenceStatus: 'blocked_review',
|
|
602
|
+
error: 'Branch is behind base and auto-rebase failed due to conflicts; resolve conflicts manually and retry.',
|
|
603
|
+
branch,
|
|
604
|
+
into: baseBranch,
|
|
605
|
+
validationSummary,
|
|
606
|
+
patchEquivalence,
|
|
607
|
+
refineStages,
|
|
608
|
+
finalBranchConvergenceState: {
|
|
609
|
+
branch,
|
|
610
|
+
baseBranch,
|
|
611
|
+
merged: false,
|
|
612
|
+
removed: false,
|
|
613
|
+
validation: 'passed',
|
|
614
|
+
patchEquivalence: 'failed',
|
|
615
|
+
status: 'blocked_review',
|
|
616
|
+
},
|
|
617
|
+
} };
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// If the actual patch-id is empty, the merge-tree produces no diff vs base —
|
|
622
|
+
// meaning the branch content is already present in base (landed via a different
|
|
623
|
+
// path, e.g. cherry-pick or direct commit). Treat this as "already merged":
|
|
624
|
+
// skip the merge step but still run cleanup so the worktree node is removed.
|
|
625
|
+
// "already merged via another path": the branch has real changes
|
|
626
|
+
// (expectedPatchId non-empty) but the merge-tree produces no diff
|
|
627
|
+
// against base (actualPatchId empty) — meaning every change in the
|
|
628
|
+
// branch is already present in base via a cherry-pick or direct commit.
|
|
629
|
+
// If both patch-ids are empty, the branch itself has no changes; that
|
|
630
|
+
// is a degenerate worktree case, not an "already merged" scenario.
|
|
631
|
+
const alreadyMergedViaOtherPath = !patchEquivalence.actualPatchId && !!patchEquivalence.expectedPatchId;
|
|
632
|
+
if (!didAutoRebase && !alreadyMergedViaOtherPath) {
|
|
633
|
+
return { kind: 'terminal', result: {
|
|
634
|
+
success: false,
|
|
635
|
+
code: 'patch_equivalence_failed',
|
|
636
|
+
convergenceStatus: 'blocked_review',
|
|
637
|
+
error: 'Refinery patch-equivalence preflight failed; merge/refine was not attempted.',
|
|
638
|
+
branch,
|
|
639
|
+
into: baseBranch,
|
|
640
|
+
validationSummary,
|
|
641
|
+
patchEquivalence,
|
|
642
|
+
refineStages,
|
|
643
|
+
finalBranchConvergenceState: {
|
|
644
|
+
branch,
|
|
645
|
+
baseBranch,
|
|
646
|
+
merged: false,
|
|
647
|
+
removed: false,
|
|
648
|
+
validation: 'passed',
|
|
649
|
+
patchEquivalence: 'failed',
|
|
650
|
+
status: 'blocked_review',
|
|
651
|
+
},
|
|
652
|
+
} };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (!didAutoRebase && alreadyMergedViaOtherPath) {
|
|
656
|
+
// Content already in base — skip merge, go straight to cleanup.
|
|
657
|
+
recordMeshRefineStage(refineStages, 'merge', 'skipped', Date.now(), {
|
|
658
|
+
reason: 'already_merged_via_other_path',
|
|
659
|
+
note: 'actualPatchId is empty; branch content is already present in base via a different commit path',
|
|
660
|
+
});
|
|
661
|
+
const cleanupStarted = Date.now();
|
|
662
|
+
const removeResult = await self.execute('remove_mesh_node', {
|
|
663
|
+
meshId,
|
|
664
|
+
nodeId,
|
|
665
|
+
sessionCleanupMode: 'preserve',
|
|
666
|
+
inlineMesh: args?.inlineMesh,
|
|
667
|
+
});
|
|
668
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
669
|
+
removed: removeResult?.removed,
|
|
670
|
+
code: removeResult?.code,
|
|
671
|
+
error: removeResult?.error,
|
|
672
|
+
});
|
|
673
|
+
try {
|
|
674
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
675
|
+
appendLedgerEntry(meshId, {
|
|
676
|
+
kind: 'node_removed',
|
|
677
|
+
nodeId,
|
|
678
|
+
payload: { alreadyMergedViaOtherPath: true, branch, into: baseBranch, validationSummary, patchEquivalence },
|
|
679
|
+
});
|
|
680
|
+
} catch { /* ledger append is best-effort */ }
|
|
681
|
+
return { kind: 'terminal', result: {
|
|
682
|
+
success: removeResult?.success !== false,
|
|
683
|
+
code: 'already_merged',
|
|
684
|
+
merged: false,
|
|
685
|
+
alreadyMergedViaOtherPath: true,
|
|
686
|
+
branch,
|
|
687
|
+
into: baseBranch,
|
|
688
|
+
removeResult,
|
|
689
|
+
validationSummary,
|
|
690
|
+
patchEquivalence,
|
|
691
|
+
refineStages,
|
|
692
|
+
finalBranchConvergenceState: {
|
|
693
|
+
branch: baseBranch,
|
|
694
|
+
mergedBranch: branch,
|
|
695
|
+
baseBranch,
|
|
696
|
+
merged: false,
|
|
697
|
+
alreadyMergedViaOtherPath: true,
|
|
698
|
+
removed: removeResult?.success !== false,
|
|
699
|
+
validation: 'passed',
|
|
700
|
+
patchEquivalence: 'already_merged',
|
|
701
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged_to_main',
|
|
702
|
+
},
|
|
703
|
+
} };
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
ctx.branchHead = branchHead;
|
|
708
|
+
ctx.patchEquivalence = patchEquivalence;
|
|
709
|
+
return { kind: 'continue', ctx };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* submodule_reachability stage: verify every submodule gitlink commit that
|
|
714
|
+
* would land via the merge is reachable from its configured remote main
|
|
715
|
+
* branch (optionally auto-publishing when policy allows). Blocks the merge
|
|
716
|
+
* when any commit is unreachable. Stores the result on the context.
|
|
717
|
+
*/
|
|
718
|
+
export async function refineSubmoduleReachabilityStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
719
|
+
const { mesh, node, repoRoot, branch, baseBranch, branchHead, validationSummary, patchEquivalence, refineStages } = ctx;
|
|
720
|
+
const submoduleReachabilityStarted = Date.now();
|
|
721
|
+
const autoPublishSubmoduleMainCommits = resolveRefineryAutoPublishSubmoduleMainCommits(mesh, node.workspace);
|
|
722
|
+
const submoduleReachability = await runMeshRefineSubmoduleReachabilityGate(repoRoot, patchEquivalence.mergedTree || branchHead, {
|
|
723
|
+
allowAutoPublishSubmoduleMainCommits: autoPublishSubmoduleMainCommits.enabled,
|
|
724
|
+
autoPublishPolicySource: autoPublishSubmoduleMainCommits.source,
|
|
725
|
+
worktreeRoot: node.workspace,
|
|
726
|
+
});
|
|
727
|
+
recordMeshRefineStage(refineStages, 'submodule_reachability', submoduleReachability.status, submoduleReachabilityStarted, {
|
|
728
|
+
checked: submoduleReachability.checked,
|
|
729
|
+
autoPublishAllowed: submoduleReachability.autoPublishAllowed,
|
|
730
|
+
autoPublishPolicySource: submoduleReachability.autoPublishPolicySource,
|
|
731
|
+
autoPublished: submoduleReachability.entries
|
|
732
|
+
.filter(entry => entry.autoPublishAttempted)
|
|
733
|
+
.map(entry => ({
|
|
734
|
+
path: entry.path,
|
|
735
|
+
commit: entry.commit,
|
|
736
|
+
remote: entry.remote,
|
|
737
|
+
remoteUrl: entry.remoteUrl,
|
|
738
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
739
|
+
refspec: entry.autoPublishRefspec,
|
|
740
|
+
succeeded: entry.autoPublishSucceeded,
|
|
741
|
+
verified: entry.autoPublishVerified,
|
|
742
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
743
|
+
error: entry.error,
|
|
744
|
+
})),
|
|
745
|
+
autoPublishSkipped: submoduleReachability.entries
|
|
746
|
+
.filter(entry => entry.autoPublishAllowed === true && entry.autoPublishAttempted !== true)
|
|
747
|
+
.map(entry => ({
|
|
748
|
+
path: entry.path,
|
|
749
|
+
commit: entry.commit,
|
|
750
|
+
remote: entry.remote,
|
|
751
|
+
remoteUrl: entry.remoteUrl,
|
|
752
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
753
|
+
reason: entry.autoPublishSkippedReason || entry.error || 'auto-publish was allowed but no publish attempt was possible',
|
|
754
|
+
})),
|
|
755
|
+
unreachable: submoduleReachability.unreachable.map(entry => ({
|
|
756
|
+
path: entry.path,
|
|
757
|
+
commit: entry.commit,
|
|
758
|
+
publishRequired: entry.publishRequired === true,
|
|
759
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
760
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
761
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
762
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
763
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
764
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
765
|
+
remote: entry.remote,
|
|
766
|
+
remoteUrl: entry.remoteUrl,
|
|
767
|
+
remoteReachable: entry.remoteReachable,
|
|
768
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
769
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
770
|
+
error: entry.error,
|
|
771
|
+
})),
|
|
772
|
+
error: submoduleReachability.error,
|
|
773
|
+
});
|
|
774
|
+
if (submoduleReachability.status === 'failed') {
|
|
775
|
+
const nextStep = buildSubmodulePublishRequiredNextStep(submoduleReachability.unreachable);
|
|
776
|
+
return { kind: 'terminal', result: {
|
|
777
|
+
success: false,
|
|
778
|
+
code: 'submodule_reachability_failed',
|
|
779
|
+
convergenceStatus: 'blocked_review',
|
|
780
|
+
publishRequired: true,
|
|
781
|
+
blockedReason: 'submodule_publish_required',
|
|
782
|
+
error: 'Refinery submodule reachability preflight failed because one or more submodule gitlink commits are not reachable from their configured remote main branch; merge/refine cleanup was not attempted.',
|
|
783
|
+
nextStep,
|
|
784
|
+
nextSteps: [
|
|
785
|
+
'Ask the user for explicit approval before pushing or publishing any submodule commit.',
|
|
786
|
+
'Push/publish each unreachable submodule commit to the configured submodule remote main branch shown in the evidence.',
|
|
787
|
+
'Rerun mesh_refine_node after remote reachability is confirmed.',
|
|
788
|
+
'Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.',
|
|
789
|
+
],
|
|
790
|
+
unreachableSubmoduleCommits: submoduleReachability.unreachable.map(entry => ({
|
|
791
|
+
path: entry.path,
|
|
792
|
+
commit: entry.commit,
|
|
793
|
+
remote: entry.remote,
|
|
794
|
+
remoteUrl: entry.remoteUrl,
|
|
795
|
+
remoteReachable: entry.remoteReachable,
|
|
796
|
+
remoteMainBranch: entry.remoteMainBranch,
|
|
797
|
+
remoteMainReachable: entry.remoteMainReachable,
|
|
798
|
+
autoPublishAllowed: entry.autoPublishAllowed,
|
|
799
|
+
autoPublishAttempted: entry.autoPublishAttempted,
|
|
800
|
+
autoPublishSucceeded: entry.autoPublishSucceeded,
|
|
801
|
+
autoPublishVerified: entry.autoPublishVerified,
|
|
802
|
+
autoPublishRefspec: entry.autoPublishRefspec,
|
|
803
|
+
autoPublishSkippedReason: entry.autoPublishSkippedReason,
|
|
804
|
+
error: entry.error,
|
|
805
|
+
})),
|
|
806
|
+
branch,
|
|
807
|
+
into: baseBranch,
|
|
808
|
+
validationSummary,
|
|
809
|
+
patchEquivalence,
|
|
810
|
+
submoduleReachability,
|
|
811
|
+
refineStages,
|
|
812
|
+
finalBranchConvergenceState: {
|
|
813
|
+
branch,
|
|
814
|
+
baseBranch,
|
|
815
|
+
merged: false,
|
|
816
|
+
removed: false,
|
|
817
|
+
validation: 'passed',
|
|
818
|
+
patchEquivalence: 'passed',
|
|
819
|
+
submoduleReachability: 'failed',
|
|
820
|
+
status: 'blocked_review',
|
|
821
|
+
reason: 'submodule_publish_required',
|
|
822
|
+
nextStep,
|
|
823
|
+
},
|
|
824
|
+
} };
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
ctx.submoduleReachability = submoduleReachability;
|
|
828
|
+
return { kind: 'continue', ctx };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* effective_diff stage (no-op guard): block a silent no-op merge where the
|
|
833
|
+
* branch produces no effective root-tree diff against base — typically a
|
|
834
|
+
* submodule that has commits but whose root-level gitlink (pointer) bump was
|
|
835
|
+
* never committed, so the merge would land nothing real on main.
|
|
836
|
+
*/
|
|
837
|
+
export async function refineEffectiveDiffStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
838
|
+
const { repoRoot, baseHead, branchHead, branch, baseBranch, validationSummary, patchEquivalence, refineStages } = ctx;
|
|
839
|
+
// No-op guard: block a silent no-op merge where the root tree is identical to base.
|
|
840
|
+
// This catches the trap where a submodule has commits but the root branch never
|
|
841
|
+
// committed the gitlink (oss-pointer) bump — merging would report success while the
|
|
842
|
+
// real change never lands on main. A committed gitlink bump shows up in the root
|
|
843
|
+
// diff, so legitimate oss-pointer refines pass through untouched.
|
|
844
|
+
const effectiveDiffStarted = Date.now();
|
|
845
|
+
const effectiveDiff = await runMeshRefineEffectiveDiffGate(repoRoot, baseHead, branchHead);
|
|
846
|
+
recordMeshRefineStage(refineStages, 'effective_diff', effectiveDiff.status, effectiveDiffStarted, {
|
|
847
|
+
hasEffectiveDiff: effectiveDiff.hasEffectiveDiff,
|
|
848
|
+
changedPaths: effectiveDiff.changedPaths,
|
|
849
|
+
submoduleHints: effectiveDiff.submoduleHints,
|
|
850
|
+
...(effectiveDiff.error ? { error: effectiveDiff.error } : {}),
|
|
851
|
+
});
|
|
852
|
+
if (effectiveDiff.status === 'failed' && !effectiveDiff.hasEffectiveDiff) {
|
|
853
|
+
const hintLines = (effectiveDiff.submoduleHints || []).map(h => ` - ${h.path}: ${h.reason}`);
|
|
854
|
+
const message = [
|
|
855
|
+
`Refinery no-op guard: branch '${branch}' has no effective root-tree diff against '${baseBranch}' (${baseHead.slice(0, 12)}); nothing would merge.`,
|
|
856
|
+
'This usually means a submodule (e.g. oss) has commits but the root branch never committed the gitlink (pointer) bump, so the merge would be a silent no-op while the real change never reaches main.',
|
|
857
|
+
hintLines.length ? `Submodules with uncommitted pointer bumps:\n${hintLines.join('\n')}` : '',
|
|
858
|
+
`Fix: commit the submodule pointer bump on '${branch}' (git add <submodule-path> && git commit), then re-run refine.`,
|
|
859
|
+
].filter(Boolean).join('\n');
|
|
860
|
+
return { kind: 'terminal', result: {
|
|
861
|
+
success: false,
|
|
862
|
+
code: 'no_effective_diff',
|
|
863
|
+
convergenceStatus: 'blocked_review',
|
|
864
|
+
error: message,
|
|
865
|
+
branch,
|
|
866
|
+
into: baseBranch,
|
|
867
|
+
validationSummary,
|
|
868
|
+
patchEquivalence,
|
|
869
|
+
effectiveDiff,
|
|
870
|
+
refineStages,
|
|
871
|
+
finalBranchConvergenceState: {
|
|
872
|
+
branch,
|
|
873
|
+
baseBranch,
|
|
874
|
+
merged: false,
|
|
875
|
+
removed: false,
|
|
876
|
+
validation: 'passed',
|
|
877
|
+
patchEquivalence: 'passed',
|
|
878
|
+
effectiveDiff: 'no_effective_diff',
|
|
879
|
+
status: 'blocked_review',
|
|
880
|
+
reason: 'no_effective_diff',
|
|
881
|
+
...(effectiveDiff.submoduleHints?.length ? { submoduleHints: effectiveDiff.submoduleHints } : {}),
|
|
882
|
+
},
|
|
883
|
+
} };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return { kind: 'continue', ctx };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* merge + finalize stage: perform the --no-ff merge, align submodule
|
|
891
|
+
* checkouts after merge, clean up (remove) the worktree node per policy,
|
|
892
|
+
* append the refinery ledger entry, and (unless approval is required) push the
|
|
893
|
+
* base branch. Always terminal — produces the final CommandRouterResult.
|
|
894
|
+
*/
|
|
895
|
+
export async function refineMergeAndFinalizeStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
896
|
+
const { meshId, nodeId, args, repoRoot, baseHead, node, branch, baseBranch, sourceNode, validationSummary, patchEquivalence, submoduleReachability, mesh, refineStages, execFileAsync } = ctx;
|
|
897
|
+
let mergeResult: Record<string, unknown> | undefined;
|
|
898
|
+
const mergeStarted = Date.now();
|
|
899
|
+
try {
|
|
900
|
+
const result = await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
901
|
+
mergeResult = {
|
|
902
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
903
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
904
|
+
durationMs: Date.now() - mergeStarted,
|
|
905
|
+
};
|
|
906
|
+
recordMeshRefineStage(refineStages, 'merge', 'passed', mergeStarted, mergeResult);
|
|
907
|
+
} catch (e: any) {
|
|
908
|
+
recordMeshRefineStage(refineStages, 'merge', 'failed', mergeStarted, {
|
|
909
|
+
error: e?.message || String(e),
|
|
910
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
911
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
912
|
+
});
|
|
913
|
+
return { kind: 'terminal', result: {
|
|
914
|
+
success: false,
|
|
915
|
+
error: `Merge failed (conflicts?): ${e.message}`,
|
|
916
|
+
validationSummary,
|
|
917
|
+
patchEquivalence,
|
|
918
|
+
refineStages,
|
|
919
|
+
finalBranchConvergenceState: {
|
|
920
|
+
branch,
|
|
921
|
+
baseBranch,
|
|
922
|
+
merged: false,
|
|
923
|
+
removed: false,
|
|
924
|
+
validation: 'passed',
|
|
925
|
+
patchEquivalence: 'passed',
|
|
926
|
+
status: 'not_mergeable',
|
|
927
|
+
},
|
|
928
|
+
} };
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const submoduleAlignmentStarted = Date.now();
|
|
932
|
+
const submoduleAlignment = await alignRefinerySubmodulesAfterMerge(repoRoot, baseHead, 'HEAD', {
|
|
933
|
+
submoduleIgnorePaths: Array.isArray(sourceNode?.policy?.submoduleIgnorePaths)
|
|
934
|
+
? sourceNode.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
935
|
+
: undefined,
|
|
936
|
+
});
|
|
937
|
+
if (submoduleAlignment.status !== 'skipped') {
|
|
938
|
+
recordMeshRefineStage(refineStages, 'submodule_alignment', submoduleAlignment.status, submoduleAlignmentStarted, {
|
|
939
|
+
changedGitlinkPaths: submoduleAlignment.changedGitlinkPaths,
|
|
940
|
+
outOfSyncPaths: submoduleAlignment.outOfSyncPaths,
|
|
941
|
+
updatedPaths: submoduleAlignment.updatedPaths,
|
|
942
|
+
verifiedPaths: submoduleAlignment.verifiedPaths,
|
|
943
|
+
command: submoduleAlignment.command,
|
|
944
|
+
error: submoduleAlignment.error,
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
if (submoduleAlignment.status === 'failed') {
|
|
948
|
+
return { kind: 'terminal', result: {
|
|
949
|
+
success: false,
|
|
950
|
+
code: 'post_merge_submodule_alignment_failed',
|
|
951
|
+
error: 'Refinery merge completed but post-merge submodule checkout alignment failed; run the reported git submodule update command and re-check base workspace status.',
|
|
952
|
+
merged: true,
|
|
953
|
+
branch,
|
|
954
|
+
into: baseBranch,
|
|
955
|
+
validationSummary,
|
|
956
|
+
patchEquivalence,
|
|
957
|
+
submoduleReachability,
|
|
958
|
+
submoduleAlignment,
|
|
959
|
+
mergeResult,
|
|
960
|
+
refineStages,
|
|
961
|
+
finalBranchConvergenceState: {
|
|
962
|
+
branch: baseBranch,
|
|
963
|
+
mergedBranch: branch,
|
|
964
|
+
baseBranch,
|
|
965
|
+
merged: true,
|
|
966
|
+
removed: false,
|
|
967
|
+
validation: 'passed',
|
|
968
|
+
patchEquivalence: 'passed',
|
|
969
|
+
submoduleReachability: 'passed',
|
|
970
|
+
submoduleAlignment: 'failed',
|
|
971
|
+
status: 'post_merge_alignment_failed',
|
|
972
|
+
nextStep: submoduleAlignment.command || 'Run git submodule update --init --recursive for the reported path(s), then re-check base workspace status.',
|
|
973
|
+
},
|
|
974
|
+
} };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const cleanupStarted = Date.now();
|
|
978
|
+
// Honor the mesh policy for delegated-session cleanup on the auto-removed
|
|
979
|
+
// worktree node (previously hardcoded to 'preserve', which orphaned the
|
|
980
|
+
// delegate session as an idle record on the coordinator daemon). Fall back
|
|
981
|
+
// to 'preserve' when no policy is set.
|
|
982
|
+
const refineSessionCleanupMode = self.normalizeMeshSessionCleanupMode(
|
|
983
|
+
mesh?.policy?.sessionCleanupOnNodeRemove,
|
|
984
|
+
);
|
|
985
|
+
// The delegate session launched for a clone worktree is frequently matched
|
|
986
|
+
// by workspace ONLY (no meta.meshNodeId binding), which remove_mesh_node's
|
|
987
|
+
// shared-daemon guard skips. Since refine knows exactly which workspace it
|
|
988
|
+
// just merged, collect that workspace's live session ids explicitly and pass
|
|
989
|
+
// them through — explicit sessionIds bypass the workspace-only-match guard so
|
|
990
|
+
// the policy-driven stop/delete actually runs.
|
|
991
|
+
let refineSessionIds: string[] | undefined;
|
|
992
|
+
if (refineSessionCleanupMode !== 'preserve' && self.deps.sessionHostControl) {
|
|
993
|
+
try {
|
|
994
|
+
const liveSessions = await self.deps.sessionHostControl.listSessions();
|
|
995
|
+
const workspace = typeof node.workspace === 'string' ? node.workspace : '';
|
|
996
|
+
refineSessionIds = liveSessions
|
|
997
|
+
.filter((record: any) => {
|
|
998
|
+
const sid = typeof record?.sessionId === 'string' ? record.sessionId : '';
|
|
999
|
+
if (!sid) return false;
|
|
1000
|
+
// Never sweep the coordinator's own session for this mesh.
|
|
1001
|
+
if (readStringValue(record?.meta?.meshCoordinatorFor) === meshId) return false;
|
|
1002
|
+
const boundToNode = readStringValue(record?.meta?.meshNodeId) === nodeId;
|
|
1003
|
+
const matchedByWorkspace = !!workspace && record?.workspace === workspace;
|
|
1004
|
+
return boundToNode || matchedByWorkspace;
|
|
1005
|
+
})
|
|
1006
|
+
.map((record: any) => String(record.sessionId));
|
|
1007
|
+
} catch {
|
|
1008
|
+
// listSessions failure is non-fatal — fall back to the policy-mode
|
|
1009
|
+
// cleanup without explicit ids (still better than hardcoded preserve).
|
|
1010
|
+
refineSessionIds = undefined;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
const removeResult = await self.execute('remove_mesh_node', {
|
|
1014
|
+
meshId,
|
|
1015
|
+
nodeId,
|
|
1016
|
+
sessionCleanupMode: refineSessionCleanupMode,
|
|
1017
|
+
...(refineSessionIds && refineSessionIds.length > 0 ? { sessionIds: refineSessionIds } : {}),
|
|
1018
|
+
inlineMesh: args?.inlineMesh,
|
|
1019
|
+
// REFINE-CLEANUP: refine reaches cleanup only AFTER a verified merge
|
|
1020
|
+
// convergence, so any residual worktree dirtiness here is incidental
|
|
1021
|
+
// (e.g. a bootstrap lockfile rewrite) — never unmerged work. `force`
|
|
1022
|
+
// sets requireClean=false so a plain-dirty worktree no longer aborts
|
|
1023
|
+
// removal with merged_cleanup_failed. Branch-ref deletion still keys off
|
|
1024
|
+
// mergeConvergence (NOT the force flag), so no merged work can be lost.
|
|
1025
|
+
force: true,
|
|
1026
|
+
});
|
|
1027
|
+
recordMeshRefineStage(refineStages, 'cleanup', removeResult?.success === false ? 'failed' : 'passed', cleanupStarted, {
|
|
1028
|
+
removed: removeResult?.removed,
|
|
1029
|
+
code: removeResult?.code,
|
|
1030
|
+
error: removeResult?.error,
|
|
1031
|
+
});
|
|
1032
|
+
|
|
1033
|
+
let ledgerError: string | undefined;
|
|
1034
|
+
const ledgerStarted = Date.now();
|
|
1035
|
+
try {
|
|
1036
|
+
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
1037
|
+
appendLedgerEntry(meshId, {
|
|
1038
|
+
kind: 'node_removed',
|
|
1039
|
+
nodeId,
|
|
1040
|
+
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary, patchEquivalence, submoduleReachability, submoduleAlignment },
|
|
1041
|
+
});
|
|
1042
|
+
recordMeshRefineStage(refineStages, 'ledger', 'passed', ledgerStarted);
|
|
1043
|
+
} catch (e: any) {
|
|
1044
|
+
ledgerError = e?.message || String(e);
|
|
1045
|
+
recordMeshRefineStage(refineStages, 'ledger', 'failed', ledgerStarted, { error: ledgerError });
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const finalBranchConvergenceState = {
|
|
1049
|
+
branch: baseBranch,
|
|
1050
|
+
mergedBranch: branch,
|
|
1051
|
+
baseBranch,
|
|
1052
|
+
merged: true,
|
|
1053
|
+
removed: removeResult?.success !== false,
|
|
1054
|
+
validation: 'passed',
|
|
1055
|
+
patchEquivalence: 'passed',
|
|
1056
|
+
submoduleAlignment: submoduleAlignment.status,
|
|
1057
|
+
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
if (removeResult?.success === false) {
|
|
1061
|
+
return { kind: 'terminal', result: {
|
|
1062
|
+
success: false,
|
|
1063
|
+
code: 'cleanup_failed',
|
|
1064
|
+
error: 'Refinery merge completed but worktree cleanup failed; manual cleanup/retry is required.',
|
|
1065
|
+
merged: true,
|
|
1066
|
+
branch,
|
|
1067
|
+
into: baseBranch,
|
|
1068
|
+
removeResult,
|
|
1069
|
+
validationSummary,
|
|
1070
|
+
patchEquivalence,
|
|
1071
|
+
submoduleReachability,
|
|
1072
|
+
submoduleAlignment,
|
|
1073
|
+
mergeResult,
|
|
1074
|
+
refineStages,
|
|
1075
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
1076
|
+
finalBranchConvergenceState,
|
|
1077
|
+
} };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Push logic: after a successful merge, either auto-push or surface push info
|
|
1081
|
+
// so coordinators don't need manual discovery after each refine.
|
|
1082
|
+
const requireApprovalForPush: boolean = (mesh as any)?.policy?.requireApprovalForPush ?? DEFAULT_MESH_POLICY.requireApprovalForPush;
|
|
1083
|
+
let pushResult: Record<string, unknown> | undefined;
|
|
1084
|
+
if (!requireApprovalForPush) {
|
|
1085
|
+
const pushStarted = Date.now();
|
|
1086
|
+
try {
|
|
1087
|
+
await execFileAsync('git', ['push', 'origin', baseBranch], { cwd: repoRoot, encoding: 'utf8' });
|
|
1088
|
+
pushResult = { pushed: true, remote: 'origin', branch: baseBranch, durationMs: Date.now() - pushStarted };
|
|
1089
|
+
recordMeshRefineStage(refineStages, 'push', 'passed', pushStarted, pushResult);
|
|
1090
|
+
finalBranchConvergenceState.status = 'merged_pushed';
|
|
1091
|
+
} catch (e: any) {
|
|
1092
|
+
pushResult = {
|
|
1093
|
+
pushed: false,
|
|
1094
|
+
remote: 'origin',
|
|
1095
|
+
branch: baseBranch,
|
|
1096
|
+
error: e?.message || String(e),
|
|
1097
|
+
stderr: e?.stderr,
|
|
1098
|
+
durationMs: Date.now() - pushStarted,
|
|
1099
|
+
};
|
|
1100
|
+
recordMeshRefineStage(refineStages, 'push', 'failed', pushStarted, pushResult);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return { kind: 'terminal', result: {
|
|
1105
|
+
success: true,
|
|
1106
|
+
merged: true,
|
|
1107
|
+
branch,
|
|
1108
|
+
into: baseBranch,
|
|
1109
|
+
removeResult,
|
|
1110
|
+
validationSummary,
|
|
1111
|
+
patchEquivalence,
|
|
1112
|
+
submoduleReachability,
|
|
1113
|
+
submoduleAlignment,
|
|
1114
|
+
mergeResult,
|
|
1115
|
+
refineStages,
|
|
1116
|
+
...(ledgerError ? { ledgerError } : {}),
|
|
1117
|
+
finalBranchConvergenceState,
|
|
1118
|
+
// Push outcome or readiness info for coordinator.
|
|
1119
|
+
...(pushResult
|
|
1120
|
+
? { pushResult }
|
|
1121
|
+
: {
|
|
1122
|
+
pushReady: true,
|
|
1123
|
+
pushCommand: `git push origin ${baseBranch}`,
|
|
1124
|
+
pushNote: 'requireApprovalForPush is enabled — run the push command or obtain user approval before pushing.',
|
|
1125
|
+
}),
|
|
1126
|
+
} };
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Batch refinery: converge multiple sibling worktree nodes onto the base branch
|
|
1131
|
+
* in one sequential pipeline, absorbing the rebase + patch-equivalence churn that
|
|
1132
|
+
* arises when several siblings touch the same submodule.
|
|
1133
|
+
*
|
|
1134
|
+
* Reuses executeMeshRefineNodeSynchronously per node — every node goes through the
|
|
1135
|
+
* exact same validation / patch-equivalence / submodule-reachability / merge / cleanup
|
|
1136
|
+
* gates, including its built-in auto-rebase onto fresh origin/<base>. Because each
|
|
1137
|
+
* node fetches origin/<base> at the start of its own refine, a node merged earlier in
|
|
1138
|
+
* the batch advances the base, and the next node's refine auto-rebases onto it before
|
|
1139
|
+
* re-running patch-equivalence. No force-push, no reset — conflicting nodes are
|
|
1140
|
+
* isolated as blocked_review while the rest of the batch proceeds.
|
|
1141
|
+
*/
|
|
1142
|
+
export async function batchRefineMeshNodes(self: DaemonCommandRouter, meshId: string, requestedNodeIds: string[] | undefined, args: any): Promise<CommandRouterResult> {
|
|
1143
|
+
// preferInline: same membership authority as refine_mesh_node — inline-cache-only
|
|
1144
|
+
// clone nodes (created in this MCP session) must resolve.
|
|
1145
|
+
const meshRecord = await self.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
1146
|
+
const mesh = meshRecord?.mesh;
|
|
1147
|
+
if (!mesh) return { success: false, error: `Mesh '${meshId}' not found` };
|
|
1148
|
+
|
|
1149
|
+
const allNodes: any[] = Array.isArray(mesh.nodes) ? mesh.nodes : [];
|
|
1150
|
+
const isConvergeable = (n: any) => n?.isLocalWorktree && typeof n.workspace === 'string' && n.workspace;
|
|
1151
|
+
|
|
1152
|
+
let targetNodes: any[];
|
|
1153
|
+
if (Array.isArray(requestedNodeIds) && requestedNodeIds.length > 0) {
|
|
1154
|
+
targetNodes = [];
|
|
1155
|
+
const missing: string[] = [];
|
|
1156
|
+
const nonWorktree: string[] = [];
|
|
1157
|
+
for (const nodeId of requestedNodeIds) {
|
|
1158
|
+
const node = allNodes.find(n => meshNodeIdMatches(n, nodeId));
|
|
1159
|
+
if (!node) { missing.push(nodeId); continue; }
|
|
1160
|
+
if (!isConvergeable(node)) { nonWorktree.push(nodeId); continue; }
|
|
1161
|
+
targetNodes.push(node);
|
|
1162
|
+
}
|
|
1163
|
+
if (missing.length || nonWorktree.length) {
|
|
1164
|
+
return {
|
|
1165
|
+
success: false,
|
|
1166
|
+
error: 'One or more requested nodes are not convergeable local worktree nodes.',
|
|
1167
|
+
...(missing.length ? { missingNodeIds: missing } : {}),
|
|
1168
|
+
...(nonWorktree.length ? { nonWorktreeNodeIds: nonWorktree } : {}),
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
} else {
|
|
1172
|
+
// Auto-collect: every local worktree node is a convergence candidate.
|
|
1173
|
+
targetNodes = allNodes.filter(isConvergeable);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (targetNodes.length === 0) {
|
|
1177
|
+
return { success: true, batch: true, dryRun: args?.dryRun !== false, nodeCount: 0, order: [], results: [], note: 'No convergeable local worktree nodes found.' };
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const { execFile } = await import('node:child_process');
|
|
1181
|
+
const { promisify } = await import('node:util');
|
|
1182
|
+
const execFileAsync = promisify(execFile);
|
|
1183
|
+
|
|
1184
|
+
// Resolve the base repo root and a base ref to analyze change areas against.
|
|
1185
|
+
const resolveRepoRootFor = (node: any): string | undefined => {
|
|
1186
|
+
const sourceNode = node.clonedFromNodeId
|
|
1187
|
+
? allNodes.find(n => meshNodeIdMatches(n, node.clonedFromNodeId))
|
|
1188
|
+
: allNodes.find(n => !n.isLocalWorktree);
|
|
1189
|
+
return sourceNode?.repoRoot || sourceNode?.workspace;
|
|
1190
|
+
};
|
|
1191
|
+
|
|
1192
|
+
// Analyze change areas for ordering. The repoRoot is shared across siblings of
|
|
1193
|
+
// the same source; resolve a base ref (origin/<base> preferred) once per repoRoot.
|
|
1194
|
+
const repoRootBaseRef = new Map<string, string>();
|
|
1195
|
+
const submodulePathsByRepoRoot = new Map<string, Set<string>>();
|
|
1196
|
+
const resolveBaseRef = async (repoRoot: string): Promise<string> => {
|
|
1197
|
+
const cached = repoRootBaseRef.get(repoRoot);
|
|
1198
|
+
if (cached) return cached;
|
|
1199
|
+
let baseBranch = 'main';
|
|
1200
|
+
try {
|
|
1201
|
+
const { stdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
1202
|
+
if (stdout.trim()) baseBranch = stdout.trim();
|
|
1203
|
+
} catch { /* fall back to main */ }
|
|
1204
|
+
let baseRef = 'HEAD';
|
|
1205
|
+
try {
|
|
1206
|
+
await execFileAsync('git', ['fetch', 'origin', baseBranch], { cwd: repoRoot, encoding: 'utf8' });
|
|
1207
|
+
} catch { /* offline / no remote — fall through to local refs */ }
|
|
1208
|
+
try {
|
|
1209
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', `origin/${baseBranch}`], { cwd: repoRoot, encoding: 'utf8' });
|
|
1210
|
+
baseRef = stdout.trim();
|
|
1211
|
+
} catch {
|
|
1212
|
+
try {
|
|
1213
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repoRoot, encoding: 'utf8' });
|
|
1214
|
+
baseRef = stdout.trim();
|
|
1215
|
+
} catch { /* leave HEAD */ }
|
|
1216
|
+
}
|
|
1217
|
+
repoRootBaseRef.set(repoRoot, baseRef);
|
|
1218
|
+
return baseRef;
|
|
1219
|
+
};
|
|
1220
|
+
|
|
1221
|
+
const changeAreas: Array<Awaited<ReturnType<typeof analyzeMeshRefineNodeChangeArea>>> = [];
|
|
1222
|
+
for (const node of targetNodes) {
|
|
1223
|
+
const repoRoot = resolveRepoRootFor(node);
|
|
1224
|
+
let branch = typeof node.worktreeBranch === 'string' ? node.worktreeBranch : '';
|
|
1225
|
+
try {
|
|
1226
|
+
const { stdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
1227
|
+
if (stdout.trim()) branch = stdout.trim();
|
|
1228
|
+
} catch { /* use stored worktreeBranch */ }
|
|
1229
|
+
|
|
1230
|
+
if (!repoRoot || !branch) {
|
|
1231
|
+
changeAreas.push({
|
|
1232
|
+
nodeId: node.id, workspace: node.workspace, branch: branch || '(unknown)',
|
|
1233
|
+
changedTopLevelPaths: [], changedFiles: [], touchedSubmodulePaths: [],
|
|
1234
|
+
touchesSubmodule: false, aheadCount: 0,
|
|
1235
|
+
error: !repoRoot ? 'source repoRoot not found' : 'branch not resolved',
|
|
1236
|
+
});
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
if (!submodulePathsByRepoRoot.has(repoRoot)) {
|
|
1240
|
+
// Resolve declared submodule paths once per repo root.
|
|
1241
|
+
let subPaths = new Set<string>();
|
|
1242
|
+
try {
|
|
1243
|
+
const { stdout } = await execFileAsync('git', ['config', '--file', '.gitmodules', '--get-regexp', 'path'], { cwd: repoRoot, encoding: 'utf8' });
|
|
1244
|
+
for (const line of stdout.split('\n')) {
|
|
1245
|
+
const trimmed = line.trim();
|
|
1246
|
+
const spaceIdx = trimmed.indexOf(' ');
|
|
1247
|
+
if (spaceIdx === -1) continue;
|
|
1248
|
+
const value = trimmed.slice(spaceIdx + 1).trim();
|
|
1249
|
+
if (value) subPaths.add(value);
|
|
1250
|
+
}
|
|
1251
|
+
} catch { subPaths = new Set(); }
|
|
1252
|
+
submodulePathsByRepoRoot.set(repoRoot, subPaths);
|
|
1253
|
+
}
|
|
1254
|
+
const baseRef = await resolveBaseRef(repoRoot);
|
|
1255
|
+
let branchRef = branch;
|
|
1256
|
+
try {
|
|
1257
|
+
const { stdout } = await execFileAsync('git', ['rev-parse', branch], { cwd: node.workspace, encoding: 'utf8' });
|
|
1258
|
+
branchRef = stdout.trim() || branch;
|
|
1259
|
+
} catch { /* use branch name */ }
|
|
1260
|
+
changeAreas.push(await analyzeMeshRefineNodeChangeArea({
|
|
1261
|
+
nodeId: node.id,
|
|
1262
|
+
workspace: node.workspace,
|
|
1263
|
+
branch,
|
|
1264
|
+
baseRef,
|
|
1265
|
+
branchRef,
|
|
1266
|
+
diffCwd: node.workspace,
|
|
1267
|
+
submodulePaths: submodulePathsByRepoRoot.get(repoRoot)!,
|
|
1268
|
+
}));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
const ordering = orderMeshRefineBatchNodes(changeAreas);
|
|
1272
|
+
const orderedNodes = ordering.order
|
|
1273
|
+
.map(nodeId => targetNodes.find(n => meshNodeIdMatches(n, nodeId)))
|
|
1274
|
+
.filter((n): n is any => !!n);
|
|
1275
|
+
|
|
1276
|
+
const dryRun = args?.dryRun !== false && args?.execute !== true;
|
|
1277
|
+
if (dryRun) {
|
|
1278
|
+
return {
|
|
1279
|
+
success: true,
|
|
1280
|
+
batch: true,
|
|
1281
|
+
dryRun: true,
|
|
1282
|
+
nodeCount: orderedNodes.length,
|
|
1283
|
+
order: ordering.order,
|
|
1284
|
+
orderingRationale: ordering.rationale,
|
|
1285
|
+
changeAreas: ordering.changeAreas,
|
|
1286
|
+
plan: orderedNodes.map(node => ({
|
|
1287
|
+
nodeId: node.id,
|
|
1288
|
+
workspace: node.workspace,
|
|
1289
|
+
validationPlan: buildMeshRefineValidationPlan(mesh, node.workspace),
|
|
1290
|
+
mergeWillRun: false,
|
|
1291
|
+
})),
|
|
1292
|
+
note: 'Dry-run: no validation, rebase, or merge was executed. Re-run with execute=true to converge nodes in this order.',
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
// Execute: refine each node in order via the shared convergence core.
|
|
1297
|
+
return runMeshRefineBatchConvergence(self, meshId, orderedNodes, ordering, args);
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
/**
|
|
1301
|
+
* Convergence core shared by the synchronous batch entry and the async batch job.
|
|
1302
|
+
* Refines each node in order: the per-node refine pipeline fetches origin/<base>
|
|
1303
|
+
* fresh, so each merged sibling advances the base before the next node's auto-rebase
|
|
1304
|
+
* + patch-equivalence re-check. A blocked/failed node is isolated; the batch
|
|
1305
|
+
* continues with the remaining nodes. Does NOT touch the per-node merge logic — it
|
|
1306
|
+
* only sequences calls to executeMeshRefineNodeSynchronously and aggregates outcomes.
|
|
1307
|
+
*/
|
|
1308
|
+
export async function runMeshRefineBatchConvergence(self: DaemonCommandRouter,
|
|
1309
|
+
meshId: string,
|
|
1310
|
+
orderedNodes: any[],
|
|
1311
|
+
ordering: { order: string[]; rationale?: unknown },
|
|
1312
|
+
args: any,
|
|
1313
|
+
): Promise<CommandRouterResult> {
|
|
1314
|
+
type BatchNodeOutcome = {
|
|
1315
|
+
nodeId: string;
|
|
1316
|
+
workspace: string;
|
|
1317
|
+
convergence: 'merged_to_main' | 'blocked_review' | 'skipped_patch_equivalent' | 'not_mergeable';
|
|
1318
|
+
code?: string;
|
|
1319
|
+
reason?: string;
|
|
1320
|
+
stage?: string;
|
|
1321
|
+
error?: string;
|
|
1322
|
+
finalBranchConvergenceState?: Record<string, unknown>;
|
|
1323
|
+
};
|
|
1324
|
+
const results: BatchNodeOutcome[] = [];
|
|
1325
|
+
for (const node of orderedNodes) {
|
|
1326
|
+
let result: Record<string, unknown>;
|
|
1327
|
+
try {
|
|
1328
|
+
result = await executeMeshRefineNodeSynchronously(self, meshId, node.id, args) as Record<string, unknown>;
|
|
1329
|
+
} catch (e: any) {
|
|
1330
|
+
result = { success: false, error: e?.message || String(e) };
|
|
1331
|
+
}
|
|
1332
|
+
const code = typeof result.code === 'string' ? result.code : '';
|
|
1333
|
+
// already_merged (branch content already on base via another path) is a
|
|
1334
|
+
// non-error skip regardless of success flag — the worktree converges with
|
|
1335
|
+
// no new merge. A real `git merge` conflict surfaces as merge_failed →
|
|
1336
|
+
// not_mergeable. Everything else that failed is isolated as blocked_review.
|
|
1337
|
+
let convergence: BatchNodeOutcome['convergence'];
|
|
1338
|
+
if (code === 'already_merged' && result.alreadyMergedViaOtherPath) {
|
|
1339
|
+
convergence = 'skipped_patch_equivalent';
|
|
1340
|
+
} else if (result.success === true) {
|
|
1341
|
+
convergence = 'merged_to_main';
|
|
1342
|
+
} else if (code === 'merge_failed') {
|
|
1343
|
+
convergence = 'not_mergeable';
|
|
1344
|
+
} else {
|
|
1345
|
+
convergence = 'blocked_review';
|
|
1346
|
+
}
|
|
1347
|
+
const fbcs = (result.finalBranchConvergenceState && typeof result.finalBranchConvergenceState === 'object')
|
|
1348
|
+
? result.finalBranchConvergenceState as Record<string, unknown>
|
|
1349
|
+
: undefined;
|
|
1350
|
+
const stage = Array.isArray(result.refineStages)
|
|
1351
|
+
? (result.refineStages as Array<Record<string, unknown>>).filter(s => s.status === 'failed').map(s => s.stage).filter(Boolean).pop() as string | undefined
|
|
1352
|
+
: undefined;
|
|
1353
|
+
results.push({
|
|
1354
|
+
nodeId: node.id,
|
|
1355
|
+
workspace: node.workspace,
|
|
1356
|
+
convergence,
|
|
1357
|
+
...(code ? { code } : {}),
|
|
1358
|
+
...(typeof result.blockedReason === 'string' ? { reason: result.blockedReason } : {}),
|
|
1359
|
+
...(stage ? { stage } : {}),
|
|
1360
|
+
...(typeof result.error === 'string' ? { error: result.error } : {}),
|
|
1361
|
+
...(fbcs ? { finalBranchConvergenceState: fbcs } : {}),
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const summary = {
|
|
1366
|
+
merged: results.filter(r => r.convergence === 'merged_to_main').length,
|
|
1367
|
+
skipped: results.filter(r => r.convergence === 'skipped_patch_equivalent').length,
|
|
1368
|
+
blocked: results.filter(r => r.convergence === 'blocked_review').length,
|
|
1369
|
+
notMergeable: results.filter(r => r.convergence === 'not_mergeable').length,
|
|
1370
|
+
};
|
|
1371
|
+
const allConverged = summary.blocked === 0 && summary.notMergeable === 0;
|
|
1372
|
+
return {
|
|
1373
|
+
success: true,
|
|
1374
|
+
batch: true,
|
|
1375
|
+
dryRun: false,
|
|
1376
|
+
nodeCount: orderedNodes.length,
|
|
1377
|
+
order: ordering.order,
|
|
1378
|
+
orderingRationale: ordering.rationale,
|
|
1379
|
+
summary,
|
|
1380
|
+
allConverged,
|
|
1381
|
+
results,
|
|
1382
|
+
...(allConverged ? {} : {
|
|
1383
|
+
nextStep: 'Resolve blocked_review / not_mergeable nodes manually (see per-node code/stage/error), then re-run mesh_refine_batch for the remaining nodes.',
|
|
1384
|
+
}),
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
export function buildRefineBatchJobKey(self: DaemonCommandRouter, meshId: string): string {
|
|
1389
|
+
return `${meshId}::batch`;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
export function buildRefineBatchJobHandle(self: DaemonCommandRouter, args: {
|
|
1393
|
+
meshId: string;
|
|
1394
|
+
nodeIds: string[];
|
|
1395
|
+
order: string[];
|
|
1396
|
+
status?: MeshRefineBatchJobStatus;
|
|
1397
|
+
startedAt?: string;
|
|
1398
|
+
completedAt?: string;
|
|
1399
|
+
jobId?: string;
|
|
1400
|
+
interactionId?: string;
|
|
1401
|
+
coordinatorDaemonId?: string;
|
|
1402
|
+
}): MeshRefineBatchJobHandle {
|
|
1403
|
+
return {
|
|
1404
|
+
success: true,
|
|
1405
|
+
async: true,
|
|
1406
|
+
batch: true,
|
|
1407
|
+
status: args.status || 'accepted',
|
|
1408
|
+
jobId: args.jobId || `refine_batch_${createInteractionId()}`,
|
|
1409
|
+
interactionId: args.interactionId || createInteractionId(),
|
|
1410
|
+
meshId: args.meshId,
|
|
1411
|
+
batchLabel: `batch:${args.nodeIds.length} node${args.nodeIds.length === 1 ? '' : 's'}`,
|
|
1412
|
+
nodeIds: args.nodeIds,
|
|
1413
|
+
nodeCount: args.nodeIds.length,
|
|
1414
|
+
order: args.order,
|
|
1415
|
+
startedAt: args.startedAt || new Date().toISOString(),
|
|
1416
|
+
...(args.completedAt ? { completedAt: args.completedAt } : {}),
|
|
1417
|
+
...(args.coordinatorDaemonId ? { targetCoordinatorDaemonId: args.coordinatorDaemonId } : {}),
|
|
1418
|
+
eventDelivery: { pendingEvents: true, ledger: true },
|
|
1419
|
+
evidence: {
|
|
1420
|
+
pendingEventsCommand: 'get_pending_mesh_events',
|
|
1421
|
+
ledgerCommand: 'get_mesh_ledger_slice',
|
|
1422
|
+
taskHistoryKind: args.status === 'completed' ? 'task_completed' : args.status === 'failed' ? 'task_failed' : 'task_dispatched',
|
|
1423
|
+
},
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/**
|
|
1428
|
+
* Emit a batch Refinery terminal/accepted event through the SAME pending-event +
|
|
1429
|
+
* forward mechanism single-node refine uses (queueRefineJobEvent), so the
|
|
1430
|
+
* coordinator's existing refine:accepted/completed/failed handling and message
|
|
1431
|
+
* renderer apply unchanged. The aggregate per-node results ride along in `result`.
|
|
1432
|
+
*/
|
|
1433
|
+
export function queueRefineBatchJobEvent(self: DaemonCommandRouter,
|
|
1434
|
+
event: 'refine:accepted' | 'refine:completed' | 'refine:failed',
|
|
1435
|
+
handle: MeshRefineBatchJobHandle,
|
|
1436
|
+
result?: Record<string, unknown>,
|
|
1437
|
+
): void {
|
|
1438
|
+
const metadataEvent = {
|
|
1439
|
+
source: 'refine_mesh_node_async_job',
|
|
1440
|
+
batch: true,
|
|
1441
|
+
jobId: handle.jobId,
|
|
1442
|
+
interactionId: handle.interactionId,
|
|
1443
|
+
meshId: handle.meshId,
|
|
1444
|
+
nodeId: handle.batchLabel,
|
|
1445
|
+
nodeIds: handle.nodeIds,
|
|
1446
|
+
workspace: undefined,
|
|
1447
|
+
status: handle.status,
|
|
1448
|
+
startedAt: handle.startedAt,
|
|
1449
|
+
completedAt: handle.completedAt,
|
|
1450
|
+
order: handle.order,
|
|
1451
|
+
...(result ? { result } : {}),
|
|
1452
|
+
};
|
|
1453
|
+
const eventPayload = {
|
|
1454
|
+
event,
|
|
1455
|
+
meshId: handle.meshId,
|
|
1456
|
+
nodeLabel: handle.batchLabel,
|
|
1457
|
+
nodeId: handle.batchLabel,
|
|
1458
|
+
metadataEvent,
|
|
1459
|
+
queuedAt: Date.now(),
|
|
1460
|
+
...(handle.targetCoordinatorDaemonId ? { targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId } : {}),
|
|
1461
|
+
};
|
|
1462
|
+
if (typeof self.deps.instanceManager?.getByCategory === 'function') {
|
|
1463
|
+
const forwarded = handleMeshForwardEvent(
|
|
1464
|
+
{ instanceManager: self.deps.instanceManager } as any,
|
|
1465
|
+
{
|
|
1466
|
+
event,
|
|
1467
|
+
meshId: handle.meshId,
|
|
1468
|
+
nodeId: handle.batchLabel,
|
|
1469
|
+
jobId: handle.jobId,
|
|
1470
|
+
interactionId: handle.interactionId,
|
|
1471
|
+
status: handle.status,
|
|
1472
|
+
startedAt: handle.startedAt,
|
|
1473
|
+
completedAt: handle.completedAt,
|
|
1474
|
+
...(result ? { result } : {}),
|
|
1475
|
+
},
|
|
1476
|
+
);
|
|
1477
|
+
if (forwarded?.success === true) return;
|
|
1478
|
+
LOG.warn('Mesh', `[Refinery] Failed to forward async refine batch event ${event}: ${forwarded?.error || 'unknown error'}`);
|
|
1479
|
+
}
|
|
1480
|
+
queuePendingMeshCoordinatorEvent(eventPayload);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
export async function appendRefineBatchJobLedger(self: DaemonCommandRouter,
|
|
1484
|
+
kind: 'task_dispatched' | 'task_completed' | 'task_failed',
|
|
1485
|
+
handle: MeshRefineBatchJobHandle,
|
|
1486
|
+
result?: Record<string, unknown>,
|
|
1487
|
+
): Promise<void> {
|
|
1488
|
+
try {
|
|
1489
|
+
const { appendLedgerEntry, buildLedgerOriginatingCoordinatorStamp } = await import('../mesh/mesh-ledger.js');
|
|
1490
|
+
// B2a: stamp the originating coordinator on dispatch (see appendRefineJobLedger).
|
|
1491
|
+
const originatingStamp = kind === 'task_dispatched'
|
|
1492
|
+
? buildLedgerOriginatingCoordinatorStamp({ coordinatorDaemonId: handle.targetCoordinatorDaemonId })
|
|
1493
|
+
: undefined;
|
|
1494
|
+
appendLedgerEntry(handle.meshId, {
|
|
1495
|
+
kind,
|
|
1496
|
+
nodeId: handle.batchLabel,
|
|
1497
|
+
payload: {
|
|
1498
|
+
source: 'refine_mesh_node_async_job',
|
|
1499
|
+
refineJob: {
|
|
1500
|
+
batch: true,
|
|
1501
|
+
jobId: handle.jobId,
|
|
1502
|
+
interactionId: handle.interactionId,
|
|
1503
|
+
status: handle.status,
|
|
1504
|
+
meshId: handle.meshId,
|
|
1505
|
+
nodeIds: handle.nodeIds,
|
|
1506
|
+
order: handle.order,
|
|
1507
|
+
targetCoordinatorDaemonId: handle.targetCoordinatorDaemonId,
|
|
1508
|
+
startedAt: handle.startedAt,
|
|
1509
|
+
completedAt: handle.completedAt,
|
|
1510
|
+
},
|
|
1511
|
+
async: true,
|
|
1512
|
+
batch: true,
|
|
1513
|
+
...(originatingStamp ? { originatingCoordinator: originatingStamp } : {}),
|
|
1514
|
+
...(result ? {
|
|
1515
|
+
success: result.success === true,
|
|
1516
|
+
result,
|
|
1517
|
+
} : {}),
|
|
1518
|
+
},
|
|
1519
|
+
});
|
|
1520
|
+
} catch (e: any) {
|
|
1521
|
+
LOG.warn('Mesh', `[Refinery] Failed to append async refine batch ledger entry: ${e?.message || e}`);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
export async function finishMeshRefineBatchJob(self: DaemonCommandRouter,
|
|
1526
|
+
handle: MeshRefineBatchJobHandle,
|
|
1527
|
+
orderedNodes: any[],
|
|
1528
|
+
ordering: { order: string[]; rationale?: unknown },
|
|
1529
|
+
args: any,
|
|
1530
|
+
): Promise<void> {
|
|
1531
|
+
const key = buildRefineBatchJobKey(self, handle.meshId);
|
|
1532
|
+
let result: Record<string, unknown>;
|
|
1533
|
+
try {
|
|
1534
|
+
result = await runMeshRefineBatchConvergence(self, handle.meshId, orderedNodes, ordering, args) as Record<string, unknown>;
|
|
1535
|
+
} catch (e: any) {
|
|
1536
|
+
result = { success: false, error: e?.message || String(e), batch: true };
|
|
1537
|
+
}
|
|
1538
|
+
const completedAt = new Date().toISOString();
|
|
1539
|
+
|
|
1540
|
+
// The batch as a whole "completed" only when every node converged (no blocked /
|
|
1541
|
+
// not_mergeable). A partial batch is reported as a terminal failure so the
|
|
1542
|
+
// coordinator inspects the per-node blockers rather than assuming a clean merge.
|
|
1543
|
+
const summary = (result.summary && typeof result.summary === 'object') ? result.summary as Record<string, number> : undefined;
|
|
1544
|
+
const allConverged = result.allConverged === true;
|
|
1545
|
+
const isTerminalSuccess = result.success === true && allConverged;
|
|
1546
|
+
|
|
1547
|
+
const nextStep = typeof result.nextStep === 'string' && result.nextStep
|
|
1548
|
+
? result.nextStep
|
|
1549
|
+
: isTerminalSuccess
|
|
1550
|
+
? 'All batched nodes converged onto base. Continue from the updated mesh state.'
|
|
1551
|
+
: 'Resolve blocked_review / not_mergeable nodes (see per-node code/stage/error in result.results), then re-run mesh_refine_batch for the remaining nodes.';
|
|
1552
|
+
const normalizedResult = {
|
|
1553
|
+
...result,
|
|
1554
|
+
batch: true,
|
|
1555
|
+
nextStep,
|
|
1556
|
+
...(summary ? {
|
|
1557
|
+
convergenceStatus: allConverged ? 'all_converged' : 'partial',
|
|
1558
|
+
} : {}),
|
|
1559
|
+
};
|
|
1560
|
+
|
|
1561
|
+
const terminalHandle = buildRefineBatchJobHandle(self, {
|
|
1562
|
+
meshId: handle.meshId,
|
|
1563
|
+
nodeIds: handle.nodeIds,
|
|
1564
|
+
order: handle.order,
|
|
1565
|
+
status: isTerminalSuccess ? 'completed' : 'failed',
|
|
1566
|
+
startedAt: handle.startedAt,
|
|
1567
|
+
completedAt,
|
|
1568
|
+
jobId: handle.jobId,
|
|
1569
|
+
interactionId: handle.interactionId,
|
|
1570
|
+
coordinatorDaemonId: handle.targetCoordinatorDaemonId,
|
|
1571
|
+
});
|
|
1572
|
+
const terminal: MeshRefineBatchTerminalJob = { ...terminalHandle, result: normalizedResult };
|
|
1573
|
+
self.terminalRefineBatchJobs.set(key, terminal);
|
|
1574
|
+
self.runningRefineBatchJobs.delete(key);
|
|
1575
|
+
self.invalidateAggregateMeshStatus(handle.meshId);
|
|
1576
|
+
await appendRefineBatchJobLedger(self, isTerminalSuccess ? 'task_completed' : 'task_failed', terminalHandle, normalizedResult);
|
|
1577
|
+
queueRefineBatchJobEvent(self, isTerminalSuccess ? 'refine:completed' : 'refine:failed', terminalHandle, normalizedResult);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
/**
|
|
1581
|
+
* Async entry for the batch Refinery execute path. Mirrors startMeshRefineJob:
|
|
1582
|
+
* resolves the plan synchronously (so target/ordering errors and the dry-run shape
|
|
1583
|
+
* stay synchronous), then for execute=true registers an in-flight batch job, returns
|
|
1584
|
+
* {async:true, status:'accepted', batch:true, ...plan} immediately, and runs the
|
|
1585
|
+
* convergence loop in the background — emitting the same terminal refine event.
|
|
1586
|
+
* Idempotent: a batch already in flight for this mesh returns the running handle
|
|
1587
|
+
* with duplicate:true rather than spawning a second background job.
|
|
1588
|
+
*/
|
|
1589
|
+
export async function startMeshRefineBatchJob(self: DaemonCommandRouter, meshId: string, requestedNodeIds: string[] | undefined, args: any): Promise<CommandRouterResult> {
|
|
1590
|
+
// Resolve the plan up-front. For dry-run this returns the synchronous plan; for
|
|
1591
|
+
// execute it returns the same plan shape but we hand convergence to the bg job.
|
|
1592
|
+
const plan = await batchRefineMeshNodes(self, meshId, requestedNodeIds, { ...args, dryRun: true, execute: false });
|
|
1593
|
+
const planRecord = plan as Record<string, unknown>;
|
|
1594
|
+
if (planRecord.success !== true) return plan;
|
|
1595
|
+
|
|
1596
|
+
// If the caller actually asked for a dry-run, return the plan as-is (sync).
|
|
1597
|
+
if (args?.dryRun === true && args?.execute !== true) return plan;
|
|
1598
|
+
|
|
1599
|
+
const order = Array.isArray(planRecord.order) ? (planRecord.order as unknown[]).filter((v): v is string => typeof v === 'string') : [];
|
|
1600
|
+
const nodeIds = order.slice();
|
|
1601
|
+
if (nodeIds.length === 0) {
|
|
1602
|
+
// No convergeable nodes — nothing to dispatch; return the empty plan synchronously.
|
|
1603
|
+
return { ...planRecord, success: true, batch: true, dryRun: false, async: false };
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const key = buildRefineBatchJobKey(self, meshId);
|
|
1607
|
+
const running = self.runningRefineBatchJobs.get(key);
|
|
1608
|
+
if (running) return { ...running, duplicate: true };
|
|
1609
|
+
|
|
1610
|
+
// Re-resolve the ordered node objects against current membership so the bg job
|
|
1611
|
+
// refines real nodes (the plan only carries ids). preferInline matches refine_mesh_node.
|
|
1612
|
+
const meshRecord = await self.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
1613
|
+
const mesh = meshRecord?.mesh;
|
|
1614
|
+
const allNodes: any[] = Array.isArray(mesh?.nodes) ? mesh.nodes : [];
|
|
1615
|
+
const orderedNodes = nodeIds
|
|
1616
|
+
.map(id => allNodes.find(n => meshNodeIdMatches(n, id)))
|
|
1617
|
+
.filter((n): n is any => !!n);
|
|
1618
|
+
if (orderedNodes.length === 0) {
|
|
1619
|
+
return { success: false, error: 'Batch nodes no longer resolvable in mesh', batch: true };
|
|
1620
|
+
}
|
|
1621
|
+
const ordering = {
|
|
1622
|
+
order,
|
|
1623
|
+
rationale: planRecord.orderingRationale,
|
|
1624
|
+
};
|
|
1625
|
+
|
|
1626
|
+
const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
1627
|
+
? args.coordinatorDaemonId.trim()
|
|
1628
|
+
: (self.deps.statusInstanceId || undefined);
|
|
1629
|
+
const handle = buildRefineBatchJobHandle(self, { meshId, nodeIds, order, coordinatorDaemonId });
|
|
1630
|
+
self.runningRefineBatchJobs.set(key, handle);
|
|
1631
|
+
await appendRefineBatchJobLedger(self, 'task_dispatched', handle);
|
|
1632
|
+
queueRefineBatchJobEvent(self, 'refine:accepted', handle);
|
|
1633
|
+
|
|
1634
|
+
setImmediate(() => {
|
|
1635
|
+
void finishMeshRefineBatchJob(self, handle, orderedNodes, ordering, args);
|
|
1636
|
+
});
|
|
1637
|
+
|
|
1638
|
+
// Return the accepted handle plus the plan so the coordinator sees the target set.
|
|
1639
|
+
return {
|
|
1640
|
+
...handle,
|
|
1641
|
+
order,
|
|
1642
|
+
orderingRationale: planRecord.orderingRationale,
|
|
1643
|
+
plan: planRecord.plan,
|
|
1644
|
+
note: 'Batch convergence accepted and running in the background. Completion/failure (with per-node results) will be delivered as a terminal refine event; do not poll repeatedly.',
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
export async function finishMeshRefineJob(self: DaemonCommandRouter, handle: MeshRefineJobHandle, args: any): Promise<void> {
|
|
1649
|
+
const key = buildRefineJobKey(self, handle.meshId, handle.targetNodeId);
|
|
1650
|
+
let result: Record<string, unknown>;
|
|
1651
|
+
try {
|
|
1652
|
+
result = await executeMeshRefineNodeSynchronously(self, handle.meshId, handle.targetNodeId, args) as Record<string, unknown>;
|
|
1653
|
+
} catch (e: any) {
|
|
1654
|
+
result = { success: false, error: e?.message || String(e) };
|
|
1655
|
+
}
|
|
1656
|
+
const completedAt = new Date().toISOString();
|
|
1657
|
+
|
|
1658
|
+
// B1: Discriminated terminal status — do not rely solely on result.success.
|
|
1659
|
+
// Map known failure codes to structured terminal kinds.
|
|
1660
|
+
type RefineTerminalKind = 'completed' | 'blocked_review' | 'validation_failed' | 'submodule_reachability_failed' | 'merge_failed' | 'cleanup_failed';
|
|
1661
|
+
const refineCode = typeof result.code === 'string' ? result.code : '';
|
|
1662
|
+
const refineTerminalKind: RefineTerminalKind = result.success === true
|
|
1663
|
+
? 'completed'
|
|
1664
|
+
: refineCode === 'blocked_review'
|
|
1665
|
+
? 'blocked_review'
|
|
1666
|
+
: refineCode === 'validation_failed' || refineCode === 'validation_dependencies_missing'
|
|
1667
|
+
? 'validation_failed'
|
|
1668
|
+
: refineCode === 'submodule_reachability_failed'
|
|
1669
|
+
? 'submodule_reachability_failed'
|
|
1670
|
+
: refineCode === 'merge_failed' || refineCode === 'patch_equivalence_failed' || refineCode === 'needs_rebase' || refineCode === 'needs_rebase_with_conflicts'
|
|
1671
|
+
? 'merge_failed'
|
|
1672
|
+
: refineCode === 'cleanup_failed'
|
|
1673
|
+
? 'cleanup_failed'
|
|
1674
|
+
: 'merge_failed'; // fallback for unclassified failures
|
|
1675
|
+
const isTerminalSuccess = refineTerminalKind === 'completed';
|
|
1676
|
+
|
|
1677
|
+
// Build structured blocker context for task_failed ledger entries so coordinators
|
|
1678
|
+
// can inspect the failure cause without parsing free-form error strings.
|
|
1679
|
+
const blockerContext: Record<string, unknown> | undefined = isTerminalSuccess ? undefined : (() => {
|
|
1680
|
+
const code = typeof result.code === 'string' ? result.code : refineTerminalKind;
|
|
1681
|
+
const stage = refineTerminalKind === 'validation_failed' ? 'validation'
|
|
1682
|
+
: refineTerminalKind === 'submodule_reachability_failed' ? 'submodule_reachability'
|
|
1683
|
+
: refineCode === 'patch_equivalence_failed' ? 'patch_equivalence'
|
|
1684
|
+
: refineCode === 'needs_rebase' || refineCode === 'needs_rebase_with_conflicts' ? 'patch_equivalence'
|
|
1685
|
+
: refineTerminalKind === 'merge_failed' ? 'merge'
|
|
1686
|
+
: refineTerminalKind === 'cleanup_failed' ? 'cleanup'
|
|
1687
|
+
: 'unknown';
|
|
1688
|
+
const ctx: Record<string, unknown> = {
|
|
1689
|
+
stage,
|
|
1690
|
+
reason: code,
|
|
1691
|
+
terminalKind: refineTerminalKind,
|
|
1692
|
+
};
|
|
1693
|
+
if (typeof result.error === 'string') ctx.error = result.error;
|
|
1694
|
+
if (typeof result.blockedReason === 'string') ctx.blockedReason = result.blockedReason;
|
|
1695
|
+
// Patch equivalence details
|
|
1696
|
+
if (stage === 'patch_equivalence' && result.patchEquivalence) {
|
|
1697
|
+
const pe = result.patchEquivalence as Record<string, unknown>;
|
|
1698
|
+
ctx.details = {
|
|
1699
|
+
expectedPatchId: pe.expectedPatchId,
|
|
1700
|
+
actualPatchId: pe.actualPatchId,
|
|
1701
|
+
status: pe.status,
|
|
1702
|
+
actionableHint: pe.actionableHint,
|
|
1703
|
+
error: pe.error,
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
// Submodule reachability details
|
|
1707
|
+
if (stage === 'submodule_reachability' && Array.isArray(result.unreachableSubmoduleCommits)) {
|
|
1708
|
+
ctx.details = {
|
|
1709
|
+
unreachableCount: (result.unreachableSubmoduleCommits as unknown[]).length,
|
|
1710
|
+
paths: (result.unreachableSubmoduleCommits as Array<Record<string, unknown>>).map(e => e.path),
|
|
1711
|
+
autoPublishAllowed: (result.unreachableSubmoduleCommits as Array<Record<string, unknown>>)[0]?.autoPublishAllowed,
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
// Validation details
|
|
1715
|
+
if (stage === 'validation' && result.validationSummary) {
|
|
1716
|
+
const vs = result.validationSummary as Record<string, unknown>;
|
|
1717
|
+
ctx.details = {
|
|
1718
|
+
failureCode: vs.failureCode,
|
|
1719
|
+
commandsRun: Array.isArray(vs.commandsRun) ? vs.commandsRun.length : undefined,
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
return ctx;
|
|
1723
|
+
})();
|
|
1724
|
+
|
|
1725
|
+
const normalizedResult = {
|
|
1726
|
+
...result,
|
|
1727
|
+
terminalKind: refineTerminalKind,
|
|
1728
|
+
...(blockerContext ? { blockerContext } : {}),
|
|
1729
|
+
...(result.nextStep === undefined && !isTerminalSuccess ? {
|
|
1730
|
+
nextStep: refineTerminalKind === 'blocked_review'
|
|
1731
|
+
? 'Request user review/approval before attempting to merge again.'
|
|
1732
|
+
: refineTerminalKind === 'validation_failed'
|
|
1733
|
+
? 'Fix failing tests or configure validation.bootstrapCommands and retry mesh_refine_node.'
|
|
1734
|
+
: refineTerminalKind === 'submodule_reachability_failed'
|
|
1735
|
+
? 'Push unreachable submodule commits to origin/main, then retry mesh_refine_node.'
|
|
1736
|
+
: refineTerminalKind === 'merge_failed'
|
|
1737
|
+
? 'Resolve merge conflicts or patch equivalence issues, then retry mesh_refine_node.'
|
|
1738
|
+
: refineTerminalKind === 'cleanup_failed'
|
|
1739
|
+
? 'Manually remove the worktree and retry or use mesh_remove_node.'
|
|
1740
|
+
: 'Inspect refineStages for the failing stage and retry.',
|
|
1741
|
+
} : {}),
|
|
1742
|
+
};
|
|
1743
|
+
|
|
1744
|
+
const terminalHandle = buildRefineJobHandle(self, {
|
|
1745
|
+
meshId: handle.meshId,
|
|
1746
|
+
nodeId: handle.targetNodeId,
|
|
1747
|
+
status: isTerminalSuccess ? 'completed' : 'failed',
|
|
1748
|
+
startedAt: handle.startedAt,
|
|
1749
|
+
completedAt,
|
|
1750
|
+
jobId: handle.jobId,
|
|
1751
|
+
interactionId: handle.interactionId,
|
|
1752
|
+
retryOfJobId: handle.retryOfJobId,
|
|
1753
|
+
node: { daemonId: handle.targetDaemonId, workspace: handle.workspace },
|
|
1754
|
+
coordinatorDaemonId: handle.targetCoordinatorDaemonId,
|
|
1755
|
+
});
|
|
1756
|
+
const terminal: MeshRefineTerminalJob = { ...terminalHandle, result: normalizedResult };
|
|
1757
|
+
self.terminalRefineJobs.set(key, terminal);
|
|
1758
|
+
self.runningRefineJobs.delete(key);
|
|
1759
|
+
self.invalidateAggregateMeshStatus(handle.meshId);
|
|
1760
|
+
await appendRefineJobLedger(self, isTerminalSuccess ? 'task_completed' : 'task_failed', terminalHandle, normalizedResult);
|
|
1761
|
+
queueRefineJobEvent(self, isTerminalSuccess ? 'refine:completed' : 'refine:failed', terminalHandle, normalizedResult);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
export async function startMeshRefineJob(self: DaemonCommandRouter, meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
1765
|
+
const key = buildRefineJobKey(self, meshId, nodeId);
|
|
1766
|
+
const running = self.runningRefineJobs.get(key);
|
|
1767
|
+
if (running) return { ...running, duplicate: true };
|
|
1768
|
+
const terminal = self.terminalRefineJobs.get(key);
|
|
1769
|
+
|
|
1770
|
+
// preferInline so inline-cache-only clone worktree nodes resolve — same
|
|
1771
|
+
// membership authority as clone_mesh_node / get_mesh. Without it refine reads
|
|
1772
|
+
// config-first and misses nodes that only live in the inline cache.
|
|
1773
|
+
const meshRecord = await self.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
1774
|
+
const mesh = meshRecord?.mesh;
|
|
1775
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
1776
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
1777
|
+
if (!node.isLocalWorktree || !node.workspace) return { success: false, error: `Refinery requires a local worktree node` };
|
|
1778
|
+
|
|
1779
|
+
// Capture the caller's coordinator daemon ID so completed/failed events are
|
|
1780
|
+
// scoped to that coordinator's pending-events queue and survive daemon restarts.
|
|
1781
|
+
const coordinatorDaemonId = typeof args?.coordinatorDaemonId === 'string' && args.coordinatorDaemonId.trim()
|
|
1782
|
+
? args.coordinatorDaemonId.trim()
|
|
1783
|
+
: (self.deps.statusInstanceId || undefined);
|
|
1784
|
+
const handle = buildRefineJobHandle(self, { meshId, nodeId, node, retryOfJobId: terminal?.jobId, coordinatorDaemonId });
|
|
1785
|
+
self.runningRefineJobs.set(key, handle);
|
|
1786
|
+
await appendRefineJobLedger(self, 'task_dispatched', handle);
|
|
1787
|
+
queueRefineJobEvent(self, 'refine:accepted', handle);
|
|
1788
|
+
|
|
1789
|
+
setImmediate(() => {
|
|
1790
|
+
void finishMeshRefineJob(self, handle, args);
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
return handle;
|
|
1794
|
+
}
|