@adhdev/daemon-core 0.9.82-rc.5 → 0.9.82-rc.500
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 +11 -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 +85 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +246 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +139 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
- package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +69 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
- package/dist/commands/chat-commands-read.d.ts +23 -0
- package/dist/commands/chat-commands-scope.d.ts +39 -0
- package/dist/commands/chat-commands-shared.d.ts +33 -0
- package/dist/commands/chat-commands-write.d.ts +14 -0
- package/dist/commands/chat-commands.d.ts +10 -23
- package/dist/commands/cli-manager.d.ts +71 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +64 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +48 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/mesh-restart.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +152 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router-aggregate-status.d.ts +24 -0
- package/dist/commands/router-mesh-session-owner.d.ts +59 -0
- package/dist/commands/router-refine.d.ts +151 -0
- package/dist/commands/router-worktree-cleanup.d.ts +133 -0
- package/dist/commands/router.d.ts +212 -11
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +11 -0
- package/dist/config/config.d.ts +19 -0
- package/dist/config/mesh-config.d.ts +132 -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/config/state-store.d.ts +30 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/cli-detector.d.ts +17 -0
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +27 -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 +85 -0
- package/dist/git/git-types.d.ts +5 -43
- package/dist/git/git-worktree.d.ts +71 -1
- package/dist/git/index.d.ts +3 -1
- package/dist/index.d.ts +74 -17
- package/dist/index.js +61082 -24164
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +57034 -20299
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/logging/debug-config.d.ts +16 -0
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +81 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +256 -0
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +203 -0
- package/dist/mesh/mesh-clone-grace.d.ts +8 -0
- package/dist/mesh/mesh-completion-synthesis.d.ts +4 -0
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +104 -0
- package/dist/mesh/mesh-event-classify.d.ts +7 -0
- package/dist/mesh/mesh-event-forwarding.d.ts +58 -0
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
- package/dist/mesh/mesh-events-pending.d.ts +258 -0
- package/dist/mesh/mesh-events-stale.d.ts +61 -0
- package/dist/mesh/mesh-events-utils.d.ts +75 -0
- package/dist/mesh/mesh-events.d.ts +6 -37
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +29 -0
- package/dist/mesh/mesh-idle-reminder.d.ts +68 -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 +139 -1
- package/dist/mesh/mesh-magi-status.d.ts +63 -0
- package/dist/mesh/mesh-missions.d.ts +281 -0
- package/dist/mesh/mesh-node-identity.d.ts +310 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +162 -0
- package/dist/mesh/mesh-reconcile-acked-hold.d.ts +17 -0
- package/dist/mesh/mesh-reconcile-config.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-identity.d.ts +6 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +84 -0
- package/dist/mesh/mesh-reconcile-v2-backstop.d.ts +16 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-gates.d.ts +457 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-remote-event-pull.d.ts +15 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +584 -0
- package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
- package/dist/mesh/mesh-task-inflight.d.ts +46 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +34 -0
- package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +403 -6
- 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 +46 -0
- package/dist/providers/chat-message-normalization.d.ts +58 -0
- package/dist/providers/cli-provider-effect-format.d.ts +30 -0
- package/dist/providers/cli-provider-history-dedup.d.ts +17 -0
- package/dist/providers/cli-provider-input-prompt.d.ts +12 -0
- package/dist/providers/cli-provider-instance-types.d.ts +47 -0
- package/dist/providers/cli-provider-instance.d.ts +461 -30
- package/dist/providers/cli-provider-status-helpers.d.ts +46 -0
- package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
- package/dist/providers/contracts.d.ts +196 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +79 -0
- package/dist/providers/native-history/antigravity-claim-registry.d.ts +39 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +137 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/constants.d.ts +12 -0
- package/dist/providers/native-history/dispatcher.d.ts +44 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +30 -0
- package/dist/providers/provider-instance.d.ts +31 -1
- package/dist/providers/provider-loader.d.ts +42 -5
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +98 -0
- package/dist/providers/spec/background-task-detector.d.ts +29 -0
- package/dist/providers/spec/cli-adapter.d.ts +224 -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 +50 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +244 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +778 -3
- package/dist/runtime-defaults.d.ts +2 -0
- package/dist/session-host/managed-host.d.ts +64 -0
- package/dist/session-host/runtime-surface.d.ts +10 -16
- package/dist/sessions/registry.d.ts +26 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +110 -55
- package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +27 -0
- package/dist/system/hash.d.ts +8 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +20 -6
- package/package.json +11 -4
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +83 -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 +85 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1379 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +1059 -1143
- package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/src/cli-adapters/provider-cli-parse.ts +19 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +121 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/pty-write-chunking.ts +106 -0
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +204 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/chat-commands-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2788 -0
- package/src/commands/chat-commands-scope.ts +54 -0
- package/src/commands/chat-commands-shared.ts +114 -0
- package/src/commands/chat-commands-write.ts +891 -0
- package/src/commands/chat-commands.ts +19 -1841
- package/src/commands/cli-manager.d.ts +2 -0
- package/src/commands/cli-manager.ts +724 -27
- package/src/commands/handler.ts +865 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +696 -0
- package/src/commands/high-family/mesh-events.ts +102 -0
- package/src/commands/high-family/mesh-status.ts +857 -0
- package/src/commands/high-family/types.ts +80 -0
- package/src/commands/low-family/coordinator-prompt.ts +125 -0
- package/src/commands/low-family/daemon-lifecycle.ts +120 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +87 -0
- package/src/commands/low-family/notification.ts +116 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +282 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +39 -0
- package/src/commands/med-family/cli-agent.ts +270 -0
- package/src/commands/med-family/fast-forward.ts +230 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +37 -0
- package/src/commands/med-family/mesh-crud.ts +1309 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +167 -0
- package/src/commands/med-family/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +153 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router-aggregate-status.ts +209 -0
- package/src/commands/router-mesh-session-owner.ts +114 -0
- package/src/commands/router-refine.ts +1794 -0
- package/src/commands/router-worktree-cleanup.ts +870 -0
- package/src/commands/router.ts +657 -2704
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +517 -24
- package/src/config/config.ts +30 -0
- package/src/config/mesh-config.ts +542 -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/config/state-store.ts +55 -0
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/daemon/dev-server.ts +0 -541
- package/src/detection/cli-detector.ts +94 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +113 -17
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +916 -51
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +17 -54
- package/src/git/git-worktree.ts +261 -4
- package/src/git/index.ts +17 -0
- package/src/index.ts +221 -13
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/command-log.ts +7 -5
- package/src/logging/debug-config.ts +25 -0
- package/src/logging/debug-trace.ts +7 -2
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +341 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +510 -0
- package/src/mesh/coordinator-prompt.ts +626 -34
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +706 -0
- package/src/mesh/mesh-clone-grace.ts +68 -0
- package/src/mesh/mesh-completion-synthesis.ts +416 -0
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-delivery-policy.ts +284 -0
- package/src/mesh/mesh-event-classify.ts +78 -0
- package/src/mesh/mesh-event-forwarding.ts +2042 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +32 -0
- package/src/mesh/mesh-events-pending.ts +1548 -0
- package/src/mesh/mesh-events-stale.ts +452 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +43 -897
- package/src/mesh/mesh-fast-forward.ts +856 -0
- package/src/mesh/mesh-host-ownership.ts +111 -0
- package/src/mesh/mesh-idle-reminder.ts +153 -0
- package/src/mesh/mesh-init.ts +350 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
- package/src/mesh/mesh-ledger.ts +898 -102
- package/src/mesh/mesh-magi-status.ts +223 -0
- package/src/mesh/mesh-missions.ts +695 -0
- package/src/mesh/mesh-node-identity.ts +2065 -0
- package/src/mesh/mesh-queue-assignment.ts +2498 -0
- package/src/mesh/mesh-reconcile-acked-hold.ts +230 -0
- package/src/mesh/mesh-reconcile-config.ts +66 -0
- package/src/mesh/mesh-reconcile-identity.ts +103 -0
- package/src/mesh/mesh-reconcile-loop.ts +1709 -0
- package/src/mesh/mesh-reconcile-v2-backstop.ts +62 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1792 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-remote-event-pull.ts +286 -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 +2318 -0
- package/src/mesh/mesh-scheduling-runtime.ts +198 -0
- package/src/mesh/mesh-task-inflight.ts +70 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +215 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +1532 -161
- package/src/mesh/p2p-relay-failure.ts +2 -2
- 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 +100 -6
- package/src/providers/chat-message-normalization.ts +128 -4
- package/src/providers/cli-provider-effect-format.ts +53 -0
- package/src/providers/cli-provider-history-dedup.ts +75 -0
- package/src/providers/cli-provider-input-prompt.ts +133 -0
- package/src/providers/cli-provider-instance-types.ts +150 -0
- package/src/providers/cli-provider-instance.ts +2914 -498
- package/src/providers/cli-provider-status-helpers.ts +123 -0
- package/src/providers/cli-provider-transcript-merge.ts +114 -0
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +207 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/manual-attendance.ts +105 -0
- package/src/providers/native-history/antigravity-claim-registry.ts +138 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +1275 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +479 -0
- package/src/providers/native-history/constants.ts +19 -0
- package/src/providers/native-history/dispatcher.ts +617 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +312 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +71 -0
- package/src/providers/provider-instance.ts +24 -1
- package/src/providers/provider-loader.ts +700 -55
- package/src/providers/provider-schema.ts +93 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +261 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +538 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +246 -0
- package/src/providers/spec/background-task-detector.ts +178 -0
- package/src/providers/spec/cli-adapter.ts +1090 -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 +1256 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +284 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +1040 -5
- package/src/runtime-defaults.ts +39 -0
- package/src/session-host/managed-host.ts +218 -0
- package/src/session-host/runtime-surface.ts +20 -80
- package/src/sessions/registry.ts +44 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +148 -55
- package/src/status/builders.ts +69 -6
- package/src/status/chat-tail-hot-sessions.ts +117 -2
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +100 -27
- package/src/system/hash.ts +23 -0
- package/src/system/host-memory.ts +29 -12
- package/src/system/load-better-sqlite3.ts +68 -0
- package/src/types.ts +26 -6
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
package/src/commands/router.ts
CHANGED
|
@@ -9,615 +9,106 @@
|
|
|
9
9
|
* 3. Everything else → delegated to commandHandler.handle()
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
import { DaemonCdpManager } from '../cdp/manager.js';
|
|
13
|
-
import { registerExtensionProviders } from '../cdp/setup.js';
|
|
14
14
|
import { DaemonCommandHandler } from './handler.js';
|
|
15
|
+
import { lowFamilyRegistry } from './low-family/index.js';
|
|
16
|
+
import { medFamilyRegistry } from './med-family/index.js';
|
|
17
|
+
import { launchIde } from './med-family/ide.js';
|
|
18
|
+
import type { MedFamilyContext } from './med-family/index.js';
|
|
19
|
+
import { highFamilyRegistry } from './high-family/index.js';
|
|
20
|
+
import type { HighFamilyContext } from './high-family/index.js';
|
|
15
21
|
import { DaemonCliManager } from './cli-manager.js';
|
|
16
|
-
import { supportsExplicitSessionResume } from './cli-manager.js';
|
|
17
|
-
import type { HostedCliRuntimeDescriptor } from './cli-manager.js';
|
|
18
22
|
import type { ProviderLoader } from '../providers/provider-loader.js';
|
|
19
23
|
import type { ProviderInstanceManager } from '../providers/provider-instance-manager.js';
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import { loadState, saveState } from '../config/state-store.js';
|
|
23
|
-
import { resolveIdeLaunchWorkspace } from '../config/workspaces.js';
|
|
24
|
-
import { appendRecentActivity, getRecentActivity, markSessionSeen, dismissSessionNotification, markSessionNotificationUnread } from '../config/recent-activity.js';
|
|
25
|
-
import { getSavedProviderSessions } from '../config/saved-sessions.js';
|
|
26
|
-
import { listProviderHistorySessions } from '../config/chat-history.js';
|
|
27
|
-
import { detectIDEs } from '../detection/ide-detector.js';
|
|
28
|
-
import { detectCLI } from '../detection/cli-detector.js';
|
|
29
|
-
import { getGitRepoStatus } from '../git/git-status.js';
|
|
30
|
-
import type { GitSubmoduleStatus } from '../git/git-types.js';
|
|
24
|
+
import { killIdeProcess, isIdeRunning } from '../launch.js';
|
|
25
|
+
import { normalizeMeshNodeId, meshNodeIdMatches } from '@adhdev/mesh-shared';
|
|
31
26
|
import { SessionRegistry } from '../sessions/registry.js';
|
|
32
27
|
import { LOG } from '../logging/logger.js';
|
|
33
28
|
import { logCommand } from '../logging/command-log.js';
|
|
34
29
|
import type { CommandLogEntry } from '../logging/command-log.js';
|
|
35
|
-
import
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import { buildMachineInfo, buildStatusSnapshot } from '../status/snapshot.js';
|
|
43
|
-
import { getSessionCompletionMarker } from '../status/snapshot.js';
|
|
44
|
-
import { execNpmCommandSync, resolveCurrentGlobalInstallSurface, spawnDetachedDaemonUpgradeHelper } from './upgrade-helper.js';
|
|
30
|
+
import { createInteractionId, recordDebugTrace } from '../logging/debug-trace.js';
|
|
31
|
+
import { getSessionHostSurfaceKind } from '../session-host/runtime-surface.js';
|
|
32
|
+
import { handleMeshForwardEvent, queuePendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
33
|
+
import { buildMeshHostRequiredFailure, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
34
|
+
import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
|
|
35
|
+
import type { WorktreeBootstrapState } from '../mesh/worktree-bootstrap-config.js';
|
|
36
|
+
import { getMeshQueueRevision } from '../mesh/mesh-work-queue.js';
|
|
45
37
|
import type { RepoMeshSessionCleanupMode } from '../repo-mesh-types.js';
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
38
|
+
import { DEFAULT_MESH_POLICY, magiAutoLaunchedSessionCleanupDecision } from '../repo-mesh-types.js';
|
|
39
|
+
import { resolve as pathResolve } from 'path';
|
|
48
40
|
import * as fs from 'fs';
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.map(entry => {
|
|
121
|
-
const submodule = readObjectRecord(entry);
|
|
122
|
-
const path = readStringValue(submodule.path);
|
|
123
|
-
const commit = readStringValue(submodule.commit);
|
|
124
|
-
const repoPath = readStringValue(submodule.repoPath, submodule.repo_root);
|
|
125
|
-
if (!path || !commit || !repoPath) return null;
|
|
126
|
-
return {
|
|
127
|
-
path,
|
|
128
|
-
commit,
|
|
129
|
-
repoPath,
|
|
130
|
-
dirty: readBooleanValue(submodule.dirty) ?? false,
|
|
131
|
-
outOfSync: readBooleanValue(submodule.outOfSync, submodule.out_of_sync) ?? false,
|
|
132
|
-
lastCheckedAt: readNumberValue(submodule.lastCheckedAt, submodule.last_checked_at) ?? Date.now(),
|
|
133
|
-
...(readStringValue(submodule.error) ? { error: readStringValue(submodule.error) } : {}),
|
|
134
|
-
};
|
|
135
|
-
})
|
|
136
|
-
.filter((entry): entry is GitSubmoduleStatus => entry !== null);
|
|
137
|
-
return submodules.length > 0 ? submodules : undefined;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function buildCachedInlineMeshGitStatus(node: any): Record<string, unknown> | undefined {
|
|
141
|
-
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
142
|
-
const cachedGit = readObjectRecord(cachedStatus.git);
|
|
143
|
-
if (Object.keys(cachedGit).length) {
|
|
144
|
-
const conflictFiles = Array.isArray(cachedGit.conflictFiles)
|
|
145
|
-
? cachedGit.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
|
|
146
|
-
: [];
|
|
147
|
-
const conflictCount = readNumberValue(cachedGit.conflicts) ?? conflictFiles.length;
|
|
148
|
-
const hasConflicts = readBooleanValue(cachedGit.hasConflicts) ?? conflictCount > 0;
|
|
149
|
-
const isGitRepo = readBooleanValue(cachedGit.isGitRepo);
|
|
150
|
-
if (isGitRepo !== undefined) {
|
|
151
|
-
const submodules = readGitSubmodules(cachedGit.submodules);
|
|
152
|
-
return {
|
|
153
|
-
workspace: readStringValue(cachedGit.workspace, node?.workspace) || '',
|
|
154
|
-
repoRoot: readStringValue(cachedGit.repoRoot, node?.repoRoot, node?.workspace) || null,
|
|
155
|
-
isGitRepo,
|
|
156
|
-
branch: readStringValue(cachedGit.branch) ?? null,
|
|
157
|
-
headCommit: readStringValue(cachedGit.headCommit) ?? null,
|
|
158
|
-
headMessage: readStringValue(cachedGit.headMessage) ?? null,
|
|
159
|
-
upstream: readStringValue(cachedGit.upstream) ?? null,
|
|
160
|
-
ahead: readNumberValue(cachedGit.ahead) ?? 0,
|
|
161
|
-
behind: readNumberValue(cachedGit.behind) ?? 0,
|
|
162
|
-
staged: readNumberValue(cachedGit.staged) ?? 0,
|
|
163
|
-
modified: readNumberValue(cachedGit.modified) ?? 0,
|
|
164
|
-
untracked: readNumberValue(cachedGit.untracked) ?? 0,
|
|
165
|
-
deleted: readNumberValue(cachedGit.deleted) ?? 0,
|
|
166
|
-
renamed: readNumberValue(cachedGit.renamed) ?? 0,
|
|
167
|
-
hasConflicts,
|
|
168
|
-
conflictFiles,
|
|
169
|
-
stashCount: readNumberValue(cachedGit.stashCount) ?? 0,
|
|
170
|
-
lastCheckedAt: readNumberValue(cachedGit.lastCheckedAt) ?? Date.now(),
|
|
171
|
-
...(submodules ? { submodules } : {}),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
const rawGit = readObjectRecord(node?.lastGit ?? node?.last_git);
|
|
177
|
-
const gitResult = readObjectRecord(rawGit.result);
|
|
178
|
-
const directStatus = readObjectRecord(rawGit.status);
|
|
179
|
-
const nestedStatus = readObjectRecord(gitResult.status);
|
|
180
|
-
const rawProbe = readObjectRecord(node?.lastProbe ?? node?.last_probe);
|
|
181
|
-
const probeGit = readObjectRecord(rawProbe.git);
|
|
182
|
-
const probeGitResult = readObjectRecord(probeGit.result);
|
|
183
|
-
const probeDirectStatus = readObjectRecord(probeGit.status);
|
|
184
|
-
const probeNestedStatus = readObjectRecord(probeGitResult.status);
|
|
185
|
-
const status = Object.keys(directStatus).length
|
|
186
|
-
? directStatus
|
|
187
|
-
: Object.keys(nestedStatus).length
|
|
188
|
-
? nestedStatus
|
|
189
|
-
: Object.keys(probeDirectStatus).length
|
|
190
|
-
? probeDirectStatus
|
|
191
|
-
: Object.keys(probeNestedStatus).length
|
|
192
|
-
? probeNestedStatus
|
|
193
|
-
: {};
|
|
194
|
-
const isGitRepo = readBooleanValue(status.isGitRepo);
|
|
195
|
-
if (!Object.keys(status).length || isGitRepo === undefined) return undefined;
|
|
196
|
-
const conflictFiles = Array.isArray(status.conflictFiles)
|
|
197
|
-
? status.conflictFiles.filter((value: unknown): value is string => typeof value === 'string')
|
|
198
|
-
: [];
|
|
199
|
-
const conflictCount = readNumberValue(status.conflicts) ?? conflictFiles.length;
|
|
200
|
-
const hasConflicts = readBooleanValue(status.hasConflicts) ?? conflictCount > 0;
|
|
201
|
-
const submodules = readGitSubmodules(status.submodules);
|
|
202
|
-
return {
|
|
203
|
-
workspace: readStringValue(status.workspace, node?.workspace) || '',
|
|
204
|
-
repoRoot: readStringValue(status.repoRoot, node?.repoRoot, node?.workspace) || null,
|
|
205
|
-
isGitRepo,
|
|
206
|
-
branch: readStringValue(status.branch) ?? null,
|
|
207
|
-
headCommit: readStringValue(status.headCommit) ?? null,
|
|
208
|
-
headMessage: readStringValue(status.headMessage) ?? null,
|
|
209
|
-
upstream: readStringValue(status.upstream) ?? null,
|
|
210
|
-
ahead: readNumberValue(status.ahead) ?? 0,
|
|
211
|
-
behind: readNumberValue(status.behind) ?? 0,
|
|
212
|
-
staged: readNumberValue(status.staged) ?? 0,
|
|
213
|
-
modified: readNumberValue(status.modified) ?? 0,
|
|
214
|
-
untracked: readNumberValue(status.untracked) ?? 0,
|
|
215
|
-
deleted: readNumberValue(status.deleted) ?? 0,
|
|
216
|
-
renamed: readNumberValue(status.renamed) ?? 0,
|
|
217
|
-
hasConflicts,
|
|
218
|
-
conflictFiles,
|
|
219
|
-
stashCount: readNumberValue(status.stashCount) ?? 0,
|
|
220
|
-
lastCheckedAt: Date.now(),
|
|
221
|
-
...(submodules ? { submodules } : {}),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function applyCachedInlineMeshNodeStatus(status: Record<string, unknown>, node: any): boolean {
|
|
226
|
-
const cachedStatus = readObjectRecord(node?.cachedStatus);
|
|
227
|
-
const git = buildCachedInlineMeshGitStatus(node);
|
|
228
|
-
const error = readStringValue(cachedStatus.error, node?.error);
|
|
229
|
-
const health = readStringValue(cachedStatus.health, node?.health);
|
|
230
|
-
const machineStatus = readStringValue(cachedStatus.machineStatus, node?.machineStatus);
|
|
231
|
-
if (!git && !error && !health) return false;
|
|
232
|
-
if (!machineStatus && !git && !error) return false;
|
|
233
|
-
if (git) status.git = git;
|
|
234
|
-
if (error) status.error = error;
|
|
235
|
-
if (health) {
|
|
236
|
-
status.health = health;
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
if (git) {
|
|
240
|
-
const dirty = Number(git.staged || 0) + Number(git.modified || 0) + Number(git.untracked || 0) + Number(git.deleted || 0) + Number(git.renamed || 0) > 0;
|
|
241
|
-
status.health = git.isGitRepo === false ? 'degraded' : dirty ? 'dirty' : 'online';
|
|
242
|
-
return true;
|
|
243
|
-
}
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
async function resolveProviderTypeFromPriority(args: {
|
|
248
|
-
nodeId: string;
|
|
249
|
-
providerPriority: string[];
|
|
250
|
-
providerLoader: ProviderLoader;
|
|
251
|
-
onStatusChange?: () => void;
|
|
252
|
-
}): Promise<{ providerType?: string; error?: string }> {
|
|
253
|
-
if (!args.providerPriority.length) {
|
|
254
|
-
return { error: `Node '${args.nodeId}' has no providerPriority policy; pass cliType explicitly or configure node.policy.providerPriority` };
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
const failed: string[] = [];
|
|
258
|
-
for (const requestedType of args.providerPriority) {
|
|
259
|
-
const normalizedType = args.providerLoader.resolveAlias(requestedType);
|
|
260
|
-
if (!args.providerLoader.isMachineProviderEnabled(normalizedType)) {
|
|
261
|
-
failed.push(`${requestedType}: disabled`);
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
const detected = await detectCLI(normalizedType, args.providerLoader, { includeVersion: false });
|
|
265
|
-
args.providerLoader.setCliDetectionResults([{
|
|
266
|
-
id: normalizedType,
|
|
267
|
-
installed: !!detected,
|
|
268
|
-
path: detected?.path,
|
|
269
|
-
}], false);
|
|
270
|
-
args.onStatusChange?.();
|
|
271
|
-
if (detected) return { providerType: normalizedType };
|
|
272
|
-
failed.push(`${requestedType}: not detected`);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return { error: `No usable provider detected for node '${args.nodeId}' from providerPriority: ${failed.join('; ')}` };
|
|
276
|
-
}
|
|
277
|
-
type MeshCoordinatorConfigFormat = 'claude_mcp_json' | 'hermes_config_yaml';
|
|
278
|
-
type MeshRefineValidationStatus = 'passed' | 'failed' | 'skipped';
|
|
279
|
-
type MeshRefineValidationCommand = {
|
|
280
|
-
command: string;
|
|
281
|
-
args: string[];
|
|
282
|
-
displayCommand: string;
|
|
283
|
-
category: string;
|
|
284
|
-
source: string;
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
type MeshRefineValidationSummary = {
|
|
288
|
-
status: MeshRefineValidationStatus;
|
|
289
|
-
required: true;
|
|
290
|
-
commandsRun: Array<Record<string, unknown>>;
|
|
291
|
-
rejectedCommands: Array<Record<string, unknown>>;
|
|
292
|
-
skippedReason?: string;
|
|
293
|
-
timeoutMs: number;
|
|
294
|
-
outputLimitBytes: number;
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
const REFINE_VALIDATION_CATEGORIES = ['typecheck', 'test', 'lint', 'build'] as const;
|
|
298
|
-
const REFINE_VALIDATION_TIMEOUT_MS = 120_000;
|
|
299
|
-
const REFINE_VALIDATION_OUTPUT_LIMIT_BYTES = 128 * 1024;
|
|
300
|
-
const REFINE_VALIDATION_SUMMARY_CHARS = 2_000;
|
|
301
|
-
const REFINE_VALIDATION_MAX_COMMANDS = 4;
|
|
302
|
-
|
|
303
|
-
function truncateValidationOutput(value: unknown): string {
|
|
304
|
-
const text = typeof value === 'string' ? value : value == null ? '' : String(value);
|
|
305
|
-
if (text.length <= REFINE_VALIDATION_SUMMARY_CHARS) return text;
|
|
306
|
-
return `${text.slice(0, REFINE_VALIDATION_SUMMARY_CHARS)}\n[truncated ${text.length - REFINE_VALIDATION_SUMMARY_CHARS} chars]`;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function readPackageScripts(workspace: string): Record<string, string> {
|
|
310
|
-
try {
|
|
311
|
-
const packageJsonPath = pathJoin(workspace, 'package.json');
|
|
312
|
-
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
313
|
-
return parsed?.scripts && typeof parsed.scripts === 'object' && !Array.isArray(parsed.scripts)
|
|
314
|
-
? parsed.scripts as Record<string, string>
|
|
315
|
-
: {};
|
|
316
|
-
} catch {
|
|
317
|
-
return {};
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function tokenizeValidationCommand(command: string): string[] | null {
|
|
322
|
-
const trimmed = command.trim();
|
|
323
|
-
if (!trimmed) return null;
|
|
324
|
-
// Fail closed: the gate never hands shell syntax to a shell. Package-manager
|
|
325
|
-
// scripts are invoked via execFile(binary, args), and metacharacters/quotes are
|
|
326
|
-
// rejected before tokenization so `npm run test && rm -rf` cannot be smuggled in.
|
|
327
|
-
if (/[;&|<>`$\\\n\r'\"]/.test(trimmed)) return null;
|
|
328
|
-
const tokens = trimmed.split(/\s+/).filter(Boolean);
|
|
329
|
-
if (!tokens.length) return null;
|
|
330
|
-
if (tokens.some(token => !/^[A-Za-z0-9_@./:=+-]+$/.test(token))) return null;
|
|
331
|
-
return tokens;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function scriptMatchesValidationCategory(scriptName: string, category: string): boolean {
|
|
335
|
-
return scriptName === category || scriptName.startsWith(`${category}:`);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
function parsePackageManagerValidationCommand(
|
|
339
|
-
rawCommand: string,
|
|
340
|
-
category: string,
|
|
341
|
-
scripts: Record<string, string>,
|
|
342
|
-
source: string,
|
|
343
|
-
): { command?: MeshRefineValidationCommand; rejected?: Record<string, unknown> } {
|
|
344
|
-
const tokens = tokenizeValidationCommand(rawCommand);
|
|
345
|
-
if (!tokens) {
|
|
346
|
-
return { rejected: { command: rawCommand, category, source, reason: 'unsafe command string is not allowlisted' } };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const [binary, second, third, ...rest] = tokens;
|
|
350
|
-
let scriptName = '';
|
|
351
|
-
let command = binary;
|
|
352
|
-
let args: string[] = [];
|
|
353
|
-
|
|
354
|
-
if ((binary === 'npm' || binary === 'pnpm' || binary === 'bun') && second === 'run' && third) {
|
|
355
|
-
scriptName = third;
|
|
356
|
-
args = ['run', scriptName, ...rest];
|
|
357
|
-
} else if (binary === 'npm' && second === 'test' && !third) {
|
|
358
|
-
scriptName = 'test';
|
|
359
|
-
args = ['test'];
|
|
360
|
-
} else if (binary === 'yarn' && second === 'run' && third) {
|
|
361
|
-
scriptName = third;
|
|
362
|
-
args = ['run', scriptName, ...rest];
|
|
363
|
-
} else if (binary === 'yarn' && second && !third) {
|
|
364
|
-
scriptName = second;
|
|
365
|
-
args = [scriptName];
|
|
366
|
-
} else {
|
|
367
|
-
return { rejected: { command: rawCommand, category, source, reason: 'command is not a supported package-manager script invocation' } };
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
if (!scriptName || !Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
371
|
-
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script is not declared in package.json' } };
|
|
372
|
-
}
|
|
373
|
-
if (!scriptMatchesValidationCategory(scriptName, category)) {
|
|
374
|
-
return { rejected: { command: rawCommand, category, source, script: scriptName, reason: 'script name is outside the validation category allowlist' } };
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return {
|
|
378
|
-
command: {
|
|
379
|
-
command,
|
|
380
|
-
args,
|
|
381
|
-
displayCommand: [command, ...args].join(' '),
|
|
382
|
-
category,
|
|
383
|
-
source,
|
|
384
|
-
},
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function collectProjectContextValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string; confidence?: string }> {
|
|
389
|
-
const commands = mesh?.projectContext?.commands;
|
|
390
|
-
if (!commands || typeof commands !== 'object' || Array.isArray(commands)) return [];
|
|
391
|
-
const candidates: Array<{ command: string; category: string; source: string; confidence?: string }> = [];
|
|
392
|
-
for (const category of REFINE_VALIDATION_CATEGORIES) {
|
|
393
|
-
const entries = Array.isArray(commands[category]) ? commands[category] : [];
|
|
394
|
-
for (const entry of entries) {
|
|
395
|
-
if (typeof entry?.command !== 'string') continue;
|
|
396
|
-
candidates.push({
|
|
397
|
-
command: entry.command,
|
|
398
|
-
category,
|
|
399
|
-
source: typeof entry.sourcePath === 'string' ? entry.sourcePath : 'projectContext.commands',
|
|
400
|
-
confidence: typeof entry.confidence === 'string' ? entry.confidence : undefined,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
return candidates.sort((a, b) => {
|
|
405
|
-
const rank = (value?: string) => value === 'high' ? 0 : value === 'medium' ? 1 : 2;
|
|
406
|
-
return rank(a.confidence) - rank(b.confidence);
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function collectPolicyValidationCandidates(mesh: any): Array<{ command: string; category: string; source: string }> {
|
|
411
|
-
const policy = mesh?.policy && typeof mesh.policy === 'object' && !Array.isArray(mesh.policy) ? mesh.policy : {};
|
|
412
|
-
const configured = Array.isArray(policy.validationCommands)
|
|
413
|
-
? policy.validationCommands
|
|
414
|
-
: Array.isArray(policy.validationGate?.commands)
|
|
415
|
-
? policy.validationGate.commands
|
|
416
|
-
: [];
|
|
417
|
-
return configured
|
|
418
|
-
.map((entry: any) => typeof entry === 'string' ? { command: entry, category: '', source: 'mesh.policy.validationCommands' } : entry)
|
|
419
|
-
.filter((entry: any) => entry && typeof entry.command === 'string')
|
|
420
|
-
.map((entry: any) => {
|
|
421
|
-
const commandText = entry.command.trim();
|
|
422
|
-
const category = REFINE_VALIDATION_CATEGORIES.find(cat => commandText.includes(` ${cat}`)) ?? '';
|
|
423
|
-
return { command: commandText, category, source: 'mesh.policy.validationCommands' };
|
|
424
|
-
})
|
|
425
|
-
.filter((entry: any) => !!entry.category);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function selectMeshRefineValidationCommands(mesh: any, workspace: string): { commands: MeshRefineValidationCommand[]; rejectedCommands: Array<Record<string, unknown>>; source: string } {
|
|
429
|
-
const scripts = readPackageScripts(workspace);
|
|
430
|
-
const rejectedCommands: Array<Record<string, unknown>> = [];
|
|
431
|
-
const selected: MeshRefineValidationCommand[] = [];
|
|
432
|
-
const seen = new Set<string>();
|
|
433
|
-
const candidates = [
|
|
434
|
-
...collectPolicyValidationCandidates(mesh),
|
|
435
|
-
...collectProjectContextValidationCandidates(mesh),
|
|
436
|
-
];
|
|
437
|
-
|
|
438
|
-
for (const candidate of candidates) {
|
|
439
|
-
const parsed = parsePackageManagerValidationCommand(candidate.command, candidate.category, scripts, candidate.source);
|
|
440
|
-
if (parsed.rejected) {
|
|
441
|
-
rejectedCommands.push(parsed.rejected);
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
if (!parsed.command || seen.has(parsed.command.displayCommand)) continue;
|
|
445
|
-
selected.push(parsed.command);
|
|
446
|
-
seen.add(parsed.command.displayCommand);
|
|
447
|
-
if (selected.length >= REFINE_VALIDATION_MAX_COMMANDS) break;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (!selected.length && candidates.length === 0) {
|
|
451
|
-
for (const category of REFINE_VALIDATION_CATEGORIES) {
|
|
452
|
-
if (!Object.prototype.hasOwnProperty.call(scripts, category)) continue;
|
|
453
|
-
const fallback = parsePackageManagerValidationCommand(`npm run ${category}`, category, scripts, 'package.json:scripts');
|
|
454
|
-
if (fallback.command && !seen.has(fallback.command.displayCommand)) {
|
|
455
|
-
selected.push(fallback.command);
|
|
456
|
-
seen.add(fallback.command.displayCommand);
|
|
457
|
-
} else if (fallback.rejected) {
|
|
458
|
-
rejectedCommands.push(fallback.rejected);
|
|
459
|
-
}
|
|
460
|
-
if (selected.length >= 2) break;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
return {
|
|
465
|
-
commands: selected,
|
|
466
|
-
rejectedCommands,
|
|
467
|
-
source: selected.some(command => command.source === 'mesh.policy.validationCommands')
|
|
468
|
-
? 'mesh_policy'
|
|
469
|
-
: selected.some(command => command.source !== 'package.json:scripts')
|
|
470
|
-
? 'project_context'
|
|
471
|
-
: selected.length
|
|
472
|
-
? 'package_json_scripts'
|
|
473
|
-
: 'unavailable',
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
async function runMeshRefineValidationGate(mesh: any, workspace: string): Promise<MeshRefineValidationSummary> {
|
|
478
|
-
const { execFile } = await import('node:child_process');
|
|
479
|
-
const { promisify } = await import('node:util');
|
|
480
|
-
const execFileAsync = promisify(execFile);
|
|
481
|
-
const selection = selectMeshRefineValidationCommands(mesh, workspace);
|
|
482
|
-
const summary: MeshRefineValidationSummary = {
|
|
483
|
-
status: 'skipped',
|
|
484
|
-
required: true,
|
|
485
|
-
commandsRun: [],
|
|
486
|
-
rejectedCommands: selection.rejectedCommands,
|
|
487
|
-
skippedReason: undefined,
|
|
488
|
-
timeoutMs: REFINE_VALIDATION_TIMEOUT_MS,
|
|
489
|
-
outputLimitBytes: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
490
|
-
};
|
|
491
|
-
|
|
492
|
-
if (!selection.commands.length) {
|
|
493
|
-
summary.skippedReason = 'validation_unavailable: no allowlisted projectContext, mesh policy, or package.json build/test/typecheck/lint command was available';
|
|
494
|
-
return summary;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
for (const candidate of selection.commands) {
|
|
498
|
-
const startedAt = Date.now();
|
|
499
|
-
try {
|
|
500
|
-
const result = await execFileAsync(candidate.command, candidate.args, {
|
|
501
|
-
cwd: workspace,
|
|
502
|
-
encoding: 'utf8',
|
|
503
|
-
timeout: REFINE_VALIDATION_TIMEOUT_MS,
|
|
504
|
-
maxBuffer: REFINE_VALIDATION_OUTPUT_LIMIT_BYTES,
|
|
505
|
-
env: { ...process.env, CI: process.env.CI || '1' },
|
|
506
|
-
});
|
|
507
|
-
summary.commandsRun.push({
|
|
508
|
-
command: candidate.command,
|
|
509
|
-
args: candidate.args,
|
|
510
|
-
displayCommand: candidate.displayCommand,
|
|
511
|
-
category: candidate.category,
|
|
512
|
-
source: candidate.source,
|
|
513
|
-
passed: true,
|
|
514
|
-
exitCode: 0,
|
|
515
|
-
durationMs: Date.now() - startedAt,
|
|
516
|
-
stdout: truncateValidationOutput(result.stdout),
|
|
517
|
-
stderr: truncateValidationOutput(result.stderr),
|
|
518
|
-
});
|
|
519
|
-
} catch (error: any) {
|
|
520
|
-
summary.commandsRun.push({
|
|
521
|
-
command: candidate.command,
|
|
522
|
-
args: candidate.args,
|
|
523
|
-
displayCommand: candidate.displayCommand,
|
|
524
|
-
category: candidate.category,
|
|
525
|
-
source: candidate.source,
|
|
526
|
-
passed: false,
|
|
527
|
-
exitCode: typeof error?.code === 'number' ? error.code : null,
|
|
528
|
-
signal: typeof error?.signal === 'string' ? error.signal : null,
|
|
529
|
-
timedOut: error?.killed === true || /timed out/i.test(String(error?.message || '')),
|
|
530
|
-
durationMs: Date.now() - startedAt,
|
|
531
|
-
stdout: truncateValidationOutput(error?.stdout),
|
|
532
|
-
stderr: truncateValidationOutput(error?.stderr || error?.message),
|
|
533
|
-
});
|
|
534
|
-
summary.status = 'failed';
|
|
535
|
-
return summary;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
summary.status = 'passed';
|
|
540
|
-
return summary;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
function loadYamlModule(): { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string } {
|
|
544
|
-
return yaml as { load: (input: string) => any; dump: (input: any, options?: Record<string, any>) => string };
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function getMcpServersKey(format: MeshCoordinatorConfigFormat): 'mcpServers' | 'mcp_servers' {
|
|
548
|
-
return format === 'hermes_config_yaml' ? 'mcp_servers' : 'mcpServers';
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function parseMeshCoordinatorMcpConfig(text: string, format: MeshCoordinatorConfigFormat): Record<string, any> {
|
|
552
|
-
if (!text.trim()) return {};
|
|
553
|
-
if (format === 'claude_mcp_json') return JSON.parse(text);
|
|
554
|
-
const parsed = loadYamlModule().load(text);
|
|
555
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
function serializeMeshCoordinatorMcpConfig(config: Record<string, any>, format: MeshCoordinatorConfigFormat): string {
|
|
559
|
-
if (format === 'claude_mcp_json') return JSON.stringify(config, null, 2);
|
|
560
|
-
return loadYamlModule().dump(config, { noRefs: true, lineWidth: 120 });
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function resolveHermesUserHome(): string {
|
|
564
|
-
const explicitHome = process.env.HERMES_HOME?.trim();
|
|
565
|
-
return explicitHome || pathJoin(homedir(), '.hermes');
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
function loadHermesCoordinatorBaseConfig(targetConfigPath: string): { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } {
|
|
569
|
-
const sourceHome = resolveHermesUserHome();
|
|
570
|
-
const sourceConfigPath = pathJoin(sourceHome, 'config.yaml');
|
|
571
|
-
if (!fs.existsSync(sourceConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
|
|
572
|
-
if (pathResolve(sourceConfigPath) === pathResolve(targetConfigPath)) return { config: {}, sourceHome, sourceConfigPath };
|
|
573
|
-
|
|
574
|
-
const parsed = parseMeshCoordinatorMcpConfig(fs.readFileSync(sourceConfigPath, 'utf-8'), 'hermes_config_yaml');
|
|
575
|
-
const { mcp_servers: _mcpServers, ...baseConfig } = parsed;
|
|
576
|
-
return { config: baseConfig, sourceHome, sourceConfigPath };
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function stripHermesCoordinatorTempModelProviderOverrides(config: Record<string, any>): Record<string, any> {
|
|
580
|
-
const {
|
|
581
|
-
model: _model,
|
|
582
|
-
provider: _provider,
|
|
583
|
-
default_model: _defaultModel,
|
|
584
|
-
defaultProvider: _defaultProvider,
|
|
585
|
-
default_provider: _defaultProviderSnake,
|
|
586
|
-
modelProvider: _modelProvider,
|
|
587
|
-
model_provider: _modelProviderSnake,
|
|
588
|
-
...sanitized
|
|
589
|
-
} = config;
|
|
590
|
-
const delegation = sanitized.delegation;
|
|
591
|
-
if (delegation && typeof delegation === 'object' && !Array.isArray(delegation)) {
|
|
592
|
-
const {
|
|
593
|
-
model: _delegationModel,
|
|
594
|
-
provider: _delegationProvider,
|
|
595
|
-
modelProvider: _delegationModelProvider,
|
|
596
|
-
model_provider: _delegationModelProviderSnake,
|
|
597
|
-
...delegationRest
|
|
598
|
-
} = delegation;
|
|
599
|
-
if (Object.keys(delegationRest).length > 0) {
|
|
600
|
-
sanitized.delegation = delegationRest;
|
|
601
|
-
} else {
|
|
602
|
-
delete sanitized.delegation;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
return sanitized;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
function copyHermesCoordinatorCredentialFiles(sourceHome: string, targetHome: string) {
|
|
609
|
-
if (pathResolve(sourceHome) === pathResolve(targetHome)) return;
|
|
610
|
-
for (const fileName of ['.env', 'auth.json']) {
|
|
611
|
-
const sourcePath = pathJoin(sourceHome, fileName);
|
|
612
|
-
const targetPath = pathJoin(targetHome, fileName);
|
|
613
|
-
if (!fs.existsSync(sourcePath)) continue;
|
|
614
|
-
try {
|
|
615
|
-
fs.copyFileSync(sourcePath, targetPath);
|
|
616
|
-
} catch (error: any) {
|
|
617
|
-
LOG.warn('MeshCoordinator', `Could not copy Hermes ${fileName} into isolated coordinator home: ${error?.message || error}`);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
41
|
+
import { execFileSync } from 'node:child_process';
|
|
42
|
+
|
|
43
|
+
// ─── Extracted-module imports (symbols the dispatch class consumes) ───
|
|
44
|
+
import {
|
|
45
|
+
foldMeshNodeIdentityToCanonical,
|
|
46
|
+
inlineMeshCarriesTransientNodeTruth,
|
|
47
|
+
MESH_DIRECT_PROBE_REUSE_MS,
|
|
48
|
+
MeshGitProbeCache,
|
|
49
|
+
normalizeInlineMeshNodeIdentity,
|
|
50
|
+
readInlineMeshNodeId,
|
|
51
|
+
readObjectRecord,
|
|
52
|
+
readStringValue,
|
|
53
|
+
reconcileInlineMeshCache,
|
|
54
|
+
sanitizeInlineMesh,
|
|
55
|
+
} from '../mesh/mesh-node-identity.js';
|
|
56
|
+
import {
|
|
57
|
+
alignRefinerySubmodulesAfterMerge,
|
|
58
|
+
buildMeshRefineValidationPlan,
|
|
59
|
+
buildSubmodulePublishRequiredNextStep,
|
|
60
|
+
checkWorktreeChangesPatchEquivalentInRef,
|
|
61
|
+
MeshRefineAsyncJobStatus,
|
|
62
|
+
MeshRefineBatchJobHandle,
|
|
63
|
+
MeshRefineBatchJobStatus,
|
|
64
|
+
MeshRefineBatchTerminalJob,
|
|
65
|
+
MeshRefineJobHandle,
|
|
66
|
+
MeshRefineTerminalJob,
|
|
67
|
+
MeshWorktreePatchContainmentSummary,
|
|
68
|
+
recordMeshRefineStage,
|
|
69
|
+
RefineContext,
|
|
70
|
+
RefineExecFileAsync,
|
|
71
|
+
RefineStageOutcome,
|
|
72
|
+
resolveRefineryAutoPublishSubmoduleMainCommits,
|
|
73
|
+
runMeshRefineEffectiveDiffGate,
|
|
74
|
+
runMeshRefinePatchEquivalenceGate,
|
|
75
|
+
runMeshRefineSubmoduleReachabilityGate,
|
|
76
|
+
runMeshRefineValidationGate,
|
|
77
|
+
truncateValidationOutput,
|
|
78
|
+
} from '../mesh/mesh-refine-gates.js';
|
|
79
|
+
// ─── Refinery job orchestration (bodies extracted from this file) ───
|
|
80
|
+
import {
|
|
81
|
+
batchRefineMeshNodes,
|
|
82
|
+
resumePendingRefineJobsOnStartup,
|
|
83
|
+
startMeshRefineBatchJob,
|
|
84
|
+
startMeshRefineJob,
|
|
85
|
+
} from './router-refine.js';
|
|
86
|
+
// ─── Worktree / mesh-session cleanup (bodies extracted from this file) ───
|
|
87
|
+
import {
|
|
88
|
+
bestEffortRemoveWorktreeDir,
|
|
89
|
+
cleanupLocalWorktreeNode,
|
|
90
|
+
cleanupMeshSessions,
|
|
91
|
+
getWorktreeForceCleanupConvergence,
|
|
92
|
+
isCompletedHostedSession,
|
|
93
|
+
precheckLocalWorktreeRemovable,
|
|
94
|
+
recordIntentionalMeshSessionStop,
|
|
95
|
+
sessionMatchesMeshNode,
|
|
96
|
+
} from './router-worktree-cleanup.js';
|
|
97
|
+
// ─── Aggregate mesh-status cache (bodies extracted from this file) ───
|
|
98
|
+
import {
|
|
99
|
+
getCachedAggregateMeshStatus,
|
|
100
|
+
hydrateCachedAggregateMeshStatusFromInline,
|
|
101
|
+
rememberAggregateMeshStatus,
|
|
102
|
+
} from './router-aggregate-status.js';
|
|
103
|
+
// ─── Remote mesh-session owner resolution (bodies extracted from this file) ───
|
|
104
|
+
import { resolveRemoteMeshSessionOwnerDaemonId } from './router-mesh-session-owner.js';
|
|
105
|
+
|
|
106
|
+
// ─── Barrel re-exports: node-identity / git-freshness, refine gates, coordinator config ───
|
|
107
|
+
// These modules were split out of router.ts. Re-export their public surface so the
|
|
108
|
+
// many existing `from '.../commands/router.js'` named imports keep resolving here.
|
|
109
|
+
export * from '../mesh/mesh-node-identity.js';
|
|
110
|
+
export * from '../mesh/mesh-refine-gates.js';
|
|
111
|
+
export * from '../mesh/mesh-coordinator-config.js';
|
|
621
112
|
|
|
622
113
|
// ─── Types ───
|
|
623
114
|
|
|
@@ -644,12 +135,14 @@ export interface CommandRouterDeps {
|
|
|
644
135
|
/** Reference to detected IDEs array (mutable — router updates it) */
|
|
645
136
|
detectedIdes: { value: any[] };
|
|
646
137
|
sessionRegistry: SessionRegistry;
|
|
647
|
-
/** Callback
|
|
138
|
+
/** Callback after CDP manager created (transport-specific extras) */
|
|
648
139
|
onCdpManagerCreated?: (ideType: string, manager: DaemonCdpManager) => void;
|
|
649
140
|
/** Callback after IDE connected (e.g., startAgentStreamPolling) */
|
|
650
141
|
onIdeConnected?: () => void;
|
|
651
142
|
/** Callback after status change (stop_ide, restart) */
|
|
652
143
|
onStatusChange?: () => void;
|
|
144
|
+
/** Callback when a mesh state is invalidated */
|
|
145
|
+
onMeshStateChange?: (meshId: string) => void;
|
|
653
146
|
/** Callback after chat-related commands */
|
|
654
147
|
onPostChatCommand?: () => void;
|
|
655
148
|
/** Get a connected CDP manager (for agent stream reset check) */
|
|
@@ -661,6 +154,10 @@ export interface CommandRouterDeps {
|
|
|
661
154
|
statusVersion?: string;
|
|
662
155
|
/** Session host control plane */
|
|
663
156
|
sessionHostControl?: SessionHostControlPlane | null;
|
|
157
|
+
/** Selected-coordinator mesh peer telemetry surface for target daemons, when supported by the runtime. */
|
|
158
|
+
getMeshPeerConnectionStatus?: (daemonId: string) => Record<string, unknown> | null;
|
|
159
|
+
/** Dispatch a command to a remote mesh node via P2P/relay. Injected by cloud runtime; absent in standalone. */
|
|
160
|
+
dispatchMeshCommand?: (daemonId: string, cmd: string, args: Record<string, unknown>) => Promise<unknown>;
|
|
664
161
|
}
|
|
665
162
|
|
|
666
163
|
export interface CommandRouterResult {
|
|
@@ -673,7 +170,30 @@ const CHAT_COMMANDS = [
|
|
|
673
170
|
'send_chat', 'new_chat', 'switch_chat', 'set_mode',
|
|
674
171
|
'change_model',
|
|
675
172
|
];
|
|
676
|
-
|
|
173
|
+
|
|
174
|
+
// [Z] Session-scoped commands that must be forwarded to the owning REMOTE worker daemon
|
|
175
|
+
// when their target session is not hosted on this coordinator. These are the interactive
|
|
176
|
+
// controlbar / modal mutations the dashboard issues against a specific session:
|
|
177
|
+
// - invoke_provider_script: controlbar Model/Mode selectors run a provider script
|
|
178
|
+
// - resolve_action: approve/reject a modal prompt
|
|
179
|
+
// - set_mode / change_model / set_thought_level: direct control mutations
|
|
180
|
+
// send_chat is intentionally NOT here — it already reaches the worker by its own route and
|
|
181
|
+
// double-forwarding would be redundant. read_chat is also excluded: it can serve historical
|
|
182
|
+
// transcript data locally and has its own inactive-session fallback in the CommandHandler.
|
|
183
|
+
const MESH_FORWARDABLE_SESSION_COMMANDS = new Set([
|
|
184
|
+
'invoke_provider_script',
|
|
185
|
+
'resolve_action',
|
|
186
|
+
'set_mode',
|
|
187
|
+
'change_model',
|
|
188
|
+
'set_thought_level',
|
|
189
|
+
// agent_command (send_chat / clear_history / stop) is session-scoped too: a command
|
|
190
|
+
// explicitly naming a targetSessionId MUST reach that session wherever it lives, never a
|
|
191
|
+
// different local session. Without forwarding, a misrouted/relayed send_chat for a REMOTE
|
|
192
|
+
// worker session that reaches the wrong daemon used to fuzzy-inject the task body into that
|
|
193
|
+
// daemon's own CLI session (TASKECHO coordinator self-echo). Forwarding to the owning daemon
|
|
194
|
+
// delivers it to the real worker instead. (findAdapter is also fail-closed as the backstop.)
|
|
195
|
+
'agent_command',
|
|
196
|
+
]);
|
|
677
197
|
|
|
678
198
|
function normalizeCommandSource(source: string): CommandLogEntry['source'] {
|
|
679
199
|
switch (source) {
|
|
@@ -696,140 +216,458 @@ function normalizeCommandArgsWithInteractionId(args: any): Record<string, unknow
|
|
|
696
216
|
return base;
|
|
697
217
|
}
|
|
698
218
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
sessionCount: records.length,
|
|
751
|
-
liveRuntimeCount: groups.liveRuntimes.length,
|
|
752
|
-
recoverySnapshotCount: groups.recoverySnapshots.length,
|
|
753
|
-
inactiveRecordCount: groups.inactiveRecords.length,
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function summarizeSessionHostDiagnostics(result: unknown): Record<string, unknown> {
|
|
758
|
-
const diagnostics = result && typeof result === 'object' ? result as Record<string, any> : {};
|
|
759
|
-
const sessions = Array.isArray(diagnostics.sessions) ? diagnostics.sessions : [];
|
|
760
|
-
return {
|
|
761
|
-
runtimeCount: typeof diagnostics.runtimeCount === 'number' ? diagnostics.runtimeCount : undefined,
|
|
762
|
-
...summarizeSessionHostRecords(sessions),
|
|
219
|
+
/**
|
|
220
|
+
* Confine a spec path to ~/.adhdev/providers, defeating both prefix-bypass
|
|
221
|
+
* (e.g. ".../providers-evil") and symlink escape. Resolves the real path of
|
|
222
|
+
* the *parent* directory (the file may not exist yet for writes), requires the
|
|
223
|
+
* basename to be a literal `*.json`, and re-joins under the verified parent so
|
|
224
|
+
* the returned path can't point outside the tree. Used by get/write_spec_source.
|
|
225
|
+
*/
|
|
226
|
+
export function normalizeStandaloneHostCommandUrl(hostAddress: string): string {
|
|
227
|
+
const raw = hostAddress.trim();
|
|
228
|
+
if (!raw) throw new Error('hostAddress required');
|
|
229
|
+
const url = new URL(raw.replace(/^ws:/, 'http:').replace(/^wss:/, 'https:'));
|
|
230
|
+
url.pathname = '/api/v1/command';
|
|
231
|
+
url.search = '';
|
|
232
|
+
url.hash = '';
|
|
233
|
+
return url.toString();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function buildMemberJoinNode(mesh: any, args: any, fallbackDaemonId?: string): Record<string, unknown> | null {
|
|
237
|
+
const requestedNodeId = typeof args?.memberNodeId === 'string' ? args.memberNodeId.trim() : '';
|
|
238
|
+
const explicit = args?.memberNode && typeof args.memberNode === 'object' && !Array.isArray(args.memberNode)
|
|
239
|
+
? args.memberNode as Record<string, any>
|
|
240
|
+
: null;
|
|
241
|
+
const configured = Array.isArray(mesh?.nodes)
|
|
242
|
+
? (requestedNodeId
|
|
243
|
+
? mesh.nodes.find((node: any) => meshNodeIdMatches(node, requestedNodeId))
|
|
244
|
+
: mesh.nodes[0])
|
|
245
|
+
: null;
|
|
246
|
+
const source = explicit || configured;
|
|
247
|
+
const workspace = typeof source?.workspace === 'string' && source.workspace.trim()
|
|
248
|
+
? source.workspace.trim()
|
|
249
|
+
: typeof args?.workspace === 'string' && args.workspace.trim()
|
|
250
|
+
? args.workspace.trim()
|
|
251
|
+
: process.cwd();
|
|
252
|
+
if (!workspace) return null;
|
|
253
|
+
const nodeId = typeof source?.id === 'string' && source.id.trim()
|
|
254
|
+
? source.id.trim()
|
|
255
|
+
: typeof source?.nodeId === 'string' && source.nodeId.trim()
|
|
256
|
+
? source.nodeId.trim()
|
|
257
|
+
: undefined;
|
|
258
|
+
const baseOverrides = source?.userOverrides && typeof source.userOverrides === 'object' && !Array.isArray(source.userOverrides)
|
|
259
|
+
? source.userOverrides as Record<string, unknown>
|
|
260
|
+
: {};
|
|
261
|
+
// This payload is built ON THE MEMBER DAEMON, so process.platform/process.arch
|
|
262
|
+
// are the member's OWN machine. Stamp them into userOverrides so the host
|
|
263
|
+
// stores the member's real platform/arch on the node record — the coordinator's
|
|
264
|
+
// buildMeshNodeCapabilityTags then advertises os=<member-os> instead of the
|
|
265
|
+
// coordinator's own platform. Only fill values the operator hasn't already set.
|
|
266
|
+
const userOverrides: Record<string, unknown> = {
|
|
267
|
+
...baseOverrides,
|
|
268
|
+
...(typeof baseOverrides.platform === 'string' && baseOverrides.platform.trim() ? {} : { platform: process.platform }),
|
|
269
|
+
...(typeof baseOverrides.arch === 'string' && baseOverrides.arch.trim() ? {} : { arch: process.arch }),
|
|
763
270
|
};
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function summarizeSessionHostPruneResult(result: unknown): Record<string, unknown> {
|
|
767
|
-
const value = result && typeof result === 'object' ? result as Record<string, any> : {};
|
|
768
271
|
return {
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
272
|
+
...(nodeId ? { id: nodeId } : {}),
|
|
273
|
+
workspace,
|
|
274
|
+
...(typeof source?.repoRoot === 'string' && source.repoRoot.trim() ? { repoRoot: source.repoRoot.trim() } : {}),
|
|
275
|
+
...(typeof source?.daemonId === 'string' && source.daemonId.trim() ? { daemonId: source.daemonId.trim() } : fallbackDaemonId ? { daemonId: fallbackDaemonId } : {}),
|
|
276
|
+
...(typeof source?.machineId === 'string' && source.machineId.trim() ? { machineId: source.machineId.trim() } : {}),
|
|
277
|
+
userOverrides,
|
|
278
|
+
policy: source?.policy && typeof source.policy === 'object' && !Array.isArray(source.policy) ? source.policy : {},
|
|
279
|
+
role: 'member',
|
|
772
280
|
};
|
|
773
281
|
}
|
|
774
282
|
|
|
775
283
|
export class DaemonCommandRouter {
|
|
776
|
-
private
|
|
284
|
+
/** Public (not private) so the extracted ./router-refine.ts orchestration can reach it via `self`. */
|
|
285
|
+
deps: CommandRouterDeps;
|
|
777
286
|
/** In-memory cache for cloud-originating meshes passed via inlineMesh.
|
|
778
287
|
* Allows the MCP server to query mesh data via get_mesh even when
|
|
779
288
|
* the mesh doesn't exist in the local meshes.json file. */
|
|
780
289
|
private inlineMeshCache = new Map<string, any>();
|
|
290
|
+
/** Tombstones for inline mesh nodes removed via remove_mesh_node, keyed by
|
|
291
|
+
* meshId → set of removed nodeIds. The dashboard keeps echoing the removed
|
|
292
|
+
* node in the inlineMesh it attaches to every command; without a tombstone,
|
|
293
|
+
* reconcileInlineMeshCache MERGEs it straight back (resurrection). A
|
|
294
|
+
* tombstoned node is skipped during reconcile only while its workspace is
|
|
295
|
+
* absent from disk — a genuine re-registration (same nodeId, workspace back
|
|
296
|
+
* on disk) clears the tombstone and merges normally, preserving clone
|
|
297
|
+
* worktree visibility and legitimate node re-creation. */
|
|
298
|
+
private removedInlineMeshNodeIds = new Map<string, Set<string>>();
|
|
299
|
+
/** Coordinator-owned whole-mesh aggregate status snapshots. Browser callers read this by default.
|
|
300
|
+
* Public (not private) so the extracted ./router-aggregate-status.ts orchestration can reach it via `self`. */
|
|
301
|
+
aggregateMeshStatusCache = new Map<string, { builtAt: number; snapshot: any; queueRevision: string }>();
|
|
302
|
+
/** Shared per-peer git_status probe dedup + recently-probed reuse gate.
|
|
303
|
+
* Spans separate mesh_status/get_mesh calls so the dashboard auto-retry
|
|
304
|
+
* loop cannot storm a slow peer with back-to-back refreshUpstream probes. */
|
|
305
|
+
private meshGitProbeCache = new MeshGitProbeCache(MESH_DIRECT_PROBE_REUSE_MS);
|
|
306
|
+
/** Meshes with a background SWR freshen (async mesh_status refresh) already in
|
|
307
|
+
* flight — so a burst of interactive detail-opens serves the cached snapshot
|
|
308
|
+
* and coalesces onto ONE background refresh instead of storming the peers. */
|
|
309
|
+
private swrRefreshInFlight = new Set<string>();
|
|
310
|
+
/** In-memory async Refinery jobs keyed by meshId:nodeId to reject/return duplicate in-flight requests.
|
|
311
|
+
* Public (not private) so the extracted ./router-refine.ts orchestration can reach it via `self`. */
|
|
312
|
+
runningRefineJobs = new Map<string, MeshRefineJobHandle>();
|
|
313
|
+
/** Terminal async Refinery jobs preserve a clear answer after the worktree node has been removed. */
|
|
314
|
+
terminalRefineJobs = new Map<string, MeshRefineTerminalJob>();
|
|
315
|
+
/** In-memory async batch Refinery jobs keyed by meshId (one batch convergence per mesh at a time). */
|
|
316
|
+
runningRefineBatchJobs = new Map<string, MeshRefineBatchJobHandle>();
|
|
317
|
+
/** Terminal async batch Refinery jobs preserve the last batch outcome for late readers. */
|
|
318
|
+
terminalRefineBatchJobs = new Map<string, MeshRefineBatchTerminalJob>();
|
|
781
319
|
|
|
782
320
|
constructor(deps: CommandRouterDeps) {
|
|
783
321
|
this.deps = deps;
|
|
784
322
|
}
|
|
785
323
|
|
|
324
|
+
// ─── Aggregate mesh-status cache ────────────────────────────────────
|
|
325
|
+
// Implementation lives in ./router-aggregate-status.ts (behavior-preserving
|
|
326
|
+
// code move). Kept here as thin delegators: getCachedAggregateMeshStatus /
|
|
327
|
+
// rememberAggregateMeshStatus are bound into HighFamilyContext, so callers
|
|
328
|
+
// reach these via `self.` for correct instance dispatch.
|
|
329
|
+
|
|
330
|
+
private hydrateCachedAggregateMeshStatusFromInline(snapshot: any, mesh: any, options?: { requireDirectPeerTruth?: boolean }): any {
|
|
331
|
+
return hydrateCachedAggregateMeshStatusFromInline(this, snapshot, mesh, options);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private getCachedAggregateMeshStatus(
|
|
335
|
+
meshId: string,
|
|
336
|
+
mesh?: any,
|
|
337
|
+
options?: { requireDirectPeerTruth?: boolean; allowStalePending?: boolean },
|
|
338
|
+
): any | null {
|
|
339
|
+
return getCachedAggregateMeshStatus(this, meshId, mesh, options);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private rememberAggregateMeshStatus(meshId: string, snapshot: any, refreshReason: string): any {
|
|
343
|
+
return rememberAggregateMeshStatus(this, meshId, snapshot, refreshReason);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
public getCachedInlineMeshNodes(): any[] {
|
|
347
|
+
const nodes: any[] = [];
|
|
348
|
+
for (const mesh of this.inlineMeshCache.values()) {
|
|
349
|
+
if (Array.isArray(mesh?.nodes)) {
|
|
350
|
+
nodes.push(...mesh.nodes);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return nodes;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// ─── Remote mesh-session owner resolution ───────────────────────────
|
|
357
|
+
// Implementation lives in ./router-mesh-session-owner.ts (behavior-preserving
|
|
358
|
+
// code move). resolveRemoteMeshSessionOwnerDaemonId stays public (the [Z]
|
|
359
|
+
// session-scoped forward in executeDaemonCommand and a unit test call it), so
|
|
360
|
+
// it's kept here as a thin delegator.
|
|
361
|
+
|
|
362
|
+
public resolveRemoteMeshSessionOwnerDaemonId(sessionId: string, ownerNodeIdHint?: string): string | undefined {
|
|
363
|
+
return resolveRemoteMeshSessionOwnerDaemonId(this, sessionId, ownerNodeIdHint);
|
|
364
|
+
}
|
|
365
|
+
|
|
786
366
|
public getCachedInlineMesh(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
787
367
|
if (inlineMesh && typeof inlineMesh === 'object') {
|
|
788
|
-
this.
|
|
789
|
-
return inlineMesh as any;
|
|
368
|
+
return this.warmInlineMeshCache(meshId, inlineMesh);
|
|
790
369
|
}
|
|
791
370
|
return this.inlineMeshCache.get(meshId);
|
|
792
371
|
}
|
|
793
372
|
|
|
794
|
-
private
|
|
373
|
+
private warmInlineMeshCache(meshId: string, inlineMesh?: unknown): any | undefined {
|
|
374
|
+
if (!inlineMesh || typeof inlineMesh !== 'object') return undefined;
|
|
375
|
+
// Save-boundary node-id normalization: reconcile each node's identity so
|
|
376
|
+
// `id` and `nodeId` agree before it enters the cache, so reconcile keys
|
|
377
|
+
// and the round-trip through the status serializer stay form-stable.
|
|
378
|
+
const sanitizedInlineMesh = this.applyInlineMeshNodeTombstones(
|
|
379
|
+
meshId,
|
|
380
|
+
sanitizeInlineMesh(normalizeInlineMeshNodeIdentity(inlineMesh as any)),
|
|
381
|
+
);
|
|
382
|
+
const cached = this.inlineMeshCache.get(meshId);
|
|
383
|
+
if (cached) {
|
|
384
|
+
const merged = reconcileInlineMeshCache(cached, sanitizedInlineMesh);
|
|
385
|
+
this.inlineMeshCache.set(meshId, merged);
|
|
386
|
+
return merged;
|
|
387
|
+
}
|
|
388
|
+
this.inlineMeshCache.set(meshId, sanitizedInlineMesh as any);
|
|
389
|
+
return sanitizedInlineMesh as any;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async getMeshForCommand(
|
|
795
393
|
meshId: string,
|
|
796
394
|
inlineMesh?: unknown,
|
|
797
395
|
options?: { preferInline?: boolean },
|
|
798
|
-
): Promise<{ mesh: any; inline: boolean } | null> {
|
|
799
|
-
|
|
396
|
+
): Promise<{ mesh: any; inline: boolean; source: 'inline_cache' | 'inline_bootstrap' | 'local_config' } | null> {
|
|
397
|
+
// Default to inline-cache-preferred: a caller that omits the flag still sees
|
|
398
|
+
// inline-cache-only (worktree clone) nodes in the resolved mesh view, closing
|
|
399
|
+
// the CLAIMSTALL gap where a missed `preferInline: true` silently dropped them.
|
|
400
|
+
// An explicit `preferInline: false` is still honored for any local-config-only
|
|
401
|
+
// read that deliberately bypasses the inline cache.
|
|
402
|
+
const preferInline = options?.preferInline !== false;
|
|
800
403
|
if (preferInline) {
|
|
801
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
802
|
-
if (cached)
|
|
404
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
405
|
+
if (cached) {
|
|
406
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
407
|
+
const merged = reconcileInlineMeshCache(
|
|
408
|
+
cached,
|
|
409
|
+
this.applyInlineMeshNodeTombstones(meshId, inlineMesh as any),
|
|
410
|
+
);
|
|
411
|
+
this.inlineMeshCache.set(meshId, sanitizeInlineMesh(normalizeInlineMeshNodeIdentity(merged)));
|
|
412
|
+
return { mesh: merged, inline: true, source: 'inline_cache' };
|
|
413
|
+
}
|
|
414
|
+
return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
415
|
+
}
|
|
416
|
+
if (inlineMeshCarriesTransientNodeTruth(inlineMesh)) {
|
|
417
|
+
this.warmInlineMeshCache(meshId, inlineMesh);
|
|
418
|
+
return { mesh: inlineMesh, inline: true, source: 'inline_bootstrap' };
|
|
419
|
+
}
|
|
803
420
|
}
|
|
804
421
|
try {
|
|
805
422
|
const { getMesh } = await import('../config/mesh-config.js');
|
|
806
423
|
const mesh = getMesh(meshId);
|
|
807
|
-
if (mesh) return { mesh, inline: false };
|
|
424
|
+
if (mesh) return { mesh, inline: false, source: 'local_config' };
|
|
808
425
|
} catch { /* fall through to inline cache */ }
|
|
809
|
-
const cached = this.getCachedInlineMesh(meshId
|
|
810
|
-
|
|
426
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
427
|
+
if (cached) return { mesh: cached, inline: true, source: 'inline_cache' };
|
|
428
|
+
const warmedInline = this.warmInlineMeshCache(meshId, inlineMesh);
|
|
429
|
+
return warmedInline ? { mesh: warmedInline, inline: true, source: 'inline_bootstrap' } : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
invalidateAggregateMeshStatus(meshId: string): void {
|
|
433
|
+
this.aggregateMeshStatusCache.delete(meshId);
|
|
434
|
+
this.deps.onMeshStateChange?.(meshId);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Build the MedFamilyContext handed to RF-ROUTER MED family handlers. Binds the
|
|
439
|
+
* router-private collaborators those handlers need (mesh resolution, owner
|
|
440
|
+
* gating, inline-cache mutation, worktree / session cleanup, refine job
|
|
441
|
+
* starters, IDE stop/launch) plus the inline-mesh and git-probe caches. The
|
|
442
|
+
* `launchIde` field closes over the freshly-built context so restart_session /
|
|
443
|
+
* restart_ide invoke the IDE launch directly instead of recursing through
|
|
444
|
+
* executeDaemonCommand('launch_ide').
|
|
445
|
+
*/
|
|
446
|
+
private buildMedFamilyContext(): MedFamilyContext {
|
|
447
|
+
const ctx: MedFamilyContext = {
|
|
448
|
+
deps: this.deps,
|
|
449
|
+
getMeshForCommand: this.getMeshForCommand.bind(this),
|
|
450
|
+
getCachedInlineMesh: this.getCachedInlineMesh.bind(this),
|
|
451
|
+
requireMeshHostMutationOwner: this.requireMeshHostMutationOwner.bind(this),
|
|
452
|
+
invalidateAggregateMeshStatus: this.invalidateAggregateMeshStatus.bind(this),
|
|
453
|
+
updateInlineMeshNode: this.updateInlineMeshNode.bind(this),
|
|
454
|
+
removeInlineMeshNode: this.removeInlineMeshNode.bind(this),
|
|
455
|
+
normalizeMeshSessionCleanupMode: this.normalizeMeshSessionCleanupMode.bind(this),
|
|
456
|
+
cleanupMeshSessions: this.cleanupMeshSessions.bind(this),
|
|
457
|
+
cleanupLocalWorktreeNode: this.cleanupLocalWorktreeNode.bind(this),
|
|
458
|
+
precheckLocalWorktreeRemovable: this.precheckLocalWorktreeRemovable.bind(this),
|
|
459
|
+
startMeshRefineJob: this.startMeshRefineJob.bind(this),
|
|
460
|
+
batchRefineMeshNodes: this.batchRefineMeshNodes.bind(this),
|
|
461
|
+
startMeshRefineBatchJob: this.startMeshRefineBatchJob.bind(this),
|
|
462
|
+
stopIde: this.stopIde.bind(this),
|
|
463
|
+
launchIde: (args: any) => launchIde(ctx, args),
|
|
464
|
+
inlineMeshCache: this.inlineMeshCache,
|
|
465
|
+
meshGitProbeCache: this.meshGitProbeCache,
|
|
466
|
+
};
|
|
467
|
+
return ctx;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Build the HighFamilyContext handed to RF-ROUTER HIGH family handlers. Binds
|
|
472
|
+
* the router-private collaborators those handlers need (mesh resolution, the
|
|
473
|
+
* aggregate-status memory cache + its bound read/write helpers, the
|
|
474
|
+
* running-refine-job table, inline-mesh + git-probe caches, and the router's
|
|
475
|
+
* own `execute` for the get_mesh_review_inbox mesh_status re-entry). HIGH
|
|
476
|
+
* handlers reach more router-owned state than MED, but the binding shape is
|
|
477
|
+
* the same: bound methods + direct field references, none reachable from
|
|
478
|
+
* `deps`.
|
|
479
|
+
*/
|
|
480
|
+
private buildHighFamilyContext(): HighFamilyContext {
|
|
481
|
+
return {
|
|
482
|
+
deps: this.deps,
|
|
483
|
+
getMeshForCommand: this.getMeshForCommand.bind(this),
|
|
484
|
+
getCachedAggregateMeshStatus: this.getCachedAggregateMeshStatus.bind(this),
|
|
485
|
+
rememberAggregateMeshStatus: this.rememberAggregateMeshStatus.bind(this),
|
|
486
|
+
execute: this.execute.bind(this),
|
|
487
|
+
aggregateMeshStatusCache: this.aggregateMeshStatusCache,
|
|
488
|
+
swrRefreshInFlight: this.swrRefreshInFlight,
|
|
489
|
+
runningRefineJobs: this.runningRefineJobs,
|
|
490
|
+
inlineMeshCache: this.inlineMeshCache,
|
|
491
|
+
meshGitProbeCache: this.meshGitProbeCache,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
private async requireMeshHostMutationOwner(meshId: string, inlineMesh: unknown, operation: string): Promise<CommandRouterResult | null> {
|
|
497
|
+
const meshRecord = await this.getMeshForCommand(meshId, inlineMesh, { preferInline: true });
|
|
498
|
+
const mesh = meshRecord?.mesh;
|
|
499
|
+
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
500
|
+
const meshHost = resolveMeshHostStatus(mesh);
|
|
501
|
+
if (!meshHost.canOwnCoordinator || !meshHost.canOwnQueue) {
|
|
502
|
+
return { ...buildMeshHostRequiredFailure(mesh, operation), success: false, meshId };
|
|
503
|
+
}
|
|
504
|
+
return null;
|
|
811
505
|
}
|
|
812
506
|
|
|
813
507
|
private updateInlineMeshNode(meshId: string, mesh: any, node: any): void {
|
|
814
|
-
|
|
815
|
-
|
|
508
|
+
const incomingId = normalizeMeshNodeId(node);
|
|
509
|
+
if (!mesh || !Array.isArray(mesh.nodes) || !incomingId) return;
|
|
510
|
+
const idx = mesh.nodes.findIndex((entry: any) => meshNodeIdMatches(entry, incomingId));
|
|
816
511
|
if (idx >= 0) mesh.nodes[idx] = node;
|
|
817
512
|
else mesh.nodes.push(node);
|
|
818
513
|
mesh.updatedAt = new Date().toISOString();
|
|
514
|
+
// Canonicalize node identity in place (id and nodeId kept equal): clone
|
|
515
|
+
// nodes are created with `id` and re-inserted here (bypassing
|
|
516
|
+
// warmInlineMeshCache), so this is a second save boundary. In-place
|
|
517
|
+
// folding preserves the caller's `mesh` / nodes-array references, which
|
|
518
|
+
// persistWorktreeSetupState reuses across subsequent calls.
|
|
519
|
+
for (const entry of mesh.nodes) foldMeshNodeIdentityToCanonical(entry);
|
|
819
520
|
this.inlineMeshCache.set(meshId, mesh);
|
|
521
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
820
522
|
}
|
|
821
523
|
|
|
822
524
|
private removeInlineMeshNode(meshId: string, mesh: any, nodeId: string): boolean {
|
|
823
525
|
if (!mesh || !Array.isArray(mesh.nodes)) return false;
|
|
824
|
-
const idx = mesh.nodes.findIndex((entry: any) => entry
|
|
526
|
+
const idx = mesh.nodes.findIndex((entry: any) => meshNodeIdMatches(entry, nodeId));
|
|
825
527
|
if (idx === -1) return false;
|
|
528
|
+
const canonicalNodeId = readInlineMeshNodeId(mesh.nodes[idx]) || nodeId;
|
|
826
529
|
mesh.nodes.splice(idx, 1);
|
|
827
530
|
mesh.updatedAt = new Date().toISOString();
|
|
828
531
|
this.inlineMeshCache.set(meshId, mesh);
|
|
532
|
+
// Tombstone the removed node so the dashboard's stale inlineMesh echo does
|
|
533
|
+
// not MERGE it back on the next command (see removedInlineMeshNodeIds).
|
|
534
|
+
this.tombstoneRemovedInlineMeshNode(meshId, canonicalNodeId);
|
|
535
|
+
if (canonicalNodeId !== nodeId) this.tombstoneRemovedInlineMeshNode(meshId, nodeId);
|
|
536
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
829
537
|
return true;
|
|
830
538
|
}
|
|
831
539
|
|
|
832
|
-
|
|
540
|
+
/**
|
|
541
|
+
* WORKTREE-BOOTSTRAP-COORD-STATE: mark a worktree node's bootstrap as reaching a
|
|
542
|
+
* terminal state (complete / failed) in THIS daemon's mesh view.
|
|
543
|
+
*
|
|
544
|
+
* Root cause this fixes: clone_mesh_node forwards the clone+bootstrap to the
|
|
545
|
+
* source node's daemon (the worktree's machine). persistWorktreeSetupState
|
|
546
|
+
* therefore flips worktreeBootstrap.status to 'complete' on the WORKER daemon's
|
|
547
|
+
* mesh object — never on the coordinator's. The coordinator only ever holds the
|
|
548
|
+
* 'running' state it stamped from the forwarded clone reply. The claim path's
|
|
549
|
+
* bootstrap gate (mesh-event-forwarding agent:ready / mesh-queue-assignment)
|
|
550
|
+
* reads the coordinator's mesh via getMeshWithCache, sees status==='running'
|
|
551
|
+
* forever, and DEFERS every claim — so the worktree_bootstrap_complete re-fire
|
|
552
|
+
* (triggerMeshQueue) loops against a gate that never opens: claim never lands,
|
|
553
|
+
* the idle session is re-registered each tick, and auto-launch keeps spawning
|
|
554
|
+
* fresh sessions (runaway worktree-session multiplication).
|
|
555
|
+
*
|
|
556
|
+
* Called from the worktree_bootstrap_complete/_failed event handler BEFORE the
|
|
557
|
+
* queue re-fire so the gate sees the terminal state and the deferred claim can
|
|
558
|
+
* finally land. Updates the inline cache (clone worktree nodes are inline-only)
|
|
559
|
+
* and, when the node also exists in local config, persists there too; both paths
|
|
560
|
+
* invalidate the aggregate status cache. Best-effort and idempotent.
|
|
561
|
+
*/
|
|
562
|
+
public markWorktreeBootstrapTerminalState(meshId: string, nodeId: string, status: 'complete' | 'failed', opts?: { workspace?: string }): void {
|
|
563
|
+
if (!meshId || !nodeId) return;
|
|
564
|
+
const terminalBootstrap = (prev?: Record<string, unknown>): Record<string, unknown> => ({
|
|
565
|
+
...(prev && typeof prev === 'object' ? prev : {}),
|
|
566
|
+
status,
|
|
567
|
+
completedAt: (prev as any)?.completedAt ?? new Date().toISOString(),
|
|
568
|
+
});
|
|
569
|
+
const stamp = (mesh: any): boolean => {
|
|
570
|
+
if (!mesh || !Array.isArray(mesh.nodes)) return false;
|
|
571
|
+
const node = mesh.nodes.find((entry: any) => meshNodeIdMatches(entry, nodeId));
|
|
572
|
+
if (!node) return false;
|
|
573
|
+
const prev = (node.worktreeBootstrap && typeof node.worktreeBootstrap === 'object')
|
|
574
|
+
? node.worktreeBootstrap as Record<string, unknown>
|
|
575
|
+
: {};
|
|
576
|
+
if (prev.status === status) return false;
|
|
577
|
+
node.worktreeBootstrap = terminalBootstrap(prev);
|
|
578
|
+
return true;
|
|
579
|
+
};
|
|
580
|
+
let changed = false;
|
|
581
|
+
// Inline cache (the authoritative view for inline-only clone worktree nodes).
|
|
582
|
+
try {
|
|
583
|
+
const cached = this.getCachedInlineMesh(meshId);
|
|
584
|
+
if (cached && stamp(cached)) {
|
|
585
|
+
cached.updatedAt = new Date().toISOString();
|
|
586
|
+
this.inlineMeshCache.set(meshId, cached);
|
|
587
|
+
changed = true;
|
|
588
|
+
} else if (!cached || !(Array.isArray(cached.nodes) && cached.nodes.some((entry: any) => meshNodeIdMatches(entry, nodeId)))) {
|
|
589
|
+
// Fix (3) HYDRATE-ON-MISS: the terminal bootstrap event arrived for a node this
|
|
590
|
+
// coordinator's inline view does NOT hold — the clone reply that would have seeded
|
|
591
|
+
// the worktree node never reached this daemon (or the inline mesh for this id is
|
|
592
|
+
// empty). With no node entry the claim gate has nothing to open, so the deferred
|
|
593
|
+
// claim (mesh-event-forwarding agent:ready / mesh-queue-assignment) strands forever
|
|
594
|
+
// and the coordinator relaunch/stop-loops. Instead of returning early, upsert a
|
|
595
|
+
// minimal worktree node carrying the terminal bootstrap status from the event
|
|
596
|
+
// payload so the gate — which reads this same inline view — sees a non-'running'
|
|
597
|
+
// state and the registered idle session can finally claim. Identity is canonicalized
|
|
598
|
+
// by updateInlineMeshNode (id/nodeId folded via the shared 3-form normalizer), so the
|
|
599
|
+
// remote-clone node-id form resolves the same way every claim-path consumer matches.
|
|
600
|
+
const shell = (cached && typeof cached === 'object')
|
|
601
|
+
? cached
|
|
602
|
+
: { id: meshId, nodes: [] as any[], updatedAt: new Date().toISOString() };
|
|
603
|
+
if (!Array.isArray(shell.nodes)) shell.nodes = [];
|
|
604
|
+
const hydratedNode: any = {
|
|
605
|
+
id: nodeId,
|
|
606
|
+
nodeId,
|
|
607
|
+
isLocalWorktree: true,
|
|
608
|
+
...(opts?.workspace ? { workspace: opts.workspace } : {}),
|
|
609
|
+
worktreeBootstrap: terminalBootstrap(),
|
|
610
|
+
};
|
|
611
|
+
this.updateInlineMeshNode(meshId, shell, hydratedNode);
|
|
612
|
+
changed = true;
|
|
613
|
+
}
|
|
614
|
+
} catch { /* best-effort */ }
|
|
615
|
+
if (changed) this.invalidateAggregateMeshStatus(meshId);
|
|
616
|
+
// Local config (a worktree node registered via addNode also lives here).
|
|
617
|
+
// Done in a detached dynamic-import chain so the method stays sync; both the
|
|
618
|
+
// stamp and the persist are best-effort, and the inline-cache stamp above is
|
|
619
|
+
// what the coordinator's claim gate reads.
|
|
620
|
+
void import('../config/mesh-config.js')
|
|
621
|
+
.then(({ getMesh, updateNode }) => {
|
|
622
|
+
const local = getMesh(meshId);
|
|
623
|
+
if (local && stamp(local)) {
|
|
624
|
+
const node = local.nodes.find((entry: any) => meshNodeIdMatches(entry, nodeId));
|
|
625
|
+
if (node) updateNode(meshId, node.id, { worktreeBootstrap: node.worktreeBootstrap } as any);
|
|
626
|
+
this.invalidateAggregateMeshStatus(meshId);
|
|
627
|
+
}
|
|
628
|
+
})
|
|
629
|
+
.catch(() => { /* persistence is best-effort */ });
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private tombstoneRemovedInlineMeshNode(meshId: string, nodeId: string): void {
|
|
633
|
+
if (!nodeId) return;
|
|
634
|
+
let set = this.removedInlineMeshNodeIds.get(meshId);
|
|
635
|
+
if (!set) {
|
|
636
|
+
set = new Set<string>();
|
|
637
|
+
this.removedInlineMeshNodeIds.set(meshId, set);
|
|
638
|
+
}
|
|
639
|
+
set.add(nodeId);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/** Filter an incoming inline mesh against this mesh's tombstones before it is
|
|
643
|
+
* reconciled into the cache. A tombstoned node is dropped only while its
|
|
644
|
+
* workspace is still absent from disk; if the workspace is back (genuine
|
|
645
|
+
* re-registration), the tombstone is cleared and the node merges normally. */
|
|
646
|
+
private applyInlineMeshNodeTombstones(meshId: string, incoming: any): any {
|
|
647
|
+
const tombstones = this.removedInlineMeshNodeIds.get(meshId);
|
|
648
|
+
if (!tombstones?.size || !incoming || typeof incoming !== 'object' || !Array.isArray(incoming.nodes)) {
|
|
649
|
+
return incoming;
|
|
650
|
+
}
|
|
651
|
+
let dropped = false;
|
|
652
|
+
const nodes = incoming.nodes.filter((node: any) => {
|
|
653
|
+
const nodeId = readInlineMeshNodeId(node);
|
|
654
|
+
if (!nodeId || !tombstones.has(nodeId)) return true;
|
|
655
|
+
const workspace = readStringValue(node?.workspace);
|
|
656
|
+
// Genuine re-registration: same nodeId, workspace back on disk →
|
|
657
|
+
// clear the tombstone and let the node merge normally.
|
|
658
|
+
if (workspace && fs.existsSync(workspace)) {
|
|
659
|
+
tombstones.delete(nodeId);
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
dropped = true;
|
|
663
|
+
return false;
|
|
664
|
+
});
|
|
665
|
+
if (tombstones.size === 0) this.removedInlineMeshNodeIds.delete(meshId);
|
|
666
|
+
if (!dropped) return incoming;
|
|
667
|
+
return { ...incoming, nodes };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
normalizeMeshSessionCleanupMode(value: unknown): RepoMeshSessionCleanupMode {
|
|
833
671
|
return value === 'stop'
|
|
834
672
|
|| value === 'delete_stopped'
|
|
835
673
|
|| value === 'stop_and_delete'
|
|
@@ -838,445 +676,77 @@ export class DaemonCommandRouter {
|
|
|
838
676
|
: 'preserve';
|
|
839
677
|
}
|
|
840
678
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
679
|
+
// ─── Worktree / mesh-session cleanup ────────────────────────────────
|
|
680
|
+
// Implementation lives in ./router-worktree-cleanup.ts (behavior-preserving
|
|
681
|
+
// code move). Kept here as thin delegators: cleanupMeshSessions /
|
|
682
|
+
// cleanupLocalWorktreeNode / precheckLocalWorktreeRemovable are bound into
|
|
683
|
+
// MedFamilyContext; bestEffortRemoveWorktreeDir is overridden on the instance
|
|
684
|
+
// by a unit test, so callers reach these via `self.` for correct dispatch.
|
|
685
|
+
|
|
686
|
+
sessionMatchesMeshNode(record: any, node: any, nodeId: string, sessionIds?: Set<string>): boolean {
|
|
687
|
+
return sessionMatchesMeshNode(this, record, node, nodeId, sessionIds);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async bestEffortRemoveWorktreeDir(dir: string): Promise<{ removed: boolean; residue: boolean; error?: string }> {
|
|
691
|
+
return bestEffortRemoveWorktreeDir(this, dir);
|
|
849
692
|
}
|
|
850
693
|
|
|
851
|
-
|
|
694
|
+
async precheckLocalWorktreeRemovable(args: {
|
|
852
695
|
mesh: any;
|
|
853
696
|
node: any;
|
|
854
697
|
nodeId: string;
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
success: false,
|
|
860
|
-
code: 'mesh_worktree_cleanup_missing_workspace',
|
|
861
|
-
error: `Worktree node '${args.nodeId}' is missing workspace metadata`,
|
|
862
|
-
recoveryHint: 'Inspect the mesh node record before removing it, or remove stale metadata manually only after confirming no managed worktree remains.',
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const worktreeExists = fs.existsSync(workspace);
|
|
867
|
-
const sourceNode = args.node?.clonedFromNodeId
|
|
868
|
-
? args.mesh?.nodes?.find((n: any) => n.id === args.node.clonedFromNodeId || n.nodeId === args.node.clonedFromNodeId)
|
|
869
|
-
: args.mesh?.nodes?.find((n: any) => !n.isLocalWorktree);
|
|
870
|
-
const repoRoot = typeof sourceNode?.repoRoot === 'string' && sourceNode.repoRoot.trim()
|
|
871
|
-
? sourceNode.repoRoot.trim()
|
|
872
|
-
: typeof sourceNode?.workspace === 'string' && sourceNode.workspace.trim()
|
|
873
|
-
? sourceNode.workspace.trim()
|
|
874
|
-
: '';
|
|
875
|
-
|
|
876
|
-
if (!worktreeExists) {
|
|
877
|
-
return { success: true, skipped: true, removedPath: workspace, repoRoot: repoRoot || undefined, reason: 'worktree_path_missing' };
|
|
878
|
-
}
|
|
879
|
-
if (!repoRoot || !fs.existsSync(repoRoot)) {
|
|
880
|
-
return {
|
|
881
|
-
success: false,
|
|
882
|
-
code: 'mesh_worktree_cleanup_missing_source_repo',
|
|
883
|
-
error: `Refusing to remove worktree '${workspace}' because the source repo root is unavailable`,
|
|
884
|
-
recoveryHint: 'Run mesh_remove_node from the machine that owns the source repo, or verify the source node metadata before retrying.',
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
if (typeof args.node?.worktreeBranch !== 'string' || !args.node.worktreeBranch.trim()) {
|
|
888
|
-
return {
|
|
889
|
-
success: false,
|
|
890
|
-
code: 'mesh_worktree_cleanup_missing_branch',
|
|
891
|
-
error: `Refusing to remove worktree '${workspace}' because worktreeBranch metadata is missing`,
|
|
892
|
-
recoveryHint: 'Confirm this is an ADHDev-managed worktree before removing it manually; managed worktree nodes include worktreeBranch metadata.',
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
const { resolveWorktreePath, listWorktrees, removeWorktree } = await import('../git/git-worktree.js');
|
|
897
|
-
const normalizePath = (value: string) => {
|
|
898
|
-
const resolved = pathResolve(value);
|
|
899
|
-
try { return fs.realpathSync(resolved); } catch { return resolved; }
|
|
900
|
-
};
|
|
901
|
-
const expectedPath = normalizePath(resolveWorktreePath(repoRoot, String(args.mesh?.name || args.mesh?.id || 'mesh'), args.node.worktreeBranch));
|
|
902
|
-
const actualPath = normalizePath(workspace);
|
|
903
|
-
if (actualPath !== expectedPath) {
|
|
904
|
-
return {
|
|
905
|
-
success: false,
|
|
906
|
-
code: 'mesh_worktree_cleanup_unexpected_path',
|
|
907
|
-
error: `Refusing to remove worktree '${workspace}' because it is not at the expected managed path '${expectedPath}'`,
|
|
908
|
-
recoveryHint: 'Use git worktree list/status to inspect the path. Retry only after confirming the mesh node metadata points to an ADHDev-managed worktree.',
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const entries = await listWorktrees(repoRoot);
|
|
913
|
-
const managedEntry = entries.find(entry => normalizePath(entry.path) === actualPath);
|
|
914
|
-
if (!managedEntry) {
|
|
915
|
-
return {
|
|
916
|
-
success: false,
|
|
917
|
-
code: 'mesh_worktree_cleanup_not_registered',
|
|
918
|
-
error: `Refusing to remove '${workspace}' because it is not registered in git worktree list for '${repoRoot}'`,
|
|
919
|
-
recoveryHint: 'Inspect git worktree list --porcelain from the source repo. If the path was already removed, prune git worktrees before retrying.',
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
if (managedEntry.branch && managedEntry.branch !== args.node.worktreeBranch) {
|
|
923
|
-
return {
|
|
924
|
-
success: false,
|
|
925
|
-
code: 'mesh_worktree_cleanup_branch_mismatch',
|
|
926
|
-
error: `Refusing to remove '${workspace}' because git reports branch '${managedEntry.branch}', expected '${args.node.worktreeBranch}'`,
|
|
927
|
-
recoveryHint: 'Inspect the worktree branch and mesh metadata before retrying cleanup.',
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
const forceFallbackConvergence = await this.getWorktreeForceCleanupConvergence({
|
|
932
|
-
repoRoot,
|
|
933
|
-
workspace,
|
|
934
|
-
node: args.node,
|
|
935
|
-
});
|
|
698
|
+
force?: boolean;
|
|
699
|
+
}): Promise<{ ok: true } | { ok: false; code: string; error: string; recoveryHint: string }> {
|
|
700
|
+
return precheckLocalWorktreeRemovable(this, args);
|
|
701
|
+
}
|
|
936
702
|
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
removedPath: result.removedPath,
|
|
945
|
-
repoRoot,
|
|
946
|
-
...(result.fallback ? {
|
|
947
|
-
fallback: result.fallback,
|
|
948
|
-
forced: result.forced,
|
|
949
|
-
reason: result.reason,
|
|
950
|
-
convergence: forceFallbackConvergence,
|
|
951
|
-
} : {}),
|
|
952
|
-
};
|
|
953
|
-
} catch (e: any) {
|
|
954
|
-
const message = String(e?.message || e || 'worktree cleanup failed');
|
|
955
|
-
const dirty = message.includes('dirty worktree') || message.includes('local changes');
|
|
956
|
-
const submoduleForceBlocked = /working trees containing submodules cannot be moved or removed/i.test(message) && !forceFallbackConvergence.allow;
|
|
957
|
-
return {
|
|
958
|
-
success: false,
|
|
959
|
-
code: dirty
|
|
960
|
-
? 'mesh_worktree_cleanup_dirty'
|
|
961
|
-
: submoduleForceBlocked
|
|
962
|
-
? 'mesh_worktree_cleanup_force_fallback_blocked'
|
|
963
|
-
: 'mesh_worktree_cleanup_failed',
|
|
964
|
-
error: submoduleForceBlocked
|
|
965
|
-
? `${message}; refusing --force fallback because convergence could not be verified: ${forceFallbackConvergence.error || 'unknown convergence state'}`
|
|
966
|
-
: message,
|
|
967
|
-
recoveryHint: dirty
|
|
968
|
-
? 'Commit, stash, or intentionally discard the worktree changes before retrying mesh_remove_node. The mesh registry entry is preserved until cleanup is safe.'
|
|
969
|
-
: submoduleForceBlocked
|
|
970
|
-
? 'Verify the worktree branch is merged/contained in the source default branch (for example origin/main) or mark the node with a safe branchConvergence final state before retrying. The mesh registry entry is preserved.'
|
|
971
|
-
: 'Inspect git worktree status/list from the source repo and retry after resolving the reported cleanup failure.',
|
|
972
|
-
...(submoduleForceBlocked ? { convergence: forceFallbackConvergence } : {}),
|
|
973
|
-
};
|
|
974
|
-
}
|
|
703
|
+
async cleanupLocalWorktreeNode(args: {
|
|
704
|
+
mesh: any;
|
|
705
|
+
node: any;
|
|
706
|
+
nodeId: string;
|
|
707
|
+
force?: boolean;
|
|
708
|
+
}): Promise<{ success: true; skipped?: boolean; removedPath?: string; repoRoot?: string; reason?: string; fallback?: string; forced?: boolean; convergence?: Record<string, unknown>; recovered?: boolean; residue?: boolean; residueWarning?: string; residueError?: string; branchRefDeleted?: boolean; branchRefReason?: string; branchRefForced?: boolean; branchRefWarning?: string } | { success: false; code: string; error: string; recoveryHint: string; convergence?: Record<string, unknown> }> {
|
|
709
|
+
return cleanupLocalWorktreeNode(this, args);
|
|
975
710
|
}
|
|
976
711
|
|
|
977
|
-
|
|
712
|
+
async getWorktreeForceCleanupConvergence(args: {
|
|
978
713
|
repoRoot: string;
|
|
979
714
|
workspace: string;
|
|
980
715
|
node: any;
|
|
981
716
|
}): Promise<{ allow: boolean; status?: string; source?: string; ref?: string; error?: string }> {
|
|
982
|
-
|
|
983
|
-
? args.node.branchConvergence.status
|
|
984
|
-
: '';
|
|
985
|
-
if (metadataStatus === 'merged_to_main' || metadataStatus === 'cleanup_candidate') {
|
|
986
|
-
return { allow: true, status: metadataStatus, source: 'node_branch_convergence' };
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
const { execFile } = await import('node:child_process');
|
|
990
|
-
const { promisify } = await import('node:util');
|
|
991
|
-
const execFileAsync = promisify(execFile);
|
|
992
|
-
const runGit = async (gitArgs: string[], cwd: string): Promise<string> => {
|
|
993
|
-
const { stdout } = await execFileAsync('git', gitArgs, {
|
|
994
|
-
cwd,
|
|
995
|
-
encoding: 'utf8',
|
|
996
|
-
timeout: 30_000,
|
|
997
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
998
|
-
windowsHide: true,
|
|
999
|
-
});
|
|
1000
|
-
return String(stdout || '').trim();
|
|
1001
|
-
};
|
|
1002
|
-
|
|
1003
|
-
let head = '';
|
|
1004
|
-
try {
|
|
1005
|
-
head = await runGit(['rev-parse', 'HEAD'], args.workspace);
|
|
1006
|
-
} catch (e: any) {
|
|
1007
|
-
return { allow: false, error: `could not resolve worktree HEAD: ${e?.message || e}` };
|
|
1008
|
-
}
|
|
1009
|
-
if (!head) return { allow: false, error: 'worktree HEAD is empty' };
|
|
1010
|
-
|
|
1011
|
-
const candidateRefs: string[] = [];
|
|
1012
|
-
try {
|
|
1013
|
-
const defaultBranch = await runGit(['branch', '--show-current'], args.repoRoot);
|
|
1014
|
-
if (defaultBranch) {
|
|
1015
|
-
candidateRefs.push(defaultBranch, `origin/${defaultBranch}`);
|
|
1016
|
-
}
|
|
1017
|
-
} catch { /* fall through to common refs */ }
|
|
1018
|
-
candidateRefs.push('origin/main', 'origin/master', 'main', 'master');
|
|
1019
|
-
|
|
1020
|
-
const seen = new Set<string>();
|
|
1021
|
-
const checkedRefs: string[] = [];
|
|
1022
|
-
for (const ref of candidateRefs) {
|
|
1023
|
-
if (!ref || seen.has(ref)) continue;
|
|
1024
|
-
seen.add(ref);
|
|
1025
|
-
let commit = '';
|
|
1026
|
-
try {
|
|
1027
|
-
commit = await runGit(['rev-parse', '--verify', `${ref}^{commit}`], args.repoRoot);
|
|
1028
|
-
} catch {
|
|
1029
|
-
continue;
|
|
1030
|
-
}
|
|
1031
|
-
checkedRefs.push(ref);
|
|
1032
|
-
try {
|
|
1033
|
-
await runGit(['merge-base', '--is-ancestor', head, commit], args.repoRoot);
|
|
1034
|
-
return { allow: true, status: 'merged_to_default_ref', source: 'git_merge_base', ref };
|
|
1035
|
-
} catch {
|
|
1036
|
-
// Not contained in this candidate ref; keep checking other safe refs.
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
|
-
return {
|
|
1041
|
-
allow: false,
|
|
1042
|
-
status: metadataStatus || undefined,
|
|
1043
|
-
error: checkedRefs.length
|
|
1044
|
-
? `worktree HEAD is not contained in checked refs: ${checkedRefs.join(', ')}`
|
|
1045
|
-
: 'no default/main refs were available for convergence verification',
|
|
1046
|
-
};
|
|
717
|
+
return getWorktreeForceCleanupConvergence(this, args);
|
|
1047
718
|
}
|
|
1048
719
|
|
|
1049
|
-
|
|
1050
|
-
return
|
|
720
|
+
isCompletedHostedSession(record: any): boolean {
|
|
721
|
+
return isCompletedHostedSession(this, record);
|
|
1051
722
|
}
|
|
1052
723
|
|
|
1053
|
-
|
|
724
|
+
async recordIntentionalMeshSessionStop(args: {
|
|
1054
725
|
meshId: string;
|
|
1055
726
|
nodeId: string;
|
|
1056
727
|
node: any;
|
|
1057
728
|
sessionId: string;
|
|
1058
729
|
mode: RepoMeshSessionCleanupMode;
|
|
1059
|
-
source: 'mesh_cleanup_sessions' | 'mesh_remove_node';
|
|
730
|
+
source: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
|
|
1060
731
|
action: 'stop_session' | 'delete_session_force';
|
|
1061
732
|
}): Promise<void> {
|
|
1062
|
-
|
|
1063
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
1064
|
-
appendLedgerEntry(args.meshId, {
|
|
1065
|
-
kind: 'session_stopped',
|
|
1066
|
-
nodeId: args.nodeId,
|
|
1067
|
-
sessionId: args.sessionId,
|
|
1068
|
-
payload: {
|
|
1069
|
-
intentional: true,
|
|
1070
|
-
reason: 'operator_cleanup',
|
|
1071
|
-
intentionalStopReason: 'operator_cleanup',
|
|
1072
|
-
source: args.source,
|
|
1073
|
-
cleanupMode: args.mode,
|
|
1074
|
-
action: args.action,
|
|
1075
|
-
workspace: typeof args.node?.workspace === 'string' ? args.node.workspace : undefined,
|
|
1076
|
-
},
|
|
1077
|
-
});
|
|
1078
|
-
} catch (e: any) {
|
|
1079
|
-
LOG.warn('MeshCleanup', `Failed to record intentional cleanup stop for ${args.sessionId}: ${e?.message || e}`);
|
|
1080
|
-
}
|
|
733
|
+
return recordIntentionalMeshSessionStop(this, args);
|
|
1081
734
|
}
|
|
1082
735
|
|
|
1083
|
-
|
|
736
|
+
async cleanupMeshSessions(args: {
|
|
1084
737
|
meshId: string;
|
|
1085
738
|
nodeId: string;
|
|
1086
739
|
node: any;
|
|
1087
740
|
mode: RepoMeshSessionCleanupMode;
|
|
1088
741
|
sessionIds?: string[];
|
|
1089
742
|
dryRun?: boolean;
|
|
1090
|
-
source?: 'mesh_cleanup_sessions' | 'mesh_remove_node';
|
|
743
|
+
source?: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
|
|
744
|
+
requireAutoLaunchedForTaskIds?: Record<string, string>;
|
|
745
|
+
reclaimOrphans?: boolean;
|
|
746
|
+
liveMeshNodeIds?: string[];
|
|
1091
747
|
}): Promise<{ success: boolean; [key: string]: unknown }> {
|
|
1092
|
-
|
|
1093
|
-
return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
|
|
1094
|
-
}
|
|
1095
|
-
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
1096
|
-
|
|
1097
|
-
const requestedSessionIds = Array.isArray(args.sessionIds)
|
|
1098
|
-
? new Set(args.sessionIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean))
|
|
1099
|
-
: undefined;
|
|
1100
|
-
const sessions = await this.deps.sessionHostControl.listSessions();
|
|
1101
|
-
const matched = sessions.filter(record => this.sessionMatchesMeshNode(record, args.node, args.nodeId, requestedSessionIds));
|
|
1102
|
-
const hasExplicitSessionIds = !!requestedSessionIds?.size;
|
|
1103
|
-
const stoppedSessionIds: string[] = [];
|
|
1104
|
-
const deletedSessionIds: string[] = [];
|
|
1105
|
-
const skippedSessionIds: string[] = [];
|
|
1106
|
-
const skippedLiveSessionIds: string[] = [];
|
|
1107
|
-
const deleteUnsupportedSessionIds: string[] = [];
|
|
1108
|
-
const recordsRemainSessionIds: string[] = [];
|
|
1109
|
-
const errors: Array<{ sessionId: string; error: string }> = [];
|
|
1110
|
-
const cleanupSource = args.source || 'mesh_cleanup_sessions';
|
|
1111
|
-
const markedIntentionalStopSessionIds = new Set<string>();
|
|
1112
|
-
const markIntentionalStop = async (sessionId: string, action: 'stop_session' | 'delete_session_force') => {
|
|
1113
|
-
if (args.dryRun || markedIntentionalStopSessionIds.has(sessionId)) return;
|
|
1114
|
-
markedIntentionalStopSessionIds.add(sessionId);
|
|
1115
|
-
await this.recordIntentionalMeshSessionStop({
|
|
1116
|
-
meshId: args.meshId,
|
|
1117
|
-
nodeId: args.nodeId,
|
|
1118
|
-
node: args.node,
|
|
1119
|
-
sessionId,
|
|
1120
|
-
mode: args.mode,
|
|
1121
|
-
source: cleanupSource,
|
|
1122
|
-
action,
|
|
1123
|
-
});
|
|
1124
|
-
};
|
|
1125
|
-
const matchedBySurfaceKind = {
|
|
1126
|
-
live_runtime: 0,
|
|
1127
|
-
recovery_snapshot: 0,
|
|
1128
|
-
inactive_record: 0,
|
|
1129
|
-
};
|
|
1130
|
-
|
|
1131
|
-
for (const record of matched) {
|
|
1132
|
-
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1133
|
-
matchedBySurfaceKind[surfaceKind] += 1;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
for (const record of matched) {
|
|
1137
|
-
const sessionId = String(record.sessionId);
|
|
1138
|
-
const completed = this.isCompletedHostedSession(record);
|
|
1139
|
-
const surfaceKind = getSessionHostSurfaceKind(record);
|
|
1140
|
-
const liveRuntime = surfaceKind === 'live_runtime';
|
|
1141
|
-
if (!hasExplicitSessionIds && liveRuntime) {
|
|
1142
|
-
skippedSessionIds.push(sessionId);
|
|
1143
|
-
skippedLiveSessionIds.push(sessionId);
|
|
1144
|
-
continue;
|
|
1145
|
-
}
|
|
1146
|
-
try {
|
|
1147
|
-
if (args.mode === 'stop') {
|
|
1148
|
-
if (!completed) {
|
|
1149
|
-
if (!args.dryRun) {
|
|
1150
|
-
await markIntentionalStop(sessionId, 'stop_session');
|
|
1151
|
-
await this.deps.sessionHostControl.stopSession(sessionId);
|
|
1152
|
-
}
|
|
1153
|
-
stoppedSessionIds.push(sessionId);
|
|
1154
|
-
} else {
|
|
1155
|
-
skippedSessionIds.push(sessionId);
|
|
1156
|
-
}
|
|
1157
|
-
continue;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
if (args.mode === 'delete_stopped') {
|
|
1161
|
-
if (completed) {
|
|
1162
|
-
if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: false });
|
|
1163
|
-
deletedSessionIds.push(sessionId);
|
|
1164
|
-
} else {
|
|
1165
|
-
skippedSessionIds.push(sessionId);
|
|
1166
|
-
}
|
|
1167
|
-
continue;
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
if (args.mode === 'stop_and_delete') {
|
|
1171
|
-
if (!completed) await markIntentionalStop(sessionId, 'delete_session_force');
|
|
1172
|
-
if (!args.dryRun) await this.deps.sessionHostControl.deleteSession(sessionId, { force: true });
|
|
1173
|
-
deletedSessionIds.push(sessionId);
|
|
1174
|
-
continue;
|
|
1175
|
-
}
|
|
1176
|
-
} catch (e: any) {
|
|
1177
|
-
const message = e?.message || String(e);
|
|
1178
|
-
if (message.includes('Unsupported session host request: delete_session')
|
|
1179
|
-
&& (args.mode === 'delete_stopped' || args.mode === 'stop_and_delete')) {
|
|
1180
|
-
deleteUnsupportedSessionIds.push(sessionId);
|
|
1181
|
-
recordsRemainSessionIds.push(sessionId);
|
|
1182
|
-
if (args.mode === 'stop_and_delete' && !completed) {
|
|
1183
|
-
try {
|
|
1184
|
-
await markIntentionalStop(sessionId, 'stop_session');
|
|
1185
|
-
await this.deps.sessionHostControl.stopSession(sessionId);
|
|
1186
|
-
stoppedSessionIds.push(sessionId);
|
|
1187
|
-
} catch (stopError: any) {
|
|
1188
|
-
errors.push({ sessionId, error: stopError?.message || String(stopError) });
|
|
1189
|
-
continue;
|
|
1190
|
-
}
|
|
1191
|
-
}
|
|
1192
|
-
skippedSessionIds.push(sessionId);
|
|
1193
|
-
continue;
|
|
1194
|
-
}
|
|
1195
|
-
errors.push({ sessionId, error: message });
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
const deleteUnsupported = deleteUnsupportedSessionIds.length > 0;
|
|
1200
|
-
return {
|
|
1201
|
-
success: errors.length === 0,
|
|
1202
|
-
mode: args.mode,
|
|
1203
|
-
dryRun: args.dryRun === true,
|
|
1204
|
-
matchedCount: matched.length,
|
|
1205
|
-
matchedBySurfaceKind,
|
|
1206
|
-
stoppedSessionIds,
|
|
1207
|
-
deletedSessionIds,
|
|
1208
|
-
skippedSessionIds,
|
|
1209
|
-
skippedLiveSessionIds,
|
|
1210
|
-
...(deleteUnsupported ? {
|
|
1211
|
-
deleteUnsupported: true,
|
|
1212
|
-
effectiveCleanup: args.mode === 'stop_and_delete'
|
|
1213
|
-
? 'stopped_only_records_remain'
|
|
1214
|
-
: 'delete_unsupported_records_remain',
|
|
1215
|
-
deleteUnsupportedSessionIds,
|
|
1216
|
-
recordsRemainSessionIds,
|
|
1217
|
-
} : {}),
|
|
1218
|
-
...(errors.length ? { errors } : {}),
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
private async traceSessionHostAction<T>(
|
|
1223
|
-
action: string,
|
|
1224
|
-
args: any,
|
|
1225
|
-
run: () => Promise<T>,
|
|
1226
|
-
summarizeResult?: (result: T) => Record<string, unknown>,
|
|
1227
|
-
): Promise<T> {
|
|
1228
|
-
const interactionId = typeof args?._interactionId === 'string' ? args._interactionId : undefined;
|
|
1229
|
-
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : undefined;
|
|
1230
|
-
const requestedPayload: Record<string, unknown> = { action };
|
|
1231
|
-
if (sessionId) requestedPayload.sessionId = sessionId;
|
|
1232
|
-
if (typeof args?.clientId === 'string') requestedPayload.clientId = args.clientId;
|
|
1233
|
-
if (typeof args?.signal === 'string') requestedPayload.signal = args.signal;
|
|
1234
|
-
if (typeof args?.providerType === 'string') requestedPayload.providerType = args.providerType;
|
|
1235
|
-
if (typeof args?.workspace === 'string') requestedPayload.workspace = args.workspace;
|
|
1236
|
-
if (typeof args?.dryRun === 'boolean') requestedPayload.dryRun = args.dryRun;
|
|
1237
|
-
|
|
1238
|
-
recordDebugTrace({
|
|
1239
|
-
interactionId,
|
|
1240
|
-
category: 'session_host',
|
|
1241
|
-
stage: 'action_requested',
|
|
1242
|
-
level: 'info',
|
|
1243
|
-
sessionId,
|
|
1244
|
-
payload: requestedPayload,
|
|
1245
|
-
});
|
|
1246
|
-
|
|
1247
|
-
try {
|
|
1248
|
-
const result = await run();
|
|
1249
|
-
recordDebugTrace({
|
|
1250
|
-
interactionId,
|
|
1251
|
-
category: 'session_host',
|
|
1252
|
-
stage: 'action_result',
|
|
1253
|
-
level: 'info',
|
|
1254
|
-
sessionId,
|
|
1255
|
-
payload: {
|
|
1256
|
-
...requestedPayload,
|
|
1257
|
-
success: true,
|
|
1258
|
-
...(summarizeResult ? summarizeResult(result) : {}),
|
|
1259
|
-
},
|
|
1260
|
-
});
|
|
1261
|
-
return result;
|
|
1262
|
-
} catch (error: any) {
|
|
1263
|
-
recordDebugTrace({
|
|
1264
|
-
interactionId,
|
|
1265
|
-
category: 'session_host',
|
|
1266
|
-
stage: 'action_failed',
|
|
1267
|
-
level: 'error',
|
|
1268
|
-
sessionId,
|
|
1269
|
-
payload: {
|
|
1270
|
-
...requestedPayload,
|
|
1271
|
-
error: error?.message || String(error),
|
|
1272
|
-
failureKind: getWriteConflictOwnerClientId(error) ? 'write_conflict' : 'request_failed',
|
|
1273
|
-
conflictOwnerClientId: getWriteConflictOwnerClientId(error),
|
|
1274
|
-
},
|
|
1275
|
-
});
|
|
1276
|
-
throw error;
|
|
1277
|
-
}
|
|
748
|
+
return cleanupMeshSessions(this, args);
|
|
1278
749
|
}
|
|
1279
|
-
|
|
1280
750
|
/**
|
|
1281
751
|
* Unified command routing.
|
|
1282
752
|
* Returns result for all commands:
|
|
@@ -1347,1631 +817,114 @@ export class DaemonCommandRouter {
|
|
|
1347
817
|
}
|
|
1348
818
|
}
|
|
1349
819
|
|
|
1350
|
-
// ─── Daemon-level command core ───────────────────
|
|
1351
|
-
|
|
1352
|
-
/**
|
|
1353
|
-
* Daemon-level command execution (IDE start/stop/restart, CLI, detect, logs).
|
|
1354
|
-
* Returns null if not handled at this level → caller delegates to CommandHandler.
|
|
1355
|
-
*/
|
|
1356
|
-
private async executeDaemonCommand(cmd: string, args: any): Promise<CommandRouterResult | null> {
|
|
1357
|
-
switch (cmd) {
|
|
1358
|
-
// ─── CLI / ACP commands ───
|
|
1359
|
-
case 'mesh_forward_event': {
|
|
1360
|
-
return handleMeshForwardEvent({ instanceManager: this.deps.instanceManager } as any, args as Record<string, unknown>);
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
case 'get_pending_mesh_events': {
|
|
1364
|
-
const events = drainPendingMeshCoordinatorEvents();
|
|
1365
|
-
return { success: true, events };
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
case 'launch_cli':
|
|
1369
|
-
case 'stop_cli':
|
|
1370
|
-
case 'set_cli_view_mode':
|
|
1371
|
-
case 'agent_command': {
|
|
1372
|
-
return this.deps.cliManager.handleCliCommand(cmd, args);
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
// ─── Logs ───
|
|
1376
|
-
case 'get_logs': {
|
|
1377
|
-
const count = parseInt(args?.count) || parseInt(args?.lines) || 100;
|
|
1378
|
-
const minLevel = args?.minLevel || 'info';
|
|
1379
|
-
const sinceTs = args?.since || 0;
|
|
1380
|
-
|
|
1381
|
-
try {
|
|
1382
|
-
// Priority 1: ring buffer (fast and structured)
|
|
1383
|
-
let logs = getRecentLogs(count, minLevel);
|
|
1384
|
-
if (sinceTs > 0) {
|
|
1385
|
-
logs = logs.filter((l: any) => l.ts > sinceTs);
|
|
1386
|
-
}
|
|
1387
|
-
if (logs.length > 0) {
|
|
1388
|
-
return { success: true, logs, totalBuffered: logs.length };
|
|
1389
|
-
}
|
|
1390
|
-
// Incremental polling must not fall back to unfiltered file text: the file
|
|
1391
|
-
// format is not timestamp-filterable, and returning its tail makes the UI
|
|
1392
|
-
// replace structured logs with old raw fallback lines when nothing new exists.
|
|
1393
|
-
if (sinceTs > 0) {
|
|
1394
|
-
return { success: true, logs: [], totalBuffered: 0 };
|
|
1395
|
-
}
|
|
1396
|
-
// Priority 2: file fallback
|
|
1397
|
-
if (fs.existsSync(LOG_PATH)) {
|
|
1398
|
-
const content = fs.readFileSync(LOG_PATH, 'utf-8');
|
|
1399
|
-
const allLines = content.split('\n');
|
|
1400
|
-
const recent = allLines.slice(-count).join('\n');
|
|
1401
|
-
return { success: true, logs: recent, totalLines: allLines.length };
|
|
1402
|
-
}
|
|
1403
|
-
return { success: true, logs: [], totalBuffered: 0 };
|
|
1404
|
-
} catch (e: any) {
|
|
1405
|
-
return { success: false, error: e.message };
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
case 'get_debug_trace': {
|
|
1410
|
-
const count = parseInt(args?.count) || parseInt(args?.limit) || 100;
|
|
1411
|
-
const sinceTs = Number(args?.since) || 0;
|
|
1412
|
-
const interactionId = typeof args?.interactionId === 'string' ? args.interactionId : undefined;
|
|
1413
|
-
const category = typeof args?.category === 'string' ? args.category : undefined;
|
|
1414
|
-
const trace = getRecentDebugTrace({ interactionId, category, limit: count })
|
|
1415
|
-
.filter((entry) => !sinceTs || entry.ts > sinceTs);
|
|
1416
|
-
return { success: true, trace, count: trace.length };
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
case 'session_host_get_diagnostics': {
|
|
1420
|
-
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
1421
|
-
const diagnostics = await this.traceSessionHostAction('session_host_get_diagnostics', args, () => this.deps.sessionHostControl!.getDiagnostics({
|
|
1422
|
-
includeSessions: args?.includeSessions !== false,
|
|
1423
|
-
limit: Number(args?.limit) || undefined,
|
|
1424
|
-
}), (result) => ({
|
|
1425
|
-
includeSessions: args?.includeSessions !== false,
|
|
1426
|
-
limit: Number(args?.limit) || undefined,
|
|
1427
|
-
...summarizeSessionHostDiagnostics(result),
|
|
1428
|
-
}));
|
|
1429
|
-
return { success: true, diagnostics };
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
case 'session_host_list_sessions': {
|
|
1433
|
-
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
1434
|
-
const sessions = await this.traceSessionHostAction('session_host_list_sessions', args, () => this.deps.sessionHostControl!.listSessions(), (records) => summarizeSessionHostRecords(records));
|
|
1435
|
-
return { success: true, sessions };
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
case 'session_host_stop_session': {
|
|
1439
|
-
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
1440
|
-
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
1441
|
-
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
1442
|
-
const record = await this.traceSessionHostAction('session_host_stop_session', args, () => this.deps.sessionHostControl!.stopSession(sessionId), (result) => summarizeSessionHostRecord(result));
|
|
1443
|
-
return { success: true, record };
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
case 'session_host_resume_session': {
|
|
1447
|
-
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
1448
|
-
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
1449
|
-
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
1450
|
-
const record = await this.traceSessionHostAction('session_host_resume_session', args, async () => {
|
|
1451
|
-
const nextRecord = await this.deps.sessionHostControl!.resumeSession(sessionId);
|
|
1452
|
-
const hosted = toHostedCliRuntimeDescriptor(nextRecord);
|
|
1453
|
-
if (hosted) {
|
|
1454
|
-
await this.deps.cliManager.restoreHostedSessions([hosted]);
|
|
1455
|
-
}
|
|
1456
|
-
return nextRecord;
|
|
1457
|
-
}, (result) => ({
|
|
1458
|
-
...summarizeSessionHostRecord(result),
|
|
1459
|
-
restoredHostedSession: !!toHostedCliRuntimeDescriptor(result),
|
|
1460
|
-
}));
|
|
1461
|
-
return { success: true, record };
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
case 'session_host_restart_session': {
|
|
1465
|
-
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
1466
|
-
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
1467
|
-
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
1468
|
-
const record = await this.traceSessionHostAction('session_host_restart_session', args, async () => {
|
|
1469
|
-
const nextRecord = await this.deps.sessionHostControl!.restartSession(sessionId);
|
|
1470
|
-
const hosted = toHostedCliRuntimeDescriptor(nextRecord);
|
|
1471
|
-
if (hosted) {
|
|
1472
|
-
await this.deps.cliManager.restoreHostedSessions([hosted]);
|
|
1473
|
-
}
|
|
1474
|
-
return nextRecord;
|
|
1475
|
-
}, (result) => ({
|
|
1476
|
-
...summarizeSessionHostRecord(result),
|
|
1477
|
-
restoredHostedSession: !!toHostedCliRuntimeDescriptor(result),
|
|
1478
|
-
}));
|
|
1479
|
-
return { success: true, record };
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
case 'session_host_send_signal': {
|
|
1483
|
-
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
1484
|
-
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
1485
|
-
const signal = typeof args?.signal === 'string' ? args.signal : '';
|
|
1486
|
-
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
1487
|
-
if (!signal) return { success: false, error: 'signal required' };
|
|
1488
|
-
const record = await this.traceSessionHostAction('session_host_send_signal', args, () => this.deps.sessionHostControl!.sendSignal(sessionId, signal), (result) => summarizeSessionHostRecord(result));
|
|
1489
|
-
return { success: true, record };
|
|
1490
|
-
}
|
|
1491
820
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
1496
|
-
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
1497
|
-
if (!clientId) return { success: false, error: 'clientId required' };
|
|
1498
|
-
const record = await this.traceSessionHostAction('session_host_force_detach_client', args, () => this.deps.sessionHostControl!.forceDetachClient(sessionId, clientId), (result) => summarizeSessionHostRecord(result));
|
|
1499
|
-
return { success: true, record };
|
|
1500
|
-
}
|
|
821
|
+
// ─── Refinery job orchestration ─────────────────────────────────────
|
|
822
|
+
// Implementation lives in ./router-refine.ts (behavior-preserving code move).
|
|
823
|
+
// Only the externally-referenced entry points remain here as thin delegators.
|
|
1501
824
|
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
providerType: typeof args?.providerType === 'string' ? args.providerType : undefined,
|
|
1506
|
-
workspace: typeof args?.workspace === 'string' ? args.workspace : undefined,
|
|
1507
|
-
dryRun: args?.dryRun === true,
|
|
1508
|
-
}), (value) => summarizeSessionHostPruneResult(value));
|
|
1509
|
-
return { success: true, result };
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
case 'session_host_acquire_write': {
|
|
1513
|
-
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
1514
|
-
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
1515
|
-
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
1516
|
-
const ownerType = args?.ownerType === 'agent' ? 'agent' : 'user';
|
|
1517
|
-
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
1518
|
-
if (!clientId) return { success: false, error: 'clientId required' };
|
|
1519
|
-
const record = await this.traceSessionHostAction('session_host_acquire_write', args, () => this.deps.sessionHostControl!.acquireWrite({
|
|
1520
|
-
sessionId,
|
|
1521
|
-
clientId,
|
|
1522
|
-
ownerType,
|
|
1523
|
-
force: args?.force !== false,
|
|
1524
|
-
}), (result) => ({
|
|
1525
|
-
...summarizeSessionHostRecord(result),
|
|
1526
|
-
ownerType,
|
|
1527
|
-
}));
|
|
1528
|
-
return { success: true, record };
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
case 'session_host_release_write': {
|
|
1532
|
-
if (!this.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
1533
|
-
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
1534
|
-
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
1535
|
-
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
1536
|
-
if (!clientId) return { success: false, error: 'clientId required' };
|
|
1537
|
-
const record = await this.traceSessionHostAction('session_host_release_write', args, () => this.deps.sessionHostControl!.releaseWrite({
|
|
1538
|
-
sessionId,
|
|
1539
|
-
clientId,
|
|
1540
|
-
}), (result) => summarizeSessionHostRecord(result));
|
|
1541
|
-
return { success: true, record };
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
case 'list_saved_sessions': {
|
|
1545
|
-
const providerType = typeof args?.providerType === 'string'
|
|
1546
|
-
? args.providerType.trim()
|
|
1547
|
-
: typeof args?.agentType === 'string'
|
|
1548
|
-
? args.agentType.trim()
|
|
1549
|
-
: '';
|
|
1550
|
-
const kind = args?.kind === 'acp' ? 'acp' : 'cli';
|
|
1551
|
-
if (!providerType) {
|
|
1552
|
-
return { success: false, error: 'providerType required' };
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
const wantsAll = args?.all === true;
|
|
1556
|
-
const offset = wantsAll ? 0 : Math.max(0, Number(args?.offset) || 0);
|
|
1557
|
-
const limit = wantsAll ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.min(100, Number(args?.limit) || 30));
|
|
1558
|
-
const requestedWorkspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
1559
|
-
const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
|
|
1560
|
-
? args.providerSessionId.trim()
|
|
1561
|
-
: typeof args?.activeProviderSessionId === 'string'
|
|
1562
|
-
? args.activeProviderSessionId.trim()
|
|
1563
|
-
: '';
|
|
1564
|
-
const providerMeta = this.deps.providerLoader.resolve?.(providerType) || this.deps.providerLoader.getMeta(providerType);
|
|
1565
|
-
const { sessions: historySessions, hasMore, source } = listProviderHistorySessions(providerType, {
|
|
1566
|
-
canonicalHistory: providerMeta?.canonicalHistory,
|
|
1567
|
-
offset,
|
|
1568
|
-
limit,
|
|
1569
|
-
historyBehavior: providerMeta?.historyBehavior,
|
|
1570
|
-
scripts: providerMeta?.scripts as any,
|
|
1571
|
-
});
|
|
1572
|
-
const state = loadState();
|
|
1573
|
-
const savedSessions = getSavedProviderSessions(state, { providerType, kind });
|
|
1574
|
-
const recentSessions = getRecentActivity(state, 200)
|
|
1575
|
-
.filter(entry => entry.providerType === providerType && entry.kind === kind && entry.providerSessionId);
|
|
1576
|
-
const savedSessionById = new Map(savedSessions.map(entry => [entry.providerSessionId, entry]));
|
|
1577
|
-
const recentSessionById = new Map(recentSessions.map(entry => [entry.providerSessionId!, entry]));
|
|
1578
|
-
const canResumeById = supportsExplicitSessionResume(providerMeta?.resume);
|
|
1579
|
-
|
|
1580
|
-
return {
|
|
1581
|
-
success: true,
|
|
1582
|
-
sessions: historySessions.map(session => {
|
|
1583
|
-
const saved = savedSessionById.get(session.historySessionId);
|
|
1584
|
-
const recent = recentSessionById.get(session.historySessionId);
|
|
1585
|
-
const workspace = saved?.workspace
|
|
1586
|
-
|| recent?.workspace
|
|
1587
|
-
|| session.workspace
|
|
1588
|
-
|| (requestedWorkspace && requestedProviderSessionId === session.historySessionId ? requestedWorkspace : undefined);
|
|
1589
|
-
return {
|
|
1590
|
-
id: session.historySessionId,
|
|
1591
|
-
providerSessionId: session.historySessionId,
|
|
1592
|
-
providerType,
|
|
1593
|
-
providerName: saved?.providerName || recent?.providerName || providerType,
|
|
1594
|
-
kind: saved?.kind || recent?.kind || kind,
|
|
1595
|
-
title: saved?.title || recent?.title || session.sessionTitle || session.preview || providerType,
|
|
1596
|
-
workspace,
|
|
1597
|
-
summaryMetadata: saved?.summaryMetadata || recent?.summaryMetadata,
|
|
1598
|
-
preview: session.preview,
|
|
1599
|
-
messageCount: session.messageCount,
|
|
1600
|
-
firstMessageAt: session.firstMessageAt,
|
|
1601
|
-
lastMessageAt: session.lastMessageAt,
|
|
1602
|
-
canResume: !!workspace && canResumeById,
|
|
1603
|
-
historySource: session.source,
|
|
1604
|
-
sourcePath: session.sourcePath,
|
|
1605
|
-
sourceMtimeMs: session.sourceMtimeMs,
|
|
1606
|
-
};
|
|
1607
|
-
}),
|
|
1608
|
-
hasMore,
|
|
1609
|
-
source,
|
|
1610
|
-
};
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
// ─── restart_session: IDE / CLI / ACP unified ───
|
|
1614
|
-
case 'restart_session': {
|
|
1615
|
-
const targetType = args?.cliType || args?.agentType || args?.ideType;
|
|
1616
|
-
if (!targetType) throw new Error('cliType or ideType required');
|
|
1617
|
-
|
|
1618
|
-
// Check if IDE (in cdpManagers or provider category is ide)
|
|
1619
|
-
const isIde = this.deps.cdpManagers.has(targetType) ||
|
|
1620
|
-
this.deps.providerLoader.getMeta(targetType)?.category === 'ide';
|
|
1621
|
-
|
|
1622
|
-
if (isIde) {
|
|
1623
|
-
// IDE restart: stop (with process kill) → launch
|
|
1624
|
-
await this.stopIde(targetType, true);
|
|
1625
|
-
const launchResult = await this.executeDaemonCommand('launch_ide', { ideType: targetType, enableCdp: true, workspace: args?.workspace });
|
|
1626
|
-
return { success: true, restarted: true, ideType: targetType, launch: launchResult };
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
// CLI/ACP restart: delegate to CliManager
|
|
1630
|
-
return this.deps.cliManager.handleCliCommand(cmd, args);
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
// ─── IDE stop ───
|
|
1634
|
-
case 'stop_ide': {
|
|
1635
|
-
const ideType = args?.ideType;
|
|
1636
|
-
if (!ideType) throw new Error('ideType required');
|
|
1637
|
-
const killProcess = args?.killProcess !== false; // default true
|
|
1638
|
-
await this.stopIde(ideType, killProcess);
|
|
1639
|
-
try {
|
|
1640
|
-
const results = await detectIDEs(this.deps.providerLoader);
|
|
1641
|
-
this.deps.detectedIdes.value = results;
|
|
1642
|
-
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
1643
|
-
} catch { /* ignore detection refresh errors */ }
|
|
1644
|
-
return { success: true, ideType, stopped: true, processKilled: killProcess };
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
// ─── IDE restart ───
|
|
1648
|
-
case 'restart_ide': {
|
|
1649
|
-
const ideType = args?.ideType;
|
|
1650
|
-
if (!ideType) throw new Error('ideType required');
|
|
1651
|
-
await this.stopIde(ideType, true); // always kill process on restart
|
|
1652
|
-
const launchResult = await this.executeDaemonCommand('launch_ide', { ideType, enableCdp: true, workspace: args?.workspace });
|
|
1653
|
-
return { success: true, ideType, restarted: true, launch: launchResult };
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
// ─── IDE launch + CDP connect ───
|
|
1657
|
-
case 'launch_ide': {
|
|
1658
|
-
const ideKey = args?.ideId || args?.ideType;
|
|
1659
|
-
const resolvedWorkspace = resolveIdeLaunchWorkspace(
|
|
1660
|
-
{
|
|
1661
|
-
workspace: args?.workspace,
|
|
1662
|
-
workspaceId: args?.workspaceId,
|
|
1663
|
-
useDefaultWorkspace: args?.useDefaultWorkspace,
|
|
1664
|
-
},
|
|
1665
|
-
loadConfig(),
|
|
1666
|
-
);
|
|
1667
|
-
const launchArgs = {
|
|
1668
|
-
ideId: ideKey,
|
|
1669
|
-
workspace: resolvedWorkspace,
|
|
1670
|
-
newWindow: args?.newWindow,
|
|
1671
|
-
};
|
|
1672
|
-
LOG.info('LaunchIDE', `target=${ideKey || 'auto'}`);
|
|
1673
|
-
const result = await launchWithCdp(launchArgs);
|
|
1674
|
-
|
|
1675
|
-
if (result.success && result.port && result.ideId && !this.deps.cdpManagers.has(result.ideId)) {
|
|
1676
|
-
const logFn = this.deps.getCdpLogFn
|
|
1677
|
-
? this.deps.getCdpLogFn(result.ideId)
|
|
1678
|
-
: LOG.forComponent(`CDP:${result.ideId}`).asLogFn();
|
|
1679
|
-
const provider = this.deps.providerLoader.getMeta(result.ideId);
|
|
1680
|
-
const manager = new DaemonCdpManager(result.port, logFn, undefined, provider?.targetFilter);
|
|
1681
|
-
const connected = await manager.connect();
|
|
1682
|
-
if (connected) {
|
|
1683
|
-
// Register active extension providers for this IDE in CDP manager
|
|
1684
|
-
registerExtensionProviders(this.deps.providerLoader, manager, result.ideId);
|
|
1685
|
-
this.deps.cdpManagers.set(result.ideId, manager);
|
|
1686
|
-
LOG.info('CDP', `Connected: ${result.ideId} (port ${result.port})`);
|
|
1687
|
-
LOG.info('CDP', `${this.deps.cdpManagers.size} IDE(s) connected`);
|
|
1688
|
-
|
|
1689
|
-
// Notify consumer (e.g. setupIdeInstance)
|
|
1690
|
-
this.deps.onCdpManagerCreated?.(result.ideId, manager);
|
|
1691
|
-
}
|
|
1692
|
-
}
|
|
1693
|
-
this.deps.onIdeConnected?.();
|
|
1694
|
-
try {
|
|
1695
|
-
const results = await detectIDEs(this.deps.providerLoader);
|
|
1696
|
-
this.deps.detectedIdes.value = results;
|
|
1697
|
-
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
1698
|
-
} catch { /* ignore detection refresh errors */ }
|
|
1699
|
-
if (result.success && resolvedWorkspace) {
|
|
1700
|
-
try {
|
|
1701
|
-
const next = appendRecentActivity(loadState(), {
|
|
1702
|
-
kind: 'ide',
|
|
1703
|
-
providerType: result.ideId || ideKey,
|
|
1704
|
-
providerName: result.ideId || ideKey,
|
|
1705
|
-
workspace: resolvedWorkspace,
|
|
1706
|
-
title: result.ideId || ideKey,
|
|
1707
|
-
});
|
|
1708
|
-
saveState(next);
|
|
1709
|
-
} catch { /* ignore activity persist errors */ }
|
|
1710
|
-
} else if (result.success && (result.ideId || ideKey)) {
|
|
1711
|
-
try {
|
|
1712
|
-
saveState(appendRecentActivity(loadState(), {
|
|
1713
|
-
kind: 'ide',
|
|
1714
|
-
providerType: result.ideId || ideKey,
|
|
1715
|
-
providerName: result.ideId || ideKey,
|
|
1716
|
-
title: result.ideId || ideKey,
|
|
1717
|
-
}));
|
|
1718
|
-
} catch { /* ignore activity persist errors */ }
|
|
1719
|
-
}
|
|
1720
|
-
return { ...result };
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
// ─── Detect providers ───
|
|
1724
|
-
case 'detect_provider': {
|
|
1725
|
-
const providerType = typeof args?.providerType === 'string' ? args.providerType.trim() : '';
|
|
1726
|
-
if (!providerType) return { success: false, error: 'providerType is required' };
|
|
1727
|
-
const normalizedType = this.deps.providerLoader.resolveAlias(providerType);
|
|
1728
|
-
const provider = this.deps.providerLoader.getByAlias(providerType);
|
|
1729
|
-
if (!provider) return { success: false, error: `Provider not found: ${providerType}` };
|
|
1730
|
-
if (provider.category !== 'cli' && provider.category !== 'acp') {
|
|
1731
|
-
return { success: false, error: `Provider detection is only supported for CLI/ACP providers: ${providerType}` };
|
|
1732
|
-
}
|
|
1733
|
-
if (!this.deps.providerLoader.isMachineProviderEnabled(normalizedType)) {
|
|
1734
|
-
return { success: false, error: `Provider is disabled on this machine: ${providerType}` };
|
|
1735
|
-
}
|
|
1736
|
-
const detected = await detectCLI(normalizedType, this.deps.providerLoader, { includeVersion: false });
|
|
1737
|
-
this.deps.providerLoader.setCliDetectionResults([{
|
|
1738
|
-
id: normalizedType,
|
|
1739
|
-
installed: !!detected,
|
|
1740
|
-
path: detected?.path,
|
|
1741
|
-
}], false);
|
|
1742
|
-
this.deps.onStatusChange?.();
|
|
1743
|
-
return {
|
|
1744
|
-
success: true,
|
|
1745
|
-
providerType: normalizedType,
|
|
1746
|
-
detected: !!detected,
|
|
1747
|
-
path: detected?.path || null,
|
|
1748
|
-
};
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
// ─── Detect IDEs ───
|
|
1752
|
-
case 'detect_ides': {
|
|
1753
|
-
const results = await detectIDEs(this.deps.providerLoader);
|
|
1754
|
-
this.deps.detectedIdes.value = results;
|
|
1755
|
-
this.deps.providerLoader.setIdeDetectionResults(results, true);
|
|
1756
|
-
return { success: true, detectedInfo: results };
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
// ─── Set User Name ───
|
|
1760
|
-
case 'set_user_name': {
|
|
1761
|
-
const name = args?.userName;
|
|
1762
|
-
if (!name || typeof name !== 'string') throw new Error('userName required');
|
|
1763
|
-
updateConfig({ userName: name });
|
|
1764
|
-
return { success: true, userName: name };
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
case 'get_status_metadata': {
|
|
1768
|
-
const snapshot = buildStatusSnapshot({
|
|
1769
|
-
allStates: this.deps.instanceManager.collectAllStates(),
|
|
1770
|
-
cdpManagers: this.deps.cdpManagers,
|
|
1771
|
-
providerLoader: this.deps.providerLoader,
|
|
1772
|
-
detectedIdes: this.deps.detectedIdes.value,
|
|
1773
|
-
instanceId: this.deps.statusInstanceId || loadConfig().machineId || 'daemon',
|
|
1774
|
-
version: this.deps.statusVersion || 'unknown',
|
|
1775
|
-
profile: 'metadata',
|
|
1776
|
-
});
|
|
1777
|
-
return { success: true, status: snapshot };
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
case 'get_machine_runtime_stats': {
|
|
1781
|
-
return {
|
|
1782
|
-
success: true,
|
|
1783
|
-
machine: buildMachineInfo('full'),
|
|
1784
|
-
timestamp: Date.now(),
|
|
1785
|
-
};
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
case 'mark_session_seen': {
|
|
1789
|
-
const sessionId = args?.sessionId;
|
|
1790
|
-
if (!sessionId || typeof sessionId !== 'string') {
|
|
1791
|
-
return { success: false, error: 'sessionId is required' };
|
|
1792
|
-
}
|
|
1793
|
-
const currentState = loadState();
|
|
1794
|
-
const prevSeenAt = currentState.sessionReads?.[sessionId] || 0;
|
|
1795
|
-
const sessionEntries = buildSessionEntries(
|
|
1796
|
-
this.deps.instanceManager.collectAllStates(),
|
|
1797
|
-
this.deps.cdpManagers,
|
|
1798
|
-
);
|
|
1799
|
-
const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
|
|
1800
|
-
const requestedCompletionMarker = typeof args?.completionMarker === 'string'
|
|
1801
|
-
? args.completionMarker.trim()
|
|
1802
|
-
: '';
|
|
1803
|
-
const completionMarker = requestedCompletionMarker || (targetSession ? getSessionCompletionMarker(targetSession) : '');
|
|
1804
|
-
const requestedProviderSessionId = typeof args?.providerSessionId === 'string'
|
|
1805
|
-
? args.providerSessionId.trim()
|
|
1806
|
-
: '';
|
|
1807
|
-
const providerSessionId = requestedProviderSessionId || targetSession?.providerSessionId;
|
|
1808
|
-
const next = markSessionSeen(
|
|
1809
|
-
currentState,
|
|
1810
|
-
sessionId,
|
|
1811
|
-
typeof args?.seenAt === 'number' ? args.seenAt : Date.now(),
|
|
1812
|
-
completionMarker,
|
|
1813
|
-
providerSessionId,
|
|
1814
|
-
);
|
|
1815
|
-
if (READ_DEBUG_ENABLED) {
|
|
1816
|
-
LOG.info('RecentRead', `mark_session_seen sessionId=${sessionId} seenAt=${String(args?.seenAt || '')} prevSeenAt=${String(prevSeenAt)} nextSeenAt=${String(next.sessionReads?.[sessionId] || 0)} marker=${completionMarker || '-'}`);
|
|
1817
|
-
}
|
|
1818
|
-
saveState(next);
|
|
1819
|
-
this.deps.onStatusChange?.();
|
|
1820
|
-
return {
|
|
1821
|
-
success: true,
|
|
1822
|
-
sessionId,
|
|
1823
|
-
seenAt: next.sessionReads?.[sessionId] || Date.now(),
|
|
1824
|
-
completionMarker,
|
|
1825
|
-
};
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
case 'delete_notification': {
|
|
1829
|
-
const sessionId = args?.sessionId;
|
|
1830
|
-
const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
|
|
1831
|
-
if (!sessionId || typeof sessionId !== 'string') {
|
|
1832
|
-
return { success: false, error: 'sessionId is required' };
|
|
1833
|
-
}
|
|
1834
|
-
if (!notificationId) {
|
|
1835
|
-
return { success: false, error: 'notificationId is required' };
|
|
1836
|
-
}
|
|
1837
|
-
const sessionEntries = buildSessionEntries(
|
|
1838
|
-
this.deps.instanceManager.collectAllStates(),
|
|
1839
|
-
this.deps.cdpManagers,
|
|
1840
|
-
);
|
|
1841
|
-
const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
|
|
1842
|
-
const next = dismissSessionNotification(
|
|
1843
|
-
loadState(),
|
|
1844
|
-
sessionId,
|
|
1845
|
-
notificationId,
|
|
1846
|
-
targetSession?.providerSessionId,
|
|
1847
|
-
);
|
|
1848
|
-
saveState(next);
|
|
1849
|
-
this.deps.onStatusChange?.();
|
|
1850
|
-
return {
|
|
1851
|
-
success: true,
|
|
1852
|
-
sessionId,
|
|
1853
|
-
notificationId,
|
|
1854
|
-
};
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
case 'mark_notification_unread': {
|
|
1858
|
-
const sessionId = args?.sessionId;
|
|
1859
|
-
const notificationId = typeof args?.notificationId === 'string' ? args.notificationId.trim() : '';
|
|
1860
|
-
if (!sessionId || typeof sessionId !== 'string') {
|
|
1861
|
-
return { success: false, error: 'sessionId is required' };
|
|
1862
|
-
}
|
|
1863
|
-
if (!notificationId) {
|
|
1864
|
-
return { success: false, error: 'notificationId is required' };
|
|
1865
|
-
}
|
|
1866
|
-
const sessionEntries = buildSessionEntries(
|
|
1867
|
-
this.deps.instanceManager.collectAllStates(),
|
|
1868
|
-
this.deps.cdpManagers,
|
|
1869
|
-
);
|
|
1870
|
-
const targetSession = sessionEntries.find((entry) => entry.id === sessionId);
|
|
1871
|
-
const next = markSessionNotificationUnread(
|
|
1872
|
-
loadState(),
|
|
1873
|
-
sessionId,
|
|
1874
|
-
notificationId,
|
|
1875
|
-
targetSession?.providerSessionId,
|
|
1876
|
-
);
|
|
1877
|
-
saveState(next);
|
|
1878
|
-
this.deps.onStatusChange?.();
|
|
1879
|
-
return {
|
|
1880
|
-
success: true,
|
|
1881
|
-
sessionId,
|
|
1882
|
-
notificationId,
|
|
1883
|
-
};
|
|
1884
|
-
}
|
|
1885
|
-
|
|
1886
|
-
// ─── Daemon Self-Upgrade ───
|
|
1887
|
-
case 'daemon_upgrade': {
|
|
1888
|
-
LOG.info('Upgrade', 'Remote upgrade requested from dashboard');
|
|
1889
|
-
try {
|
|
1890
|
-
// Detect package name for upgrade
|
|
1891
|
-
const isStandalone = this.deps.packageName === '@adhdev/daemon-standalone'
|
|
1892
|
-
|| process.argv[1]?.includes('daemon-standalone');
|
|
1893
|
-
const pkgName = isStandalone ? '@adhdev/daemon-standalone' : 'adhdev';
|
|
1894
|
-
const npmSurface = resolveCurrentGlobalInstallSurface({ packageName: pkgName });
|
|
1895
|
-
const channel = resolveUpgradeChannel(args);
|
|
1896
|
-
const npmTag = CHANNEL_NPM_TAG[channel];
|
|
1897
|
-
|
|
1898
|
-
// Check channel-pinned dist-tag and resolve it to a concrete install version.
|
|
1899
|
-
const latest = String(execNpmCommandSync(['view', `${pkgName}@${npmTag}`, 'version'], { encoding: 'utf-8', timeout: 10000 }, npmSurface)).trim();
|
|
1900
|
-
LOG.info('Upgrade', `Latest ${pkgName}@${npmTag}: v${latest}`);
|
|
1901
|
-
updateConfig({ updateChannel: channel, serverUrl: CHANNEL_SERVER_URL[channel] } as any);
|
|
1902
|
-
let currentInstalled: string | null = null;
|
|
1903
|
-
try {
|
|
1904
|
-
const currentJson = String(execNpmCommandSync(['ls', '-g', pkgName, '--depth=0', '--json'], {
|
|
1905
|
-
encoding: 'utf-8',
|
|
1906
|
-
timeout: 10000,
|
|
1907
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1908
|
-
}, npmSurface)).trim();
|
|
1909
|
-
const parsed = JSON.parse(currentJson);
|
|
1910
|
-
currentInstalled = parsed?.dependencies?.[pkgName]?.version || null;
|
|
1911
|
-
} catch {
|
|
1912
|
-
// ignore ls failures; upgrade can still proceed
|
|
1913
|
-
}
|
|
1914
|
-
|
|
1915
|
-
const runningVersion = typeof this.deps.statusVersion === 'string'
|
|
1916
|
-
? this.deps.statusVersion.trim().replace(/^v/, '')
|
|
1917
|
-
: null;
|
|
1918
|
-
if (currentInstalled === latest && runningVersion === latest) {
|
|
1919
|
-
LOG.info('Upgrade', `Already on ${channel} channel version v${latest}; skipping install`);
|
|
1920
|
-
return { success: true, upgraded: false, alreadyLatest: true, version: latest, channel, npmTag };
|
|
1921
|
-
}
|
|
1922
|
-
if (currentInstalled === latest && runningVersion && runningVersion !== latest) {
|
|
1923
|
-
LOG.info('Upgrade', `Installed package is v${latest}, but running daemon is v${runningVersion}; scheduling restart`);
|
|
1924
|
-
}
|
|
1925
|
-
|
|
1926
|
-
spawnDetachedDaemonUpgradeHelper({
|
|
1927
|
-
packageName: pkgName,
|
|
1928
|
-
targetVersion: latest,
|
|
1929
|
-
parentPid: process.pid,
|
|
1930
|
-
restartArgv: process.argv.slice(1),
|
|
1931
|
-
cwd: process.cwd(),
|
|
1932
|
-
sessionHostAppName: process.env.ADHDEV_SESSION_HOST_NAME || 'adhdev',
|
|
1933
|
-
});
|
|
1934
|
-
LOG.info('Upgrade', `Scheduled detached ${channel} upgrade to v${latest}`);
|
|
1935
|
-
|
|
1936
|
-
// Exit after the command response has been sent so the helper can replace the package cleanly.
|
|
1937
|
-
setTimeout(() => {
|
|
1938
|
-
LOG.info('Upgrade', 'Exiting daemon so detached upgrader can continue...');
|
|
1939
|
-
process.exit(0);
|
|
1940
|
-
}, 3000);
|
|
1941
|
-
|
|
1942
|
-
return { success: true, upgraded: true, version: latest, restarting: true, channel, npmTag };
|
|
1943
|
-
} catch (e: any) {
|
|
1944
|
-
LOG.error('Upgrade', `Failed: ${e.message}`);
|
|
1945
|
-
return { success: false, error: e.message };
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
// ─── Machine Settings ───
|
|
1950
|
-
case 'set_machine_nickname': {
|
|
1951
|
-
const nickname = args?.nickname;
|
|
1952
|
-
updateConfig({ machineNickname: nickname || null });
|
|
1953
|
-
return { success: true };
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
// ─── Mesh CRUD (local meshes.json) ───
|
|
1957
|
-
case 'list_meshes': {
|
|
1958
|
-
try {
|
|
1959
|
-
const { listMeshes } = await import('../config/mesh-config.js');
|
|
1960
|
-
return { success: true, meshes: listMeshes() };
|
|
1961
|
-
} catch (e: any) {
|
|
1962
|
-
return { success: false, error: e.message };
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
case 'get_mesh': {
|
|
1967
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1968
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1969
|
-
try {
|
|
1970
|
-
const { getMesh } = await import('../config/mesh-config.js');
|
|
1971
|
-
const mesh = getMesh(meshId);
|
|
1972
|
-
if (mesh) return { success: true, mesh };
|
|
1973
|
-
} catch { /* fall through to inline cache */ }
|
|
1974
|
-
// Fallback: check in-memory cache for cloud-originating meshes
|
|
1975
|
-
const cached = this.inlineMeshCache.get(meshId);
|
|
1976
|
-
if (cached) return { success: true, mesh: cached };
|
|
1977
|
-
return { success: false, error: 'Mesh not found' };
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
case 'create_mesh': {
|
|
1981
|
-
const name = typeof args?.name === 'string' ? args.name.trim() : '';
|
|
1982
|
-
const repoIdentity = typeof args?.repoIdentity === 'string' ? args.repoIdentity.trim() : '';
|
|
1983
|
-
const repoRemoteUrl = typeof args?.repoRemoteUrl === 'string' ? args.repoRemoteUrl.trim() : undefined;
|
|
1984
|
-
const defaultBranch = typeof args?.defaultBranch === 'string' ? args.defaultBranch.trim() : undefined;
|
|
1985
|
-
if (!name) return { success: false, error: 'name required' };
|
|
1986
|
-
try {
|
|
1987
|
-
const { createMesh } = await import('../config/mesh-config.js');
|
|
1988
|
-
const mesh = createMesh({ name, repoIdentity, repoRemoteUrl, defaultBranch, policy: args?.policy });
|
|
1989
|
-
return { success: true, mesh };
|
|
1990
|
-
} catch (e: any) {
|
|
1991
|
-
return { success: false, error: e.message };
|
|
1992
|
-
}
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
case 'update_mesh': {
|
|
1996
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
1997
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
1998
|
-
try {
|
|
1999
|
-
const { updateMesh } = await import('../config/mesh-config.js');
|
|
2000
|
-
const patch: Record<string, unknown> = {};
|
|
2001
|
-
if (typeof args?.name === 'string') patch.name = args.name;
|
|
2002
|
-
if (typeof args?.defaultBranch === 'string') patch.defaultBranch = args.defaultBranch;
|
|
2003
|
-
if (args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)) patch.policy = args.policy;
|
|
2004
|
-
if (args?.coordinator && typeof args.coordinator === 'object' && !Array.isArray(args.coordinator)) patch.coordinator = args.coordinator;
|
|
2005
|
-
if (!Object.keys(patch).length) return { success: false, error: 'No updates provided' };
|
|
2006
|
-
const mesh = updateMesh(meshId, patch as any);
|
|
2007
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2008
|
-
this.inlineMeshCache.set(meshId, mesh);
|
|
2009
|
-
return { success: true, mesh };
|
|
2010
|
-
} catch (e: any) {
|
|
2011
|
-
return { success: false, error: e.message };
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
case 'delete_mesh': {
|
|
2016
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2017
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2018
|
-
try {
|
|
2019
|
-
const { deleteMesh } = await import('../config/mesh-config.js');
|
|
2020
|
-
const deleted = deleteMesh(meshId);
|
|
2021
|
-
return { success: true, deleted };
|
|
2022
|
-
} catch (e: any) {
|
|
2023
|
-
return { success: false, error: e.message };
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
case 'get_mesh_ledger': {
|
|
2028
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2029
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2030
|
-
try {
|
|
2031
|
-
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2032
|
-
const tail = typeof args?.tail === 'number' ? args.tail : 20;
|
|
2033
|
-
const since = typeof args?.since === 'string' ? args.since : undefined;
|
|
2034
|
-
const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
|
|
2035
|
-
const entries = readLedgerEntries(meshId, { tail, since, kind });
|
|
2036
|
-
const summary = getLedgerSummary(meshId);
|
|
2037
|
-
return { success: true, entries, summary };
|
|
2038
|
-
} catch (e: any) {
|
|
2039
|
-
return { success: false, error: e.message };
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
case 'get_mesh_ledger_slice': {
|
|
2044
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2045
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2046
|
-
try {
|
|
2047
|
-
const { readLedgerSlice } = await import('../mesh/mesh-ledger.js');
|
|
2048
|
-
const kind = Array.isArray(args?.kind) ? args.kind.filter((k: any) => typeof k === 'string') : undefined;
|
|
2049
|
-
const slice = readLedgerSlice(meshId, {
|
|
2050
|
-
afterId: typeof args?.afterId === 'string' ? args.afterId : undefined,
|
|
2051
|
-
since: typeof args?.since === 'string' ? args.since : undefined,
|
|
2052
|
-
kind,
|
|
2053
|
-
limit: typeof args?.limit === 'number' ? args.limit : undefined,
|
|
2054
|
-
});
|
|
2055
|
-
return { success: true, slice };
|
|
2056
|
-
} catch (e: any) {
|
|
2057
|
-
return { success: false, error: e.message };
|
|
2058
|
-
}
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
case 'import_mesh_ledger_slice': {
|
|
2062
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2063
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2064
|
-
try {
|
|
2065
|
-
const { appendRemoteLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2066
|
-
const entries = Array.isArray(args?.entries)
|
|
2067
|
-
? args.entries as any[]
|
|
2068
|
-
: Array.isArray(args?.slice?.entries)
|
|
2069
|
-
? args.slice.entries as any[]
|
|
2070
|
-
: [];
|
|
2071
|
-
const result = appendRemoteLedgerEntries(meshId, entries as any);
|
|
2072
|
-
return { success: true, result, summary: getLedgerSummary(meshId) };
|
|
2073
|
-
} catch (e: any) {
|
|
2074
|
-
return { success: false, error: e.message };
|
|
2075
|
-
}
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
case 'get_mesh_queue': {
|
|
2079
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2080
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2081
|
-
try {
|
|
2082
|
-
const { getMeshQueueStats, getQueue } = await import('../mesh/mesh-work-queue.js');
|
|
2083
|
-
const status = Array.isArray(args?.status)
|
|
2084
|
-
? args.status.map((s: any) => typeof s === 'string' ? s.trim() : '').filter(Boolean)
|
|
2085
|
-
: undefined;
|
|
2086
|
-
const queue = getQueue(meshId, { status: status as any });
|
|
2087
|
-
const summary = getMeshQueueStats(meshId);
|
|
2088
|
-
return {
|
|
2089
|
-
success: true,
|
|
2090
|
-
queue,
|
|
2091
|
-
summary,
|
|
2092
|
-
sourceOfTruth: {
|
|
2093
|
-
kind: 'mesh_work_queue_file',
|
|
2094
|
-
activeStatuses: ['pending', 'assigned'],
|
|
2095
|
-
historicalStatuses: ['completed', 'failed', 'cancelled'],
|
|
2096
|
-
notes: 'pending/assigned are active work; completed/failed/cancelled are historical records.',
|
|
2097
|
-
},
|
|
2098
|
-
};
|
|
2099
|
-
} catch (e: any) {
|
|
2100
|
-
return { success: false, error: e.message };
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
case 'cancel_mesh_queue_task': {
|
|
2105
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2106
|
-
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2107
|
-
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
2108
|
-
try {
|
|
2109
|
-
const { cancelTask } = await import('../mesh/mesh-work-queue.js');
|
|
2110
|
-
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
2111
|
-
const task = cancelTask(meshId, taskId, { reason });
|
|
2112
|
-
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
2113
|
-
return { success: true, task };
|
|
2114
|
-
} catch (e: any) {
|
|
2115
|
-
return { success: false, error: e.message };
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
case 'requeue_mesh_queue_task': {
|
|
2120
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2121
|
-
const taskId = typeof args?.taskId === 'string' ? args.taskId.trim() : '';
|
|
2122
|
-
if (!meshId || !taskId) return { success: false, error: 'meshId and taskId required' };
|
|
2123
|
-
try {
|
|
2124
|
-
const { requeueTask } = await import('../mesh/mesh-work-queue.js');
|
|
2125
|
-
const task = requeueTask(meshId, taskId, {
|
|
2126
|
-
reason: typeof args?.reason === 'string' ? args.reason : undefined,
|
|
2127
|
-
targetNodeId: typeof args?.targetNodeId === 'string' ? args.targetNodeId.trim() : undefined,
|
|
2128
|
-
targetSessionId: typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : undefined,
|
|
2129
|
-
clearTargetNode: args?.clearTargetNode === true,
|
|
2130
|
-
clearTargetSession: args?.clearTargetSession !== false,
|
|
2131
|
-
});
|
|
2132
|
-
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
2133
|
-
return { success: true, task };
|
|
2134
|
-
} catch (e: any) {
|
|
2135
|
-
return { success: false, error: e.message };
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
case 'add_mesh_node': {
|
|
2140
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2141
|
-
const workspace = typeof args?.workspace === 'string' ? args.workspace.trim() : '';
|
|
2142
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2143
|
-
if (!workspace) return { success: false, error: 'workspace required' };
|
|
2144
|
-
try {
|
|
2145
|
-
const { addNode } = await import('../config/mesh-config.js');
|
|
2146
|
-
const providerPriority = Array.isArray(args?.providerPriority)
|
|
2147
|
-
? args.providerPriority.map((type: any) => typeof type === 'string' ? type.trim() : '').filter(Boolean)
|
|
2148
|
-
: [];
|
|
2149
|
-
const readOnly = args?.readOnly === true;
|
|
2150
|
-
const policy = {
|
|
2151
|
-
...(readOnly ? { readOnly: true } : {}),
|
|
2152
|
-
...(providerPriority.length ? { providerPriority } : {}),
|
|
2153
|
-
};
|
|
2154
|
-
const node = addNode(meshId, { workspace, ...(policy ? { policy } : {}) });
|
|
2155
|
-
if (!node) return { success: false, error: 'Mesh not found' };
|
|
2156
|
-
return { success: true, node };
|
|
2157
|
-
} catch (e: any) {
|
|
2158
|
-
return { success: false, error: e.message };
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
|
|
2162
|
-
case 'update_mesh_node': {
|
|
2163
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2164
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2165
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2166
|
-
try {
|
|
2167
|
-
const { updateNode } = await import('../config/mesh-config.js');
|
|
2168
|
-
const policy = args?.policy && typeof args.policy === 'object' && !Array.isArray(args.policy)
|
|
2169
|
-
? { ...(args.policy as Record<string, unknown>) }
|
|
2170
|
-
: {};
|
|
2171
|
-
if (Array.isArray(args?.providerPriority)) {
|
|
2172
|
-
const providerPriority = args.providerPriority
|
|
2173
|
-
.map((type: any) => typeof type === 'string' ? type.trim() : '')
|
|
2174
|
-
.filter(Boolean);
|
|
2175
|
-
delete (policy as any).provider_priority;
|
|
2176
|
-
if (providerPriority.length) {
|
|
2177
|
-
(policy as any).providerPriority = providerPriority;
|
|
2178
|
-
} else {
|
|
2179
|
-
delete (policy as any).providerPriority;
|
|
2180
|
-
}
|
|
2181
|
-
}
|
|
2182
|
-
const node = updateNode(meshId, nodeId, { policy: policy as any });
|
|
2183
|
-
if (!node) return { success: false, error: 'Mesh node not found' };
|
|
2184
|
-
return { success: true, node };
|
|
2185
|
-
} catch (e: any) {
|
|
2186
|
-
return { success: false, error: e.message };
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
case 'cleanup_mesh_sessions': {
|
|
2191
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2192
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2193
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2194
|
-
try {
|
|
2195
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2196
|
-
const mesh = meshRecord?.mesh;
|
|
2197
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2198
|
-
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2199
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
2200
|
-
const mode = this.normalizeMeshSessionCleanupMode(args?.mode ?? mesh?.policy?.sessionCleanupOnNodeRemove);
|
|
2201
|
-
const sessionIds = Array.isArray(args?.sessionIds)
|
|
2202
|
-
? args.sessionIds.map((id: any) => typeof id === 'string' ? id.trim() : '').filter(Boolean)
|
|
2203
|
-
: undefined;
|
|
2204
|
-
const result = await this.cleanupMeshSessions({
|
|
2205
|
-
meshId,
|
|
2206
|
-
nodeId,
|
|
2207
|
-
node,
|
|
2208
|
-
mode,
|
|
2209
|
-
sessionIds,
|
|
2210
|
-
dryRun: args?.dryRun === true,
|
|
2211
|
-
source: 'mesh_cleanup_sessions',
|
|
2212
|
-
});
|
|
2213
|
-
return result;
|
|
2214
|
-
} catch (e: any) {
|
|
2215
|
-
return { success: false, error: e.message };
|
|
2216
|
-
}
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
case 'refine_mesh_node': {
|
|
2220
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2221
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2222
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2223
|
-
try {
|
|
2224
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2225
|
-
const mesh = meshRecord?.mesh;
|
|
2226
|
-
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2227
|
-
if (!node) return { success: false, error: `Node '${nodeId}' not found in mesh` };
|
|
2228
|
-
|
|
2229
|
-
if (!node.isLocalWorktree || !node.workspace) {
|
|
2230
|
-
return { success: false, error: `Refinery requires a local worktree node` };
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
const sourceNode = node.clonedFromNodeId
|
|
2234
|
-
? mesh?.nodes.find((n: any) => n.id === node.clonedFromNodeId || n.nodeId === node.clonedFromNodeId)
|
|
2235
|
-
: mesh?.nodes.find((n: any) => !n.isLocalWorktree);
|
|
2236
|
-
const repoRoot = sourceNode?.repoRoot || sourceNode?.workspace;
|
|
2237
|
-
if (!repoRoot) return { success: false, error: 'Source node repoRoot not found' };
|
|
2238
|
-
|
|
2239
|
-
const { execFile } = await import('node:child_process');
|
|
2240
|
-
const { promisify } = await import('node:util');
|
|
2241
|
-
const execFileAsync = promisify(execFile);
|
|
2242
|
-
|
|
2243
|
-
const { stdout: branchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: node.workspace, encoding: 'utf8' });
|
|
2244
|
-
const branch = branchStdout.trim();
|
|
2245
|
-
if (!branch) return { success: false, error: 'Could not determine branch of the worktree node' };
|
|
2246
|
-
|
|
2247
|
-
const { stdout: baseBranchStdout } = await execFileAsync('git', ['branch', '--show-current'], { cwd: repoRoot, encoding: 'utf8' });
|
|
2248
|
-
const baseBranch = baseBranchStdout.trim();
|
|
2249
|
-
|
|
2250
|
-
const validationSummary = await runMeshRefineValidationGate(mesh, node.workspace);
|
|
2251
|
-
if (validationSummary.status === 'failed') {
|
|
2252
|
-
return {
|
|
2253
|
-
success: false,
|
|
2254
|
-
code: 'validation_failed',
|
|
2255
|
-
convergenceStatus: 'blocked_review',
|
|
2256
|
-
error: 'Refinery validation gate failed; merge/refine was not attempted.',
|
|
2257
|
-
branch,
|
|
2258
|
-
into: baseBranch,
|
|
2259
|
-
validationSummary,
|
|
2260
|
-
finalBranchConvergenceState: {
|
|
2261
|
-
branch,
|
|
2262
|
-
baseBranch,
|
|
2263
|
-
merged: false,
|
|
2264
|
-
removed: false,
|
|
2265
|
-
validation: 'failed',
|
|
2266
|
-
status: 'blocked_review',
|
|
2267
|
-
},
|
|
2268
|
-
};
|
|
2269
|
-
}
|
|
2270
|
-
if (validationSummary.status === 'skipped') {
|
|
2271
|
-
return {
|
|
2272
|
-
success: false,
|
|
2273
|
-
code: 'validation_unavailable',
|
|
2274
|
-
convergenceStatus: 'blocked_review',
|
|
2275
|
-
error: 'Refinery validation gate is required but no allowlisted validation command was available; merge/refine was not attempted.',
|
|
2276
|
-
branch,
|
|
2277
|
-
into: baseBranch,
|
|
2278
|
-
validationSummary,
|
|
2279
|
-
finalBranchConvergenceState: {
|
|
2280
|
-
branch,
|
|
2281
|
-
baseBranch,
|
|
2282
|
-
merged: false,
|
|
2283
|
-
removed: false,
|
|
2284
|
-
validation: 'unavailable',
|
|
2285
|
-
status: 'blocked_review',
|
|
2286
|
-
},
|
|
2287
|
-
};
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
try {
|
|
2291
|
-
await execFileAsync('git', ['merge', '--no-ff', branch, '-m', `Auto-merge branch '${branch}' via Refinery`], { cwd: repoRoot, encoding: 'utf8' });
|
|
2292
|
-
} catch (e: any) {
|
|
2293
|
-
return {
|
|
2294
|
-
success: false,
|
|
2295
|
-
error: `Merge failed (conflicts?): ${e.message}`,
|
|
2296
|
-
validationSummary,
|
|
2297
|
-
finalBranchConvergenceState: {
|
|
2298
|
-
branch,
|
|
2299
|
-
baseBranch,
|
|
2300
|
-
merged: false,
|
|
2301
|
-
removed: false,
|
|
2302
|
-
validation: 'passed',
|
|
2303
|
-
status: 'not_mergeable',
|
|
2304
|
-
},
|
|
2305
|
-
};
|
|
2306
|
-
}
|
|
2307
|
-
|
|
2308
|
-
const removeResult = await this.execute('remove_mesh_node', {
|
|
2309
|
-
meshId,
|
|
2310
|
-
nodeId,
|
|
2311
|
-
sessionCleanupMode: 'kill',
|
|
2312
|
-
inlineMesh: args?.inlineMesh,
|
|
2313
|
-
});
|
|
2314
|
-
|
|
2315
|
-
try {
|
|
2316
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2317
|
-
appendLedgerEntry(meshId, {
|
|
2318
|
-
kind: 'node_removed',
|
|
2319
|
-
nodeId,
|
|
2320
|
-
payload: { refined: true, mergedBranch: branch, into: baseBranch, validationSummary },
|
|
2321
|
-
});
|
|
2322
|
-
} catch {}
|
|
2323
|
-
|
|
2324
|
-
return {
|
|
2325
|
-
success: true,
|
|
2326
|
-
merged: true,
|
|
2327
|
-
branch,
|
|
2328
|
-
into: baseBranch,
|
|
2329
|
-
removeResult,
|
|
2330
|
-
validationSummary,
|
|
2331
|
-
finalBranchConvergenceState: {
|
|
2332
|
-
branch: baseBranch,
|
|
2333
|
-
mergedBranch: branch,
|
|
2334
|
-
baseBranch,
|
|
2335
|
-
merged: true,
|
|
2336
|
-
removed: removeResult?.success !== false,
|
|
2337
|
-
validation: 'passed',
|
|
2338
|
-
status: removeResult?.success === false ? 'merged_cleanup_failed' : 'merged',
|
|
2339
|
-
},
|
|
2340
|
-
};
|
|
2341
|
-
} catch (e: any) {
|
|
2342
|
-
return { success: false, error: e.message };
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
case 'remove_mesh_node': {
|
|
2347
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2348
|
-
const nodeId = typeof args?.nodeId === 'string' ? args.nodeId.trim() : '';
|
|
2349
|
-
if (!meshId || !nodeId) return { success: false, error: 'meshId and nodeId required' };
|
|
2350
|
-
try {
|
|
2351
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2352
|
-
const mesh = meshRecord?.mesh;
|
|
2353
|
-
const node = mesh?.nodes?.find((n: any) => n.id === nodeId || n.nodeId === nodeId);
|
|
2354
|
-
|
|
2355
|
-
const sessionCleanupMode = this.normalizeMeshSessionCleanupMode(
|
|
2356
|
-
args?.sessionCleanupMode ?? args?.session_cleanup_mode ?? mesh?.policy?.sessionCleanupOnNodeRemove,
|
|
2357
|
-
);
|
|
2358
|
-
let sessionCleanup: Record<string, unknown> | undefined;
|
|
2359
|
-
if (node && sessionCleanupMode !== 'preserve') {
|
|
2360
|
-
sessionCleanup = await this.cleanupMeshSessions({ meshId, nodeId, node, mode: sessionCleanupMode, source: 'mesh_remove_node' });
|
|
2361
|
-
if (sessionCleanup.success === false) return { success: false, removed: false, sessionCleanup };
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
let worktreeCleanup: Record<string, unknown> | undefined;
|
|
2365
|
-
if (node?.isLocalWorktree) {
|
|
2366
|
-
const cleanupResult = await this.cleanupLocalWorktreeNode({ mesh, node, nodeId });
|
|
2367
|
-
if (cleanupResult.success === false) {
|
|
2368
|
-
return {
|
|
2369
|
-
success: false,
|
|
2370
|
-
removed: false,
|
|
2371
|
-
code: cleanupResult.code,
|
|
2372
|
-
error: cleanupResult.error,
|
|
2373
|
-
recoveryHint: cleanupResult.recoveryHint,
|
|
2374
|
-
...(sessionCleanup ? { sessionCleanup } : {}),
|
|
2375
|
-
worktreeCleanup: cleanupResult,
|
|
2376
|
-
};
|
|
2377
|
-
}
|
|
2378
|
-
worktreeCleanup = cleanupResult;
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
let removed = false;
|
|
2382
|
-
if (meshRecord?.inline) {
|
|
2383
|
-
removed = this.removeInlineMeshNode(meshId, mesh, nodeId);
|
|
2384
|
-
} else {
|
|
2385
|
-
const { removeNode } = await import('../config/mesh-config.js');
|
|
2386
|
-
removed = removeNode(meshId, nodeId);
|
|
2387
|
-
}
|
|
2388
|
-
|
|
2389
|
-
// Record in task ledger
|
|
2390
|
-
if (removed) {
|
|
2391
|
-
try {
|
|
2392
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2393
|
-
appendLedgerEntry(meshId, {
|
|
2394
|
-
kind: 'node_removed',
|
|
2395
|
-
nodeId,
|
|
2396
|
-
payload: {
|
|
2397
|
-
worktree: !!node?.isLocalWorktree,
|
|
2398
|
-
sessionCleanupMode,
|
|
2399
|
-
workspace: typeof node?.workspace === 'string' ? node.workspace : undefined,
|
|
2400
|
-
daemonId: typeof node?.daemonId === 'string' ? node.daemonId : undefined,
|
|
2401
|
-
worktreeBranch: typeof node?.worktreeBranch === 'string' ? node.worktreeBranch : undefined,
|
|
2402
|
-
worktreeCleanupFallback: typeof worktreeCleanup?.fallback === 'string' ? worktreeCleanup.fallback : undefined,
|
|
2403
|
-
forced: worktreeCleanup?.forced === true ? true : undefined,
|
|
2404
|
-
forceFallbackReason: typeof worktreeCleanup?.reason === 'string' ? worktreeCleanup.reason : undefined,
|
|
2405
|
-
},
|
|
2406
|
-
});
|
|
2407
|
-
} catch { /* ledger append is best-effort */ }
|
|
2408
|
-
}
|
|
2409
|
-
|
|
2410
|
-
return { success: true, removed, ...(sessionCleanup ? { sessionCleanup } : {}), ...(worktreeCleanup ? { worktreeCleanup } : {}) };
|
|
2411
|
-
} catch (e: any) {
|
|
2412
|
-
return { success: false, error: e.message };
|
|
2413
|
-
}
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
case 'clone_mesh_node': {
|
|
2417
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2418
|
-
const sourceNodeId = typeof args?.sourceNodeId === 'string' ? args.sourceNodeId.trim() : '';
|
|
2419
|
-
const branch = typeof args?.branch === 'string' ? args.branch.trim() : '';
|
|
2420
|
-
const baseBranch = typeof args?.baseBranch === 'string' ? args.baseBranch.trim() : undefined;
|
|
2421
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2422
|
-
if (!sourceNodeId) return { success: false, error: 'sourceNodeId required' };
|
|
2423
|
-
if (!branch) return { success: false, error: 'branch required' };
|
|
2424
|
-
|
|
2425
|
-
try {
|
|
2426
|
-
const meshRecord = await this.getMeshForCommand(meshId, args?.inlineMesh);
|
|
2427
|
-
const mesh = meshRecord?.mesh;
|
|
2428
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2429
|
-
|
|
2430
|
-
const sourceNode = mesh.nodes?.find((n: any) => n.id === sourceNodeId || n.nodeId === sourceNodeId);
|
|
2431
|
-
if (!sourceNode) return { success: false, error: `Source node '${sourceNodeId}' not found in mesh` };
|
|
2432
|
-
|
|
2433
|
-
const repoRoot = sourceNode.repoRoot || sourceNode.workspace;
|
|
2434
|
-
const { createWorktree } = await import('../git/git-worktree.js');
|
|
2435
|
-
const result = await createWorktree({
|
|
2436
|
-
repoRoot,
|
|
2437
|
-
branch,
|
|
2438
|
-
baseBranch,
|
|
2439
|
-
meshName: mesh.name,
|
|
2440
|
-
});
|
|
2441
|
-
|
|
2442
|
-
let node: any;
|
|
2443
|
-
if (meshRecord.inline) {
|
|
2444
|
-
const { randomUUID } = await import('crypto');
|
|
2445
|
-
node = {
|
|
2446
|
-
id: `node_${randomUUID().replace(/-/g, '')}`,
|
|
2447
|
-
workspace: result.worktreePath,
|
|
2448
|
-
repoRoot: result.worktreePath,
|
|
2449
|
-
daemonId: sourceNode.daemonId,
|
|
2450
|
-
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
2451
|
-
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
2452
|
-
policy: { ...(sourceNode.policy || {}) },
|
|
2453
|
-
isLocalWorktree: true,
|
|
2454
|
-
worktreeBranch: result.branch,
|
|
2455
|
-
clonedFromNodeId: sourceNodeId,
|
|
2456
|
-
};
|
|
2457
|
-
this.updateInlineMeshNode(meshId, mesh, node);
|
|
2458
|
-
} else {
|
|
2459
|
-
const { addNode } = await import('../config/mesh-config.js');
|
|
2460
|
-
node = addNode(meshId, {
|
|
2461
|
-
workspace: result.worktreePath,
|
|
2462
|
-
repoRoot: result.worktreePath,
|
|
2463
|
-
daemonId: sourceNode.daemonId,
|
|
2464
|
-
machineId: sourceNode.machineId ?? (sourceNode as any).machine_id,
|
|
2465
|
-
userOverrides: { ...(sourceNode.userOverrides || {}) },
|
|
2466
|
-
isLocalWorktree: true,
|
|
2467
|
-
worktreeBranch: result.branch,
|
|
2468
|
-
clonedFromNodeId: sourceNodeId,
|
|
2469
|
-
policy: { ...(sourceNode.policy || {}) },
|
|
2470
|
-
});
|
|
2471
|
-
if (!node) return { success: false, error: 'Failed to register worktree node' };
|
|
2472
|
-
}
|
|
825
|
+
async resumePendingRefineJobsOnStartup(): Promise<void> {
|
|
826
|
+
return resumePendingRefineJobsOnStartup(this);
|
|
827
|
+
}
|
|
2473
828
|
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
try {
|
|
2478
|
-
const { runGit } = await import('../git/git-executor.js');
|
|
2479
|
-
await runGit(
|
|
2480
|
-
{ workspace: result.worktreePath, repoRoot: result.worktreePath, isGitRepo: true },
|
|
2481
|
-
['submodule', 'update', '--init', '--recursive'],
|
|
2482
|
-
{ timeoutMs: 120000 },
|
|
2483
|
-
);
|
|
2484
|
-
} catch (subErr: any) {
|
|
2485
|
-
// Submodule init is best-effort; don't fail the clone
|
|
2486
|
-
console.warn('[mesh] Submodule init failed for worktree:', subErr.message);
|
|
2487
|
-
}
|
|
2488
|
-
}
|
|
829
|
+
private async batchRefineMeshNodes(meshId: string, requestedNodeIds: string[] | undefined, args: any): Promise<CommandRouterResult> {
|
|
830
|
+
return batchRefineMeshNodes(this, meshId, requestedNodeIds, args);
|
|
831
|
+
}
|
|
2489
832
|
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
appendLedgerEntry(meshId, {
|
|
2494
|
-
kind: 'node_cloned',
|
|
2495
|
-
nodeId: node.id,
|
|
2496
|
-
payload: { sourceNodeId, branch: result.branch, worktreePath: result.worktreePath, submodulesInitialized: initSubmodules },
|
|
2497
|
-
});
|
|
2498
|
-
} catch { /* ledger append is best-effort */ }
|
|
2499
|
-
|
|
2500
|
-
return {
|
|
2501
|
-
success: true,
|
|
2502
|
-
node,
|
|
2503
|
-
worktreePath: result.worktreePath,
|
|
2504
|
-
branch: result.branch,
|
|
2505
|
-
};
|
|
2506
|
-
} catch (e: any) {
|
|
2507
|
-
return { success: false, error: e.message };
|
|
2508
|
-
}
|
|
2509
|
-
}
|
|
2510
|
-
case 'trigger_mesh_queue': {
|
|
2511
|
-
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
2512
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2513
|
-
try {
|
|
2514
|
-
const { triggerMeshQueue } = await import('../mesh/mesh-events.js');
|
|
2515
|
-
if (meshId) {
|
|
2516
|
-
triggerMeshQueue(this.deps as any, meshId);
|
|
2517
|
-
}
|
|
2518
|
-
return { success: true };
|
|
2519
|
-
} catch (e: any) {
|
|
2520
|
-
return { success: false, error: e.message };
|
|
2521
|
-
}
|
|
2522
|
-
}
|
|
833
|
+
private async startMeshRefineBatchJob(meshId: string, requestedNodeIds: string[] | undefined, args: any): Promise<CommandRouterResult> {
|
|
834
|
+
return startMeshRefineBatchJob(this, meshId, requestedNodeIds, args);
|
|
835
|
+
}
|
|
2523
836
|
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
let cliType = typeof args?.cliType === 'string' ? args.cliType.trim() : '';
|
|
2528
|
-
if (!meshId) return { success: false, error: 'meshId required' };
|
|
2529
|
-
|
|
2530
|
-
try {
|
|
2531
|
-
const { buildCoordinatorSystemPrompt } = await import('../mesh/coordinator-prompt.js');
|
|
2532
|
-
|
|
2533
|
-
// Support inline mesh data from cloud (bypasses local meshes.json lookup)
|
|
2534
|
-
let mesh: any;
|
|
2535
|
-
if (args?.inlineMesh && typeof args.inlineMesh === 'object') {
|
|
2536
|
-
mesh = args.inlineMesh;
|
|
2537
|
-
// Cache cloud mesh so the MCP server can retrieve it via get_mesh
|
|
2538
|
-
this.inlineMeshCache.set(meshId, mesh);
|
|
2539
|
-
} else {
|
|
2540
|
-
const { getMesh } = await import('../config/mesh-config.js');
|
|
2541
|
-
mesh = getMesh(meshId);
|
|
2542
|
-
}
|
|
2543
|
-
if (!mesh) return { success: false, error: 'Mesh not found' };
|
|
2544
|
-
if (!Array.isArray(mesh.nodes) || mesh.nodes.length === 0) return { success: false, error: 'No nodes in mesh' };
|
|
2545
|
-
|
|
2546
|
-
const requestedCoordinatorNodeId = typeof args?.coordinatorNodeId === 'string'
|
|
2547
|
-
? args.coordinatorNodeId.trim()
|
|
2548
|
-
: '';
|
|
2549
|
-
const preferredCoordinatorNodeId = requestedCoordinatorNodeId
|
|
2550
|
-
|| (typeof mesh.coordinator?.preferredNodeId === 'string' ? mesh.coordinator.preferredNodeId.trim() : '');
|
|
2551
|
-
const coordinatorNode = preferredCoordinatorNodeId
|
|
2552
|
-
? mesh.nodes.find((node: any) => node?.id === preferredCoordinatorNodeId || node?.nodeId === preferredCoordinatorNodeId)
|
|
2553
|
-
: mesh.nodes[0];
|
|
2554
|
-
if (!coordinatorNode) {
|
|
2555
|
-
return {
|
|
2556
|
-
success: false,
|
|
2557
|
-
code: 'mesh_coordinator_node_not_found',
|
|
2558
|
-
error: `Coordinator node ${preferredCoordinatorNodeId} was not found in mesh`,
|
|
2559
|
-
meshId,
|
|
2560
|
-
cliType,
|
|
2561
|
-
};
|
|
2562
|
-
}
|
|
2563
|
-
const workspace = typeof coordinatorNode.workspace === 'string' ? coordinatorNode.workspace.trim() : '';
|
|
2564
|
-
if (!workspace) return { success: false, error: 'Coordinator node workspace required', meshId, cliType };
|
|
2565
|
-
if (!cliType) {
|
|
2566
|
-
const resolved = await resolveProviderTypeFromPriority({
|
|
2567
|
-
nodeId: String(coordinatorNode.id || coordinatorNode.nodeId || preferredCoordinatorNodeId || 'coordinator'),
|
|
2568
|
-
providerPriority: readProviderPriorityFromPolicy(coordinatorNode.policy),
|
|
2569
|
-
providerLoader: this.deps.providerLoader,
|
|
2570
|
-
onStatusChange: this.deps.onStatusChange,
|
|
2571
|
-
});
|
|
2572
|
-
if (!resolved.providerType) {
|
|
2573
|
-
return {
|
|
2574
|
-
success: false,
|
|
2575
|
-
code: 'mesh_coordinator_provider_priority_unusable',
|
|
2576
|
-
error: resolved.error || 'No usable provider found from node providerPriority',
|
|
2577
|
-
meshId,
|
|
2578
|
-
cliType,
|
|
2579
|
-
workspace,
|
|
2580
|
-
};
|
|
2581
|
-
}
|
|
2582
|
-
cliType = resolved.providerType;
|
|
2583
|
-
}
|
|
2584
|
-
const providerMeta = this.deps.providerLoader.resolve?.(cliType) || this.deps.providerLoader.getMeta(cliType);
|
|
2585
|
-
const coordinatorSetup = resolveMeshCoordinatorSetup({
|
|
2586
|
-
provider: providerMeta,
|
|
2587
|
-
cliType,
|
|
2588
|
-
meshId,
|
|
2589
|
-
workspace,
|
|
2590
|
-
});
|
|
2591
|
-
|
|
2592
|
-
if (coordinatorSetup.kind === 'unsupported') {
|
|
2593
|
-
return {
|
|
2594
|
-
success: false,
|
|
2595
|
-
code: 'mesh_coordinator_unsupported',
|
|
2596
|
-
error: coordinatorSetup.reason,
|
|
2597
|
-
meshId,
|
|
2598
|
-
cliType,
|
|
2599
|
-
workspace,
|
|
2600
|
-
};
|
|
2601
|
-
}
|
|
837
|
+
private async startMeshRefineJob(meshId: string, nodeId: string, args: any): Promise<CommandRouterResult> {
|
|
838
|
+
return startMeshRefineJob(this, meshId, nodeId, args);
|
|
839
|
+
}
|
|
2602
840
|
|
|
2603
|
-
|
|
2604
|
-
return {
|
|
2605
|
-
success: false,
|
|
2606
|
-
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
2607
|
-
error: coordinatorSetup.instructions,
|
|
2608
|
-
meshId,
|
|
2609
|
-
cliType,
|
|
2610
|
-
workspace,
|
|
2611
|
-
meshCoordinatorSetup: coordinatorSetup,
|
|
2612
|
-
};
|
|
2613
|
-
}
|
|
841
|
+
// ─── Daemon-level command core ───────────────────
|
|
2614
842
|
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
// Gemini CLI auto-loads GEMINI.md from CWD as project context.
|
|
2656
|
-
// Write a temporary GEMINI.md to the workspace before launch.
|
|
2657
|
-
try {
|
|
2658
|
-
const { writeFileSync: wfs, existsSync: efs, readFileSync: rfs } = await import('node:fs');
|
|
2659
|
-
const geminiMdPath = `${workspace}/GEMINI.md`;
|
|
2660
|
-
const marker = '<!-- adhdev-mesh-coordinator-prompt -->';
|
|
2661
|
-
const markerEnd = '<!-- /adhdev-mesh-coordinator-prompt -->';
|
|
2662
|
-
const block = `${marker}\n${cliCmdSystemPrompt}\n${markerEnd}`;
|
|
2663
|
-
if (efs(geminiMdPath)) {
|
|
2664
|
-
const existing = rfs(geminiMdPath, 'utf-8');
|
|
2665
|
-
// Replace existing block or append
|
|
2666
|
-
const replaced = existing.replace(
|
|
2667
|
-
new RegExp(`${marker}[\\s\\S]*?${markerEnd}`, 'g'),
|
|
2668
|
-
block,
|
|
2669
|
-
);
|
|
2670
|
-
wfs(geminiMdPath, replaced.includes(marker) ? replaced : `${existing}\n\n${block}`);
|
|
2671
|
-
} else {
|
|
2672
|
-
wfs(geminiMdPath, block);
|
|
2673
|
-
}
|
|
2674
|
-
LOG.info('MeshCoordinator', `Wrote coordinator prompt to ${workspace}/GEMINI.md`);
|
|
2675
|
-
} catch (e: any) {
|
|
2676
|
-
LOG.warn('MeshCoordinator', `Could not write GEMINI.md: ${e?.message || e}`);
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
|
|
2681
|
-
const cliCmdLaunch: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
2682
|
-
cliType,
|
|
2683
|
-
dir: workspace,
|
|
2684
|
-
cliArgs: cliCmdArgs.length > 0 ? cliCmdArgs : undefined,
|
|
2685
|
-
env: Object.keys(cliCmdEnv).length > 0 ? cliCmdEnv : undefined,
|
|
2686
|
-
settings: { meshCoordinatorFor: meshId },
|
|
843
|
+
/**
|
|
844
|
+
* Daemon-level command execution (IDE start/stop/restart, CLI, detect, logs).
|
|
845
|
+
* Returns null if not handled at this level → caller delegates to CommandHandler.
|
|
846
|
+
*/
|
|
847
|
+
private async executeDaemonCommand(cmd: string, args: any): Promise<CommandRouterResult | null> {
|
|
848
|
+
// [Z] Remote mesh worker session-scoped command forward.
|
|
849
|
+
//
|
|
850
|
+
// Session-scoped commands issued from the dashboard (the controlbar Model/Mode
|
|
851
|
+
// selectors → invoke_provider_script, and modal approval → resolve_action, plus the
|
|
852
|
+
// direct set_mode/change_model/set_thought_level mutations) target a session by
|
|
853
|
+
// targetSessionId. agent_command (send_chat / clear_history / stop) is included for the
|
|
854
|
+
// same reason: a command naming a session must reach THAT session, never a different
|
|
855
|
+
// local one. When that session is a mesh worker hosted on a REMOTE daemon, this
|
|
856
|
+
// coordinator never holds its live instance, so the CommandHandler delegation would
|
|
857
|
+
// fail with "Live session not found" — or, for agent_command, findAdapter would have
|
|
858
|
+
// fuzzy-injected the message into the coordinator's own CLI session (TASKECHO). Forward
|
|
859
|
+
// to the owning worker daemon — the same daemon that already executes send_chat for that
|
|
860
|
+
// session — so the command acts on the real worker. _meshDirectDispatch prevents
|
|
861
|
+
// re-forwarding once the call lands on the owning daemon (it then handles the session
|
|
862
|
+
// locally). A locally-hosted worker (or any session this coordinator owns) resolves to
|
|
863
|
+
// undefined below and falls through to normal local handling — no regression.
|
|
864
|
+
if (MESH_FORWARDABLE_SESSION_COMMANDS.has(cmd) && this.deps.dispatchMeshCommand && !args?._meshDirectDispatch) {
|
|
865
|
+
const targetSessionId = readStringValue(args?.targetSessionId, args?.sessionId, args?.instanceId);
|
|
866
|
+
if (targetSessionId) {
|
|
867
|
+
const localInstance = this.deps.instanceManager?.getInstance(targetSessionId);
|
|
868
|
+
const localRegistry = this.deps.sessionRegistry?.get?.(targetSessionId);
|
|
869
|
+
if (!localInstance && !localRegistry) {
|
|
870
|
+
// CANCEL-STOP-RELAY: pass the authoritative owning nodeId (when the caller
|
|
871
|
+
// shipped one in meshContext, e.g. mesh_queue_cancel's assignedNodeId) as the
|
|
872
|
+
// deterministic owner-resolution fallback. The session-id cache scan stays the
|
|
873
|
+
// primary path; the hint only kicks in when that scan misses (worktree-clone
|
|
874
|
+
// worker session not yet in / form-mismatched against the cached snapshot).
|
|
875
|
+
const meshContext = readObjectRecord(args?.meshContext);
|
|
876
|
+
const ownerNodeIdHint = readStringValue(meshContext.nodeId);
|
|
877
|
+
const ownerDaemonId = this.resolveRemoteMeshSessionOwnerDaemonId(targetSessionId, ownerNodeIdHint);
|
|
878
|
+
if (ownerDaemonId) {
|
|
879
|
+
LOG.info('Mesh', `[Mesh] Forwarding session-scoped '${cmd}' for remote worker session ${targetSessionId.split('_')[0]} → daemon ${ownerDaemonId.slice(0, 12)}`);
|
|
880
|
+
const forwarded = await this.deps.dispatchMeshCommand(ownerDaemonId, cmd, {
|
|
881
|
+
...(typeof args === 'object' && args !== null ? args as Record<string, unknown> : {}),
|
|
882
|
+
_meshDirectDispatch: true,
|
|
2687
883
|
});
|
|
2688
|
-
|
|
2689
|
-
if (!cliCmdLaunch?.success) {
|
|
2690
|
-
return { success: false, error: cliCmdLaunch?.error || 'Failed to launch CLI session' };
|
|
2691
|
-
}
|
|
2692
|
-
|
|
2693
|
-
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator (cli_command) for mesh ${meshId}`);
|
|
2694
|
-
try {
|
|
2695
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2696
|
-
appendLedgerEntry(meshId, {
|
|
2697
|
-
kind: 'coordinator_started',
|
|
2698
|
-
sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
|
|
2699
|
-
providerType: cliType,
|
|
2700
|
-
payload: { workspace },
|
|
2701
|
-
});
|
|
2702
|
-
} catch { /* best-effort */ }
|
|
2703
|
-
|
|
2704
|
-
return {
|
|
2705
|
-
success: true,
|
|
2706
|
-
meshId,
|
|
2707
|
-
cliType,
|
|
2708
|
-
workspace,
|
|
2709
|
-
sessionId: cliCmdLaunch.sessionId || cliCmdLaunch.id,
|
|
2710
|
-
mcpRegistered: true,
|
|
2711
|
-
};
|
|
2712
|
-
}
|
|
2713
|
-
|
|
2714
|
-
const configFormat = coordinatorSetup.configFormat as MeshCoordinatorConfigFormat;
|
|
2715
|
-
if (configFormat !== 'claude_mcp_json' && configFormat !== 'hermes_config_yaml') {
|
|
2716
|
-
return {
|
|
2717
|
-
success: false,
|
|
2718
|
-
code: 'mesh_coordinator_unsupported',
|
|
2719
|
-
error: `Unsupported auto-import MCP config format: ${String(coordinatorSetup.configFormat)}`,
|
|
2720
|
-
meshId,
|
|
2721
|
-
cliType,
|
|
2722
|
-
workspace,
|
|
2723
|
-
};
|
|
2724
|
-
}
|
|
2725
|
-
|
|
2726
|
-
// Build the coordinator prompt before mutating workspace config or launching.
|
|
2727
|
-
// Prompt generation failures are configuration/data-shape errors; fail closed so
|
|
2728
|
-
// broken mesh state is visible instead of silently launching with weaker rules.
|
|
2729
|
-
let systemPrompt = '';
|
|
2730
|
-
try {
|
|
2731
|
-
systemPrompt = buildCoordinatorSystemPrompt({ mesh, coordinatorCliType: cliType });
|
|
2732
|
-
} catch (error: any) {
|
|
2733
|
-
const message = error?.message || String(error);
|
|
2734
|
-
LOG.error('MeshCoordinator', `Failed to build coordinator prompt: ${message}`);
|
|
2735
|
-
return {
|
|
2736
|
-
success: false,
|
|
2737
|
-
code: 'mesh_coordinator_prompt_failed',
|
|
2738
|
-
error: `Failed to build Repo Mesh coordinator prompt: ${message}`,
|
|
2739
|
-
meshId,
|
|
2740
|
-
cliType,
|
|
2741
|
-
workspace,
|
|
2742
|
-
};
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
// 1. Write provider-declared MCP config for CLIs that auto-import it.
|
|
2746
|
-
const { existsSync, readFileSync, writeFileSync, copyFileSync, mkdirSync } = await import('fs');
|
|
2747
|
-
const { dirname } = await import('path');
|
|
2748
|
-
const mcpConfigPath = coordinatorSetup.configPath;
|
|
2749
|
-
const hermesManualFallback = cliType === 'hermes-cli' && configFormat === 'hermes_config_yaml'
|
|
2750
|
-
? createHermesManualMeshCoordinatorSetup(meshId, workspace)
|
|
2751
|
-
: null;
|
|
2752
|
-
let hermesBaseConfig: { config: Record<string, any>; sourceHome: string; sourceConfigPath: string } | null = null;
|
|
2753
|
-
if (hermesManualFallback) {
|
|
2754
|
-
try {
|
|
2755
|
-
hermesBaseConfig = loadHermesCoordinatorBaseConfig(mcpConfigPath);
|
|
2756
|
-
} catch (error: any) {
|
|
2757
|
-
const message = `Failed to parse Hermes base config for automatic coordinator setup: ${error?.message || error}`;
|
|
2758
|
-
LOG.error('MeshCoordinator', message);
|
|
2759
|
-
return { success: false, code: 'mesh_coordinator_config_parse_failed', error: message, meshId, cliType, workspace };
|
|
2760
|
-
}
|
|
2761
|
-
}
|
|
2762
|
-
const returnManualFallback = (message: string) => ({
|
|
2763
|
-
success: false,
|
|
2764
|
-
code: 'mesh_coordinator_manual_mcp_setup_required',
|
|
2765
|
-
error: message,
|
|
2766
|
-
meshId,
|
|
2767
|
-
cliType,
|
|
2768
|
-
workspace,
|
|
2769
|
-
meshCoordinatorSetup: hermesManualFallback,
|
|
2770
|
-
});
|
|
2771
|
-
|
|
2772
|
-
// Merge ADHDev mesh server into existing config.
|
|
2773
|
-
// Pass full mesh data as env var so the MCP server can bootstrap
|
|
2774
|
-
// without depending on meshes.json or a running daemon.
|
|
2775
|
-
const mcpServerEntry: Record<string, any> = {
|
|
2776
|
-
command: coordinatorSetup.mcpServer.command,
|
|
2777
|
-
args: coordinatorSetup.mcpServer.args,
|
|
2778
|
-
};
|
|
2779
|
-
if (args?.inlineMesh) {
|
|
2780
|
-
const modeArgIndex = coordinatorSetup.mcpServer.args.findIndex((value: string) => value === '--mode');
|
|
2781
|
-
const mcpTransport = modeArgIndex >= 0 ? coordinatorSetup.mcpServer.args[modeArgIndex + 1] : 'ipc';
|
|
2782
|
-
mcpServerEntry.env = {
|
|
2783
|
-
ADHDEV_INLINE_MESH: JSON.stringify(mesh),
|
|
2784
|
-
ADHDEV_MCP_TRANSPORT: mcpTransport === 'local' ? 'local' : 'ipc',
|
|
2785
|
-
};
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
|
-
try {
|
|
2789
|
-
mkdirSync(dirname(mcpConfigPath), { recursive: true });
|
|
2790
|
-
} catch (error: any) {
|
|
2791
|
-
const message = `Could not prepare MCP config path for automatic setup: ${error?.message || error}`;
|
|
2792
|
-
LOG.error('MeshCoordinator', message);
|
|
2793
|
-
if (hermesManualFallback) return returnManualFallback(message);
|
|
2794
|
-
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
2795
|
-
}
|
|
2796
|
-
|
|
2797
|
-
// Backup existing MCP config if present.
|
|
2798
|
-
const hadExistingMcpConfig = existsSync(mcpConfigPath);
|
|
2799
|
-
let existingMcpConfig: Record<string, any> = hermesBaseConfig?.config || {};
|
|
2800
|
-
if (hermesBaseConfig) {
|
|
2801
|
-
copyHermesCoordinatorCredentialFiles(hermesBaseConfig.sourceHome, dirname(mcpConfigPath));
|
|
2802
|
-
}
|
|
2803
|
-
if (hadExistingMcpConfig) {
|
|
2804
|
-
try {
|
|
2805
|
-
const parsedExistingMcpConfig = parseMeshCoordinatorMcpConfig(readFileSync(mcpConfigPath, 'utf-8'), configFormat);
|
|
2806
|
-
const existingCoordinatorConfig = hermesManualFallback
|
|
2807
|
-
? stripHermesCoordinatorTempModelProviderOverrides(parsedExistingMcpConfig)
|
|
2808
|
-
: parsedExistingMcpConfig;
|
|
2809
|
-
existingMcpConfig = { ...existingMcpConfig, ...existingCoordinatorConfig };
|
|
2810
|
-
copyFileSync(mcpConfigPath, mcpConfigPath + '.backup');
|
|
2811
|
-
} catch (error: any) {
|
|
2812
|
-
LOG.error('MeshCoordinator', `Failed to parse existing MCP config ${mcpConfigPath}: ${error?.message || error}`);
|
|
2813
|
-
return {
|
|
2814
|
-
success: false,
|
|
2815
|
-
code: 'mesh_coordinator_config_parse_failed',
|
|
2816
|
-
error: `Failed to parse existing MCP config at ${mcpConfigPath}`,
|
|
2817
|
-
};
|
|
2818
|
-
}
|
|
2819
|
-
}
|
|
2820
|
-
|
|
2821
|
-
const mcpServersKey = getMcpServersKey(configFormat);
|
|
2822
|
-
const existingServers = existingMcpConfig[mcpServersKey];
|
|
2823
|
-
const mcpConfig = {
|
|
2824
|
-
...existingMcpConfig,
|
|
2825
|
-
[mcpServersKey]: {
|
|
2826
|
-
...(existingServers && typeof existingServers === 'object' && !Array.isArray(existingServers) ? existingServers : {}),
|
|
2827
|
-
[coordinatorSetup.serverName]: mcpServerEntry,
|
|
2828
|
-
},
|
|
2829
|
-
};
|
|
2830
|
-
try {
|
|
2831
|
-
writeFileSync(mcpConfigPath, serializeMeshCoordinatorMcpConfig(mcpConfig, configFormat), 'utf-8');
|
|
2832
|
-
} catch (error: any) {
|
|
2833
|
-
const message = `Could not write MCP config for automatic setup: ${error?.message || error}`;
|
|
2834
|
-
LOG.error('MeshCoordinator', message);
|
|
2835
|
-
if (hermesManualFallback) return returnManualFallback(message);
|
|
2836
|
-
return { success: false, code: 'mesh_coordinator_config_write_failed', error: message, meshId, cliType, workspace };
|
|
884
|
+
return (forwarded ?? { success: false, error: 'no response from remote worker daemon' }) as CommandRouterResult;
|
|
2837
885
|
}
|
|
2838
|
-
LOG.info('MeshCoordinator', `Wrote ${mcpConfigPath} with ${coordinatorSetup.serverName} server`);
|
|
2839
|
-
|
|
2840
|
-
const cliArgs: string[] = [];
|
|
2841
|
-
const launchEnv: Record<string, string> = {};
|
|
2842
|
-
if (configFormat === 'hermes_config_yaml') {
|
|
2843
|
-
launchEnv.HERMES_HOME = dirname(mcpConfigPath);
|
|
2844
|
-
launchEnv.HERMES_IGNORE_USER_CONFIG = '';
|
|
2845
|
-
}
|
|
2846
|
-
if (systemPrompt) {
|
|
2847
|
-
if (configFormat === 'hermes_config_yaml') {
|
|
2848
|
-
launchEnv.HERMES_EPHEMERAL_SYSTEM_PROMPT = systemPrompt;
|
|
2849
|
-
} else {
|
|
2850
|
-
cliArgs.push('--append-system-prompt', systemPrompt);
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
if (cliType === 'claude-cli') {
|
|
2854
|
-
cliArgs.push('--mcp-config', coordinatorSetup.configPath);
|
|
2855
|
-
}
|
|
2856
|
-
|
|
2857
|
-
// 3. Launch CLI session via existing cliManager.
|
|
2858
|
-
// Provider-specific prompt injection remains fail-closed: Claude gets
|
|
2859
|
-
// explicit CLI args, while Hermes reads HERMES_EPHEMERAL_SYSTEM_PROMPT.
|
|
2860
|
-
const launchResult: any = await this.deps.cliManager.handleCliCommand('launch_cli', {
|
|
2861
|
-
cliType,
|
|
2862
|
-
dir: workspace,
|
|
2863
|
-
cliArgs: cliArgs.length > 0 ? cliArgs : undefined,
|
|
2864
|
-
env: Object.keys(launchEnv).length > 0 ? launchEnv : undefined,
|
|
2865
|
-
settings: {
|
|
2866
|
-
meshCoordinatorFor: meshId
|
|
2867
|
-
}
|
|
2868
|
-
});
|
|
2869
|
-
|
|
2870
|
-
if (!launchResult?.success) {
|
|
2871
|
-
return { success: false, error: launchResult?.error || 'Failed to launch CLI session' };
|
|
2872
|
-
}
|
|
2873
|
-
|
|
2874
|
-
LOG.info('MeshCoordinator', `Launched ${cliType} coordinator for mesh ${meshId} in ${workspace}`);
|
|
2875
|
-
|
|
2876
|
-
// Record coordinator launch in task ledger
|
|
2877
|
-
try {
|
|
2878
|
-
const { appendLedgerEntry } = await import('../mesh/mesh-ledger.js');
|
|
2879
|
-
appendLedgerEntry(meshId, {
|
|
2880
|
-
kind: 'coordinator_started',
|
|
2881
|
-
sessionId: launchResult.sessionId || launchResult.id,
|
|
2882
|
-
providerType: cliType,
|
|
2883
|
-
payload: { workspace },
|
|
2884
|
-
});
|
|
2885
|
-
} catch { /* ledger append is best-effort */ }
|
|
2886
|
-
|
|
2887
|
-
return {
|
|
2888
|
-
success: true,
|
|
2889
|
-
meshId,
|
|
2890
|
-
cliType,
|
|
2891
|
-
workspace,
|
|
2892
|
-
sessionId: launchResult.sessionId || launchResult.id,
|
|
2893
|
-
mcpConfigWritten: true,
|
|
2894
|
-
};
|
|
2895
|
-
} catch (e: any) {
|
|
2896
|
-
LOG.error('MeshCoordinator', `Failed: ${e.message}`);
|
|
2897
|
-
return { success: false, error: e.message };
|
|
2898
886
|
}
|
|
2899
887
|
}
|
|
888
|
+
}
|
|
2900
889
|
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
const { readLedgerEntries, getLedgerSummary } = await import('../mesh/mesh-ledger.js');
|
|
2914
|
-
const ledgerEntries = readLedgerEntries(meshId, { tail: 20 });
|
|
2915
|
-
const ledgerSummary = getLedgerSummary(meshId);
|
|
2916
|
-
|
|
2917
|
-
const nodeStatuses = [];
|
|
2918
|
-
for (const node of mesh.nodes || []) {
|
|
2919
|
-
const status: Record<string, unknown> = {
|
|
2920
|
-
nodeId: node.id || node.nodeId,
|
|
2921
|
-
machineLabel: node.machineLabel || node.id || node.nodeId,
|
|
2922
|
-
workspace: node.workspace,
|
|
2923
|
-
repoRoot: node.repoRoot,
|
|
2924
|
-
isLocalWorktree: node.isLocalWorktree,
|
|
2925
|
-
worktreeBranch: node.worktreeBranch,
|
|
2926
|
-
daemonId: node.daemonId,
|
|
2927
|
-
machineId: node.machineId,
|
|
2928
|
-
health: 'unknown',
|
|
2929
|
-
providers: node.providers || [],
|
|
2930
|
-
activeSessions: [],
|
|
2931
|
-
};
|
|
2932
|
-
if (node.workspace && typeof node.workspace === 'string') {
|
|
2933
|
-
if (!fs.existsSync(node.workspace as string) && applyCachedInlineMeshNodeStatus(status, node)) {
|
|
2934
|
-
nodeStatuses.push(status);
|
|
2935
|
-
continue;
|
|
2936
|
-
}
|
|
2937
|
-
try {
|
|
2938
|
-
const gitStatus = await getGitRepoStatus(node.workspace as string, { timeoutMs: 10_000 });
|
|
2939
|
-
status.git = gitStatus;
|
|
2940
|
-
if (gitStatus.isGitRepo) {
|
|
2941
|
-
const dirty = (gitStatus.staged + gitStatus.modified + gitStatus.untracked + gitStatus.deleted + gitStatus.renamed) > 0;
|
|
2942
|
-
status.health = gitStatus.branch ? (dirty ? 'dirty' : 'online') : 'degraded';
|
|
2943
|
-
} else {
|
|
2944
|
-
status.health = 'degraded';
|
|
2945
|
-
if (gitStatus.error && !status.error) status.error = gitStatus.error;
|
|
2946
|
-
}
|
|
2947
|
-
} catch {
|
|
2948
|
-
if (!applyCachedInlineMeshNodeStatus(status, node)) {
|
|
2949
|
-
status.health = 'degraded';
|
|
2950
|
-
}
|
|
2951
|
-
}
|
|
2952
|
-
} else {
|
|
2953
|
-
applyCachedInlineMeshNodeStatus(status, node);
|
|
2954
|
-
}
|
|
2955
|
-
nodeStatuses.push(status);
|
|
2956
|
-
}
|
|
890
|
+
// RF-ROUTER LOW family: low-coupling commands (session-host control, spec
|
|
891
|
+
// provider-dev, refine/change-impact config) are handled by the registry
|
|
892
|
+
// before the switch. A hit returns the same CommandRouterResult the inlined
|
|
893
|
+
// case used to; a miss falls through to the switch unchanged.
|
|
894
|
+
const lowFamilyHandler = lowFamilyRegistry.get(cmd);
|
|
895
|
+
if (lowFamilyHandler) {
|
|
896
|
+
return await lowFamilyHandler({
|
|
897
|
+
deps: this.deps,
|
|
898
|
+
getMeshForCommand: this.getMeshForCommand.bind(this),
|
|
899
|
+
}, args);
|
|
900
|
+
}
|
|
2957
901
|
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
}
|
|
2971
|
-
}
|
|
902
|
+
// RF-ROUTER MED family: medium-coupling commands (CLI/ACP agent, IDE
|
|
903
|
+
// lifecycle, mesh CRUD, mesh queue, mesh host pairing, fast-forward /
|
|
904
|
+
// refine convergence) are handled by the registry after the LOW family and
|
|
905
|
+
// before the switch. Unlike LOW handlers, MED handlers need router-private
|
|
906
|
+
// collaborators, so the context carries bound methods + the inline-mesh /
|
|
907
|
+
// git-probe caches + the launchIde helper (which breaks the original
|
|
908
|
+
// launch_ide ↔ restart_* self-recursion). A hit returns the same
|
|
909
|
+
// CommandRouterResult the inlined case used to; a miss falls through.
|
|
910
|
+
const medFamilyHandler = medFamilyRegistry.get(cmd);
|
|
911
|
+
if (medFamilyHandler) {
|
|
912
|
+
return await medFamilyHandler(this.buildMedFamilyContext(), args);
|
|
913
|
+
}
|
|
2972
914
|
|
|
2973
|
-
|
|
2974
|
-
|
|
915
|
+
// RF-ROUTER HIGH family: high-coupling commands (mesh coordinator-event
|
|
916
|
+
// relay + interactive prompt, mesh coordinator launch, mesh aggregate
|
|
917
|
+
// status + review inbox) are handled by the registry after the LOW and
|
|
918
|
+
// MED families and before the (now empty) switch. HIGH handlers reach the
|
|
919
|
+
// most router-owned state — the aggregate-status memory cache and the
|
|
920
|
+
// running-refine-job table — so the context carries those plus bound
|
|
921
|
+
// read/write helpers and the router's own `execute` (the
|
|
922
|
+
// get_mesh_review_inbox mesh_status re-entry). A hit returns the same
|
|
923
|
+
// CommandRouterResult the inlined case used to; a miss falls through to
|
|
924
|
+
// CommandHandler delegation.
|
|
925
|
+
const highFamilyHandler = highFamilyRegistry.get(cmd);
|
|
926
|
+
if (highFamilyHandler) {
|
|
927
|
+
return await highFamilyHandler(this.buildHighFamilyContext(), args);
|
|
2975
928
|
}
|
|
2976
929
|
|
|
2977
930
|
return null; // Not handled at this level → delegate to CommandHandler
|
|
@@ -3022,7 +975,7 @@ export class DaemonCommandRouter {
|
|
|
3022
975
|
|
|
3023
976
|
// 3. Kill OS process if requested
|
|
3024
977
|
if (killProcess) {
|
|
3025
|
-
const running = isIdeRunning(ideType);
|
|
978
|
+
const running = await isIdeRunning(ideType);
|
|
3026
979
|
if (running) {
|
|
3027
980
|
LOG.info('StopIDE', `Killing IDE process: ${ideType}`);
|
|
3028
981
|
const killed = await killIdeProcess(ideType);
|