@adhdev/daemon-core 0.9.82-rc.43 → 0.9.82-rc.430
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 +200 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +126 -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 +7 -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 +15 -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 +60 -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 +29 -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 +142 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +276 -2
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +100 -1
- package/dist/config/mesh-json-config.d.ts +199 -0
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- 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 +11 -1
- 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 +57 -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 +64 -14
- package/dist/index.js +49391 -19228
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +48574 -18570
- 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/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 +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +89 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +174 -0
- package/dist/mesh/mesh-clone-grace.d.ts +8 -0
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-event-classify.d.ts +5 -0
- package/dist/mesh/mesh-event-forwarding.d.ts +41 -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 +69 -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 +9 -0
- package/dist/mesh/mesh-init.d.ts +86 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
- package/dist/mesh/mesh-ledger.d.ts +77 -1
- package/dist/mesh/mesh-magi-status.d.ts +63 -0
- package/dist/mesh/mesh-missions.d.ts +189 -0
- package/dist/mesh/mesh-node-identity.d.ts +289 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +113 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +65 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-gates.d.ts +428 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -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 +450 -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 +30 -0
- package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +290 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +216 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +145 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +20 -0
- package/dist/providers/chat-message-normalization.d.ts +31 -1
- package/dist/providers/cli-provider-instance.d.ts +221 -3
- package/dist/providers/contracts.d.ts +139 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +63 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -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 +27 -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 +29 -0
- package/dist/providers/provider-instance.d.ts +23 -1
- package/dist/providers/provider-loader.d.ts +26 -4
- 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 +91 -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 +422 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +76 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +174 -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 +222 -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 +501 -13
- package/dist/runtime-defaults.d.ts +2 -0
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +59 -2
- 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 +26 -0
- package/dist/system/hash.d.ts +8 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +63 -12
- 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 +1246 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +914 -1144
- 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/WINDOWS-UPGRADE-LOCK-FAILURE.md +198 -0
- package/src/commands/chat-commands-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2327 -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.ts +544 -26
- package/src/commands/handler.ts +841 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +678 -0
- package/src/commands/high-family/mesh-events.ts +80 -0
- package/src/commands/high-family/mesh-status.ts +710 -0
- package/src/commands/high-family/types.ts +76 -0
- package/src/commands/low-family/coordinator-prompt.ts +72 -0
- package/src/commands/low-family/daemon-lifecycle.ts +107 -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 +274 -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 +238 -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 +1066 -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 +143 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router.ts +2711 -3254
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +483 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +495 -22
- package/src/config/mesh-json-config.ts +376 -0
- package/src/config/recent-activity.ts +8 -2
- package/src/config/repo-settings.ts +111 -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 +28 -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 +59 -15
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +767 -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 +194 -12
- 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/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 +338 -0
- package/src/mesh/coordinator-prompt.ts +381 -33
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +645 -0
- package/src/mesh/mesh-clone-grace.ts +68 -0
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-event-classify.ts +51 -0
- package/src/mesh/mesh-event-forwarding.ts +1888 -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 +656 -0
- package/src/mesh/mesh-events-stale.ts +389 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +33 -1035
- package/src/mesh/mesh-fast-forward.ts +843 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-init.ts +260 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
- package/src/mesh/mesh-ledger.ts +656 -102
- package/src/mesh/mesh-magi-status.ts +223 -0
- package/src/mesh/mesh-missions.ts +449 -0
- package/src/mesh/mesh-node-identity.ts +1887 -0
- package/src/mesh/mesh-queue-assignment.ts +1918 -0
- package/src/mesh/mesh-reconcile-loop.ts +1780 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1673 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +1853 -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 +185 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +1243 -141
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +423 -0
- package/src/mesh/worktree-bootstrap-config.ts +450 -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 +57 -5
- package/src/providers/chat-message-normalization.ts +92 -4
- package/src/providers/cli-provider-instance.ts +1933 -104
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +150 -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 +85 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -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 +339 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -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 +19 -1
- package/src/providers/provider-loader.ts +668 -50
- package/src/providers/provider-schema.ts +87 -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 +250 -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 +504 -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 +235 -0
- package/src/providers/spec/cli-adapter.ts +1056 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1410 -0
- package/src/providers/spec/fsm-evaluator.ts +272 -0
- package/src/providers/spec/fsm-loader.ts +120 -0
- package/src/providers/spec/fsm-types.ts +273 -0
- package/src/providers/spec/native-history-executor.ts +1128 -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 +262 -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 +740 -14
- package/src/runtime-defaults.ts +39 -0
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +64 -1
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +95 -26
- 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 +5 -0
- 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,1673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mesh refine validation gates & gitlink fast-forward evaluation
|
|
3
|
+
*
|
|
4
|
+
* Extracted from commands/router.ts (behavior-preserving move). Contains:
|
|
5
|
+
* - the MeshCoordinator config-format type
|
|
6
|
+
* - refine validation / patch-equivalence / effective-diff / submodule
|
|
7
|
+
* reachability gates and their summary types + job handles
|
|
8
|
+
* - gitlink trivial-fast-forward evaluation and submodule alignment helpers
|
|
9
|
+
*
|
|
10
|
+
* router.ts re-exports every public symbol from here so existing import paths
|
|
11
|
+
* keep working. `CommandRouterResult` is imported type-only from router.ts
|
|
12
|
+
* (erased at compile time — no runtime import cycle).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getGitRepoStatus } from '../git/git-status.js';
|
|
16
|
+
import * as yaml from 'js-yaml';
|
|
17
|
+
import { loadMeshRefineConfig, resolveMeshRefineValidationPlan } from '../mesh/refine-config.js';
|
|
18
|
+
import type { MeshRefineValidationCommandPlan } from '../mesh/refine-config.js';
|
|
19
|
+
import { evaluateWorktreeBootstrapState, loadMeshWorktreeBootstrapConfig, runMeshWorktreeBootstrap } from '../mesh/worktree-bootstrap-config.js';
|
|
20
|
+
import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
|
|
21
|
+
import { basename as pathBasename, join as pathJoin, resolve as pathResolve } from 'path';
|
|
22
|
+
import * as fs from 'fs';
|
|
23
|
+
import { execFileSync } from 'node:child_process';
|
|
24
|
+
import { resolveWin32Executable, buildWin32ExecFileSpawn } from '../cli-adapters/resolve-executable.js';
|
|
25
|
+
import type { CommandRouterResult } from '../commands/router.js';
|
|
26
|
+
|
|
27
|
+
// Fix (4): resolve the git executable to an absolute path once on win32. A bare `git` handed to
|
|
28
|
+
// execFile(Sync)/execFileAsync is resolved by libuv's spawn search, which appends only .com/.exe
|
|
29
|
+
// (no PATHEXT) over the inherited PATH — so a `git.cmd`/`git.exe` that `where` finds is missed and
|
|
30
|
+
// the spawn ENOENTs (the live win32 refine/batch failure). resolveWin32Executable is the same helper
|
|
31
|
+
// the validation/bootstrap spawn path already uses. No-op on non-win32 (returns 'git' verbatim), and
|
|
32
|
+
// no shell:true is used anywhere here so there is no quoting risk.
|
|
33
|
+
const GIT = process.platform === 'win32' ? resolveWin32Executable('git') : 'git';
|
|
34
|
+
|
|
35
|
+
export type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
36
|
+
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
37
|
+
type MeshRefineValidationCommand = MeshRefineValidationCommandPlan;
|
|
38
|
+
|
|
39
|
+
type MeshRefineValidationSummary = {
|
|
40
|
+
status: MeshRefineValidationStatus;
|
|
41
|
+
required: true;
|
|
42
|
+
commandsRun: Array<Record<string, unknown>>;
|
|
43
|
+
bootstrapCommandsRun: Array<Record<string, unknown>>;
|
|
44
|
+
rejectedCommands: Array<Record<string, unknown>>;
|
|
45
|
+
skippedReason?: string;
|
|
46
|
+
failureKind?: string;
|
|
47
|
+
failureCode?: string;
|
|
48
|
+
/** Human-readable cause when failureKind === 'spawn_resolution_failed' (win32 .cmd shim, etc). */
|
|
49
|
+
spawnResolutionError?: string;
|
|
50
|
+
timeoutMs: number;
|
|
51
|
+
outputLimitBytes: number;
|
|
52
|
+
configSource?: string;
|
|
53
|
+
configSourceType?: string;
|
|
54
|
+
suggestions?: unknown[];
|
|
55
|
+
suggestedConfig?: unknown;
|
|
56
|
+
/**
|
|
57
|
+
* M2-3: the bootstrap stage recorded separately from validation so review
|
|
58
|
+
* surfaces can distinguish environment failures from validation failures.
|
|
59
|
+
* cached — worktree_bootstrap was 'ready' (staleInputs unchanged), skipped
|
|
60
|
+
* ran — worktree_bootstrap was stale/never-ran and re-ran successfully
|
|
61
|
+
* failed — bootstrap run failed (refine stops before validation)
|
|
62
|
+
* skipped — refine config validation.bootstrap === 'skip'
|
|
63
|
+
* legacy — deprecated validation.bootstrapCommands path was used
|
|
64
|
+
* not_configured — no bootstrap definition anywhere
|
|
65
|
+
*/
|
|
66
|
+
bootstrap?: {
|
|
67
|
+
stage: 'cached' | 'ran' | 'failed' | 'skipped' | 'legacy' | 'not_configured';
|
|
68
|
+
status?: string;
|
|
69
|
+
skipped?: boolean;
|
|
70
|
+
configSource?: string;
|
|
71
|
+
staleReason?: string;
|
|
72
|
+
error?: string;
|
|
73
|
+
commandsRun?: Array<Record<string, unknown>>;
|
|
74
|
+
};
|
|
75
|
+
/** M2-2: deprecation notices from the refine config (e.g. bootstrapCommands). */
|
|
76
|
+
deprecationWarnings?: string[];
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type MeshRefineStageStatus = 'passed' | 'failed' | 'skipped';
|
|
80
|
+
|
|
81
|
+
type MeshRefinePatchEquivalenceSummary = {
|
|
82
|
+
status: MeshRefineStageStatus;
|
|
83
|
+
equivalent: boolean;
|
|
84
|
+
baseHead: string;
|
|
85
|
+
branchHead: string;
|
|
86
|
+
mergeBase?: string;
|
|
87
|
+
mergedTree?: string;
|
|
88
|
+
expectedPatchId?: string;
|
|
89
|
+
actualPatchId?: string;
|
|
90
|
+
durationMs: number;
|
|
91
|
+
error?: string;
|
|
92
|
+
stdout?: string;
|
|
93
|
+
stderr?: string;
|
|
94
|
+
actionableHint?: MeshRefineSubmoduleConflictHint;
|
|
95
|
+
/**
|
|
96
|
+
* Set when a `merge-tree` submodule conflict was reclassified as a trivial
|
|
97
|
+
* gitlink fast-forward and the gate passed via a synthesized merge tree.
|
|
98
|
+
*/
|
|
99
|
+
gitlinkTrivialFastForward?: {
|
|
100
|
+
resolved: boolean;
|
|
101
|
+
gitlinks: Array<{ path: string; baseCommit?: string; branchCommit?: string; fastForward: boolean }>;
|
|
102
|
+
reason?: string;
|
|
103
|
+
};
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
type MeshRefineEffectiveDiffSummary = {
|
|
107
|
+
status: MeshRefineStageStatus;
|
|
108
|
+
/** True when there is at least one root-tree change between base and branch (incl. gitlink bumps). */
|
|
109
|
+
hasEffectiveDiff: boolean;
|
|
110
|
+
baseHead: string;
|
|
111
|
+
branchHead: string;
|
|
112
|
+
/** Root-level paths that differ between base and branch (capped). */
|
|
113
|
+
changedPaths?: string[];
|
|
114
|
+
/** Submodule paths with uncommitted/divergent commits but NO committed gitlink bump in the root tree. */
|
|
115
|
+
submoduleHints?: Array<{ path: string; reason: string }>;
|
|
116
|
+
durationMs: number;
|
|
117
|
+
error?: string;
|
|
118
|
+
stdout?: string;
|
|
119
|
+
stderr?: string;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
type MeshRefineSubmoduleConflictHint = {
|
|
123
|
+
kind: 'submodule_conflict';
|
|
124
|
+
message: string;
|
|
125
|
+
conflicts: Array<{
|
|
126
|
+
path: string;
|
|
127
|
+
baseCommit?: string;
|
|
128
|
+
branchCommit?: string;
|
|
129
|
+
}>;
|
|
130
|
+
nextSteps: string[];
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
type MeshRefineSubmoduleAlignmentSummary = {
|
|
134
|
+
status: 'passed' | 'failed' | 'skipped';
|
|
135
|
+
changedGitlinkPaths: string[];
|
|
136
|
+
outOfSyncPaths: string[];
|
|
137
|
+
updatedPaths: string[];
|
|
138
|
+
verifiedPaths: string[];
|
|
139
|
+
durationMs: number;
|
|
140
|
+
reason?: string;
|
|
141
|
+
command?: string;
|
|
142
|
+
error?: string;
|
|
143
|
+
stdout?: string;
|
|
144
|
+
stderr?: string;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
type MeshRefineSubmoduleReachabilityEntry = {
|
|
148
|
+
path: string;
|
|
149
|
+
commit: string;
|
|
150
|
+
reachable: boolean;
|
|
151
|
+
publishRequired?: boolean;
|
|
152
|
+
autoPublishAllowed?: boolean;
|
|
153
|
+
autoPublishAttempted?: boolean;
|
|
154
|
+
autoPublishSucceeded?: boolean;
|
|
155
|
+
autoPublishVerified?: boolean;
|
|
156
|
+
autoPublishRefspec?: string;
|
|
157
|
+
autoPublishSkippedReason?: string;
|
|
158
|
+
importedFromWorktree?: boolean;
|
|
159
|
+
checkedLocal?: boolean;
|
|
160
|
+
localReachable?: boolean;
|
|
161
|
+
remote?: string;
|
|
162
|
+
remoteUrl?: string;
|
|
163
|
+
remoteReachable?: boolean;
|
|
164
|
+
remoteMainBranch?: string;
|
|
165
|
+
remoteMainReachable?: boolean;
|
|
166
|
+
fetchedFromOrigin?: boolean;
|
|
167
|
+
error?: string;
|
|
168
|
+
publishStdout?: string;
|
|
169
|
+
publishStderr?: string;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
type MeshRefineSubmoduleReachabilitySummary = {
|
|
173
|
+
status: MeshRefineStageStatus;
|
|
174
|
+
checked: number;
|
|
175
|
+
unreachable: MeshRefineSubmoduleReachabilityEntry[];
|
|
176
|
+
entries: MeshRefineSubmoduleReachabilityEntry[];
|
|
177
|
+
durationMs: number;
|
|
178
|
+
autoPublishAllowed?: boolean;
|
|
179
|
+
autoPublishPolicySource?: string;
|
|
180
|
+
error?: string;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export type MeshRefineAsyncJobStatus = 'accepted' | 'completed' | 'failed';
|
|
184
|
+
|
|
185
|
+
export type MeshRefineJobHandle = {
|
|
186
|
+
success: true;
|
|
187
|
+
async: true;
|
|
188
|
+
status: MeshRefineAsyncJobStatus;
|
|
189
|
+
jobId: string;
|
|
190
|
+
interactionId: string;
|
|
191
|
+
meshId: string;
|
|
192
|
+
nodeId: string;
|
|
193
|
+
targetNodeId: string;
|
|
194
|
+
targetDaemonId?: string;
|
|
195
|
+
workspace?: string;
|
|
196
|
+
startedAt: string;
|
|
197
|
+
completedAt?: string;
|
|
198
|
+
duplicate?: boolean;
|
|
199
|
+
retryOfJobId?: string;
|
|
200
|
+
/**
|
|
201
|
+
* The coordinator daemon ID that initiated this refine job.
|
|
202
|
+
* When set, events for this job are scoped to that coordinator's
|
|
203
|
+
* pending-events queue instead of the shared broadcast queue.
|
|
204
|
+
*/
|
|
205
|
+
targetCoordinatorDaemonId?: string;
|
|
206
|
+
eventDelivery: {
|
|
207
|
+
pendingEvents: true;
|
|
208
|
+
ledger: true;
|
|
209
|
+
};
|
|
210
|
+
evidence: {
|
|
211
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
212
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
213
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
214
|
+
};
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export type MeshRefineTerminalJob = MeshRefineJobHandle & { result?: Record<string, unknown> };
|
|
218
|
+
|
|
219
|
+
export type MeshRefineBatchJobStatus = 'accepted' | 'completed' | 'failed';
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Async handle returned by the batch Refinery the instant a convergence run is
|
|
223
|
+
* accepted. Mirrors {@link MeshRefineJobHandle} (async:true / status:'accepted' +
|
|
224
|
+
* terminal pending-event + ledger delivery) but scopes a whole batch of sibling
|
|
225
|
+
* nodes rather than a single node. The synthetic `batchLabel` is used as the
|
|
226
|
+
* `nodeLabel` for the shared refine event/message renderer.
|
|
227
|
+
*/
|
|
228
|
+
export type MeshRefineBatchJobHandle = {
|
|
229
|
+
success: true;
|
|
230
|
+
async: true;
|
|
231
|
+
batch: true;
|
|
232
|
+
status: MeshRefineBatchJobStatus;
|
|
233
|
+
jobId: string;
|
|
234
|
+
interactionId: string;
|
|
235
|
+
meshId: string;
|
|
236
|
+
batchLabel: string;
|
|
237
|
+
nodeIds: string[];
|
|
238
|
+
nodeCount: number;
|
|
239
|
+
order: string[];
|
|
240
|
+
startedAt: string;
|
|
241
|
+
completedAt?: string;
|
|
242
|
+
duplicate?: boolean;
|
|
243
|
+
targetCoordinatorDaemonId?: string;
|
|
244
|
+
eventDelivery: {
|
|
245
|
+
pendingEvents: true;
|
|
246
|
+
ledger: true;
|
|
247
|
+
};
|
|
248
|
+
evidence: {
|
|
249
|
+
pendingEventsCommand: 'get_pending_mesh_events';
|
|
250
|
+
ledgerCommand: 'get_mesh_ledger_slice';
|
|
251
|
+
taskHistoryKind: 'task_dispatched' | 'task_completed' | 'task_failed';
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export type MeshRefineBatchTerminalJob = MeshRefineBatchJobHandle & { result?: Record<string, unknown> };
|
|
256
|
+
|
|
257
|
+
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
258
|
+
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
259
|
+
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
260
|
+
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
261
|
+
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
262
|
+
const REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
263
|
+
|
|
264
|
+
export function truncateValidationOutput(value: unknown): string {
|
|
265
|
+
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
266
|
+
if (text.length <= REFINE_VALIDATION_SUMMARY_CHARS) return text;
|
|
267
|
+
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A spawn-resolution failure is when the executable itself could not be found by
|
|
272
|
+
* the OS spawn boundary — `spawn <cmd> ENOENT` — as opposed to the command
|
|
273
|
+
* running and exiting non-zero. On win32 this is the .cmd-shim case: libuv's
|
|
274
|
+
* spawn search appends only .com/.exe, so a bare `npm`/`npx`/`tsc` (which are
|
|
275
|
+
* .cmd shims) ENOENTs even though it is installed. It carries no stderr, so it
|
|
276
|
+
* must be detected by error.code/syscall, not by string-matching output.
|
|
277
|
+
*/
|
|
278
|
+
export function isSpawnResolutionError(error: any): boolean {
|
|
279
|
+
if (!error) return false;
|
|
280
|
+
if (error.code === 'ENOENT' && typeof error.syscall === 'string' && error.syscall.startsWith('spawn')) return true;
|
|
281
|
+
// Fall back to code alone: execFile sets syscall on the spawn boundary error,
|
|
282
|
+
// but guard for environments/mocks that only surface the code.
|
|
283
|
+
return error.code === 'ENOENT' && (error.syscall === undefined || String(error.syscall).startsWith('spawn'));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function describeSpawnError(error: any, command: string, spawnResolutionFailed: boolean): string {
|
|
287
|
+
if (spawnResolutionFailed) {
|
|
288
|
+
const hint = process.platform === 'win32'
|
|
289
|
+
? ' On Windows, npm-family commands (npm/npx/tsc/vitest) are .cmd shims that the bare-command spawn search does not resolve; configure an absolute path or ensure the command is on PATH.'
|
|
290
|
+
: '';
|
|
291
|
+
return `Could not resolve executable "${command}" (spawn ENOENT).${hint}`;
|
|
292
|
+
}
|
|
293
|
+
return String(error?.message || error);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function recordMeshRefineStage(
|
|
297
|
+
stages: Array<Record<string, unknown>>,
|
|
298
|
+
stage: string,
|
|
299
|
+
status: MeshRefineStageStatus,
|
|
300
|
+
startedAt: number,
|
|
301
|
+
details?: Record<string, unknown>,
|
|
302
|
+
): void {
|
|
303
|
+
stages.push({
|
|
304
|
+
stage,
|
|
305
|
+
status,
|
|
306
|
+
durationMs: Date.now() - startedAt,
|
|
307
|
+
...(details || {}),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function buildSubmodulePublishRequiredNextStep(entries: MeshRefineSubmoduleReachabilityEntry[]): string {
|
|
312
|
+
const refs = entries
|
|
313
|
+
.map(entry => `${entry.path}@${entry.commit}`)
|
|
314
|
+
.join(', ');
|
|
315
|
+
return `Ask the user for explicit approval to push/publish the unreachable submodule commit(s) (${refs}) to the configured submodule remote main branch, then rerun mesh_refine_node. Do not merge the root branch until every submodule gitlink commit is reachable from submodule origin/main.`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Async git exec helper used across the synchronous-refine stage pipeline. Bound
|
|
320
|
+
* once in the orchestrator and threaded through RefineContext so every stage runs
|
|
321
|
+
* git the same way (execFile + promisify, utf8). Returns the child's stdout/stderr.
|
|
322
|
+
*/
|
|
323
|
+
export type RefineExecFileAsync = (file: string, args: string[], options: { cwd: string; encoding: 'utf8' }) => Promise<{ stdout: string; stderr: string }>;
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Accumulated state shared by the synchronous-refine stages. The orchestrator
|
|
327
|
+
* (executeMeshRefineNodeSynchronously) seeds this in the resolve_refs stage and
|
|
328
|
+
* each later stage reads / extends it. `branchHead` and `patchEquivalence` are the
|
|
329
|
+
* only fields a stage mutates after creation (auto-rebase updates both), so they
|
|
330
|
+
* are carried on the mutable context rather than re-threaded through return types.
|
|
331
|
+
*/
|
|
332
|
+
export interface RefineContext {
|
|
333
|
+
meshId: string;
|
|
334
|
+
nodeId: string;
|
|
335
|
+
args: any;
|
|
336
|
+
refineStages: Array<Record<string, unknown>>;
|
|
337
|
+
execFileAsync: RefineExecFileAsync;
|
|
338
|
+
mesh: any;
|
|
339
|
+
node: any;
|
|
340
|
+
sourceNode: any;
|
|
341
|
+
repoRoot: string;
|
|
342
|
+
branch: string;
|
|
343
|
+
baseBranch: string;
|
|
344
|
+
baseHead: string;
|
|
345
|
+
branchHead: string;
|
|
346
|
+
validationSummary: Awaited<ReturnType<typeof runMeshRefineValidationGate>>;
|
|
347
|
+
patchEquivalence: Awaited<ReturnType<typeof runMeshRefinePatchEquivalenceGate>>;
|
|
348
|
+
submoduleReachability: Awaited<ReturnType<typeof runMeshRefineSubmoduleReachabilityGate>>;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Stage outcome for the synchronous-refine pipeline. A stage either produces a
|
|
353
|
+
* terminal CommandRouterResult (an early-exit gate failure, or a successful
|
|
354
|
+
* already-merged short-circuit), in which case the orchestrator returns it
|
|
355
|
+
* immediately, or it returns `continue` with the (possibly extended) context for
|
|
356
|
+
* the next stage. This makes the orchestrator a flat sequence of stage calls
|
|
357
|
+
* while preserving the original body's exact early-return control flow.
|
|
358
|
+
*/
|
|
359
|
+
export type RefineStageOutcome =
|
|
360
|
+
| { kind: 'terminal'; result: CommandRouterResult }
|
|
361
|
+
| { kind: 'continue'; ctx: RefineContext };
|
|
362
|
+
|
|
363
|
+
export function resolveRefineryAutoPublishSubmoduleMainCommits(mesh: any, workspace: string): { enabled: boolean; source?: string } {
|
|
364
|
+
if (mesh?.policy?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
365
|
+
process.stderr.write(
|
|
366
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via mesh.policy. `
|
|
367
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
368
|
+
);
|
|
369
|
+
return { enabled: true, source: 'mesh.policy.allowAutoPublishSubmoduleMainCommits' };
|
|
370
|
+
}
|
|
371
|
+
const loaded = loadMeshRefineConfig(mesh, workspace);
|
|
372
|
+
if (loaded.config?.allowAutoPublishSubmoduleMainCommits === true) {
|
|
373
|
+
process.stderr.write(
|
|
374
|
+
`[adhdev-mesh] WARNING: allowAutoPublishSubmoduleMainCommits is ENABLED via ${loaded.path || loaded.source}. `
|
|
375
|
+
+ `Refinery may push unreachable submodule commits to submodule origin/main without additional user approval.\n`,
|
|
376
|
+
);
|
|
377
|
+
return { enabled: true, source: loaded.path || loaded.source };
|
|
378
|
+
}
|
|
379
|
+
return { enabled: false };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function computeGitPatchId(
|
|
383
|
+
cwd: string,
|
|
384
|
+
fromRef: string,
|
|
385
|
+
toRef: string,
|
|
386
|
+
excludePaths: string[] = [],
|
|
387
|
+
): Promise<string> {
|
|
388
|
+
const { execFileSync } = await import('node:child_process');
|
|
389
|
+
// When excludePaths is non-empty we drop those paths from the diff via
|
|
390
|
+
// `:(exclude)` pathspecs. This is used to omit gitlink paths that have
|
|
391
|
+
// already been proven a safe fast-forward: their patch hunks legitimately
|
|
392
|
+
// differ between the expected (mergeBase→branch) and actual (base→merged)
|
|
393
|
+
// diffs because base may have advanced the same gitlink, so comparing them
|
|
394
|
+
// would spuriously fail patch-equivalence even though the merge is sound.
|
|
395
|
+
const diffArgs = ['diff', '--patch', '--full-index', fromRef, toRef];
|
|
396
|
+
if (excludePaths.length > 0) {
|
|
397
|
+
diffArgs.push('--', '.', ...excludePaths.map(path => `:(exclude)${path}`));
|
|
398
|
+
}
|
|
399
|
+
const diff = execFileSync(GIT, diffArgs, {
|
|
400
|
+
cwd,
|
|
401
|
+
encoding: 'utf8',
|
|
402
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
403
|
+
});
|
|
404
|
+
if (!diff.trim()) return '';
|
|
405
|
+
const patchId = execFileSync(GIT, ['patch-id', '--stable'], {
|
|
406
|
+
cwd,
|
|
407
|
+
input: diff,
|
|
408
|
+
encoding: 'utf8',
|
|
409
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
410
|
+
}).trim();
|
|
411
|
+
return patchId.split(/\s+/)[0] || '';
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export async function runMeshRefinePatchEquivalenceGate(
|
|
415
|
+
repoRoot: string,
|
|
416
|
+
baseHead: string,
|
|
417
|
+
branchHead: string,
|
|
418
|
+
): Promise<MeshRefinePatchEquivalenceSummary> {
|
|
419
|
+
const startedAt = Date.now();
|
|
420
|
+
try {
|
|
421
|
+
const { execFileSync } = await import('node:child_process');
|
|
422
|
+
const git = (args: string[]) => execFileSync(GIT, args, {
|
|
423
|
+
cwd: repoRoot,
|
|
424
|
+
encoding: 'utf8',
|
|
425
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
426
|
+
});
|
|
427
|
+
const mergeBase = git(['merge-base', baseHead, branchHead]).trim();
|
|
428
|
+
|
|
429
|
+
// `git merge-tree --write-tree` refuses to merge gitlinks that differ
|
|
430
|
+
// across base/branch even when the advance is a strict fast-forward,
|
|
431
|
+
// failing with "Recursive merging with submodules currently only
|
|
432
|
+
// supports trivial cases". When that happens we check whether the
|
|
433
|
+
// conflict is *entirely* trivial-ff gitlinks and, if so, synthesize the
|
|
434
|
+
// merged tree ourselves (base tree + branch-side gitlinks).
|
|
435
|
+
let mergedTree = '';
|
|
436
|
+
let mergeTreeStdout = '';
|
|
437
|
+
let gitlinkTrivialFastForward: MeshRefinePatchEquivalenceSummary['gitlinkTrivialFastForward'];
|
|
438
|
+
try {
|
|
439
|
+
mergeTreeStdout = git(['merge-tree', '--write-tree', baseHead, branchHead]);
|
|
440
|
+
mergedTree = mergeTreeStdout.trim().split(/\s+/)[0] || '';
|
|
441
|
+
} catch (mergeTreeErr: any) {
|
|
442
|
+
const output = `${mergeTreeErr?.message || ''}\n${mergeTreeErr?.stdout || ''}\n${mergeTreeErr?.stderr || ''}`;
|
|
443
|
+
const isSubmoduleConflict = /(submodule|160000)/i.test(output)
|
|
444
|
+
|| /Recursive merging with submodules/i.test(output);
|
|
445
|
+
if (!isSubmoduleConflict) throw mergeTreeErr;
|
|
446
|
+
const evaluation = evaluateGitlinkTrivialFastForward(repoRoot, baseHead, branchHead);
|
|
447
|
+
if (!evaluation.trivial) {
|
|
448
|
+
return {
|
|
449
|
+
status: 'failed',
|
|
450
|
+
equivalent: false,
|
|
451
|
+
baseHead,
|
|
452
|
+
branchHead,
|
|
453
|
+
mergeBase: mergeBase || undefined,
|
|
454
|
+
durationMs: Date.now() - startedAt,
|
|
455
|
+
error: mergeTreeErr?.message || String(mergeTreeErr),
|
|
456
|
+
stdout: truncateValidationOutput(mergeTreeErr?.stdout),
|
|
457
|
+
stderr: truncateValidationOutput(mergeTreeErr?.stderr),
|
|
458
|
+
gitlinkTrivialFastForward: { resolved: false, gitlinks: evaluation.gitlinks, reason: evaluation.reason },
|
|
459
|
+
actionableHint: buildPatchEquivalenceSubmoduleConflictHint(repoRoot, baseHead, branchHead, output),
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
// All conflicting gitlinks fast-forward and nothing else conflicts:
|
|
463
|
+
// synthesize the merge result as base's tree with branch-side gitlinks.
|
|
464
|
+
mergedTree = synthesizeTrivialFastForwardMergeTree(repoRoot, baseHead, branchHead, evaluation.gitlinks) || '';
|
|
465
|
+
gitlinkTrivialFastForward = { resolved: true, gitlinks: evaluation.gitlinks };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!mergeBase || !mergedTree) {
|
|
469
|
+
return {
|
|
470
|
+
status: 'failed',
|
|
471
|
+
equivalent: false,
|
|
472
|
+
baseHead,
|
|
473
|
+
branchHead,
|
|
474
|
+
mergeBase: mergeBase || undefined,
|
|
475
|
+
mergedTree: mergedTree || undefined,
|
|
476
|
+
durationMs: Date.now() - startedAt,
|
|
477
|
+
error: 'patch equivalence preflight could not resolve merge-base or synthetic merge tree',
|
|
478
|
+
stdout: truncateValidationOutput(mergeTreeStdout),
|
|
479
|
+
gitlinkTrivialFastForward,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
// Exclude *proven fast-forward* gitlink paths from BOTH patch-ids. When
|
|
483
|
+
// base has advanced a submodule pointer (a sibling merged into main ahead
|
|
484
|
+
// of us) the gitlink hunk's old-value differs between the expected diff
|
|
485
|
+
// (mergeBase→branch, showing the full base→branch advance) and the actual
|
|
486
|
+
// diff (base→merged, showing only the shorter advanced-base→branch
|
|
487
|
+
// advance). That mismatch would spuriously fail equivalence even though
|
|
488
|
+
// advancing the pointer to the branch side is a provably safe
|
|
489
|
+
// fast-forward — this is the root cause of the diverged-base
|
|
490
|
+
// patch_equivalence_failed misjudgment.
|
|
491
|
+
//
|
|
492
|
+
// We exclude ONLY gitlinks whose base-side commit is an ancestor of the
|
|
493
|
+
// branch-side commit (a strict ff, both objects available locally). A
|
|
494
|
+
// non-ff or ambiguous gitlink (a genuine submodule divergence, or objects
|
|
495
|
+
// not fetched locally) is deliberately left in the diff so its differing
|
|
496
|
+
// hunk still drives the comparison — this preserves the original behavior
|
|
497
|
+
// and prevents a false pass on a real divergence.
|
|
498
|
+
const ffGitlinkExcludePaths = collectFastForwardGitlinkPaths(repoRoot, baseHead, branchHead);
|
|
499
|
+
const expectedPatchId = await computeGitPatchId(repoRoot, mergeBase, branchHead, ffGitlinkExcludePaths);
|
|
500
|
+
const actualPatchId = await computeGitPatchId(repoRoot, baseHead, mergedTree, ffGitlinkExcludePaths);
|
|
501
|
+
const equivalent = expectedPatchId === actualPatchId;
|
|
502
|
+
return {
|
|
503
|
+
status: equivalent ? 'passed' : 'failed',
|
|
504
|
+
equivalent,
|
|
505
|
+
baseHead,
|
|
506
|
+
branchHead,
|
|
507
|
+
mergeBase,
|
|
508
|
+
mergedTree,
|
|
509
|
+
expectedPatchId,
|
|
510
|
+
actualPatchId,
|
|
511
|
+
durationMs: Date.now() - startedAt,
|
|
512
|
+
gitlinkTrivialFastForward,
|
|
513
|
+
};
|
|
514
|
+
} catch (e: any) {
|
|
515
|
+
return {
|
|
516
|
+
status: 'failed',
|
|
517
|
+
equivalent: false,
|
|
518
|
+
baseHead,
|
|
519
|
+
branchHead,
|
|
520
|
+
durationMs: Date.now() - startedAt,
|
|
521
|
+
error: e?.message || String(e),
|
|
522
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
523
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
524
|
+
actionableHint: buildPatchEquivalenceSubmoduleConflictHint(
|
|
525
|
+
repoRoot,
|
|
526
|
+
baseHead,
|
|
527
|
+
branchHead,
|
|
528
|
+
`${e?.message || ''}\n${e?.stdout || ''}\n${e?.stderr || ''}`,
|
|
529
|
+
),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export type MeshWorktreePatchContainmentSummary = {
|
|
535
|
+
/** True only when merging worktreeHead into ref introduces no new patch. */
|
|
536
|
+
contained: boolean;
|
|
537
|
+
ref: string;
|
|
538
|
+
worktreeHead: string;
|
|
539
|
+
mergeBase?: string;
|
|
540
|
+
mergedTree?: string;
|
|
541
|
+
/** patch-id of (ref -> synthesized merge tree); empty string when nothing new is added. */
|
|
542
|
+
residualPatchId?: string;
|
|
543
|
+
durationMs: number;
|
|
544
|
+
/** Set when the check could not run (treated conservatively as NOT contained). */
|
|
545
|
+
error?: string;
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Patch-equivalence containment check for the worktree force-cleanup convergence
|
|
550
|
+
* guard. Answers a narrower question than {@link runMeshRefinePatchEquivalenceGate}:
|
|
551
|
+
* "are the worktree branch's changes ALREADY present in `ref` (e.g. origin/main),
|
|
552
|
+
* even though the worktree HEAD's commit SHA is not an ancestor of ref?"
|
|
553
|
+
*
|
|
554
|
+
* This is the cherry-pick / squash / rebase case: the same content landed on the
|
|
555
|
+
* default ref under a different commit SHA, so `merge-base --is-ancestor` (the
|
|
556
|
+
* primary cleanup guard) reports the worktree as un-converged and refuses to
|
|
557
|
+
* remove it. Refinery already accepts patch-equivalent landings via merge-tree +
|
|
558
|
+
* patch-id; this brings the same notion of "convergence" to the cleanup guard.
|
|
559
|
+
*
|
|
560
|
+
* Mechanism: synthesize the merge of `worktreeHead` into `ref` (reusing the same
|
|
561
|
+
* trivial-gitlink-fast-forward handling as the refine gate) and compute the
|
|
562
|
+
* patch-id of (ref -> mergedTree). If that residual diff is EMPTY, merging the
|
|
563
|
+
* worktree adds nothing new on top of ref — its changes are already present there
|
|
564
|
+
* and the worktree is safe to remove. A non-empty residual means the worktree
|
|
565
|
+
* still carries content not in ref, so it is NOT contained and must stay blocked.
|
|
566
|
+
*
|
|
567
|
+
* Conservative by construction: any merge-tree / patch-id failure, a genuine
|
|
568
|
+
* (non-trivial) submodule conflict, or any thrown error yields `contained: false`
|
|
569
|
+
* so an exception can never widen the cleanup allow-list.
|
|
570
|
+
*/
|
|
571
|
+
export async function checkWorktreeChangesPatchEquivalentInRef(
|
|
572
|
+
repoRoot: string,
|
|
573
|
+
ref: string,
|
|
574
|
+
worktreeHead: string,
|
|
575
|
+
): Promise<MeshWorktreePatchContainmentSummary> {
|
|
576
|
+
const startedAt = Date.now();
|
|
577
|
+
try {
|
|
578
|
+
const { execFileSync } = await import('node:child_process');
|
|
579
|
+
const git = (gitArgs: string[]) => execFileSync(GIT, gitArgs, {
|
|
580
|
+
cwd: repoRoot,
|
|
581
|
+
encoding: 'utf8',
|
|
582
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
583
|
+
});
|
|
584
|
+
const mergeBase = git(['merge-base', ref, worktreeHead]).trim();
|
|
585
|
+
|
|
586
|
+
// Reuse the refine gate's trivial-gitlink-fast-forward handling: a clean
|
|
587
|
+
// submodule pointer fast-forward must not block the cleanup, but a real
|
|
588
|
+
// (non-ff) submodule divergence must keep it blocked.
|
|
589
|
+
let mergedTree = '';
|
|
590
|
+
try {
|
|
591
|
+
mergedTree = git(['merge-tree', '--write-tree', ref, worktreeHead]).trim().split(/\s+/)[0] || '';
|
|
592
|
+
} catch (mergeTreeErr: any) {
|
|
593
|
+
const output = `${mergeTreeErr?.message || ''}\n${mergeTreeErr?.stdout || ''}\n${mergeTreeErr?.stderr || ''}`;
|
|
594
|
+
const isSubmoduleConflict = /(submodule|160000)/i.test(output)
|
|
595
|
+
|| /Recursive merging with submodules/i.test(output);
|
|
596
|
+
if (!isSubmoduleConflict) throw mergeTreeErr;
|
|
597
|
+
const evaluation = evaluateGitlinkTrivialFastForward(repoRoot, ref, worktreeHead);
|
|
598
|
+
if (!evaluation.trivial) {
|
|
599
|
+
// A genuine submodule divergence (or unfetched objects): we cannot
|
|
600
|
+
// prove containment, so block conservatively.
|
|
601
|
+
return {
|
|
602
|
+
contained: false,
|
|
603
|
+
ref,
|
|
604
|
+
worktreeHead,
|
|
605
|
+
mergeBase: mergeBase || undefined,
|
|
606
|
+
durationMs: Date.now() - startedAt,
|
|
607
|
+
error: `merge-tree submodule conflict is not a trivial fast-forward: ${evaluation.reason || 'unknown'}`,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
mergedTree = synthesizeTrivialFastForwardMergeTree(repoRoot, ref, worktreeHead, evaluation.gitlinks) || '';
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (!mergedTree) {
|
|
614
|
+
return {
|
|
615
|
+
contained: false,
|
|
616
|
+
ref,
|
|
617
|
+
worktreeHead,
|
|
618
|
+
mergeBase: mergeBase || undefined,
|
|
619
|
+
durationMs: Date.now() - startedAt,
|
|
620
|
+
error: 'could not resolve synthetic merge tree for containment check',
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Exclude proven fast-forward gitlinks from the residual diff for the same
|
|
625
|
+
// reason the refine gate does: advancing a submodule pointer to a strict
|
|
626
|
+
// descendant is a safe fast-forward and must not count as "new content".
|
|
627
|
+
const ffGitlinkExcludePaths = collectFastForwardGitlinkPaths(repoRoot, ref, worktreeHead);
|
|
628
|
+
const residualPatchId = await computeGitPatchId(repoRoot, ref, mergedTree, ffGitlinkExcludePaths);
|
|
629
|
+
const contained = residualPatchId === '';
|
|
630
|
+
return {
|
|
631
|
+
contained,
|
|
632
|
+
ref,
|
|
633
|
+
worktreeHead,
|
|
634
|
+
mergeBase: mergeBase || undefined,
|
|
635
|
+
mergedTree,
|
|
636
|
+
residualPatchId,
|
|
637
|
+
durationMs: Date.now() - startedAt,
|
|
638
|
+
};
|
|
639
|
+
} catch (e: any) {
|
|
640
|
+
return {
|
|
641
|
+
contained: false,
|
|
642
|
+
ref,
|
|
643
|
+
worktreeHead,
|
|
644
|
+
durationMs: Date.now() - startedAt,
|
|
645
|
+
error: e?.message || String(e),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* No-op guard: detect a "silent no-op" merge before the Refinery merge runs.
|
|
652
|
+
*
|
|
653
|
+
* A silent no-op occurs when the refine target branch's ROOT tree is byte-identical
|
|
654
|
+
* to the merge base (origin/main). This is the trap where a submodule (e.g. oss) has
|
|
655
|
+
* real commits but the root branch never committed the gitlink (oss-pointer) bump, so
|
|
656
|
+
* the root diff Refinery would merge is empty. Merging that produces a merge commit with
|
|
657
|
+
* no content change — reported as "success" while the actual work never reaches main.
|
|
658
|
+
*
|
|
659
|
+
* A committed gitlink bump (the legitimate oss-pointer bump) DOES show up in the root
|
|
660
|
+
* tree diff (as a 160000-mode entry), so this guard does NOT block legitimate refines —
|
|
661
|
+
* it only fires when the root tree diff vs base is COMPLETELY empty.
|
|
662
|
+
*
|
|
663
|
+
* Runs after the patch-equivalence gate; the "already merged via other path" case
|
|
664
|
+
* (branch has real changes already present in base) is handled upstream and never
|
|
665
|
+
* reaches here, so an empty root diff at this point is genuinely a no-op.
|
|
666
|
+
*/
|
|
667
|
+
export async function runMeshRefineEffectiveDiffGate(
|
|
668
|
+
repoRoot: string,
|
|
669
|
+
baseHead: string,
|
|
670
|
+
branchHead: string,
|
|
671
|
+
): Promise<MeshRefineEffectiveDiffSummary> {
|
|
672
|
+
const startedAt = Date.now();
|
|
673
|
+
try {
|
|
674
|
+
const { execFileSync } = await import('node:child_process');
|
|
675
|
+
const git = (args: string[], opts?: { cwd?: string }) => execFileSync(GIT, args, {
|
|
676
|
+
cwd: opts?.cwd || repoRoot,
|
|
677
|
+
encoding: 'utf8',
|
|
678
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
679
|
+
});
|
|
680
|
+
// Root tree diff between base and branch. --raw surfaces gitlink (160000) entries,
|
|
681
|
+
// so a committed submodule-pointer bump counts as an effective change. An empty
|
|
682
|
+
// result means the branch's root tree is identical to base → nothing would merge.
|
|
683
|
+
const rawDiff = git(['diff', '--raw', baseHead, branchHead]).trim();
|
|
684
|
+
if (rawDiff) {
|
|
685
|
+
const changedPaths = rawDiff
|
|
686
|
+
.split('\n')
|
|
687
|
+
.map(line => line.split('\t').slice(1).join('\t').trim())
|
|
688
|
+
.filter(Boolean)
|
|
689
|
+
.slice(0, 50);
|
|
690
|
+
return {
|
|
691
|
+
status: 'passed',
|
|
692
|
+
hasEffectiveDiff: true,
|
|
693
|
+
baseHead,
|
|
694
|
+
branchHead,
|
|
695
|
+
changedPaths,
|
|
696
|
+
durationMs: Date.now() - startedAt,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// No root diff → silent no-op. Try to surface which submodule(s) have commits that
|
|
701
|
+
// were never captured by a committed gitlink bump, to make the message actionable.
|
|
702
|
+
const submoduleHints: Array<{ path: string; reason: string }> = [];
|
|
703
|
+
try {
|
|
704
|
+
// `git submodule status` flags submodules whose checked-out commit differs from
|
|
705
|
+
// the recorded gitlink with a leading '+'. That difference is exactly the
|
|
706
|
+
// uncommitted-pointer-bump situation this guard exists to catch.
|
|
707
|
+
const status = git(['submodule', 'status']);
|
|
708
|
+
for (const line of status.split('\n')) {
|
|
709
|
+
const trimmed = line.trimEnd();
|
|
710
|
+
if (!trimmed) continue;
|
|
711
|
+
if (trimmed.startsWith('+')) {
|
|
712
|
+
const parts = trimmed.slice(1).trim().split(/\s+/);
|
|
713
|
+
const path = parts[1] || parts[0] || '(unknown)';
|
|
714
|
+
submoduleHints.push({
|
|
715
|
+
path,
|
|
716
|
+
reason: 'submodule checked-out commit differs from the committed gitlink (pointer bump not committed on the root branch)',
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
} catch { /* submodule status is best-effort */ }
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
status: 'failed',
|
|
724
|
+
hasEffectiveDiff: false,
|
|
725
|
+
baseHead,
|
|
726
|
+
branchHead,
|
|
727
|
+
...(submoduleHints.length ? { submoduleHints } : {}),
|
|
728
|
+
durationMs: Date.now() - startedAt,
|
|
729
|
+
};
|
|
730
|
+
} catch (e: any) {
|
|
731
|
+
// On error, do NOT block the merge — fail open so a probe failure can't wedge refine.
|
|
732
|
+
return {
|
|
733
|
+
status: 'skipped',
|
|
734
|
+
hasEffectiveDiff: true,
|
|
735
|
+
baseHead,
|
|
736
|
+
branchHead,
|
|
737
|
+
durationMs: Date.now() - startedAt,
|
|
738
|
+
error: e?.message || String(e),
|
|
739
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
740
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function buildPatchEquivalenceSubmoduleConflictHint(
|
|
746
|
+
repoRoot: string,
|
|
747
|
+
baseHead: string,
|
|
748
|
+
branchHead: string,
|
|
749
|
+
output: string,
|
|
750
|
+
): MeshRefineSubmoduleConflictHint | undefined {
|
|
751
|
+
if (!/(submodule|160000)/i.test(output) || !/(conflict|failed to merge)/i.test(output)) return undefined;
|
|
752
|
+
const conflicts = readChangedGitlinkPaths(repoRoot, baseHead, branchHead)
|
|
753
|
+
.map(path => ({
|
|
754
|
+
path,
|
|
755
|
+
baseCommit: readTreeObject(repoRoot, baseHead, path),
|
|
756
|
+
branchCommit: readTreeObject(repoRoot, branchHead, path),
|
|
757
|
+
}));
|
|
758
|
+
if (conflicts.length === 0) return undefined;
|
|
759
|
+
return {
|
|
760
|
+
kind: 'submodule_conflict',
|
|
761
|
+
message: 'Refinery could not synthesize a safe merge tree because the branch and base point the same submodule path at different commits.',
|
|
762
|
+
conflicts,
|
|
763
|
+
nextSteps: [
|
|
764
|
+
'Inspect the listed submodule path in both base and branch: baseCommit is the commit currently recorded by the base workspace, branchCommit is the commit recorded by the worktree branch.',
|
|
765
|
+
'Resolve the submodule first by checking out or creating the intended submodule commit, then commit the chosen gitlink in the root branch.',
|
|
766
|
+
'Ensure the chosen submodule commit is reachable from the configured submodule remote main branch, then rerun mesh_refine_node.',
|
|
767
|
+
],
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function readChangedGitlinkPaths(repoRoot: string, fromRef: string, toRef: string): string[] {
|
|
772
|
+
try {
|
|
773
|
+
const output = execFileSync(GIT, ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
|
|
774
|
+
cwd: repoRoot,
|
|
775
|
+
encoding: 'utf8',
|
|
776
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
777
|
+
});
|
|
778
|
+
const paths = new Set<string>();
|
|
779
|
+
for (const line of output.split('\n')) {
|
|
780
|
+
if (!line.trim()) continue;
|
|
781
|
+
const metaAndPath = line.split('\t');
|
|
782
|
+
const meta = metaAndPath[0] || '';
|
|
783
|
+
const path = metaAndPath[metaAndPath.length - 1]?.trim();
|
|
784
|
+
if (!path) continue;
|
|
785
|
+
const parts = meta.split(/\s+/);
|
|
786
|
+
if (parts[0]?.includes('160000') || parts[1]?.includes('160000')) {
|
|
787
|
+
paths.add(path);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return [...paths].sort();
|
|
791
|
+
} catch {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function readTreeObject(repoRoot: string, ref: string, path: string): string | undefined {
|
|
797
|
+
try {
|
|
798
|
+
const output = execFileSync(GIT, ['ls-tree', ref, '--', path], {
|
|
799
|
+
cwd: repoRoot,
|
|
800
|
+
encoding: 'utf8',
|
|
801
|
+
maxBuffer: 1024 * 1024,
|
|
802
|
+
}).trim();
|
|
803
|
+
const match = output.match(/\bcommit\s+([0-9a-f]{40})\b/i);
|
|
804
|
+
return match?.[1];
|
|
805
|
+
} catch {
|
|
806
|
+
return undefined;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Resolve the absolute path to the repo's real git directory. In a linked
|
|
812
|
+
* worktree, `.git` is a file pointing elsewhere, so we cannot assume a `.git`
|
|
813
|
+
* subdirectory exists — a temporary index file must live in the actual git dir.
|
|
814
|
+
*/
|
|
815
|
+
function resolveGitDir(repoRoot: string): string {
|
|
816
|
+
const out = execFileSync(GIT, ['rev-parse', '--absolute-git-dir'], {
|
|
817
|
+
cwd: repoRoot,
|
|
818
|
+
encoding: 'utf8',
|
|
819
|
+
maxBuffer: 1024 * 1024,
|
|
820
|
+
}).trim();
|
|
821
|
+
return out;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Result of evaluating whether a `git merge-tree --write-tree` submodule
|
|
826
|
+
* conflict is in fact a trivial gitlink fast-forward that should pass the
|
|
827
|
+
* patch-equivalence gate.
|
|
828
|
+
*
|
|
829
|
+
* `git merge-tree` (and `git merge` with the default recursive strategy)
|
|
830
|
+
* refuses to 3-way merge gitlinks unless the case is "trivial" — and it
|
|
831
|
+
* treats *any* gitlink that differs across merge-base/base/branch as
|
|
832
|
+
* non-trivial, even when the branch-side commit is a strict descendant of the
|
|
833
|
+
* base-side commit (i.e. a real fast-forward). Refinery only ever wants to
|
|
834
|
+
* accept the branch's recorded gitlink, so a fast-forwardable bump is safe to
|
|
835
|
+
* resolve to the branch side without any conflict.
|
|
836
|
+
*/
|
|
837
|
+
type GitlinkTrivialFastForwardEvaluation = {
|
|
838
|
+
/** True only when the merge-tree conflict is *fully* explained by trivial-ff gitlinks. */
|
|
839
|
+
trivial: boolean;
|
|
840
|
+
/** Why the evaluation declined to treat the conflict as trivial (set when trivial=false). */
|
|
841
|
+
reason?: string;
|
|
842
|
+
/** Per-path detail for the changed gitlinks that were inspected. */
|
|
843
|
+
gitlinks: Array<{
|
|
844
|
+
path: string;
|
|
845
|
+
baseCommit?: string;
|
|
846
|
+
branchCommit?: string;
|
|
847
|
+
fastForward: boolean;
|
|
848
|
+
}>;
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Check, inside a submodule repo, whether `baseCommit` is an ancestor of
|
|
853
|
+
* `branchCommit` (i.e. advancing the gitlink from base→branch is a pure
|
|
854
|
+
* fast-forward). Returns false on any error or when either commit is missing
|
|
855
|
+
* locally — safety first, ambiguity stays "not a fast-forward".
|
|
856
|
+
*/
|
|
857
|
+
function isSubmoduleFastForward(submoduleRepoPath: string, baseCommit: string, branchCommit: string): boolean {
|
|
858
|
+
if (!baseCommit || !branchCommit) return false;
|
|
859
|
+
if (baseCommit === branchCommit) return true;
|
|
860
|
+
try {
|
|
861
|
+
if (!fs.existsSync(submoduleRepoPath)) return false;
|
|
862
|
+
// Both commits must exist locally for the ancestry check to be meaningful.
|
|
863
|
+
execFileSync(GIT, ['cat-file', '-e', `${baseCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: 'ignore' });
|
|
864
|
+
execFileSync(GIT, ['cat-file', '-e', `${branchCommit}^{commit}`], { cwd: submoduleRepoPath, stdio: 'ignore' });
|
|
865
|
+
// exit 0 ⇒ baseCommit is an ancestor of branchCommit ⇒ branch fast-forwards base.
|
|
866
|
+
execFileSync(GIT, ['merge-base', '--is-ancestor', baseCommit, branchCommit], { cwd: submoduleRepoPath, stdio: 'ignore' });
|
|
867
|
+
return true;
|
|
868
|
+
} catch {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Read the set of paths that differ between two refs, tagging whether each is a
|
|
875
|
+
* gitlink (submodule, mode 160000) on either side. Returns one entry per
|
|
876
|
+
* changed path. Empty on error.
|
|
877
|
+
*/
|
|
878
|
+
function readChangedPathKinds(repoRoot: string, fromRef: string, toRef: string): Array<{ path: string; isGitlink: boolean }> {
|
|
879
|
+
try {
|
|
880
|
+
const output = execFileSync(GIT, ['diff', '--raw', '--no-abbrev', fromRef, toRef], {
|
|
881
|
+
cwd: repoRoot,
|
|
882
|
+
encoding: 'utf8',
|
|
883
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
884
|
+
});
|
|
885
|
+
const result: Array<{ path: string; isGitlink: boolean }> = [];
|
|
886
|
+
const seen = new Set<string>();
|
|
887
|
+
for (const line of output.split('\n')) {
|
|
888
|
+
if (!line.trim()) continue;
|
|
889
|
+
const metaAndPath = line.split('\t');
|
|
890
|
+
const meta = metaAndPath[0] || '';
|
|
891
|
+
const path = metaAndPath[metaAndPath.length - 1]?.trim();
|
|
892
|
+
if (!path || seen.has(path)) continue;
|
|
893
|
+
seen.add(path);
|
|
894
|
+
const parts = meta.split(/\s+/);
|
|
895
|
+
const isGitlink = !!(parts[0]?.includes('160000') || parts[1]?.includes('160000'));
|
|
896
|
+
result.push({ path, isGitlink });
|
|
897
|
+
}
|
|
898
|
+
return result;
|
|
899
|
+
} catch {
|
|
900
|
+
return [];
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Return the changed gitlink paths between base and branch whose advance is a
|
|
906
|
+
* strict fast-forward (the base-side commit is an ancestor of the branch-side
|
|
907
|
+
* commit inside that submodule's repo). These are the paths whose patch-id hunk
|
|
908
|
+
* may legitimately differ when base has advanced the same submodule, so they
|
|
909
|
+
* are safe to exclude from the patch-equivalence comparison. A non-ff (genuinely
|
|
910
|
+
* diverged) gitlink is deliberately excluded from this set so it still fails the
|
|
911
|
+
* gate.
|
|
912
|
+
*/
|
|
913
|
+
export function collectFastForwardGitlinkPaths(repoRoot: string, baseHead: string, branchHead: string): string[] {
|
|
914
|
+
return readChangedGitlinkPaths(repoRoot, baseHead, branchHead).filter(path => {
|
|
915
|
+
const baseCommit = readTreeObject(repoRoot, baseHead, path);
|
|
916
|
+
const branchCommit = readTreeObject(repoRoot, branchHead, path);
|
|
917
|
+
if (!baseCommit || !branchCommit) return false;
|
|
918
|
+
return isSubmoduleFastForward(pathResolve(repoRoot, path), baseCommit, branchCommit);
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/**
|
|
923
|
+
* Decide whether a merge-tree submodule conflict between base and branch is a
|
|
924
|
+
* trivial gitlink fast-forward (and nothing else).
|
|
925
|
+
*
|
|
926
|
+
* The conflict is treated as trivial ONLY when:
|
|
927
|
+
* 1. at least one changed gitlink exists,
|
|
928
|
+
* 2. every changed gitlink fast-forwards (base-commit is an ancestor of the
|
|
929
|
+
* branch-commit inside that submodule's repo), and
|
|
930
|
+
* 3. the *only* paths that changed on both sides of the merge (i.e. the paths
|
|
931
|
+
* that could possibly produce a 3-way conflict — the intersection of
|
|
932
|
+
* mergeBase→base and mergeBase→branch changes) are gitlinks. Any
|
|
933
|
+
* overlapping non-gitlink path means a genuine content conflict could be
|
|
934
|
+
* hiding behind the submodule failure, so we keep the block.
|
|
935
|
+
*
|
|
936
|
+
* If any of these fail, the conflict is left as a genuine block. This never
|
|
937
|
+
* passes a regular-file conflict or a diverged (non-ff) gitlink.
|
|
938
|
+
*/
|
|
939
|
+
export function evaluateGitlinkTrivialFastForward(
|
|
940
|
+
repoRoot: string,
|
|
941
|
+
baseHead: string,
|
|
942
|
+
branchHead: string,
|
|
943
|
+
): GitlinkTrivialFastForwardEvaluation {
|
|
944
|
+
const changedGitlinks = readChangedGitlinkPaths(repoRoot, baseHead, branchHead).map(path => {
|
|
945
|
+
const baseCommit = readTreeObject(repoRoot, baseHead, path);
|
|
946
|
+
const branchCommit = readTreeObject(repoRoot, branchHead, path);
|
|
947
|
+
const submoduleRepoPath = pathResolve(repoRoot, path);
|
|
948
|
+
const fastForward = !!baseCommit && !!branchCommit
|
|
949
|
+
&& isSubmoduleFastForward(submoduleRepoPath, baseCommit, branchCommit);
|
|
950
|
+
return { path, baseCommit, branchCommit, fastForward };
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
if (changedGitlinks.length === 0) {
|
|
954
|
+
return { trivial: false, reason: 'no_changed_gitlinks', gitlinks: changedGitlinks };
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const nonFastForward = changedGitlinks.filter(entry => !entry.fastForward);
|
|
958
|
+
if (nonFastForward.length > 0) {
|
|
959
|
+
return {
|
|
960
|
+
trivial: false,
|
|
961
|
+
reason: `diverged_gitlinks:${nonFastForward.map(entry => entry.path).join(',')}`,
|
|
962
|
+
gitlinks: changedGitlinks,
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
// Prove there is no *other* conflict (regular files, or a gitlink that
|
|
967
|
+
// diverged on both sides). A 3-way merge can only conflict on a path that
|
|
968
|
+
// changed on BOTH sides relative to the merge-base. Compute that overlap and
|
|
969
|
+
// require every overlapping path to be a gitlink — non-gitlink overlap means
|
|
970
|
+
// a genuine content conflict that must stay blocked.
|
|
971
|
+
let mergeBase = '';
|
|
972
|
+
try {
|
|
973
|
+
mergeBase = execFileSync(GIT, ['merge-base', baseHead, branchHead], {
|
|
974
|
+
cwd: repoRoot,
|
|
975
|
+
encoding: 'utf8',
|
|
976
|
+
maxBuffer: 1024 * 1024,
|
|
977
|
+
}).trim();
|
|
978
|
+
} catch {
|
|
979
|
+
return { trivial: false, reason: 'merge_base_unresolved', gitlinks: changedGitlinks };
|
|
980
|
+
}
|
|
981
|
+
if (!mergeBase) {
|
|
982
|
+
return { trivial: false, reason: 'merge_base_unresolved', gitlinks: changedGitlinks };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const baseSideChanges = readChangedPathKinds(repoRoot, mergeBase, baseHead);
|
|
986
|
+
const branchSideChanges = readChangedPathKinds(repoRoot, mergeBase, branchHead);
|
|
987
|
+
const baseChangedPaths = new Map(baseSideChanges.map(entry => [entry.path, entry]));
|
|
988
|
+
// Overlapping paths = candidates for a real 3-way conflict.
|
|
989
|
+
const overlapping = branchSideChanges.filter(entry => baseChangedPaths.has(entry.path));
|
|
990
|
+
const nonGitlinkOverlap = overlapping.filter(entry => {
|
|
991
|
+
const baseEntry = baseChangedPaths.get(entry.path);
|
|
992
|
+
return !(entry.isGitlink && baseEntry?.isGitlink);
|
|
993
|
+
});
|
|
994
|
+
if (nonGitlinkOverlap.length > 0) {
|
|
995
|
+
return {
|
|
996
|
+
trivial: false,
|
|
997
|
+
reason: `non_gitlink_overlap:${nonGitlinkOverlap.map(entry => entry.path).join(',')}`,
|
|
998
|
+
gitlinks: changedGitlinks,
|
|
999
|
+
};
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
return { trivial: true, gitlinks: changedGitlinks };
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Build a tree identical to `commitish`'s tree except every gitlink in `paths`
|
|
1007
|
+
* is rewritten to `placeholderCommit`. Used to neutralize submodule pointers so
|
|
1008
|
+
* `git merge-tree` stops bailing on the "Recursive merging with submodules"
|
|
1009
|
+
* limitation and can 3-way merge the surrounding regular-file content. Returns
|
|
1010
|
+
* the tree SHA, or undefined on failure.
|
|
1011
|
+
*/
|
|
1012
|
+
function buildTreeWithGitlinksEqualized(
|
|
1013
|
+
repoRoot: string,
|
|
1014
|
+
commitish: string,
|
|
1015
|
+
paths: string[],
|
|
1016
|
+
placeholderCommit: string,
|
|
1017
|
+
): string | undefined {
|
|
1018
|
+
try {
|
|
1019
|
+
const tree = execFileSync(GIT, ['rev-parse', `${commitish}^{tree}`], {
|
|
1020
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
1021
|
+
}).trim();
|
|
1022
|
+
if (!tree) return undefined;
|
|
1023
|
+
const updates = paths.map(path => `160000 commit ${placeholderCommit}\t${path}`).join('\n');
|
|
1024
|
+
if (!updates) return tree;
|
|
1025
|
+
const tmpIndex = pathJoin(resolveGitDir(repoRoot), `adhdev-refine-eq-${commitish.slice(0, 12)}.index`);
|
|
1026
|
+
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
1027
|
+
try {
|
|
1028
|
+
execFileSync(GIT, ['read-tree', tree], { cwd: repoRoot, env, stdio: 'ignore' });
|
|
1029
|
+
execFileSync(GIT, ['update-index', '--index-info'], {
|
|
1030
|
+
cwd: repoRoot, env, input: `${updates}\n`, encoding: 'utf8',
|
|
1031
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
1032
|
+
});
|
|
1033
|
+
const newTree = execFileSync(GIT, ['write-tree'], { cwd: repoRoot, env, encoding: 'utf8' }).trim();
|
|
1034
|
+
return newTree || undefined;
|
|
1035
|
+
} finally {
|
|
1036
|
+
try { fs.rmSync(tmpIndex, { force: true }); } catch { /* ignore */ }
|
|
1037
|
+
}
|
|
1038
|
+
} catch {
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Synthesize the merge result for a trivial gitlink fast-forward.
|
|
1045
|
+
*
|
|
1046
|
+
* `git merge-tree` bails whenever a gitlink differs across base/branch even
|
|
1047
|
+
* when the advance is a strict fast-forward, so we synthesize the result it
|
|
1048
|
+
* *would* have produced. Crucially, when the merge-base of base and branch is
|
|
1049
|
+
* NOT `baseHead` (i.e. base has diverged — a sibling was merged into main
|
|
1050
|
+
* ahead of us), `baseHead`'s tree does not contain our branch's own
|
|
1051
|
+
* non-gitlink changes. Simply overlaying gitlinks onto `baseHead`'s tree would
|
|
1052
|
+
* therefore drop those changes and break patch-equivalence.
|
|
1053
|
+
*
|
|
1054
|
+
* To handle the diverged case correctly we run a REAL 3-way merge of the
|
|
1055
|
+
* regular-file content (with the conflicting gitlinks temporarily equalized to
|
|
1056
|
+
* a common placeholder so merge-tree won't bail), then overlay each changed
|
|
1057
|
+
* gitlink's branch-side commit onto the merged result. This preserves both
|
|
1058
|
+
* sides' non-gitlink changes.
|
|
1059
|
+
*
|
|
1060
|
+
* Returns the tree SHA, or undefined on failure / genuine non-gitlink
|
|
1061
|
+
* conflict. Caller must have already proven (via
|
|
1062
|
+
* evaluateGitlinkTrivialFastForward) that every changed gitlink fast-forwards
|
|
1063
|
+
* and no other path conflicts.
|
|
1064
|
+
*/
|
|
1065
|
+
function synthesizeTrivialFastForwardMergeTree(
|
|
1066
|
+
repoRoot: string,
|
|
1067
|
+
baseHead: string,
|
|
1068
|
+
branchHead: string,
|
|
1069
|
+
gitlinks: Array<{ path: string; branchCommit?: string }>,
|
|
1070
|
+
): string | undefined {
|
|
1071
|
+
try {
|
|
1072
|
+
const branchGitlinks = gitlinks.filter(entry => entry.branchCommit);
|
|
1073
|
+
const gitlinkPaths = branchGitlinks.map(entry => entry.path);
|
|
1074
|
+
|
|
1075
|
+
// Establish the regular-file content of the merge via a real 3-way merge
|
|
1076
|
+
// with the conflicting gitlinks neutralized. The placeholder is the
|
|
1077
|
+
// merge-base's value for a gitlink (or, failing that, any branch-side
|
|
1078
|
+
// commit) — it only needs to be identical across all three trees.
|
|
1079
|
+
const mergeBase = execFileSync(GIT, ['merge-base', baseHead, branchHead], {
|
|
1080
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
1081
|
+
}).trim();
|
|
1082
|
+
|
|
1083
|
+
let mergedContentTree: string | undefined;
|
|
1084
|
+
if (mergeBase && gitlinkPaths.length > 0) {
|
|
1085
|
+
const placeholder = readTreeObject(repoRoot, mergeBase, gitlinkPaths[0])
|
|
1086
|
+
|| branchGitlinks[0].branchCommit!;
|
|
1087
|
+
const baseEqTree = buildTreeWithGitlinksEqualized(repoRoot, mergeBase, gitlinkPaths, placeholder);
|
|
1088
|
+
const oursEqTree = buildTreeWithGitlinksEqualized(repoRoot, baseHead, gitlinkPaths, placeholder);
|
|
1089
|
+
const theirsEqTree = buildTreeWithGitlinksEqualized(repoRoot, branchHead, gitlinkPaths, placeholder);
|
|
1090
|
+
if (baseEqTree && oursEqTree && theirsEqTree) {
|
|
1091
|
+
try {
|
|
1092
|
+
// merge-tree --write-tree needs commits (to derive a merge-base);
|
|
1093
|
+
// synthesize ours/theirs as children of a common base commit.
|
|
1094
|
+
const baseEqCommit = execFileSync(GIT, ['commit-tree', baseEqTree, '-m', 'refine-ff-base'], {
|
|
1095
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
1096
|
+
}).trim();
|
|
1097
|
+
const oursEqCommit = execFileSync(GIT, ['commit-tree', oursEqTree, '-p', baseEqCommit, '-m', 'refine-ff-ours'], {
|
|
1098
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
1099
|
+
}).trim();
|
|
1100
|
+
const theirsEqCommit = execFileSync(GIT, ['commit-tree', theirsEqTree, '-p', baseEqCommit, '-m', 'refine-ff-theirs'], {
|
|
1101
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
1102
|
+
}).trim();
|
|
1103
|
+
const mergeOut = execFileSync(GIT, ['merge-tree', '--write-tree', oursEqCommit, theirsEqCommit], {
|
|
1104
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1105
|
+
}).trim();
|
|
1106
|
+
mergedContentTree = mergeOut.split(/\s+/)[0] || undefined;
|
|
1107
|
+
} catch {
|
|
1108
|
+
// A real conflict in the equalized merge means a genuine
|
|
1109
|
+
// non-gitlink content conflict the evaluator did not foresee
|
|
1110
|
+
// (or unavailable objects). Fall through to the simple synth.
|
|
1111
|
+
mergedContentTree = undefined;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
// Fallback: when there is no diverged base (merge-base === baseHead) the
|
|
1117
|
+
// regular-file content of the merge is exactly baseHead's tree, so just
|
|
1118
|
+
// overlay the gitlinks. Also used when the real merge could not run.
|
|
1119
|
+
const contentTree = mergedContentTree
|
|
1120
|
+
|| execFileSync(GIT, ['rev-parse', `${baseHead}^{tree}`], {
|
|
1121
|
+
cwd: repoRoot, encoding: 'utf8', maxBuffer: 1024 * 1024,
|
|
1122
|
+
}).trim();
|
|
1123
|
+
if (!contentTree) return undefined;
|
|
1124
|
+
|
|
1125
|
+
const updates = branchGitlinks
|
|
1126
|
+
.map(entry => `160000 commit ${entry.branchCommit}\t${entry.path}`)
|
|
1127
|
+
.join('\n');
|
|
1128
|
+
if (!updates) return contentTree;
|
|
1129
|
+
const tmpIndex = pathJoin(resolveGitDir(repoRoot), `adhdev-refine-ff-${baseHead.slice(0, 12)}-${branchHead.slice(0, 12)}.index`);
|
|
1130
|
+
const env = { ...process.env, GIT_INDEX_FILE: tmpIndex };
|
|
1131
|
+
try {
|
|
1132
|
+
execFileSync(GIT, ['read-tree', contentTree], { cwd: repoRoot, env, stdio: 'ignore' });
|
|
1133
|
+
execFileSync(GIT, ['update-index', '--index-info'], {
|
|
1134
|
+
cwd: repoRoot,
|
|
1135
|
+
env,
|
|
1136
|
+
input: `${updates}\n`,
|
|
1137
|
+
encoding: 'utf8',
|
|
1138
|
+
stdio: ['pipe', 'ignore', 'ignore'],
|
|
1139
|
+
});
|
|
1140
|
+
const newTree = execFileSync(GIT, ['write-tree'], { cwd: repoRoot, env, encoding: 'utf8' }).trim();
|
|
1141
|
+
return newTree || undefined;
|
|
1142
|
+
} finally {
|
|
1143
|
+
try { fs.rmSync(tmpIndex, { force: true }); } catch { /* ignore */ }
|
|
1144
|
+
}
|
|
1145
|
+
} catch {
|
|
1146
|
+
return undefined;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
export async function alignRefinerySubmodulesAfterMerge(
|
|
1151
|
+
repoRoot: string,
|
|
1152
|
+
previousBaseHead: string,
|
|
1153
|
+
currentHead: string,
|
|
1154
|
+
options: { submoduleIgnorePaths?: string[] } = {},
|
|
1155
|
+
): Promise<MeshRefineSubmoduleAlignmentSummary> {
|
|
1156
|
+
const startedAt = Date.now();
|
|
1157
|
+
const changedGitlinkPaths = readChangedGitlinkPaths(repoRoot, previousBaseHead, currentHead)
|
|
1158
|
+
.filter(path => !(options.submoduleIgnorePaths || []).includes(path));
|
|
1159
|
+
const preStatus = await getGitRepoStatus(repoRoot, {
|
|
1160
|
+
includeSubmodules: true,
|
|
1161
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1162
|
+
timeoutMs: 15_000,
|
|
1163
|
+
// Decision path — the out-of-sync submodule set drives a mutating `submodule
|
|
1164
|
+
// update`. Must not act on a TTL-cached status; bypass the C1 cache.
|
|
1165
|
+
forceFresh: true,
|
|
1166
|
+
});
|
|
1167
|
+
const outOfSyncPaths = (preStatus.submodules || [])
|
|
1168
|
+
.filter(submodule => submodule.dirty || submodule.outOfSync || !!submodule.error)
|
|
1169
|
+
.map(submodule => submodule.path);
|
|
1170
|
+
const updatePaths = [...new Set([...changedGitlinkPaths, ...outOfSyncPaths])].sort();
|
|
1171
|
+
|
|
1172
|
+
if (updatePaths.length === 0) {
|
|
1173
|
+
return {
|
|
1174
|
+
status: 'skipped',
|
|
1175
|
+
changedGitlinkPaths,
|
|
1176
|
+
outOfSyncPaths,
|
|
1177
|
+
updatedPaths: [],
|
|
1178
|
+
verifiedPaths: [],
|
|
1179
|
+
durationMs: Date.now() - startedAt,
|
|
1180
|
+
reason: 'no_changed_or_out_of_sync_submodules',
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const commandArgs = ['submodule', 'update', '--init', '--recursive', '--', ...updatePaths];
|
|
1185
|
+
try {
|
|
1186
|
+
const { execFile } = await import('node:child_process');
|
|
1187
|
+
const { promisify } = await import('node:util');
|
|
1188
|
+
const execFileAsync = promisify(execFile);
|
|
1189
|
+
const result = await execFileAsync(GIT, commandArgs, {
|
|
1190
|
+
cwd: repoRoot,
|
|
1191
|
+
encoding: 'utf8',
|
|
1192
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1193
|
+
timeout: 60_000,
|
|
1194
|
+
});
|
|
1195
|
+
const postStatus = await getGitRepoStatus(repoRoot, {
|
|
1196
|
+
includeSubmodules: true,
|
|
1197
|
+
submoduleIgnorePaths: options.submoduleIgnorePaths,
|
|
1198
|
+
timeoutMs: 15_000,
|
|
1199
|
+
// Re-read AFTER `submodule update` mutated the tree — MUST be fresh, never the
|
|
1200
|
+
// cached preStatus from moments ago (which would falsely report still-dirty).
|
|
1201
|
+
forceFresh: true,
|
|
1202
|
+
});
|
|
1203
|
+
const remaining = (postStatus.submodules || [])
|
|
1204
|
+
.filter(submodule => updatePaths.includes(submodule.path) && (submodule.dirty || submodule.outOfSync || !!submodule.error));
|
|
1205
|
+
return {
|
|
1206
|
+
status: remaining.length === 0 ? 'passed' : 'failed',
|
|
1207
|
+
changedGitlinkPaths,
|
|
1208
|
+
outOfSyncPaths,
|
|
1209
|
+
updatedPaths: updatePaths,
|
|
1210
|
+
verifiedPaths: updatePaths.filter(path => !remaining.some(submodule => submodule.path === path)),
|
|
1211
|
+
durationMs: Date.now() - startedAt,
|
|
1212
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1213
|
+
stdout: truncateValidationOutput(result.stdout),
|
|
1214
|
+
stderr: truncateValidationOutput(result.stderr),
|
|
1215
|
+
...(remaining.length > 0 ? { error: `Submodule checkout remained out of sync after update: ${remaining.map(entry => entry.path).join(', ')}` } : {}),
|
|
1216
|
+
};
|
|
1217
|
+
} catch (e: any) {
|
|
1218
|
+
return {
|
|
1219
|
+
status: 'failed',
|
|
1220
|
+
changedGitlinkPaths,
|
|
1221
|
+
outOfSyncPaths,
|
|
1222
|
+
updatedPaths: updatePaths,
|
|
1223
|
+
verifiedPaths: [],
|
|
1224
|
+
durationMs: Date.now() - startedAt,
|
|
1225
|
+
command: `git ${commandArgs.join(' ')}`,
|
|
1226
|
+
error: e?.message || String(e),
|
|
1227
|
+
stdout: truncateValidationOutput(e?.stdout),
|
|
1228
|
+
stderr: truncateValidationOutput(e?.stderr),
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
export async function runMeshRefineSubmoduleReachabilityGate(
|
|
1234
|
+
repoRoot: string,
|
|
1235
|
+
mergedTree: string,
|
|
1236
|
+
options: { allowAutoPublishSubmoduleMainCommits?: boolean; autoPublishPolicySource?: string; worktreeRoot?: string } = {},
|
|
1237
|
+
): Promise<MeshRefineSubmoduleReachabilitySummary> {
|
|
1238
|
+
const startedAt = Date.now();
|
|
1239
|
+
const entries: MeshRefineSubmoduleReachabilityEntry[] = [];
|
|
1240
|
+
try {
|
|
1241
|
+
const { execFile } = await import('node:child_process');
|
|
1242
|
+
const { promisify } = await import('node:util');
|
|
1243
|
+
const execFileAsync = promisify(execFile);
|
|
1244
|
+
const runGit = async (cwd: string, args: string[]): Promise<string> => {
|
|
1245
|
+
const { stdout } = await execFileAsync(GIT, args, {
|
|
1246
|
+
cwd,
|
|
1247
|
+
encoding: 'utf8',
|
|
1248
|
+
timeout: 30_000,
|
|
1249
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1250
|
+
windowsHide: true,
|
|
1251
|
+
});
|
|
1252
|
+
return String(stdout || '');
|
|
1253
|
+
};
|
|
1254
|
+
const verifyRemoteMainContainsCommit = async (submodulePath: string, commit: string, branch = 'main'): Promise<void> => {
|
|
1255
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', 'origin', `refs/heads/${branch}:refs/remotes/origin/${branch}`]);
|
|
1256
|
+
await runGit(submodulePath, ['merge-base', '--is-ancestor', commit, `refs/remotes/origin/${branch}`]);
|
|
1257
|
+
};
|
|
1258
|
+
const publishCommitToRemoteMain = async (submodulePath: string, commit: string, branch = 'main'): Promise<{ stdout: string; stderr: string; refspec: string }> => {
|
|
1259
|
+
const refspec = `${commit}:refs/heads/${branch}`;
|
|
1260
|
+
const { stdout, stderr } = await execFileAsync(GIT, ['push', 'origin', refspec], {
|
|
1261
|
+
cwd: submodulePath,
|
|
1262
|
+
encoding: 'utf8',
|
|
1263
|
+
timeout: 30_000,
|
|
1264
|
+
maxBuffer: REFINE_PATCH_EQUIVALENCE_OUTPUT_LIMIT_BYTES,
|
|
1265
|
+
windowsHide: true,
|
|
1266
|
+
});
|
|
1267
|
+
return { stdout: String(stdout || ''), stderr: String(stderr || ''), refspec };
|
|
1268
|
+
};
|
|
1269
|
+
const importCommitFromWorktreeSubmodule = async (submodulePath: string, worktreeSubmodulePath: string, commit: string): Promise<boolean> => {
|
|
1270
|
+
if (!fs.existsSync(worktreeSubmodulePath)) return false;
|
|
1271
|
+
try {
|
|
1272
|
+
await runGit(worktreeSubmodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1273
|
+
} catch {
|
|
1274
|
+
return false;
|
|
1275
|
+
}
|
|
1276
|
+
await runGit(submodulePath, ['-c', 'protocol.file.allow=always', 'fetch', worktreeSubmodulePath, commit]);
|
|
1277
|
+
await runGit(submodulePath, ['cat-file', '-e', `${commit}^{commit}`]);
|
|
1278
|
+
return true;
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
const treeOutput = await runGit(repoRoot, ['ls-tree', '-r', '-z', mergedTree]);
|
|
1282
|
+
const gitlinks = treeOutput
|
|
1283
|
+
.split('\0')
|
|
1284
|
+
.filter(Boolean)
|
|
1285
|
+
.map(record => {
|
|
1286
|
+
const match = /^160000\s+commit\s+([0-9a-f]{40})\t(.+)$/.exec(record);
|
|
1287
|
+
return match ? { commit: match[1], path: match[2] } : null;
|
|
1288
|
+
})
|
|
1289
|
+
.filter((entry): entry is { commit: string; path: string } => !!entry);
|
|
1290
|
+
|
|
1291
|
+
for (const gitlink of gitlinks) {
|
|
1292
|
+
const submodulePath = pathResolve(repoRoot, gitlink.path);
|
|
1293
|
+
const entry: MeshRefineSubmoduleReachabilityEntry = {
|
|
1294
|
+
path: gitlink.path,
|
|
1295
|
+
commit: gitlink.commit,
|
|
1296
|
+
reachable: false,
|
|
1297
|
+
};
|
|
1298
|
+
try {
|
|
1299
|
+
if (!fs.existsSync(submodulePath)) {
|
|
1300
|
+
entry.error = `Submodule checkout missing at ${gitlink.path}`;
|
|
1301
|
+
entry.publishRequired = true;
|
|
1302
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1303
|
+
entry.autoPublishAllowed = true;
|
|
1304
|
+
entry.autoPublishAttempted = false;
|
|
1305
|
+
entry.autoPublishSkippedReason = `submodule checkout missing at ${gitlink.path}; cannot perform non-force push to origin/main`;
|
|
1306
|
+
}
|
|
1307
|
+
entries.push(entry);
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
entry.checkedLocal = true;
|
|
1312
|
+
try {
|
|
1313
|
+
await runGit(submodulePath, ['cat-file', '-e', `${gitlink.commit}^{commit}`]);
|
|
1314
|
+
entry.localReachable = true;
|
|
1315
|
+
} catch {
|
|
1316
|
+
entry.localReachable = false;
|
|
1317
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && options.worktreeRoot) {
|
|
1318
|
+
try {
|
|
1319
|
+
const imported = await importCommitFromWorktreeSubmodule(
|
|
1320
|
+
submodulePath,
|
|
1321
|
+
pathResolve(options.worktreeRoot, gitlink.path),
|
|
1322
|
+
gitlink.commit,
|
|
1323
|
+
);
|
|
1324
|
+
if (imported) {
|
|
1325
|
+
entry.localReachable = true;
|
|
1326
|
+
entry.importedFromWorktree = true;
|
|
1327
|
+
}
|
|
1328
|
+
} catch (importError: any) {
|
|
1329
|
+
entry.autoPublishSkippedReason = `candidate commit was not present in the source checkout and could not be imported from worktree submodule: ${truncateValidationOutput(importError?.stderr || importError?.message || String(importError))}`;
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
// Probe the submodule remote before allowing cleanup/completion.
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
try {
|
|
1336
|
+
entry.remote = 'origin';
|
|
1337
|
+
let remoteUrl = '';
|
|
1338
|
+
try {
|
|
1339
|
+
remoteUrl = (await runGit(submodulePath, ['remote', 'get-url', 'origin'])).trim();
|
|
1340
|
+
if (!remoteUrl) throw new Error('origin remote has no URL');
|
|
1341
|
+
entry.remoteUrl = remoteUrl;
|
|
1342
|
+
} catch {
|
|
1343
|
+
entry.error = 'Submodule remote reachability check failed: no configured origin remote';
|
|
1344
|
+
entry.publishRequired = true;
|
|
1345
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1346
|
+
entry.autoPublishAllowed = true;
|
|
1347
|
+
entry.autoPublishAttempted = false;
|
|
1348
|
+
entry.autoPublishSkippedReason = 'submodule origin remote is not configured; cannot perform non-force push to origin/main';
|
|
1349
|
+
}
|
|
1350
|
+
entries.push(entry);
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
entry.remoteMainBranch = 'main';
|
|
1354
|
+
try {
|
|
1355
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1356
|
+
entry.fetchedFromOrigin = true;
|
|
1357
|
+
entry.remoteReachable = true;
|
|
1358
|
+
entry.remoteMainReachable = true;
|
|
1359
|
+
entry.reachable = true;
|
|
1360
|
+
} catch (e: any) {
|
|
1361
|
+
entry.remoteReachable = false;
|
|
1362
|
+
entry.remoteMainReachable = false;
|
|
1363
|
+
entry.publishRequired = true;
|
|
1364
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1365
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1366
|
+
if (options.allowAutoPublishSubmoduleMainCommits === true && entry.localReachable === true) {
|
|
1367
|
+
entry.autoPublishAllowed = true;
|
|
1368
|
+
entry.autoPublishAttempted = true;
|
|
1369
|
+
try {
|
|
1370
|
+
const publish = await publishCommitToRemoteMain(submodulePath, gitlink.commit, 'main');
|
|
1371
|
+
entry.autoPublishRefspec = publish.refspec;
|
|
1372
|
+
entry.publishStdout = truncateValidationOutput(publish.stdout);
|
|
1373
|
+
entry.publishStderr = truncateValidationOutput(publish.stderr);
|
|
1374
|
+
entry.autoPublishSucceeded = true;
|
|
1375
|
+
await verifyRemoteMainContainsCommit(submodulePath, gitlink.commit, 'main');
|
|
1376
|
+
entry.fetchedFromOrigin = true;
|
|
1377
|
+
entry.remoteReachable = true;
|
|
1378
|
+
entry.remoteMainReachable = true;
|
|
1379
|
+
entry.autoPublishVerified = true;
|
|
1380
|
+
entry.publishRequired = false;
|
|
1381
|
+
entry.reachable = true;
|
|
1382
|
+
entry.error = undefined;
|
|
1383
|
+
} catch (publishError: any) {
|
|
1384
|
+
entry.autoPublishSucceeded = false;
|
|
1385
|
+
entry.autoPublishVerified = false;
|
|
1386
|
+
const publishDetails = truncateValidationOutput(publishError?.stderr || publishError?.message || String(publishError));
|
|
1387
|
+
entry.error = `Submodule auto-publish to origin/main failed or could not be verified: ${publishDetails}`;
|
|
1388
|
+
}
|
|
1389
|
+
} else if (options.allowAutoPublishSubmoduleMainCommits === true) {
|
|
1390
|
+
entry.autoPublishAllowed = true;
|
|
1391
|
+
entry.autoPublishAttempted = false;
|
|
1392
|
+
entry.autoPublishSkippedReason = entry.autoPublishSkippedReason
|
|
1393
|
+
|| 'candidate commit is not reachable in the source checkout or worktree submodule, so Refinery cannot push it to origin/main';
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
} catch (e: any) {
|
|
1397
|
+
entry.remoteReachable = false;
|
|
1398
|
+
entry.remoteMainReachable = false;
|
|
1399
|
+
entry.publishRequired = true;
|
|
1400
|
+
const details = truncateValidationOutput(e?.stderr || e?.message || String(e));
|
|
1401
|
+
entry.error = `Submodule remote main reachability check failed for origin/main: ${details}`;
|
|
1402
|
+
}
|
|
1403
|
+
} catch (e: any) {
|
|
1404
|
+
entry.error = truncateValidationOutput(e?.message || String(e));
|
|
1405
|
+
entry.publishRequired = true;
|
|
1406
|
+
}
|
|
1407
|
+
entries.push(entry);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const unreachable = entries.filter(entry => !entry.reachable);
|
|
1411
|
+
return {
|
|
1412
|
+
status: unreachable.length ? 'failed' : 'passed',
|
|
1413
|
+
checked: entries.length,
|
|
1414
|
+
unreachable: unreachable.map(entry => ({ ...entry, publishRequired: entry.publishRequired !== false })),
|
|
1415
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: entry.publishRequired !== false }),
|
|
1416
|
+
durationMs: Date.now() - startedAt,
|
|
1417
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1418
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1419
|
+
};
|
|
1420
|
+
} catch (e: any) {
|
|
1421
|
+
const unreachable = entries.filter(entry => !entry.reachable).map(entry => ({ ...entry, publishRequired: true }));
|
|
1422
|
+
return {
|
|
1423
|
+
status: 'failed',
|
|
1424
|
+
checked: entries.length,
|
|
1425
|
+
unreachable,
|
|
1426
|
+
entries: entries.map(entry => entry.reachable ? entry : { ...entry, publishRequired: true }),
|
|
1427
|
+
durationMs: Date.now() - startedAt,
|
|
1428
|
+
autoPublishAllowed: options.allowAutoPublishSubmoduleMainCommits === true,
|
|
1429
|
+
autoPublishPolicySource: options.autoPublishPolicySource,
|
|
1430
|
+
error: truncateValidationOutput(e?.message || String(e)),
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
export function buildMeshRefineValidationPlan(mesh: any, workspace: string): Record<string, unknown> {
|
|
1436
|
+
const plan = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
1437
|
+
const mapCommand = (command: MeshRefineValidationCommandPlan) => ({
|
|
1438
|
+
displayCommand: command.displayCommand,
|
|
1439
|
+
category: command.category,
|
|
1440
|
+
source: command.source,
|
|
1441
|
+
cwd: command.cwd,
|
|
1442
|
+
timeoutMs: command.timeoutMs,
|
|
1443
|
+
});
|
|
1444
|
+
return {
|
|
1445
|
+
source: plan.source,
|
|
1446
|
+
sourceType: plan.sourceType,
|
|
1447
|
+
bootstrapCommands: plan.bootstrapCommands.map(mapCommand),
|
|
1448
|
+
commands: plan.commands.map(mapCommand),
|
|
1449
|
+
unavailableReason: plan.unavailableReason,
|
|
1450
|
+
rejectedCommands: plan.rejectedCommands,
|
|
1451
|
+
suggestions: plan.suggestions,
|
|
1452
|
+
suggestedConfig: plan.suggestedConfig,
|
|
1453
|
+
note: plan.sourceType === 'unavailable'
|
|
1454
|
+
? 'No validation command will be executed until a repo mesh/refine config is provided. Heuristics are suggestions only.'
|
|
1455
|
+
: 'Validation commands are resolved from repo mesh/refine config; heuristics are suggestions only.',
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
export async function runMeshRefineValidationGate(
|
|
1460
|
+
mesh: any,
|
|
1461
|
+
workspace: string,
|
|
1462
|
+
opts?: {
|
|
1463
|
+
/** M2-2: persisted node bootstrap state for staleness evaluation. */
|
|
1464
|
+
persistedBootstrapState?: WorktreeBootstrapState | null;
|
|
1465
|
+
/** M2-2: called after an inherit-mode bootstrap run so the caller can persist the new state. */
|
|
1466
|
+
onBootstrapStateChange?: (state: WorktreeBootstrapState) => void;
|
|
1467
|
+
},
|
|
1468
|
+
): Promise<MeshRefineValidationSummary> {
|
|
1469
|
+
const { execFile } = await import('node:child_process');
|
|
1470
|
+
const { promisify } = await import('node:util');
|
|
1471
|
+
const execFileAsync = promisify(execFile);
|
|
1472
|
+
const selection = resolveMeshRefineValidationPlan(mesh, workspace);
|
|
1473
|
+
const summary: MeshRefineValidationSummary = {
|
|
1474
|
+
status: 'skipped',
|
|
1475
|
+
required: true,
|
|
1476
|
+
commandsRun: [],
|
|
1477
|
+
bootstrapCommandsRun: [],
|
|
1478
|
+
rejectedCommands: selection.rejectedCommands,
|
|
1479
|
+
skippedReason: undefined,
|
|
1480
|
+
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
1481
|
+
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1482
|
+
configSource: selection.source,
|
|
1483
|
+
configSourceType: selection.sourceType,
|
|
1484
|
+
suggestions: selection.suggestions,
|
|
1485
|
+
suggestedConfig: selection.suggestedConfig,
|
|
1486
|
+
...(selection.deprecationWarnings.length > 0 ? { deprecationWarnings: selection.deprecationWarnings } : {}),
|
|
1487
|
+
};
|
|
1488
|
+
|
|
1489
|
+
if (!selection.commands.length) {
|
|
1490
|
+
summary.skippedReason = selection.unavailableReason || 'validation_unavailable: repo mesh/refine config did not provide executable validation.commands';
|
|
1491
|
+
return summary;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// ── M2-2: Bootstrap stage — refine consumes the worktree_bootstrap config
|
|
1495
|
+
// instead of defining its own. Legacy validation.bootstrapCommands run
|
|
1496
|
+
// only when no worktree_bootstrap config exists (deprecation path).
|
|
1497
|
+
let runLegacyBootstrapCommands = selection.bootstrapCommands.length > 0;
|
|
1498
|
+
if (selection.bootstrapMode === 'skip') {
|
|
1499
|
+
summary.bootstrap = { stage: 'skipped', skipped: true };
|
|
1500
|
+
runLegacyBootstrapCommands = false;
|
|
1501
|
+
} else {
|
|
1502
|
+
const wbLoad = loadMeshWorktreeBootstrapConfig(mesh, workspace);
|
|
1503
|
+
const wbUsable = !!wbLoad.config && wbLoad.sourceType !== 'invalid'
|
|
1504
|
+
&& wbLoad.config.enabled !== false && wbLoad.config.runOnClone !== false;
|
|
1505
|
+
if (wbUsable) {
|
|
1506
|
+
runLegacyBootstrapCommands = false; // worktree_bootstrap wins over deprecated bootstrapCommands
|
|
1507
|
+
const evaluated = evaluateWorktreeBootstrapState(mesh, workspace, opts?.persistedBootstrapState);
|
|
1508
|
+
if (evaluated.status === 'ready') {
|
|
1509
|
+
summary.bootstrap = { stage: 'cached', status: 'ready', skipped: true, configSource: evaluated.configSource };
|
|
1510
|
+
} else {
|
|
1511
|
+
const ran = await runMeshWorktreeBootstrap(mesh, workspace);
|
|
1512
|
+
try { opts?.onBootstrapStateChange?.(ran); } catch { /* persistence is best-effort */ }
|
|
1513
|
+
if (ran.status === 'ready') {
|
|
1514
|
+
summary.bootstrap = {
|
|
1515
|
+
stage: 'ran',
|
|
1516
|
+
status: 'ready',
|
|
1517
|
+
configSource: ran.configSource,
|
|
1518
|
+
...(evaluated.staleReason ? { staleReason: evaluated.staleReason } : {}),
|
|
1519
|
+
commandsRun: ran.commandsRun,
|
|
1520
|
+
};
|
|
1521
|
+
} else {
|
|
1522
|
+
summary.bootstrap = {
|
|
1523
|
+
stage: 'failed',
|
|
1524
|
+
status: ran.status,
|
|
1525
|
+
configSource: ran.configSource,
|
|
1526
|
+
error: ran.error,
|
|
1527
|
+
commandsRun: ran.commandsRun,
|
|
1528
|
+
};
|
|
1529
|
+
summary.status = 'failed';
|
|
1530
|
+
summary.failureKind = 'dependency_bootstrap_failed';
|
|
1531
|
+
summary.failureCode = 'dependency_bootstrap_failed';
|
|
1532
|
+
return summary;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
} else if (!runLegacyBootstrapCommands) {
|
|
1536
|
+
summary.bootstrap = { stage: 'not_configured' };
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
const commandRecord = (candidate: MeshRefineValidationCommand, cwd: string, startedAt: number, result: any, passed: boolean, extras: Record<string, unknown> = {}) => ({
|
|
1541
|
+
command: candidate.command,
|
|
1542
|
+
args: candidate.args,
|
|
1543
|
+
displayCommand: candidate.displayCommand,
|
|
1544
|
+
category: candidate.category,
|
|
1545
|
+
source: candidate.source,
|
|
1546
|
+
cwd,
|
|
1547
|
+
passed,
|
|
1548
|
+
durationMs: Date.now() - startedAt,
|
|
1549
|
+
stdout: truncateValidationOutput(result?.stdout),
|
|
1550
|
+
stderr: truncateValidationOutput(result?.stderr || result?.message),
|
|
1551
|
+
...extras,
|
|
1552
|
+
});
|
|
1553
|
+
const isPackageManagerValidation = (candidate: MeshRefineValidationCommand): boolean => {
|
|
1554
|
+
const command = pathBasename(candidate.command).replace(/\.(?:cmd|exe)$/i, '');
|
|
1555
|
+
return ['npm', 'pnpm', 'yarn', 'bun'].includes(command)
|
|
1556
|
+
&& candidate.args.some(arg => arg === 'run' || arg === 'test' || arg === 'exec');
|
|
1557
|
+
};
|
|
1558
|
+
const dependenciesLikelyMissing = (cwd: string): boolean => {
|
|
1559
|
+
if (!fs.existsSync(pathJoin(cwd, 'package.json'))) return false;
|
|
1560
|
+
if (fs.existsSync(pathJoin(cwd, 'node_modules'))) return false;
|
|
1561
|
+
return ['package-lock.json', 'npm-shrinkwrap.json', 'pnpm-lock.yaml', 'yarn.lock', 'bun.lockb', 'bun.lock']
|
|
1562
|
+
.some(lock => fs.existsSync(pathJoin(cwd, lock)));
|
|
1563
|
+
};
|
|
1564
|
+
|
|
1565
|
+
if (runLegacyBootstrapCommands) {
|
|
1566
|
+
summary.bootstrap = { stage: 'legacy' };
|
|
1567
|
+
for (const candidate of selection.bootstrapCommands) {
|
|
1568
|
+
const startedAt = Date.now();
|
|
1569
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1570
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1571
|
+
// On win32, libuv's spawn search only appends .com/.exe (not .cmd/.bat),
|
|
1572
|
+
// so a bare `npm`/`npx`/`tsc` (which are .cmd shims) throws spawn ENOENT.
|
|
1573
|
+
// Resolve to an absolute path via the same helper the PTY path uses
|
|
1574
|
+
// (no-op on non-win32 and when the command is already absolute).
|
|
1575
|
+
const resolvedCommand = resolveWin32Executable(candidate.command);
|
|
1576
|
+
// A win32 .cmd/.bat shim cannot be exec'd directly — wrap it in
|
|
1577
|
+
// cmd.exe /c (no-op off win32 / for a real .exe). Keep
|
|
1578
|
+
// resolvedCommand for diagnostics.
|
|
1579
|
+
const spawn = buildWin32ExecFileSpawn(resolvedCommand, candidate.args);
|
|
1580
|
+
try {
|
|
1581
|
+
const result = await execFileAsync(spawn.file, spawn.args, {
|
|
1582
|
+
cwd,
|
|
1583
|
+
encoding: 'utf8',
|
|
1584
|
+
timeout,
|
|
1585
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1586
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1587
|
+
...(spawn.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
1588
|
+
});
|
|
1589
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1590
|
+
} catch (error: any) {
|
|
1591
|
+
const spawnResolutionFailed = isSpawnResolutionError(error);
|
|
1592
|
+
summary.bootstrapCommandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
1593
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
1594
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
1595
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
1596
|
+
...(spawnResolutionFailed
|
|
1597
|
+
? { failureKind: 'spawn_resolution_failed', resolvedCommand }
|
|
1598
|
+
: { failureKind: 'dependency_bootstrap_failed' }),
|
|
1599
|
+
}));
|
|
1600
|
+
summary.bootstrap = { stage: 'failed', error: describeSpawnError(error, resolvedCommand, spawnResolutionFailed) };
|
|
1601
|
+
summary.status = 'failed';
|
|
1602
|
+
summary.failureKind = spawnResolutionFailed ? 'spawn_resolution_failed' : 'dependency_bootstrap_failed';
|
|
1603
|
+
summary.failureCode = spawnResolutionFailed ? 'spawn_resolution_failed' : 'dependency_bootstrap_failed';
|
|
1604
|
+
return summary;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
for (const candidate of selection.commands) {
|
|
1610
|
+
const startedAt = Date.now();
|
|
1611
|
+
const cwd = candidate.cwd ? pathResolve(workspace, candidate.cwd) : workspace;
|
|
1612
|
+
const timeout = candidate.timeoutMs || REFINE_VALIDATION_TIMEOUT_MS;
|
|
1613
|
+
const bootstrapProvidedDependencies = summary.bootstrap?.stage === 'cached' || summary.bootstrap?.stage === 'ran' || summary.bootstrap?.stage === 'legacy';
|
|
1614
|
+
if (!bootstrapProvidedDependencies && isPackageManagerValidation(candidate) && dependenciesLikelyMissing(cwd)) {
|
|
1615
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, {
|
|
1616
|
+
stderr: 'Dependencies appear to be missing: package.json and a lockfile are present, but node_modules is absent. Configure validation.bootstrapCommands in repo mesh/refine config if Refinery should install/bootstrap before validation.',
|
|
1617
|
+
}, false, {
|
|
1618
|
+
exitCode: null,
|
|
1619
|
+
skipped: true,
|
|
1620
|
+
failureKind: 'missing_dependencies',
|
|
1621
|
+
}));
|
|
1622
|
+
summary.status = 'failed';
|
|
1623
|
+
summary.failureKind = 'missing_dependencies';
|
|
1624
|
+
summary.failureCode = 'missing_dependencies';
|
|
1625
|
+
return summary;
|
|
1626
|
+
}
|
|
1627
|
+
// See the bootstrap loop above: resolve the win32 .cmd shim to an
|
|
1628
|
+
// absolute path before handing it to the spawn boundary.
|
|
1629
|
+
const resolvedCommand = resolveWin32Executable(candidate.command);
|
|
1630
|
+
const spawn = buildWin32ExecFileSpawn(resolvedCommand, candidate.args);
|
|
1631
|
+
try {
|
|
1632
|
+
const result = await execFileAsync(spawn.file, spawn.args, {
|
|
1633
|
+
cwd,
|
|
1634
|
+
encoding: 'utf8',
|
|
1635
|
+
timeout,
|
|
1636
|
+
maxBuffer: candidate.outputLimitBytes || REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
1637
|
+
env: { ...process.env, CI: process.env.CI || '1', ...(candidate.env || {}) },
|
|
1638
|
+
...(spawn.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}),
|
|
1639
|
+
});
|
|
1640
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, result, true, { exitCode: 0 }));
|
|
1641
|
+
} catch (error: any) {
|
|
1642
|
+
// ENOENT check first: a spawn-resolution failure ("spawn npm ENOENT")
|
|
1643
|
+
// carries no stderr and would otherwise fall through to an
|
|
1644
|
+
// unclassified generic failure. Classify it distinctly so the
|
|
1645
|
+
// coordinator surfaces the real cause (win32 .cmd resolution).
|
|
1646
|
+
const spawnResolutionFailed = isSpawnResolutionError(error);
|
|
1647
|
+
const stderr = truncateValidationOutput(error?.stderr || error?.message);
|
|
1648
|
+
const missingDependencyFailure = !spawnResolutionFailed
|
|
1649
|
+
&& /Cannot find module|MODULE_NOT_FOUND|node_modules|command not found|not found/i.test(stderr);
|
|
1650
|
+
summary.commandsRun.push(commandRecord(candidate, cwd, startedAt, error, false, {
|
|
1651
|
+
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
1652
|
+
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
1653
|
+
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
1654
|
+
...(spawnResolutionFailed
|
|
1655
|
+
? { failureKind: 'spawn_resolution_failed', resolvedCommand }
|
|
1656
|
+
: missingDependencyFailure ? { failureKind: 'missing_dependencies' } : {}),
|
|
1657
|
+
}));
|
|
1658
|
+
summary.status = 'failed';
|
|
1659
|
+
if (spawnResolutionFailed) {
|
|
1660
|
+
summary.failureKind = 'spawn_resolution_failed';
|
|
1661
|
+
summary.failureCode = 'spawn_resolution_failed';
|
|
1662
|
+
summary.spawnResolutionError = describeSpawnError(error, resolvedCommand, true);
|
|
1663
|
+
} else if (missingDependencyFailure) {
|
|
1664
|
+
summary.failureKind = 'missing_dependencies';
|
|
1665
|
+
summary.failureCode = 'missing_dependencies';
|
|
1666
|
+
}
|
|
1667
|
+
return summary;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
summary.status = 'passed';
|
|
1672
|
+
return summary;
|
|
1673
|
+
}
|