@adhdev/daemon-core 0.9.82-rc.46 → 0.9.82-rc.461
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 +25 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +64 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +48 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/mesh-restart.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +152 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router-refine.d.ts +137 -0
- package/dist/commands/router-worktree-cleanup.d.ts +133 -0
- package/dist/commands/router.d.ts +241 -12
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +19 -0
- package/dist/config/mesh-config.d.ts +136 -1
- package/dist/config/mesh-json-config.d.ts +199 -0
- package/dist/config/registry-resolver.d.ts +54 -0
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/cli-detector.d.ts +17 -0
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +26 -2
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +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 +67 -15
- package/dist/index.js +58583 -25541
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +58207 -25332
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/logging/debug-config.d.ts +16 -0
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +81 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +230 -0
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +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 +7 -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 +155 -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 +29 -0
- package/dist/mesh/mesh-init.d.ts +128 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
- package/dist/mesh/mesh-ledger.d.ts +133 -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 +310 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +158 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +80 -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 +493 -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 +318 -5
- package/dist/mesh/preview-freshness.d.ts +28 -0
- package/dist/mesh/refine-config.d.ts +216 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +219 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +45 -0
- package/dist/providers/chat-message-normalization.d.ts +58 -1
- package/dist/providers/cli-provider-instance.d.ts +290 -3
- package/dist/providers/contracts.d.ts +149 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +79 -0
- package/dist/providers/native-history/antigravity-claim-registry.d.ts +28 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/constants.d.ts +12 -0
- package/dist/providers/native-history/dispatcher.d.ts +31 -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 +30 -1
- package/dist/providers/provider-loader.d.ts +42 -5
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +98 -0
- package/dist/providers/spec/cli-adapter.d.ts +223 -0
- package/dist/providers/spec/evaluator.d.ts +45 -0
- package/dist/providers/spec/fsm-driver.d.ts +449 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +202 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +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 +598 -12
- package/dist/runtime-defaults.d.ts +2 -0
- package/dist/session-host/managed-host.d.ts +64 -0
- package/dist/session-host/runtime-surface.d.ts +10 -16
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +86 -55
- 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 +11 -4
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +80 -14
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +3 -1
- package/src/cli-adapter-types.ts +68 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +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/chat-commands-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2488 -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 +596 -27
- package/src/commands/handler.ts +845 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +680 -0
- package/src/commands/high-family/mesh-events.ts +89 -0
- package/src/commands/high-family/mesh-status.ts +832 -0
- package/src/commands/high-family/types.ts +80 -0
- package/src/commands/low-family/coordinator-prompt.ts +72 -0
- package/src/commands/low-family/daemon-lifecycle.ts +120 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +87 -0
- package/src/commands/low-family/notification.ts +116 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +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 +241 -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 +1287 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +167 -0
- package/src/commands/med-family/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +153 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router-refine.ts +1711 -0
- package/src/commands/router-worktree-cleanup.ts +870 -0
- package/src/commands/router.ts +1025 -4070
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +493 -24
- package/src/config/config.ts +30 -0
- package/src/config/mesh-config.ts +632 -23
- package/src/config/mesh-json-config.ts +376 -0
- package/src/config/recent-activity.ts +8 -2
- package/src/config/registry-resolver.ts +100 -0
- package/src/config/repo-settings.ts +111 -0
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/daemon/dev-server.ts +0 -541
- package/src/detection/cli-detector.ts +94 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +112 -16
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +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 +200 -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/debug-config.ts +25 -0
- package/src/logging/debug-trace.ts +7 -2
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +341 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +469 -0
- package/src/mesh/coordinator-prompt.ts +554 -34
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +656 -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 +73 -0
- package/src/mesh/mesh-event-forwarding.ts +1950 -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 +1132 -0
- package/src/mesh/mesh-events-stale.ts +440 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +35 -1035
- package/src/mesh/mesh-fast-forward.ts +856 -0
- package/src/mesh/mesh-host-ownership.ts +111 -0
- package/src/mesh/mesh-init.ts +350 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
- package/src/mesh/mesh-ledger.ts +873 -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 +2065 -0
- package/src/mesh/mesh-queue-assignment.ts +2233 -0
- package/src/mesh/mesh-reconcile-loop.ts +2397 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1686 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-review-inbox.ts +308 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +2056 -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 +1281 -141
- package/src/mesh/preview-freshness.ts +163 -0
- package/src/mesh/refine-config.ts +423 -0
- package/src/mesh/worktree-bootstrap-config.ts +599 -0
- package/src/providers/acp-provider-instance.ts +43 -10
- package/src/providers/approval-utils.d.ts +5 -0
- package/src/providers/approval-utils.ts +99 -5
- package/src/providers/chat-message-normalization.ts +126 -4
- package/src/providers/cli-provider-instance.ts +2456 -107
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +160 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/manual-attendance.ts +105 -0
- package/src/providers/native-history/antigravity-claim-registry.ts +131 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +1193 -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 +503 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +260 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +71 -0
- package/src/providers/provider-instance.ts +24 -1
- package/src/providers/provider-loader.ts +700 -55
- package/src/providers/provider-schema.ts +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 +261 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +509 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +246 -0
- package/src/providers/spec/cli-adapter.ts +1064 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1528 -0
- package/src/providers/spec/fsm-evaluator.ts +290 -0
- package/src/providers/spec/fsm-loader.ts +128 -0
- package/src/providers/spec/fsm-types.ts +301 -0
- package/src/providers/spec/native-history-executor.ts +1174 -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 +836 -13
- package/src/runtime-defaults.ts +39 -0
- package/src/session-host/managed-host.ts +218 -0
- package/src/session-host/runtime-surface.ts +20 -80
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +124 -55
- 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,1287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER MED family — mesh CRUD + node CRUD commands.
|
|
3
|
+
*
|
|
4
|
+
* Mesh records (list/get/create/update/delete_mesh) and node lifecycle
|
|
5
|
+
* (add/update/remove/clone_mesh_node, cleanup_mesh_sessions,
|
|
6
|
+
* retry_mesh_node_bootstrap). get_mesh hydrates direct git truth; the mutating
|
|
7
|
+
* node commands gate on the Mesh Host owner check and bust the aggregate-status
|
|
8
|
+
* cache; clone/remove/retry forward to the owning daemon for remote worktrees.
|
|
9
|
+
* Extracted verbatim from executeDaemonCommand; the inline-cache, session/worktree
|
|
10
|
+
* cleanup and aggregate-status collaborators come from ctx.
|
|
11
|
+
*/
|
|
12
|
+
import { daemonIdsEquivalent, meshNodeIdMatches, normalizeMeshNodeId } from '@adhdev/mesh-shared';
|
|
13
|
+
import { resolveMeshHostStatus, normalizeMeshDaemonRole } from '../../mesh/mesh-host-ownership.js';
|
|
14
|
+
import {
|
|
15
|
+
getRegisteredSubmodulePaths,
|
|
16
|
+
loadMeshWorktreeBootstrapConfig,
|
|
17
|
+
runMeshWorktreeBootstrap,
|
|
18
|
+
type WorktreeBootstrapState,
|
|
19
|
+
} from '../../mesh/worktree-bootstrap-config.js';
|
|
20
|
+
import { loadRepoSettings } from '../../config/repo-settings.js';
|
|
21
|
+
import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../../mesh/mesh-events.js';
|
|
22
|
+
import { noteRecentlyClonedNode } from '../../mesh/mesh-clone-grace.js';
|
|
23
|
+
import { loadConfig } from '../../config/config.js';
|
|
24
|
+
import {
|
|
25
|
+
hydrateInlineMeshDirectTruth,
|
|
26
|
+
normalizeProviderRoles,
|
|
27
|
+
readMeshNodeMachineId,
|
|
28
|
+
} from '../router.js';
|
|
29
|
+
import type { CommandRouterResult } from '../router.js';
|
|
30
|
+
import type { GitRepoIdentity } from '../../git/git-types.js';
|
|
31
|
+
import type { MedFamilyContext, MedFamilyHandler } from './types.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Decision for syncing a freshly-cloned worktree's `oss` submodule to its clone
|
|
35
|
+
* source node, applying resolveWorktreeBaseStartPoint's origin-tip-priority
|
|
36
|
+
* policy to the submodule.
|
|
37
|
+
*
|
|
38
|
+
* On clone, `submodule update --init` checks the worktree's `oss` out at the
|
|
39
|
+
* gitlink recorded in the FRESH root base — which the root base-stale fix
|
|
40
|
+
* branches from origin/main, so it is the up-to-date origin tip. The clone
|
|
41
|
+
* source node's *working* `oss` SHA can lag that tip. The original sync blindly
|
|
42
|
+
* checked out the source SHA whenever it merely differed, which REWINDS the
|
|
43
|
+
* submodule back onto the stale source and re-introduces staleness.
|
|
44
|
+
*
|
|
45
|
+
* Guard policy (never rewind, mirror the base-start-point resolver):
|
|
46
|
+
* - source SHA == worktree SHA → `noop`
|
|
47
|
+
* - source SHA is an ancestor of worktree → `skip_rewind` (source is behind; keep fresh tip)
|
|
48
|
+
* - worktree SHA is an ancestor of source → `advance` (source strictly newer; safe fast-forward)
|
|
49
|
+
* - neither is an ancestor (diverged) → `skip_diverged` (keep fresh tip; coordinator reconciles)
|
|
50
|
+
*
|
|
51
|
+
* Both SHAs must already be resolvable in `ossCtx` (the caller fetches the
|
|
52
|
+
* source SHA first). A non-1 git exit (unresolvable SHA / real failure) is
|
|
53
|
+
* rethrown so the caller can fall back to keeping the fresh worktree HEAD.
|
|
54
|
+
*/
|
|
55
|
+
export type OssCloneSyncAction = 'noop' | 'advance' | 'skip_rewind' | 'skip_diverged';
|
|
56
|
+
|
|
57
|
+
export async function decideOssCloneSync(
|
|
58
|
+
ossCtx: GitRepoIdentity,
|
|
59
|
+
worktreeOssSha: string,
|
|
60
|
+
sourceSha: string,
|
|
61
|
+
rg: (ctx: GitRepoIdentity, argv: string[], opts?: { timeoutMs?: number }) => Promise<unknown>,
|
|
62
|
+
): Promise<OssCloneSyncAction> {
|
|
63
|
+
if (!worktreeOssSha || !sourceSha || worktreeOssSha === sourceSha) return 'noop';
|
|
64
|
+
|
|
65
|
+
const isAncestor = async (ancestor: string, descendant: string): Promise<boolean> => {
|
|
66
|
+
try {
|
|
67
|
+
await rg(ossCtx, ['merge-base', '--is-ancestor', ancestor, descendant], { timeoutMs: 10000 });
|
|
68
|
+
return true;
|
|
69
|
+
} catch (err: any) {
|
|
70
|
+
// `merge-base --is-ancestor` exits 1 for a clean "not an ancestor".
|
|
71
|
+
// Any other exit (128 = unresolvable commit, etc.) is a real failure.
|
|
72
|
+
if (err?.exitCode === 1 || err?.code === 1) return false;
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Source is an ancestor of the fresh worktree tip → checking it out rewinds.
|
|
78
|
+
if (await isAncestor(sourceSha, worktreeOssSha)) return 'skip_rewind';
|
|
79
|
+
// Worktree tip is an ancestor of source → source is strictly newer → safe FF.
|
|
80
|
+
if (await isAncestor(worktreeOssSha, sourceSha)) return 'advance';
|
|
81
|
+
// Neither is an ancestor → diverged → keep the fresh worktree HEAD.
|
|
82
|
+
return 'skip_diverged';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sync every registered submodule of a freshly-cloned worktree to its clone source
|
|
87
|
+
* node's working submodule HEAD, applying decideOssCloneSync's origin-tip-priority
|
|
88
|
+
* rewind guard per submodule.
|
|
89
|
+
*
|
|
90
|
+
* Generic over the submodule set: the paths come from `.gitmodules` via
|
|
91
|
+
* getRegisteredSubmodulePaths, so this operates identically over EVERY registered
|
|
92
|
+
* submodule (oss, adhdev-providers, …) instead of a hardcoded 'oss' literal. In a
|
|
93
|
+
* repo whose only synced submodule is `oss` the emitted git commands are byte-identical
|
|
94
|
+
* to the original oss-only path.
|
|
95
|
+
*
|
|
96
|
+
* Best-effort by design: a failure on one submodule is logged and skipped; it never
|
|
97
|
+
* blocks the other submodules or the clone. A submodule is only ever advanced to a
|
|
98
|
+
* STRICTLY-NEWER source SHA — the fresh (origin/main-derived) worktree tip is never
|
|
99
|
+
* rewound onto a behind/diverged source.
|
|
100
|
+
*/
|
|
101
|
+
export async function syncClonedWorktreeSubmodules(
|
|
102
|
+
worktreePath: string,
|
|
103
|
+
sourceWorkspace: string,
|
|
104
|
+
rg: (ctx: GitRepoIdentity, argv: string[], opts?: { timeoutMs?: number }) => Promise<unknown>,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
const submodulePaths = getRegisteredSubmodulePaths(worktreePath);
|
|
107
|
+
if (submodulePaths.size === 0) return;
|
|
108
|
+
|
|
109
|
+
const sourceCtx: GitRepoIdentity = { workspace: sourceWorkspace, repoRoot: sourceWorkspace, isGitRepo: true };
|
|
110
|
+
const worktreeCtx: GitRepoIdentity = { workspace: worktreePath, repoRoot: worktreePath, isGitRepo: true };
|
|
111
|
+
const readStdout = (out: unknown): string =>
|
|
112
|
+
(typeof out === 'string' ? out : (out as any)?.stdout ?? '').trim();
|
|
113
|
+
|
|
114
|
+
for (const submodulePath of submodulePaths) {
|
|
115
|
+
try {
|
|
116
|
+
// Read the source node's working submodule SHA.
|
|
117
|
+
const sourceStatusOut = await rg(sourceCtx, ['submodule', 'status', submodulePath], { timeoutMs: 10000 });
|
|
118
|
+
const sourceSha = readStdout(sourceStatusOut).match(/^[+\- ]?([0-9a-f]{40})/)?.[1];
|
|
119
|
+
if (!sourceSha) continue;
|
|
120
|
+
|
|
121
|
+
// Read the worktree's freshly-checked-out submodule HEAD.
|
|
122
|
+
const subCtx: GitRepoIdentity = {
|
|
123
|
+
workspace: `${worktreePath}/${submodulePath}`,
|
|
124
|
+
repoRoot: `${worktreePath}/${submodulePath}`,
|
|
125
|
+
isGitRepo: true,
|
|
126
|
+
};
|
|
127
|
+
const worktreeSubSha = readStdout(await rg(subCtx, ['rev-parse', 'HEAD'], { timeoutMs: 10000 }));
|
|
128
|
+
if (!worktreeSubSha || worktreeSubSha === sourceSha) continue;
|
|
129
|
+
|
|
130
|
+
// Bring the source node's submodule HEAD into the worktree submodule object
|
|
131
|
+
// DB so both SHAs are resolvable for the ancestry (rewind) guard below.
|
|
132
|
+
await rg(subCtx, ['fetch', `${sourceWorkspace}/${submodulePath}`, 'HEAD'], { timeoutMs: 60000 });
|
|
133
|
+
|
|
134
|
+
// Rewind guard: the worktree submodule HEAD was just checked out from the
|
|
135
|
+
// FRESH (origin/main-derived) root base. Only advance to the source SHA when
|
|
136
|
+
// it is strictly newer — never rewind to a stale source.
|
|
137
|
+
let action: OssCloneSyncAction;
|
|
138
|
+
try {
|
|
139
|
+
action = await decideOssCloneSync(subCtx, worktreeSubSha, sourceSha, rg);
|
|
140
|
+
} catch (decideErr: any) {
|
|
141
|
+
action = 'skip_diverged';
|
|
142
|
+
console.warn(`[mesh] ${submodulePath} submodule sync guard could not resolve ancestry (kept fresh worktree HEAD): ${decideErr?.message ?? decideErr}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (action === 'advance') {
|
|
146
|
+
await rg(subCtx, ['checkout', sourceSha], { timeoutMs: 10000 });
|
|
147
|
+
await rg(worktreeCtx, ['add', submodulePath], { timeoutMs: 10000 });
|
|
148
|
+
await rg(worktreeCtx, ['commit', '-m', `chore: sync ${submodulePath} to source node HEAD on clone`], { timeoutMs: 10000 });
|
|
149
|
+
console.log(`[mesh] Advanced ${submodulePath} submodule to newer source HEAD ${sourceSha.slice(0, 8)} in worktree`);
|
|
150
|
+
} else if (action === 'skip_rewind') {
|
|
151
|
+
console.warn(`[mesh] Skipped ${submodulePath} submodule rewind on clone: source node ${submodulePath} ${sourceSha.slice(0, 8)} is an ancestor of the fresh worktree ${submodulePath} ${worktreeSubSha.slice(0, 8)} — kept fresher worktree HEAD`);
|
|
152
|
+
} else if (action === 'skip_diverged') {
|
|
153
|
+
console.warn(`[mesh] Skipped ${submodulePath} submodule sync on clone: source node ${submodulePath} ${sourceSha.slice(0, 8)} diverged from the fresh worktree ${submodulePath} ${worktreeSubSha.slice(0, 8)} — kept worktree HEAD (coordinator reconciles)`);
|
|
154
|
+
}
|
|
155
|
+
} catch (subErr: any) {
|
|
156
|
+
// Per-submodule best-effort: never let one submodule's failure block the rest.
|
|
157
|
+
console.warn(`[mesh] ${submodulePath} submodule sync to source HEAD failed (best-effort):`, subErr?.message ?? subErr);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
163
|
+
list_meshes: async (ctx: MedFamilyContext, _args: any) => {
|
|
164
|
+
try {
|
|
165
|
+
const { listMeshes } = await import('../../config/mesh-config.js');
|
|
166
|
+
const meshes = listMeshes();
|
|
167
|
+
// HOST-MISSEED-CLOUD-SURFACE: surface the SAME resolved host pin that
|
|
168
|
+
// mesh_status synthesizes (resolveMeshHostStatus) onto each list entry's
|
|
169
|
+
// meshHost. The cloud dashboard's host banner reads hostPinned from the
|
|
170
|
+
// list_meshes payload (selectedMesh.meshHost), NOT from mesh_status — so
|
|
171
|
+
// without this a host mesh whose hostDaemonId was never persisted shows
|
|
172
|
+
// 'no host yet' even though the daemon resolves this daemon as host.
|
|
173
|
+
// resolveMeshHostStatus only synthesizes localDaemonId for role:'host'
|
|
174
|
+
// (member meshes keep hostDaemonId undefined), so member meshes are not
|
|
175
|
+
// polluted. localDaemonId mirrors mesh-status.ts; when absent, the resolver
|
|
176
|
+
// falls back to the raw persisted meshHost.
|
|
177
|
+
const localDaemonId = ctx?.deps?.statusInstanceId;
|
|
178
|
+
const meshesWithHost = Array.isArray(meshes)
|
|
179
|
+
? meshes.map((mesh: any) => {
|
|
180
|
+
try {
|
|
181
|
+
return { ...mesh, meshHost: resolveMeshHostStatus(mesh, { localDaemonId }) };
|
|
182
|
+
} catch {
|
|
183
|
+
// Resolver failure on a single mesh must not drop the whole list.
|
|
184
|
+
return mesh;
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
: meshes;
|
|
188
|
+
return { success: true, meshes: meshesWithHost };
|
|
189
|
+
} catch (e: any) {
|
|
190
|
+
return { success: false, error: e.message };
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
get_mesh: async (ctx: MedFamilyContext, args: any) => {
|
|
195
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
196
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
197
|
+
const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
198
|
+
if (!meshRecord?.mesh) return { success: false, error: 'Mesh not found' };
|
|
199
|
+
|
|
200
|
+
const requireDirectPeerTruth = args?.requireDirectPeerTruth === true;
|
|
201
|
+
// Only an explicit refresh fans out a blocking peer probe.
|
|
202
|
+
// Default loads are satisfied from held standing-state git truth.
|
|
203
|
+
const probeRemotePeers = args?.refresh === true || args?.forceRefresh === true;
|
|
204
|
+
const directTruth = await hydrateInlineMeshDirectTruth({
|
|
205
|
+
mesh: meshRecord.mesh,
|
|
206
|
+
meshSource: meshRecord.source,
|
|
207
|
+
dispatchMeshCommand: ctx.deps.dispatchMeshCommand,
|
|
208
|
+
getMeshPeerConnectionStatus: ctx.deps.getMeshPeerConnectionStatus,
|
|
209
|
+
statusInstanceId: ctx.deps.statusInstanceId,
|
|
210
|
+
localMachineId: loadConfig().machineId || '',
|
|
211
|
+
probeRemotePeers,
|
|
212
|
+
probeCache: ctx.meshGitProbeCache,
|
|
213
|
+
});
|
|
214
|
+
const directTruthSatisfied = meshRecord.source !== 'inline_bootstrap' || directTruth.directEvidenceCount > 0;
|
|
215
|
+
const sourceOfTruth = {
|
|
216
|
+
membership: meshRecord.source === 'inline_cache'
|
|
217
|
+
? 'coordinator_inline_mesh_cache'
|
|
218
|
+
: meshRecord.source === 'local_config'
|
|
219
|
+
? 'local_mesh_config'
|
|
220
|
+
: 'inline_bootstrap_snapshot',
|
|
221
|
+
coordinatorOwnsLiveTruth: directTruthSatisfied,
|
|
222
|
+
directPeerTruth: {
|
|
223
|
+
required: requireDirectPeerTruth,
|
|
224
|
+
satisfied: directTruthSatisfied,
|
|
225
|
+
directEvidenceCount: directTruth.directEvidenceCount,
|
|
226
|
+
localConfirmedCount: directTruth.localConfirmedCount,
|
|
227
|
+
peerAttemptedCount: directTruth.peerAttemptedCount,
|
|
228
|
+
peerConfirmedCount: directTruth.peerConfirmedCount,
|
|
229
|
+
unavailableNodeIds: directTruth.unavailableNodeIds,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
if (requireDirectPeerTruth && !directTruthSatisfied) {
|
|
233
|
+
return {
|
|
234
|
+
success: false,
|
|
235
|
+
code: 'mesh_direct_peer_truth_unavailable',
|
|
236
|
+
error: 'Selected coordinator could not confirm direct mesh truth yet. Bootstrap inventory stays unavailable until direct get_mesh probes succeed.',
|
|
237
|
+
sourceOfTruth,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return { success: true, mesh: meshRecord.mesh, sourceOfTruth };
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
create_mesh: async (_ctx: MedFamilyContext, args: any) => {
|
|
244
|
+
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
245
|
+
const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
|
|
246
|
+
const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
|
|
247
|
+
const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
|
|
248
|
+
if (!name) return { success: false, error: 'name required' };
|
|
249
|
+
try {
|
|
250
|
+
const { createMesh } = await import('../../config/mesh-config.js');
|
|
251
|
+
const meshHost = args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)
|
|
252
|
+
? args.meshHost
|
|
253
|
+
: undefined;
|
|
254
|
+
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy, meshHost });
|
|
255
|
+
return { success: true, mesh };
|
|
256
|
+
} catch (e: any) {
|
|
257
|
+
return { success: false, error: e.message };
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
update_mesh: async (ctx: MedFamilyContext, args: any) => {
|
|
262
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
263
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
264
|
+
try {
|
|
265
|
+
const { updateMesh } = await import('../../config/mesh-config.js');
|
|
266
|
+
const patch: Record<string, unknown> = {};
|
|
267
|
+
if (typeof args?.name === 'string') patch.name = args.name;
|
|
268
|
+
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
269
|
+
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
270
|
+
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
271
|
+
if (args?.meshHost && typeof args.meshHost === 'object' && !Array.isArray(args.meshHost)) patch.meshHost = args.meshHost;
|
|
272
|
+
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
273
|
+
const mesh = updateMesh(meshId, patch as any);
|
|
274
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
275
|
+
ctx.inlineMeshCache.set(meshId, mesh);
|
|
276
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
277
|
+
return { success: true, mesh };
|
|
278
|
+
} catch (e: any) {
|
|
279
|
+
return { success: false, error: e.message };
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
// OPSRULES — emit a `.adhdev/mesh.json` DRAFT from the machine-local mesh
|
|
284
|
+
// entry. This is an export scaffold for the operator to review and commit to
|
|
285
|
+
// the repo, NOT an automatic data migration: nothing is written to disk and
|
|
286
|
+
// meshes.json is untouched. The returned `scaffold` (object) + `scaffoldJson`
|
|
287
|
+
// (2-space text) capture the coordinator prompt override/append (policy is
|
|
288
|
+
// machine-local and is intentionally NOT exported into mesh.json).
|
|
289
|
+
export_mesh_json_config: async (_ctx: MedFamilyContext, args: any) => {
|
|
290
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
291
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
292
|
+
try {
|
|
293
|
+
const { getMesh } = await import('../../config/mesh-config.js');
|
|
294
|
+
const mesh = getMesh(meshId);
|
|
295
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
296
|
+
const { buildMeshJsonConfigScaffold, serializeMeshJsonConfigScaffold, MESH_JSON_CONFIG_LOCATIONS } =
|
|
297
|
+
await import('../../config/mesh-json-config.js');
|
|
298
|
+
const scaffold = buildMeshJsonConfigScaffold(mesh);
|
|
299
|
+
const scaffoldJson = serializeMeshJsonConfigScaffold(scaffold);
|
|
300
|
+
return {
|
|
301
|
+
success: true,
|
|
302
|
+
meshId,
|
|
303
|
+
suggestedPath: MESH_JSON_CONFIG_LOCATIONS[0],
|
|
304
|
+
scaffold,
|
|
305
|
+
scaffoldJson,
|
|
306
|
+
note: 'Draft only — review and commit to the repo at the suggested path. Nothing was written; meshes.json is unchanged (local-wins).',
|
|
307
|
+
};
|
|
308
|
+
} catch (e: any) {
|
|
309
|
+
return { success: false, error: e.message };
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
// Gated WRITE path for `.adhdev/mesh.json` — the sibling of export_mesh_json_config
|
|
314
|
+
// (which only DRAFTS). Same write/overwrite/dry-run contract as mesh_init's config
|
|
315
|
+
// writer: defaults to dry-run (no write), never clobbers an existing repo mesh.json
|
|
316
|
+
// unless overwrite=true, and validates the scaffold before persisting. The scaffold
|
|
317
|
+
// is built from the machine-local mesh entry (coordinator prompt override/append);
|
|
318
|
+
// policy/operating-notes are intentionally NOT exported (see buildMeshJsonConfigScaffold).
|
|
319
|
+
write_mesh_json_config: async (_ctx: MedFamilyContext, args: any) => {
|
|
320
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
321
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
322
|
+
const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
|
|
323
|
+
const write = args?.write === true;
|
|
324
|
+
const overwrite = args?.overwrite === true;
|
|
325
|
+
try {
|
|
326
|
+
const { getMesh } = await import('../../config/mesh-config.js');
|
|
327
|
+
const mesh = getMesh(meshId);
|
|
328
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
329
|
+
const {
|
|
330
|
+
buildMeshJsonConfigScaffold,
|
|
331
|
+
serializeMeshJsonConfigScaffold,
|
|
332
|
+
loadRepoMeshJsonConfig,
|
|
333
|
+
normalizeRepoMeshDeclarativeConfig,
|
|
334
|
+
MESH_JSON_CONFIG_LOCATIONS,
|
|
335
|
+
} = await import('../../config/mesh-json-config.js');
|
|
336
|
+
const { mkdirSync, writeFileSync } = await import('fs');
|
|
337
|
+
const { dirname, join } = await import('path');
|
|
338
|
+
|
|
339
|
+
const scaffold = buildMeshJsonConfigScaffold(mesh);
|
|
340
|
+
const scaffoldJson = serializeMeshJsonConfigScaffold(scaffold);
|
|
341
|
+
const relativePath = MESH_JSON_CONFIG_LOCATIONS[0];
|
|
342
|
+
const absolutePath = join(workspace, relativePath);
|
|
343
|
+
|
|
344
|
+
// Validate before we ever touch disk — never write an unusable mesh.json.
|
|
345
|
+
const validation = normalizeRepoMeshDeclarativeConfig(scaffold);
|
|
346
|
+
if (!validation.valid) {
|
|
347
|
+
return { success: false, meshId, error: `invalid mesh.json scaffold: ${validation.errors.join('; ')}` };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// existing-wins: a repo mesh.json already present is kept unless overwrite=true.
|
|
351
|
+
const existing = loadRepoMeshJsonConfig(workspace);
|
|
352
|
+
const existingPresent = existing.sourceType === 'repo_file' || existing.sourceType === 'invalid';
|
|
353
|
+
if (existingPresent && !overwrite) {
|
|
354
|
+
return {
|
|
355
|
+
success: true,
|
|
356
|
+
meshId,
|
|
357
|
+
written: false,
|
|
358
|
+
dryRun: !write,
|
|
359
|
+
skippedReason: 'already_exists',
|
|
360
|
+
path: absolutePath,
|
|
361
|
+
relativePath,
|
|
362
|
+
existing: existing.config,
|
|
363
|
+
existingSourceType: existing.sourceType,
|
|
364
|
+
scaffold,
|
|
365
|
+
scaffoldJson,
|
|
366
|
+
note: 'A repo mesh.json already exists — kept as-is. Re-run with overwrite=true to replace it (this silently drops operator hand-edits, so present a current-vs-suggested diff first).',
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (!write) {
|
|
371
|
+
return {
|
|
372
|
+
success: true,
|
|
373
|
+
meshId,
|
|
374
|
+
written: false,
|
|
375
|
+
dryRun: true,
|
|
376
|
+
path: absolutePath,
|
|
377
|
+
relativePath,
|
|
378
|
+
scaffold,
|
|
379
|
+
scaffoldJson,
|
|
380
|
+
note: 'Dry-run: nothing written. Re-run with write=true to persist to the repo (commit target). meshes.json is untouched.',
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
385
|
+
writeFileSync(absolutePath, `${scaffoldJson}\n`, 'utf-8');
|
|
386
|
+
return {
|
|
387
|
+
success: true,
|
|
388
|
+
meshId,
|
|
389
|
+
written: true,
|
|
390
|
+
dryRun: false,
|
|
391
|
+
path: absolutePath,
|
|
392
|
+
relativePath,
|
|
393
|
+
scaffold,
|
|
394
|
+
scaffoldJson,
|
|
395
|
+
note: 'Wrote .adhdev/mesh.json (repo commit target). Commit it to the repo; meshes.json (machine-local) is unchanged.',
|
|
396
|
+
};
|
|
397
|
+
} catch (e: any) {
|
|
398
|
+
return { success: false, error: e.message };
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
|
|
402
|
+
delete_mesh: async (_ctx: MedFamilyContext, args: any) => {
|
|
403
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
404
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
405
|
+
try {
|
|
406
|
+
const { deleteMesh } = await import('../../config/mesh-config.js');
|
|
407
|
+
const deleted = deleteMesh(meshId);
|
|
408
|
+
return { success: true, deleted };
|
|
409
|
+
} catch (e: any) {
|
|
410
|
+
return { success: false, error: e.message };
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
// ─── MAGI panels (machine-local config, sibling to meshes) ───────────────
|
|
415
|
+
// Panels live in ~/.adhdev/meshes.json `magiPanels` and are pure local config
|
|
416
|
+
// (no mesh ownership). These three handlers mirror list_meshes/create_mesh/
|
|
417
|
+
// update_mesh: dynamic-import the already-exported mesh-config accessors and
|
|
418
|
+
// surface normalizeMagiPanel's structured error codes (invalid_magi_panel,
|
|
419
|
+
// magi_panel_exists) verbatim so the dashboard can render them.
|
|
420
|
+
//
|
|
421
|
+
// Permission: magi_panel_set / magi_panel_remove are WRITE commands. They are
|
|
422
|
+
// intentionally NOT listed in canPeerUsePrivilegedShareCommand (daemon-cloud
|
|
423
|
+
// data-channel-router), so a peer holding ANY share permission hits its
|
|
424
|
+
// `default → false` branch — identical owner-only gating to create_mesh /
|
|
425
|
+
// update_mesh / list_meshes (none of which are listed there either). A trusted
|
|
426
|
+
// peer (no permission = the owner) passes the top `!permission → true` guard.
|
|
427
|
+
// Mirror, don't invent: do not add a new policy tier here.
|
|
428
|
+
//
|
|
429
|
+
// Resolvability (coupling / stale / available) is deliberately NOT computed
|
|
430
|
+
// here: buildMagiFanoutPlan lives in mcp-server, unreachable from daemon-core.
|
|
431
|
+
// magi_panel_list returns the raw definitions only; the dashboard derives
|
|
432
|
+
// member resolvability client-side (web-core MagiPanelManager, reusing the
|
|
433
|
+
// MagiGroupRow coupling logic) against live mesh_status.
|
|
434
|
+
magi_panel_list: async (_ctx: MedFamilyContext, _args: any) => {
|
|
435
|
+
try {
|
|
436
|
+
const { listMagiPanels } = await import('../../config/mesh-config.js');
|
|
437
|
+
return { success: true, panels: listMagiPanels() };
|
|
438
|
+
} catch (e: any) {
|
|
439
|
+
return { success: false, error: e.message };
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
|
|
443
|
+
magi_panel_set: async (_ctx: MedFamilyContext, args: any) => {
|
|
444
|
+
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
445
|
+
if (!name) return { success: false, error: 'invalid_magi_panel: panel name is required' };
|
|
446
|
+
try {
|
|
447
|
+
const { upsertMagiPanel } = await import('../../config/mesh-config.js');
|
|
448
|
+
// normalizeMagiPanel (invoked inside upsertMagiPanel) validates members,
|
|
449
|
+
// enforces MAX_MAGI_PANEL_MEMBERS, and clamps replica counts. Its
|
|
450
|
+
// invalid_magi_panel / magi_panel_exists messages flow back as `error`.
|
|
451
|
+
const panel = upsertMagiPanel(name, args?.panel, { overwrite: args?.overwrite === true });
|
|
452
|
+
return { success: true, name, panel };
|
|
453
|
+
} catch (e: any) {
|
|
454
|
+
// Surface the structured code (invalid_magi_panel: … / magi_panel_exists: …)
|
|
455
|
+
// verbatim so the editor can map it to a field-level message.
|
|
456
|
+
return { success: false, error: e.message };
|
|
457
|
+
}
|
|
458
|
+
},
|
|
459
|
+
|
|
460
|
+
magi_panel_remove: async (_ctx: MedFamilyContext, args: any) => {
|
|
461
|
+
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
462
|
+
if (!name) return { success: false, error: 'invalid_magi_panel: panel name is required' };
|
|
463
|
+
try {
|
|
464
|
+
const { removeMagiPanel } = await import('../../config/mesh-config.js');
|
|
465
|
+
const removed = removeMagiPanel(name);
|
|
466
|
+
return { success: true, removed };
|
|
467
|
+
} catch (e: any) {
|
|
468
|
+
return { success: false, error: e.message };
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
|
|
472
|
+
// ─── MAGI kind → panel bindings (MAGI-KIND-PANEL, machine-local config) ───
|
|
473
|
+
// Per-task_kind slot lists in ~/.adhdev/meshes.json `magiKindPanels`. Same
|
|
474
|
+
// owner-only gating and structured-error precedent as the magi_panel_* handlers
|
|
475
|
+
// above (not listed in canPeerUsePrivilegedShareCommand → owner-only). set/remove
|
|
476
|
+
// are WRITE commands; list is read-only. normalizeMagiSlots (inside setMagiKindPanel)
|
|
477
|
+
// surfaces invalid_magi_kind_panel: … messages verbatim for the editor.
|
|
478
|
+
magi_kind_panel_list: async (_ctx: MedFamilyContext, _args: any) => {
|
|
479
|
+
try {
|
|
480
|
+
const { listMagiKindPanels } = await import('../../config/mesh-config.js');
|
|
481
|
+
return { success: true, kindPanels: listMagiKindPanels() };
|
|
482
|
+
} catch (e: any) {
|
|
483
|
+
return { success: false, error: e.message };
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
magi_kind_panel_set: async (_ctx: MedFamilyContext, args: any) => {
|
|
488
|
+
const kind = typeof args?.kind === 'string' ? args.kind.trim() : '';
|
|
489
|
+
if (!kind) return { success: false, error: 'invalid_magi_kind_panel: task_kind is required' };
|
|
490
|
+
try {
|
|
491
|
+
const { setMagiKindPanel } = await import('../../config/mesh-config.js');
|
|
492
|
+
// normalizeMagiTaskKindKey + normalizeMagiSlots (inside setMagiKindPanel)
|
|
493
|
+
// validate the kind and each slot (provider required; model/nodeId optional;
|
|
494
|
+
// replica counts clamped). Structured errors flow back as `error`.
|
|
495
|
+
const slots = setMagiKindPanel(kind, args?.slots);
|
|
496
|
+
return { success: true, kind, slots };
|
|
497
|
+
} catch (e: any) {
|
|
498
|
+
return { success: false, error: e.message };
|
|
499
|
+
}
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
magi_kind_panel_remove: async (_ctx: MedFamilyContext, args: any) => {
|
|
503
|
+
const kind = typeof args?.kind === 'string' ? args.kind.trim() : '';
|
|
504
|
+
if (!kind) return { success: false, error: 'invalid_magi_kind_panel: task_kind is required' };
|
|
505
|
+
try {
|
|
506
|
+
const { removeMagiKindPanel } = await import('../../config/mesh-config.js');
|
|
507
|
+
const removed = removeMagiKindPanel(kind);
|
|
508
|
+
return { success: true, removed };
|
|
509
|
+
} catch (e: any) {
|
|
510
|
+
return { success: false, error: e.message };
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
add_mesh_node: async (ctx: MedFamilyContext, args: any) => {
|
|
515
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
516
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
517
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
518
|
+
if (!workspace) return { success: false, error: 'workspace required' };
|
|
519
|
+
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node addition');
|
|
520
|
+
if (ownerFailure) return ownerFailure;
|
|
521
|
+
try {
|
|
522
|
+
const { addNode } = await import('../../config/mesh-config.js');
|
|
523
|
+
const providerPriority = Array.isArray(args?.providerPriority)
|
|
524
|
+
? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
|
|
525
|
+
: [];
|
|
526
|
+
const readOnly = args?.readOnly === true;
|
|
527
|
+
const providerRoles = normalizeProviderRoles(args?.providerRoles);
|
|
528
|
+
const policy = {
|
|
529
|
+
...(readOnly ? { readOnly: true } : {}),
|
|
530
|
+
...(providerPriority.length ? { providerPriority } : {}),
|
|
531
|
+
...(providerRoles.length ? { providerRoles } : {}),
|
|
532
|
+
};
|
|
533
|
+
const role = normalizeMeshDaemonRole(args?.role);
|
|
534
|
+
const daemonId = typeof args?.daemonId === 'string' && args.daemonId.trim() ? args.daemonId.trim() : undefined;
|
|
535
|
+
const machineId = typeof args?.machineId === 'string' && args.machineId.trim() ? args.machineId.trim() : undefined;
|
|
536
|
+
const repoRoot = typeof args?.repoRoot === 'string' && args.repoRoot.trim() ? args.repoRoot.trim() : undefined;
|
|
537
|
+
const node = addNode(meshId, {
|
|
538
|
+
workspace,
|
|
539
|
+
...(repoRoot ? { repoRoot } : {}),
|
|
540
|
+
...(daemonId ? { daemonId } : {}),
|
|
541
|
+
...(machineId ? { machineId } : {}),
|
|
542
|
+
...(policy ? { policy } : {}),
|
|
543
|
+
...(role ? { role } : {}),
|
|
544
|
+
});
|
|
545
|
+
if (!node) return { success: false, error: 'Mesh not found' };
|
|
546
|
+
// mesh_status hands back a coordinator-memory aggregate
|
|
547
|
+
// snapshot keyed on (meshId, queueRevision). Adding a
|
|
548
|
+
// node touches neither, so without an explicit cache
|
|
549
|
+
// bust the dashboard graph keeps rendering the pre-add
|
|
550
|
+
// node list (empty for a fresh mesh) even after the
|
|
551
|
+
// user clicks Refresh.
|
|
552
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
553
|
+
return { success: true, node };
|
|
554
|
+
} catch (e: any) {
|
|
555
|
+
return { success: false, error: e.message };
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
update_mesh_node: async (ctx: MedFamilyContext, args: any) => {
|
|
560
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
561
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
562
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
563
|
+
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node update');
|
|
564
|
+
if (ownerFailure) return ownerFailure;
|
|
565
|
+
try {
|
|
566
|
+
const { updateNode } = await import('../../config/mesh-config.js');
|
|
567
|
+
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
568
|
+
? { ...(args.policy as Record<string, unknown>) }
|
|
569
|
+
: {};
|
|
570
|
+
if (Array.isArray(args?.providerPriority)) {
|
|
571
|
+
const providerPriority = args.providerPriority
|
|
572
|
+
.map((type: any) => typeof type === 'string' ? type.trim() : '')
|
|
573
|
+
.filter(Boolean);
|
|
574
|
+
delete (policy as any).provider_priority;
|
|
575
|
+
if (providerPriority.length) {
|
|
576
|
+
(policy as any).providerPriority = providerPriority;
|
|
577
|
+
} else {
|
|
578
|
+
delete (policy as any).providerPriority;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
// providerRoles: per-(node, provider) role label + maxParallel cap.
|
|
582
|
+
// Passing an explicit (possibly empty) array clears/replaces the
|
|
583
|
+
// declarations; omitting the arg leaves any value already on policy
|
|
584
|
+
// untouched (a full policy object passed by the caller still carries it).
|
|
585
|
+
if (Array.isArray(args?.providerRoles)) {
|
|
586
|
+
const providerRoles = normalizeProviderRoles(args.providerRoles);
|
|
587
|
+
if (providerRoles.length) {
|
|
588
|
+
(policy as any).providerRoles = providerRoles;
|
|
589
|
+
} else {
|
|
590
|
+
delete (policy as any).providerRoles;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const patch: Record<string, unknown> = { policy: policy as any };
|
|
594
|
+
if (typeof args?.systemPrompt === 'string') {
|
|
595
|
+
const trimmed = (args.systemPrompt as string).trim();
|
|
596
|
+
patch.systemPrompt = trimmed || undefined;
|
|
597
|
+
} else if (args?.systemPrompt === null) {
|
|
598
|
+
patch.systemPrompt = undefined;
|
|
599
|
+
}
|
|
600
|
+
const node = updateNode(meshId, nodeId, patch as any);
|
|
601
|
+
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
602
|
+
// Provider priority / systemPrompt changes don't touch
|
|
603
|
+
// the queue revision, so without a manual bust the
|
|
604
|
+
// cached aggregate keeps surfacing pre-update values
|
|
605
|
+
// (priority chip, coordinator prompt preview, etc.).
|
|
606
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
607
|
+
return { success: true, node };
|
|
608
|
+
} catch (e: any) {
|
|
609
|
+
return { success: false, error: e.message };
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
|
|
613
|
+
cleanup_mesh_sessions: async (ctx: MedFamilyContext, args: any) => {
|
|
614
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
615
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
616
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
617
|
+
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'node removal');
|
|
618
|
+
if (ownerFailure) return ownerFailure;
|
|
619
|
+
try {
|
|
620
|
+
// preferInline so inline-cache-only clone nodes resolve (matches owner check above).
|
|
621
|
+
const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
622
|
+
const mesh = meshRecord?.mesh;
|
|
623
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
624
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
625
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
626
|
+
const mode = ctx.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
|
|
627
|
+
const sessionIds = Array.isArray(args?.sessionIds)
|
|
628
|
+
? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
|
|
629
|
+
: undefined;
|
|
630
|
+
// MAGI post-review auto-cleanup routes through this same command with
|
|
631
|
+
// source:'magi_session_cleanup' and a per-session autoLaunchedForQueueTaskId
|
|
632
|
+
// map, which gates cleanup to sessions THIS fan-out actually auto-launched.
|
|
633
|
+
const source = args?.source === 'magi_session_cleanup' ? 'magi_session_cleanup' : 'mesh_cleanup_sessions';
|
|
634
|
+
const requireAutoLaunchedForTaskIds = (args?.requireAutoLaunchedForTaskIds
|
|
635
|
+
&& typeof args.requireAutoLaunchedForTaskIds === 'object'
|
|
636
|
+
&& !Array.isArray(args.requireAutoLaunchedForTaskIds))
|
|
637
|
+
? args.requireAutoLaunchedForTaskIds as Record<string, string>
|
|
638
|
+
: undefined;
|
|
639
|
+
// Opt-in orphan reclaim (SESSION-ACCUMULATION-LEAK). The live-node id set
|
|
640
|
+
// is the CURRENT mesh membership; a matched live session bound to a node
|
|
641
|
+
// still in this set is an active sibling and is never reclaimed. Only when
|
|
642
|
+
// the caller passes reclaimOrphans:true does the router loosen the
|
|
643
|
+
// shared-daemon guard for workspace-only / dead-node-bound live sessions.
|
|
644
|
+
const reclaimOrphans = args?.reclaimOrphans === true;
|
|
645
|
+
const liveMeshNodeIds = Array.isArray(mesh?.nodes)
|
|
646
|
+
? mesh.nodes.map((n: any) => normalizeMeshNodeId(n)).filter(Boolean) as string[]
|
|
647
|
+
: [];
|
|
648
|
+
const result = await ctx.cleanupMeshSessions({
|
|
649
|
+
meshId,
|
|
650
|
+
nodeId,
|
|
651
|
+
node,
|
|
652
|
+
mode,
|
|
653
|
+
sessionIds,
|
|
654
|
+
dryRun: args?.dryRun === true,
|
|
655
|
+
source,
|
|
656
|
+
requireAutoLaunchedForTaskIds,
|
|
657
|
+
reclaimOrphans,
|
|
658
|
+
liveMeshNodeIds,
|
|
659
|
+
});
|
|
660
|
+
return result;
|
|
661
|
+
} catch (e: any) {
|
|
662
|
+
return { success: false, error: e.message };
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
|
|
666
|
+
remove_mesh_node: async (ctx: MedFamilyContext, args: any) => {
|
|
667
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
668
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
669
|
+
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
670
|
+
try {
|
|
671
|
+
// preferInline so removal can resolve inline-cache-only clone worktree nodes.
|
|
672
|
+
const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
673
|
+
const mesh = meshRecord?.mesh;
|
|
674
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
675
|
+
|
|
676
|
+
// Guard: refuse to remove the coordinator's OWN local base node
|
|
677
|
+
// (same machine, NOT a worktree). Removing it breaks live mesh
|
|
678
|
+
// membership — the coordinator can no longer be reached and has
|
|
679
|
+
// to be restarted. Worktree clones are always safe to remove;
|
|
680
|
+
// only the non-worktree node bound to this daemon is protected.
|
|
681
|
+
// An explicit force:true overrides for intentional mesh teardown.
|
|
682
|
+
if (node && !args?._meshDirectDispatch && node.isLocalWorktree !== true && args?.force !== true) {
|
|
683
|
+
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : '';
|
|
684
|
+
const nodeMachineId = readMeshNodeMachineId(node as Record<string, unknown>) || '';
|
|
685
|
+
const selfDaemonId = ctx.deps.statusInstanceId || '';
|
|
686
|
+
const selfMachineId = (() => { try { return loadConfig().machineId || ''; } catch { return ''; } })();
|
|
687
|
+
// Identity match is form-safe: a daemon answers to the same machine
|
|
688
|
+
// under interchangeable id forms (bare `mach_X`, cloud `daemon_mach_X`,
|
|
689
|
+
// standalone `standalone_mach_X`). statusInstanceId/loadConfig().machineId
|
|
690
|
+
// and the node's stored daemonId/machineId frequently hold DIFFERENT forms
|
|
691
|
+
// of the same machine, so a raw `===` would miss the self-match and let the
|
|
692
|
+
// coordinator delete its own live base node (the very accident this guard
|
|
693
|
+
// exists to prevent). daemonIdsEquivalent collapses every form to its
|
|
694
|
+
// machine core before comparing, so a same-machine match is caught
|
|
695
|
+
// regardless of which form each side carries. This only widens matches
|
|
696
|
+
// (every raw-`===` hit still matches) — fail-open → fail-closed.
|
|
697
|
+
const isCoordinatorBaseNode =
|
|
698
|
+
(!!selfDaemonId && (daemonIdsEquivalent(nodeDaemonId, selfDaemonId) || daemonIdsEquivalent(nodeMachineId, selfDaemonId)))
|
|
699
|
+
|| (!!selfMachineId && (daemonIdsEquivalent(nodeDaemonId, selfMachineId) || daemonIdsEquivalent(nodeMachineId, selfMachineId)));
|
|
700
|
+
if (isCoordinatorBaseNode) {
|
|
701
|
+
return {
|
|
702
|
+
success: false,
|
|
703
|
+
removed: false,
|
|
704
|
+
code: 'mesh_remove_coordinator_base_node_protected',
|
|
705
|
+
error: `Refusing to remove the coordinator's own base node '${typeof node.workspace === 'string' ? node.workspace : nodeId}'. `
|
|
706
|
+
+ `It is the local non-worktree node bound to this coordinator daemon; removing it breaks live mesh membership and forces a restart.`,
|
|
707
|
+
recoveryHint: 'Remove worktree clone nodes instead, or pass force:true only if you are intentionally tearing down this mesh and accept that the coordinator must be re-registered/restarted.',
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Default worktree session cleanup ON: when the caller OMITS a mode and the
|
|
713
|
+
// node is a local worktree, default to 'stop_and_delete' instead of the mesh
|
|
714
|
+
// policy ('preserve' by default). A worktree's chat session has no reason to
|
|
715
|
+
// outlive the worktree's node + directory + branch, and leaving it on
|
|
716
|
+
// 'preserve' is what orphaned chats after a worktree remove. An explicit mode
|
|
717
|
+
// (including an explicit 'preserve') is always honored — only the OMITTED case
|
|
718
|
+
// changes, and only for worktrees. Base nodes keep arg ?? policy ?? preserve.
|
|
719
|
+
const explicitCleanupMode = args?.sessionCleanupMode ?? args?.session_cleanup_mode;
|
|
720
|
+
const sessionCleanupMode = ctx.normalizeMeshSessionCleanupMode(
|
|
721
|
+
explicitCleanupMode
|
|
722
|
+
?? (node?.isLocalWorktree === true ? 'stop_and_delete' : undefined)
|
|
723
|
+
?? mesh?.policy?.sessionCleanupOnNodeRemove,
|
|
724
|
+
);
|
|
725
|
+
// Explicit sessionIds (e.g. supplied by refine auto-cleanup) bypass the
|
|
726
|
+
// workspace-only-match guard so a delegate session that lacks a
|
|
727
|
+
// meta.meshNodeId binding can still be stopped/deleted.
|
|
728
|
+
const explicitSessionIds = Array.isArray(args?.sessionIds)
|
|
729
|
+
? (args.sessionIds as unknown[]).filter((v): v is string => typeof v === 'string' && v.trim().length > 0).map(v => v.trim())
|
|
730
|
+
: undefined;
|
|
731
|
+
// Precheck-first: for a LOCAL worktree removal, validate removability
|
|
732
|
+
// with a purely non-destructive precheck BEFORE touching the session.
|
|
733
|
+
// The session cleanup below is destructive and irreversible
|
|
734
|
+
// (stop_and_delete), so a refusal that fires only AFTER it — as the old
|
|
735
|
+
// ordering did when removeWorktree rejected a dirty worktree — orphaned
|
|
736
|
+
// the delegated session. Running the precheck here means a refusal
|
|
737
|
+
// (dirty worktree, missing/mismatched metadata, etc.) returns with the
|
|
738
|
+
// session left fully intact. Remote-forwarded worktrees are prechecked
|
|
739
|
+
// on the owning daemon (it runs this same handler), so we only gate the
|
|
740
|
+
// local case here. Success/skip cases return ok:true and fall through to
|
|
741
|
+
// the unchanged normal flow.
|
|
742
|
+
if (node?.isLocalWorktree) {
|
|
743
|
+
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
744
|
+
const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, ctx.deps.statusInstanceId) && ctx.deps.dispatchMeshCommand
|
|
745
|
+
&& !args?._meshDirectDispatch;
|
|
746
|
+
if (!isRemoteWorktree) {
|
|
747
|
+
const precheck = await ctx.precheckLocalWorktreeRemovable({ mesh, node, nodeId, force: args?.force === true });
|
|
748
|
+
if (precheck.ok === false) {
|
|
749
|
+
return {
|
|
750
|
+
success: false,
|
|
751
|
+
removed: false,
|
|
752
|
+
code: precheck.code,
|
|
753
|
+
error: precheck.error,
|
|
754
|
+
recoveryHint: precheck.recoveryHint,
|
|
755
|
+
// No sessionCleanup key: the session was deliberately NOT
|
|
756
|
+
// touched. worktreeCleanup mirrors the destructive path's
|
|
757
|
+
// refusal shape so existing callers see the same code.
|
|
758
|
+
worktreeCleanup: { success: false, code: precheck.code, error: precheck.error, recoveryHint: precheck.recoveryHint },
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
let sessionCleanup: Record<string, unknown> | undefined;
|
|
765
|
+
if (node && sessionCleanupMode !== 'preserve') {
|
|
766
|
+
sessionCleanup = await ctx.cleanupMeshSessions({
|
|
767
|
+
meshId,
|
|
768
|
+
nodeId,
|
|
769
|
+
node,
|
|
770
|
+
mode: sessionCleanupMode,
|
|
771
|
+
...(explicitSessionIds && explicitSessionIds.length > 0 ? { sessionIds: explicitSessionIds } : {}),
|
|
772
|
+
source: 'mesh_remove_node',
|
|
773
|
+
});
|
|
774
|
+
if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
let worktreeCleanup: Record<string, unknown> | undefined;
|
|
778
|
+
if (node?.isLocalWorktree) {
|
|
779
|
+
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
780
|
+
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
781
|
+
// clean up locally, do not forward. Equivalent → local.
|
|
782
|
+
const isRemoteWorktree = nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, ctx.deps.statusInstanceId) && ctx.deps.dispatchMeshCommand
|
|
783
|
+
&& !args?._meshDirectDispatch;
|
|
784
|
+
if (isRemoteWorktree) {
|
|
785
|
+
// Worktree lives on a different machine — ask that daemon to clean it up.
|
|
786
|
+
// _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
|
|
787
|
+
const forwarded = await ctx.deps.dispatchMeshCommand!(nodeDaemonId!, 'remove_mesh_node', {
|
|
788
|
+
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
789
|
+
_meshDirectDispatch: true,
|
|
790
|
+
});
|
|
791
|
+
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
792
|
+
}
|
|
793
|
+
const cleanupResult = await ctx.cleanupLocalWorktreeNode({ mesh, node, nodeId, force: args?.force === true });
|
|
794
|
+
// De-gating: membership removal is NOT gated on the worktree
|
|
795
|
+
// directory actually being deleted. cleanupLocalWorktreeNode now
|
|
796
|
+
// returns success:true (with a residue flag) whenever the path is
|
|
797
|
+
// proven managed and the only remaining problem is leftover
|
|
798
|
+
// directory bytes (e.g. Windows EINVAL). A success:false here means
|
|
799
|
+
// a genuinely-unsafe condition — missing metadata, a non-managed /
|
|
800
|
+
// unexpected path, a branch mismatch, a dirty worktree, or an
|
|
801
|
+
// unverified force fallback — and those still block removal.
|
|
802
|
+
if (cleanupResult.success === false) {
|
|
803
|
+
return {
|
|
804
|
+
success: false,
|
|
805
|
+
removed: false,
|
|
806
|
+
code: cleanupResult.code,
|
|
807
|
+
error: cleanupResult.error,
|
|
808
|
+
recoveryHint: cleanupResult.recoveryHint,
|
|
809
|
+
...(sessionCleanup ? { sessionCleanup } : {}),
|
|
810
|
+
worktreeCleanup: cleanupResult,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
worktreeCleanup = cleanupResult;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
let removed = false;
|
|
817
|
+
if (meshRecord?.inline) {
|
|
818
|
+
removed = ctx.removeInlineMeshNode(meshId, mesh, nodeId);
|
|
819
|
+
// Inline meshes share the same aggregate snapshot cache as
|
|
820
|
+
// local-config meshes; without this bust the removed node
|
|
821
|
+
// keeps showing up in the dashboard graph until the cache
|
|
822
|
+
// ages out on its own.
|
|
823
|
+
if (removed) ctx.invalidateAggregateMeshStatus(meshId);
|
|
824
|
+
// MESH-INLINE-NODE-RESURRECTION: removeInlineMeshNode only mutates
|
|
825
|
+
// the in-memory inlineMeshCache + tombstones — it does NOT touch
|
|
826
|
+
// the file-backed config (meshes.json). When the SAME node also
|
|
827
|
+
// lives in meshes.json (a worktree node that was persisted to the
|
|
828
|
+
// file config, then resolved through the inline cache on removal),
|
|
829
|
+
// the file record survives. On daemon restart the in-memory
|
|
830
|
+
// tombstone is lost, getMesh falls back to the file config, and
|
|
831
|
+
// mesh-status renders the dead node again. So a node present in
|
|
832
|
+
// BOTH the inline cache AND meshes.json must be spliced+saved from
|
|
833
|
+
// the file config here too. Pure-inline meshes (no matching file
|
|
834
|
+
// mesh/node) are untouched — getMesh returns undefined and we skip.
|
|
835
|
+
try {
|
|
836
|
+
const { getMesh, removeNode } = await import('../../config/mesh-config.js');
|
|
837
|
+
const fileMesh = getMesh(meshId);
|
|
838
|
+
const fileNode = fileMesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
839
|
+
if (fileNode?.id) {
|
|
840
|
+
const fileRemoved = removeNode(meshId, fileNode.id);
|
|
841
|
+
if (fileRemoved) {
|
|
842
|
+
removed = true;
|
|
843
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} catch { /* file config absent / unreadable — inline-only mesh, nothing to durably delete */ }
|
|
847
|
+
// Node was already absent from the inline mesh (e.g. removed by a
|
|
848
|
+
// prior refine cleanup). Treat as removed so caller gets removed:true.
|
|
849
|
+
if (!removed && !node) removed = true;
|
|
850
|
+
} else {
|
|
851
|
+
const { removeNode } = await import('../../config/mesh-config.js');
|
|
852
|
+
removed = removeNode(meshId, nodeId);
|
|
853
|
+
// Node already absent from config (e.g. removed by a prior refine
|
|
854
|
+
// cleanup after a successful Refinery merge). Treat as removed so
|
|
855
|
+
// the response is accurate.
|
|
856
|
+
if (!removed && !node) removed = true;
|
|
857
|
+
if (removed) ctx.invalidateAggregateMeshStatus(meshId);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Record in task ledger
|
|
861
|
+
if (removed) {
|
|
862
|
+
try {
|
|
863
|
+
const { appendLedgerEntry } = await import('../../mesh/mesh-ledger.js');
|
|
864
|
+
appendLedgerEntry(meshId, {
|
|
865
|
+
kind: 'node_removed',
|
|
866
|
+
nodeId,
|
|
867
|
+
payload: {
|
|
868
|
+
worktree: !!node?.isLocalWorktree,
|
|
869
|
+
sessionCleanupMode,
|
|
870
|
+
workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
|
|
871
|
+
daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
|
|
872
|
+
worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
|
|
873
|
+
worktreeCleanupFallback: typeof worktreeCleanup?.fallback === 'string' ? worktreeCleanup.fallback : undefined,
|
|
874
|
+
forced: worktreeCleanup?.forced === true ? true : undefined,
|
|
875
|
+
forceFallbackReason: typeof worktreeCleanup?.reason === 'string' ? worktreeCleanup.reason : undefined,
|
|
876
|
+
branchRefDeleted: typeof worktreeCleanup?.branchRefDeleted === 'boolean' ? worktreeCleanup.branchRefDeleted : undefined,
|
|
877
|
+
branchRefReason: typeof worktreeCleanup?.branchRefReason === 'string' ? worktreeCleanup.branchRefReason : undefined,
|
|
878
|
+
},
|
|
879
|
+
});
|
|
880
|
+
} catch { /* ledger append is best-effort */ }
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Surface leftover-directory residue at the top level so callers
|
|
884
|
+
// see the node was dropped from the mesh even though the worktree
|
|
885
|
+
// directory could not be fully removed (best-effort, non-gating).
|
|
886
|
+
const residueWarning = worktreeCleanup?.residue === true && typeof worktreeCleanup?.residueWarning === 'string'
|
|
887
|
+
? worktreeCleanup.residueWarning
|
|
888
|
+
: undefined;
|
|
889
|
+
|
|
890
|
+
// Surface a preserved-branch warning at the top level so callers see
|
|
891
|
+
// that the branch ref was intentionally NOT deleted (unmerged work is
|
|
892
|
+
// never silently dropped). When the branch ref WAS deleted, the nested
|
|
893
|
+
// worktreeCleanup.branchRefDeleted flag already records it.
|
|
894
|
+
const branchRefWarning = typeof worktreeCleanup?.branchRefWarning === 'string'
|
|
895
|
+
? worktreeCleanup.branchRefWarning
|
|
896
|
+
: undefined;
|
|
897
|
+
|
|
898
|
+
// Orphan guard: if the session cleanup still left any LIVE session skipped
|
|
899
|
+
// (e.g. a future skip reason, or a workspace-only session on a base node),
|
|
900
|
+
// surface it at the top level so the caller knows a manual mesh_cleanup_sessions
|
|
901
|
+
// is still required for those sessionIds. Without this, a skipped-live session
|
|
902
|
+
// silently outlives a removed node (the very NODE-REMOVE-SESSION-ORPHAN bug).
|
|
903
|
+
const skippedLiveSessionIds = Array.isArray(sessionCleanup?.skippedLiveSessionIds)
|
|
904
|
+
? (sessionCleanup!.skippedLiveSessionIds as unknown[]).filter((v): v is string => typeof v === 'string')
|
|
905
|
+
: [];
|
|
906
|
+
const orphanedSessionsRemaining = skippedLiveSessionIds.length > 0;
|
|
907
|
+
const orphanNextAction = orphanedSessionsRemaining
|
|
908
|
+
? `Live session(s) [${skippedLiveSessionIds.join(', ')}] were skipped and still survive this node removal. `
|
|
909
|
+
+ `Run mesh_cleanup_sessions with mode:'stop_and_delete' and sessionIds:[${skippedLiveSessionIds.map(id => `'${id}'`).join(', ')}] to release them.`
|
|
910
|
+
: undefined;
|
|
911
|
+
return {
|
|
912
|
+
success: true,
|
|
913
|
+
removed,
|
|
914
|
+
...(residueWarning ? { residueWarning } : {}),
|
|
915
|
+
...(branchRefWarning ? { branchRefWarning } : {}),
|
|
916
|
+
...(sessionCleanup ? { sessionCleanup } : {}),
|
|
917
|
+
...(worktreeCleanup ? { worktreeCleanup } : {}),
|
|
918
|
+
...(orphanedSessionsRemaining
|
|
919
|
+
? { orphanedSessionsRemaining: true, nextAction: orphanNextAction }
|
|
920
|
+
: {}),
|
|
921
|
+
};
|
|
922
|
+
} catch (e: any) {
|
|
923
|
+
return { success: false, error: e.message };
|
|
924
|
+
}
|
|
925
|
+
},
|
|
926
|
+
|
|
927
|
+
clone_mesh_node: async (ctx: MedFamilyContext, args: any) => {
|
|
928
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
929
|
+
const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
|
|
930
|
+
const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
|
|
931
|
+
const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
|
|
932
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
933
|
+
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
934
|
+
if (!branch) return { success: false, error: 'branch required' };
|
|
935
|
+
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'worktree clone');
|
|
936
|
+
if (ownerFailure) return ownerFailure;
|
|
937
|
+
|
|
938
|
+
try {
|
|
939
|
+
// Resolve with preferInline so the clone writes the new node into the
|
|
940
|
+
// same representation that get_mesh reads back. The MCP coordinator
|
|
941
|
+
// passes inlineMesh on every mesh command, so when it owns an inline
|
|
942
|
+
// mesh the membership read path (get_mesh, preferInline: true) returns
|
|
943
|
+
// the inline cache. Without preferInline here, clone could resolve to a
|
|
944
|
+
// local-config mesh and write the node only to config — leaving the
|
|
945
|
+
// inline cache (and therefore get_mesh / refreshMeshFromDaemon) without
|
|
946
|
+
// the node, so the new worktree node is never visible in live mesh
|
|
947
|
+
// membership even though worktree_bootstrap_complete fires.
|
|
948
|
+
const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
949
|
+
const mesh = meshRecord?.mesh;
|
|
950
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
951
|
+
|
|
952
|
+
const sourceNode = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, sourceNodeId));
|
|
953
|
+
if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
|
|
954
|
+
|
|
955
|
+
// Forward to the source node's daemon if it's on a different machine.
|
|
956
|
+
// _meshDirectDispatch prevents infinite re-forwarding when the stored daemonId
|
|
957
|
+
// uses a legacy format that doesn't match the receiving daemon's statusInstanceId.
|
|
958
|
+
const sourceDaemonId = typeof sourceNode.daemonId === 'string' ? sourceNode.daemonId.trim() : undefined;
|
|
959
|
+
// daemonIdsEquivalent: an equivalent-form source daemonId is this machine —
|
|
960
|
+
// clone locally, do not forward. Equivalent → local.
|
|
961
|
+
if (sourceDaemonId && !daemonIdsEquivalent(sourceDaemonId, ctx.deps.statusInstanceId) && ctx.deps.dispatchMeshCommand
|
|
962
|
+
&& !args?._meshDirectDispatch) {
|
|
963
|
+
const forwarded = await ctx.deps.dispatchMeshCommand(sourceDaemonId, 'clone_mesh_node', {
|
|
964
|
+
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
965
|
+
_meshDirectDispatch: true,
|
|
966
|
+
});
|
|
967
|
+
// FALSE-BLOCKER-CLONE-QUEUE: open the transient grace window for the freshly
|
|
968
|
+
// cloned node on THIS coordinator daemon. The clone ran (and wrote the inline-
|
|
969
|
+
// cache node) on the remote source daemon; its inline entry has not propagated
|
|
970
|
+
// here yet, so a queue task pinned to it would transiently look like a permanent
|
|
971
|
+
// 'target_node_id_unmatched'. Recording its id marks the unmatch as transient.
|
|
972
|
+
const forwardedNodeId = (forwarded as { node?: { id?: unknown } } | null | undefined)?.node?.id;
|
|
973
|
+
if (typeof forwardedNodeId === 'string' && forwardedNodeId) noteRecentlyClonedNode(forwardedNodeId);
|
|
974
|
+
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
|
|
978
|
+
const { createWorktree } = await import('../../git/git-worktree.js');
|
|
979
|
+
const result = await createWorktree({
|
|
980
|
+
repoRoot,
|
|
981
|
+
branch,
|
|
982
|
+
baseBranch,
|
|
983
|
+
meshName: mesh.name,
|
|
984
|
+
});
|
|
985
|
+
if (result.baseSync?.warning) {
|
|
986
|
+
console.warn(`[mesh] clone_mesh_node base sync (${result.baseSync.action}): ${result.baseSync.warning}`);
|
|
987
|
+
} else if (result.baseSync && result.baseSync.action !== 'up_to_date') {
|
|
988
|
+
console.log(`[mesh] clone_mesh_node base sync: ${result.baseSync.action} (startRef=${result.baseSync.startRef})`);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
let node: any;
|
|
992
|
+
if (meshRecord.inline) {
|
|
993
|
+
const { randomUUID } = await import('crypto');
|
|
994
|
+
node = {
|
|
995
|
+
id: `node_${randomUUID().replace(/-/g, '')}`,
|
|
996
|
+
workspace: result.worktreePath,
|
|
997
|
+
repoRoot: result.worktreePath,
|
|
998
|
+
daemonId: sourceNode.daemonId,
|
|
999
|
+
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
1000
|
+
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
1001
|
+
policy: { ...(sourceNode.policy || {}) },
|
|
1002
|
+
isLocalWorktree: true,
|
|
1003
|
+
worktreeBranch: result.branch,
|
|
1004
|
+
clonedFromNodeId: sourceNodeId,
|
|
1005
|
+
};
|
|
1006
|
+
ctx.updateInlineMeshNode(meshId, mesh, node);
|
|
1007
|
+
} else {
|
|
1008
|
+
const { addNode } = await import('../../config/mesh-config.js');
|
|
1009
|
+
node = addNode(meshId, {
|
|
1010
|
+
workspace: result.worktreePath,
|
|
1011
|
+
repoRoot: result.worktreePath,
|
|
1012
|
+
daemonId: sourceNode.daemonId,
|
|
1013
|
+
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
1014
|
+
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
1015
|
+
isLocalWorktree: true,
|
|
1016
|
+
worktreeBranch: result.branch,
|
|
1017
|
+
clonedFromNodeId: sourceNodeId,
|
|
1018
|
+
policy: { ...(sourceNode.policy || {}) },
|
|
1019
|
+
});
|
|
1020
|
+
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
1021
|
+
// Also reconcile the freshly-registered node into any warmed inline
|
|
1022
|
+
// cache for this mesh. get_mesh (preferInline: true) reads the inline
|
|
1023
|
+
// cache first when one exists; if we only wrote to local config the
|
|
1024
|
+
// node would be invisible to membership reads. updateInlineMeshNode is
|
|
1025
|
+
// a no-op when no inline cache is present.
|
|
1026
|
+
const inlineForReconcile = ctx.getCachedInlineMesh(meshId);
|
|
1027
|
+
if (inlineForReconcile) ctx.updateInlineMeshNode(meshId, inlineForReconcile, node);
|
|
1028
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// FALSE-BLOCKER-CLONE-QUEUE: open the transient grace window for the freshly cloned
|
|
1032
|
+
// node. A queue task pinned to it (target_node pin) enqueued before bootstrap
|
|
1033
|
+
// completes / the inline-cache entry fully settles must be classified as a transient
|
|
1034
|
+
// skip, not a permanent 'target_node_id_unmatched' actionable blocker.
|
|
1035
|
+
if (typeof node?.id === 'string' && node.id) noteRecentlyClonedNode(node.id);
|
|
1036
|
+
|
|
1037
|
+
const persistWorktreeSetupState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
1038
|
+
node.worktreeBootstrap = bootstrapState;
|
|
1039
|
+
if (meshRecord.inline) {
|
|
1040
|
+
ctx.updateInlineMeshNode(meshId, mesh, node);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
const { updateNode } = await import('../../config/mesh-config.js');
|
|
1045
|
+
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
1046
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
1047
|
+
} catch { /* bootstrap status persistence is best-effort */ }
|
|
1048
|
+
};
|
|
1049
|
+
|
|
1050
|
+
const appendCloneLedger = async (initSubmodules: boolean, bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
1051
|
+
try {
|
|
1052
|
+
const { appendLedgerEntry } = await import('../../mesh/mesh-ledger.js');
|
|
1053
|
+
appendLedgerEntry(meshId, {
|
|
1054
|
+
kind: 'node_cloned',
|
|
1055
|
+
nodeId: node.id,
|
|
1056
|
+
payload: {
|
|
1057
|
+
sourceNodeId,
|
|
1058
|
+
branch: result.branch,
|
|
1059
|
+
worktreePath: result.worktreePath,
|
|
1060
|
+
submodulesInitialized: initSubmodules,
|
|
1061
|
+
worktreeBootstrap: {
|
|
1062
|
+
status: bootstrapState.status,
|
|
1063
|
+
required: bootstrapState.required,
|
|
1064
|
+
configSource: bootstrapState.configSource,
|
|
1065
|
+
configSourceType: bootstrapState.configSourceType,
|
|
1066
|
+
lastCommand: bootstrapState.lastCommand,
|
|
1067
|
+
exitCode: bootstrapState.exitCode,
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1070
|
+
});
|
|
1071
|
+
} catch { /* ledger append is best-effort */ }
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
const initSubmodules = (sourceNode.policy as any)?.initSubmodulesOnClone !== false;
|
|
1075
|
+
// Read the worktree bootstrap config through the unified RepoSettings
|
|
1076
|
+
// loader (file-separated `.adhdev/worktree_bootstrap.json`; machine-local
|
|
1077
|
+
// inline seam honored). runMeshWorktreeBootstrap below re-loads it to run.
|
|
1078
|
+
const loadedBootstrap = loadRepoSettings({ workspace: result.worktreePath, mesh }).worktreeBootstrap;
|
|
1079
|
+
const runningBootstrapState: WorktreeBootstrapState = {
|
|
1080
|
+
status: 'running',
|
|
1081
|
+
required: loadedBootstrap.config?.required !== false,
|
|
1082
|
+
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
1083
|
+
configSourceType: loadedBootstrap.sourceType,
|
|
1084
|
+
startedAt: new Date().toISOString(),
|
|
1085
|
+
};
|
|
1086
|
+
await persistWorktreeSetupState(runningBootstrapState);
|
|
1087
|
+
|
|
1088
|
+
const finishWorktreeSetup = async (): Promise<{ submodulesInitialized: boolean; bootstrapState: WorktreeBootstrapState }> => {
|
|
1089
|
+
let submodulesInitialized = false;
|
|
1090
|
+
if (initSubmodules) {
|
|
1091
|
+
try {
|
|
1092
|
+
const { runGit } = await import('../../git/git-executor.js');
|
|
1093
|
+
await runGit(
|
|
1094
|
+
{ workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
|
|
1095
|
+
['submodule', 'update', '--init', '--recursive'],
|
|
1096
|
+
{ timeoutMs: 120000 },
|
|
1097
|
+
);
|
|
1098
|
+
submodulesInitialized = true;
|
|
1099
|
+
|
|
1100
|
+
// Sync every registered submodule to the clone source node's
|
|
1101
|
+
// working HEAD (best-effort, generic over .gitmodules — no
|
|
1102
|
+
// hardcoded 'oss'; the rewind guard is applied per submodule).
|
|
1103
|
+
const sourceWorkspace = sourceNode.repoRoot || sourceNode.workspace;
|
|
1104
|
+
if (sourceWorkspace) {
|
|
1105
|
+
const { runGit: rg } = await import('../../git/git-executor.js');
|
|
1106
|
+
await syncClonedWorktreeSubmodules(result.worktreePath, sourceWorkspace, rg);
|
|
1107
|
+
}
|
|
1108
|
+
} catch (subErr: any) {
|
|
1109
|
+
// Submodule init is best-effort; don't fail the clone
|
|
1110
|
+
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
const bootstrapState: WorktreeBootstrapState = await runMeshWorktreeBootstrap(mesh, result.worktreePath);
|
|
1114
|
+
await persistWorktreeSetupState(bootstrapState);
|
|
1115
|
+
await appendCloneLedger(submodulesInitialized, bootstrapState);
|
|
1116
|
+
return { submodulesInitialized, bootstrapState };
|
|
1117
|
+
};
|
|
1118
|
+
|
|
1119
|
+
const requestedSetupWaitMs = Number(args?.setupWaitMs ?? args?.bootstrapWaitMs ?? 8000);
|
|
1120
|
+
const setupWaitMs = Number.isFinite(requestedSetupWaitMs)
|
|
1121
|
+
? Math.min(Math.max(requestedSetupWaitMs, 0), 14000)
|
|
1122
|
+
: 8000;
|
|
1123
|
+
const setupPromise = finishWorktreeSetup();
|
|
1124
|
+
const setupResult = await Promise.race([
|
|
1125
|
+
setupPromise.then((value) => ({ completed: true as const, value })),
|
|
1126
|
+
new Promise<{ completed: false }>((resolve) => setTimeout(() => resolve({ completed: false }), setupWaitMs)),
|
|
1127
|
+
]);
|
|
1128
|
+
|
|
1129
|
+
const emitBootstrapEvent = (eventStatus: 'bootstrap_complete' | 'bootstrap_failed', bootstrapState: WorktreeBootstrapState, startedAtMs: number, extraPayload?: Record<string, unknown>): void => {
|
|
1130
|
+
try {
|
|
1131
|
+
const durationMs = Date.now() - startedAtMs;
|
|
1132
|
+
const event = `worktree_${eventStatus}` as const;
|
|
1133
|
+
const metadataEvent = {
|
|
1134
|
+
source: 'clone_mesh_node_bootstrap',
|
|
1135
|
+
nodeId: node.id,
|
|
1136
|
+
status: eventStatus,
|
|
1137
|
+
worktreePath: result.worktreePath,
|
|
1138
|
+
durationMs,
|
|
1139
|
+
bootstrapStatus: bootstrapState.status,
|
|
1140
|
+
...(bootstrapState.error ? { error: bootstrapState.error } : {}),
|
|
1141
|
+
...(bootstrapState.exitCode !== undefined ? { exitCode: bootstrapState.exitCode } : {}),
|
|
1142
|
+
...(extraPayload || {}),
|
|
1143
|
+
};
|
|
1144
|
+
if (typeof ctx.deps.instanceManager?.getByCategory === 'function') {
|
|
1145
|
+
const forwarded = handleMeshForwardEvent(
|
|
1146
|
+
{ instanceManager: ctx.deps.instanceManager } as any,
|
|
1147
|
+
{ event, meshId, nodeId: node.id, workspace: result.worktreePath, metadataEvent },
|
|
1148
|
+
);
|
|
1149
|
+
if (forwarded?.success === true) return;
|
|
1150
|
+
}
|
|
1151
|
+
queuePendingMeshCoordinatorEvent({
|
|
1152
|
+
event,
|
|
1153
|
+
meshId,
|
|
1154
|
+
nodeLabel: node.id,
|
|
1155
|
+
nodeId: node.id,
|
|
1156
|
+
workspace: result.worktreePath,
|
|
1157
|
+
metadataEvent,
|
|
1158
|
+
queuedAt: Date.now(),
|
|
1159
|
+
});
|
|
1160
|
+
} catch { /* event emission is best-effort */ }
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
const bootstrapStartedMs = Date.now();
|
|
1164
|
+
|
|
1165
|
+
if (!setupResult.completed) {
|
|
1166
|
+
setupPromise
|
|
1167
|
+
.then(({ bootstrapState }) => {
|
|
1168
|
+
emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
|
|
1169
|
+
})
|
|
1170
|
+
.catch((error: any) => {
|
|
1171
|
+
const failedState: WorktreeBootstrapState = {
|
|
1172
|
+
...runningBootstrapState,
|
|
1173
|
+
status: 'failed',
|
|
1174
|
+
completedAt: new Date().toISOString(),
|
|
1175
|
+
error: error?.message || String(error),
|
|
1176
|
+
};
|
|
1177
|
+
void persistWorktreeSetupState(failedState);
|
|
1178
|
+
void appendCloneLedger(false, failedState);
|
|
1179
|
+
emitBootstrapEvent('bootstrap_failed', failedState, bootstrapStartedMs, { error: error?.message || String(error) });
|
|
1180
|
+
});
|
|
1181
|
+
return {
|
|
1182
|
+
success: true,
|
|
1183
|
+
async: true,
|
|
1184
|
+
status: 'accepted',
|
|
1185
|
+
node,
|
|
1186
|
+
worktreePath: result.worktreePath,
|
|
1187
|
+
branch: result.branch,
|
|
1188
|
+
...(result.baseSync ? { baseSync: result.baseSync } : {}),
|
|
1189
|
+
...(result.baseSync?.warning ? { baseStaleWarning: result.baseSync.warning } : {}),
|
|
1190
|
+
worktreeBootstrap: runningBootstrapState,
|
|
1191
|
+
worktreeSetup: {
|
|
1192
|
+
status: 'running',
|
|
1193
|
+
setupWaitMs,
|
|
1194
|
+
message: 'Worktree node is registered; submodule/bootstrap setup is continuing in the background.',
|
|
1195
|
+
},
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
const { submodulesInitialized, bootstrapState } = setupResult.value;
|
|
1200
|
+
emitBootstrapEvent('bootstrap_complete', bootstrapState, bootstrapStartedMs);
|
|
1201
|
+
return {
|
|
1202
|
+
success: true,
|
|
1203
|
+
node,
|
|
1204
|
+
worktreePath: result.worktreePath,
|
|
1205
|
+
branch: result.branch,
|
|
1206
|
+
...(result.baseSync ? { baseSync: result.baseSync } : {}),
|
|
1207
|
+
...(result.baseSync?.warning ? { baseStaleWarning: result.baseSync.warning } : {}),
|
|
1208
|
+
submodulesInitialized,
|
|
1209
|
+
worktreeBootstrap: bootstrapState,
|
|
1210
|
+
};
|
|
1211
|
+
} catch (e: any) {
|
|
1212
|
+
return { success: false, error: e.message };
|
|
1213
|
+
}
|
|
1214
|
+
},
|
|
1215
|
+
|
|
1216
|
+
retry_mesh_node_bootstrap: async (ctx: MedFamilyContext, args: any) => {
|
|
1217
|
+
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1218
|
+
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
1219
|
+
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1220
|
+
if (!nodeId) return { success: false, error: 'nodeId required' };
|
|
1221
|
+
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'bootstrap retry');
|
|
1222
|
+
if (ownerFailure) return ownerFailure;
|
|
1223
|
+
|
|
1224
|
+
try {
|
|
1225
|
+
// preferInline so bootstrap-retry can resolve inline-cache-only clone worktree nodes.
|
|
1226
|
+
const meshRecord = await ctx.getMeshForCommand(meshId, args?.inlineMesh, { preferInline: true });
|
|
1227
|
+
const mesh = meshRecord?.mesh;
|
|
1228
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
1229
|
+
|
|
1230
|
+
const node = mesh.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
1231
|
+
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
1232
|
+
if (!node.isLocalWorktree) return { success: false, error: 'Node is not a local worktree node' };
|
|
1233
|
+
|
|
1234
|
+
// Bootstrap runs scripts in the worktree path — forward to the node's daemon if remote.
|
|
1235
|
+
// _meshDirectDispatch prevents re-forwarding when stored daemonId uses legacy format.
|
|
1236
|
+
const nodeDaemonId = typeof node.daemonId === 'string' ? node.daemonId.trim() : undefined;
|
|
1237
|
+
// daemonIdsEquivalent: an equivalent-form daemonId is this machine —
|
|
1238
|
+
// bootstrap locally, do not forward. Equivalent → local.
|
|
1239
|
+
if (nodeDaemonId && !daemonIdsEquivalent(nodeDaemonId, ctx.deps.statusInstanceId) && ctx.deps.dispatchMeshCommand
|
|
1240
|
+
&& !args?._meshDirectDispatch) {
|
|
1241
|
+
const forwarded = await ctx.deps.dispatchMeshCommand(nodeDaemonId, 'retry_mesh_node_bootstrap', {
|
|
1242
|
+
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
1243
|
+
_meshDirectDispatch: true,
|
|
1244
|
+
});
|
|
1245
|
+
return (forwarded ?? { success: false, error: 'no response from remote node' }) as CommandRouterResult;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
const currentBootstrap = node.worktreeBootstrap as WorktreeBootstrapState | undefined;
|
|
1249
|
+
if (currentBootstrap?.status === 'running') {
|
|
1250
|
+
return { success: false, error: 'Bootstrap is already running for this node' };
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
const worktreePath: string = node.workspace || node.repoRoot;
|
|
1254
|
+
if (!worktreePath) return { success: false, error: 'Node has no workspace path' };
|
|
1255
|
+
|
|
1256
|
+
const loadedBootstrap = loadMeshWorktreeBootstrapConfig(mesh, worktreePath);
|
|
1257
|
+
const runningState: WorktreeBootstrapState = {
|
|
1258
|
+
status: 'running',
|
|
1259
|
+
required: loadedBootstrap.config?.required !== false,
|
|
1260
|
+
configSource: loadedBootstrap.path || loadedBootstrap.source,
|
|
1261
|
+
configSourceType: loadedBootstrap.sourceType,
|
|
1262
|
+
startedAt: new Date().toISOString(),
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
const persistState = async (bootstrapState: WorktreeBootstrapState): Promise<void> => {
|
|
1266
|
+
node.worktreeBootstrap = bootstrapState;
|
|
1267
|
+
if (meshRecord.inline) {
|
|
1268
|
+
ctx.updateInlineMeshNode(meshId, mesh, node);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
try {
|
|
1272
|
+
const { updateNode } = await import('../../config/mesh-config.js');
|
|
1273
|
+
updateNode(meshId, node.id, { worktreeBootstrap: bootstrapState });
|
|
1274
|
+
ctx.invalidateAggregateMeshStatus(meshId);
|
|
1275
|
+
} catch { /* best-effort */ }
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
await persistState(runningState);
|
|
1279
|
+
const bootstrapState = await runMeshWorktreeBootstrap(mesh, worktreePath);
|
|
1280
|
+
await persistState(bootstrapState);
|
|
1281
|
+
|
|
1282
|
+
return { success: true, bootstrapState };
|
|
1283
|
+
} catch (e: any) {
|
|
1284
|
+
return { success: false, error: e.message };
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
};
|