@adhdev/daemon-core 0.9.82-rc.45 → 0.9.82-rc.451
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot/daemon-lifecycle.d.ts +9 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/build-info.d.ts +37 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +68 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +200 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +126 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
- package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +69 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
- package/dist/commands/chat-commands-read.d.ts +7 -0
- package/dist/commands/chat-commands-scope.d.ts +39 -0
- package/dist/commands/chat-commands-shared.d.ts +33 -0
- package/dist/commands/chat-commands-write.d.ts +14 -0
- package/dist/commands/chat-commands.d.ts +10 -23
- package/dist/commands/cli-manager.d.ts +25 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +64 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +29 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/mesh-restart.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +152 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +279 -2
- package/dist/commands/upgrade-helper.d.ts +41 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +129 -1
- package/dist/config/mesh-json-config.d.ts +199 -0
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +12 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +57 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/git-worktree.d.ts +71 -1
- package/dist/git/index.d.ts +3 -1
- package/dist/index.d.ts +66 -14
- package/dist/index.js +49753 -18414
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +49614 -18438
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +81 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +89 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +174 -0
- package/dist/mesh/mesh-clone-grace.d.ts +8 -0
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-event-classify.d.ts +5 -0
- package/dist/mesh/mesh-event-forwarding.d.ts +41 -0
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
- package/dist/mesh/mesh-events-pending.d.ts +69 -0
- package/dist/mesh/mesh-events-stale.d.ts +61 -0
- package/dist/mesh/mesh-events-utils.d.ts +75 -0
- package/dist/mesh/mesh-events.d.ts +6 -49
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +29 -0
- package/dist/mesh/mesh-init.d.ts +128 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
- package/dist/mesh/mesh-ledger.d.ts +115 -1
- package/dist/mesh/mesh-magi-status.d.ts +63 -0
- package/dist/mesh/mesh-missions.d.ts +189 -0
- package/dist/mesh/mesh-node-identity.d.ts +306 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +113 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +79 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-gates.d.ts +428 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +450 -0
- package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
- package/dist/mesh/mesh-task-inflight.d.ts +46 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
- package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +300 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +216 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +145 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +20 -0
- package/dist/providers/chat-message-normalization.d.ts +32 -1
- package/dist/providers/cli-provider-instance.d.ts +254 -3
- package/dist/providers/contracts.d.ts +149 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +63 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +126 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/constants.d.ts +12 -0
- package/dist/providers/native-history/dispatcher.d.ts +27 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +29 -0
- package/dist/providers/provider-instance.d.ts +23 -1
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +86 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +98 -0
- package/dist/providers/spec/cli-adapter.d.ts +223 -0
- package/dist/providers/spec/evaluator.d.ts +45 -0
- package/dist/providers/spec/fsm-driver.d.ts +444 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +202 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +222 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +519 -13
- package/dist/runtime-defaults.d.ts +2 -0
- package/dist/session-host/managed-host.d.ts +64 -0
- package/dist/session-host/runtime-surface.d.ts +10 -16
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +86 -55
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +26 -0
- package/dist/system/hash.d.ts +8 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +63 -12
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +3 -1
- package/src/cli-adapter-types.ts +68 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1246 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +914 -1144
- package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/src/cli-adapters/provider-cli-parse.ts +19 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +121 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/pty-write-chunking.ts +106 -0
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +204 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +198 -0
- package/src/commands/chat-commands-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2488 -0
- package/src/commands/chat-commands-scope.ts +54 -0
- package/src/commands/chat-commands-shared.ts +114 -0
- package/src/commands/chat-commands-write.ts +891 -0
- package/src/commands/chat-commands.ts +19 -1841
- package/src/commands/cli-manager.ts +596 -27
- package/src/commands/handler.ts +841 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +680 -0
- package/src/commands/high-family/mesh-events.ts +89 -0
- package/src/commands/high-family/mesh-status.ts +822 -0
- package/src/commands/high-family/types.ts +80 -0
- package/src/commands/low-family/coordinator-prompt.ts +72 -0
- package/src/commands/low-family/daemon-lifecycle.ts +107 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +87 -0
- package/src/commands/low-family/notification.ts +116 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +274 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +39 -0
- package/src/commands/med-family/cli-agent.ts +238 -0
- package/src/commands/med-family/fast-forward.ts +230 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +37 -0
- package/src/commands/med-family/mesh-crud.ts +1253 -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.ts +2893 -3536
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +493 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +619 -23
- package/src/config/mesh-json-config.ts +376 -0
- package/src/config/recent-activity.ts +8 -2
- package/src/config/repo-settings.ts +111 -0
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/daemon/dev-server.ts +0 -541
- package/src/detection/cli-detector.ts +28 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +78 -15
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +767 -48
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +261 -4
- package/src/git/index.ts +17 -0
- package/src/index.ts +198 -12
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/command-log.ts +7 -5
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +341 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +338 -0
- package/src/mesh/coordinator-prompt.ts +416 -34
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +645 -0
- package/src/mesh/mesh-clone-grace.ts +68 -0
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-event-classify.ts +51 -0
- package/src/mesh/mesh-event-forwarding.ts +1897 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +32 -0
- package/src/mesh/mesh-events-pending.ts +656 -0
- package/src/mesh/mesh-events-stale.ts +389 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +33 -1035
- package/src/mesh/mesh-fast-forward.ts +843 -0
- package/src/mesh/mesh-host-ownership.ts +111 -0
- package/src/mesh/mesh-init.ts +350 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
- package/src/mesh/mesh-ledger.ts +841 -102
- package/src/mesh/mesh-magi-status.ts +223 -0
- package/src/mesh/mesh-missions.ts +449 -0
- package/src/mesh/mesh-node-identity.ts +1985 -0
- package/src/mesh/mesh-queue-assignment.ts +1924 -0
- package/src/mesh/mesh-reconcile-loop.ts +2018 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1673 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +1853 -0
- package/src/mesh/mesh-scheduling-runtime.ts +198 -0
- package/src/mesh/mesh-task-inflight.ts +70 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +1254 -141
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +423 -0
- package/src/mesh/worktree-bootstrap-config.ts +450 -0
- package/src/providers/acp-provider-instance.ts +43 -10
- package/src/providers/approval-utils.d.ts +5 -0
- package/src/providers/approval-utils.ts +57 -5
- package/src/providers/chat-message-normalization.ts +92 -4
- package/src/providers/cli-provider-instance.ts +2177 -107
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +160 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/manual-attendance.ts +85 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +1043 -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 +373 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +260 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +71 -0
- package/src/providers/provider-instance.ts +19 -1
- package/src/providers/provider-loader.ts +668 -50
- package/src/providers/provider-schema.ts +87 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +313 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +509 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +246 -0
- package/src/providers/spec/cli-adapter.ts +1064 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1481 -0
- package/src/providers/spec/fsm-evaluator.ts +290 -0
- package/src/providers/spec/fsm-loader.ts +128 -0
- package/src/providers/spec/fsm-types.ts +301 -0
- package/src/providers/spec/native-history-executor.ts +1174 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +262 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +758 -14
- package/src/runtime-defaults.ts +39 -0
- package/src/session-host/managed-host.ts +218 -0
- package/src/session-host/runtime-surface.ts +20 -80
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +124 -55
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +95 -26
- package/src/system/hash.ts +23 -0
- package/src/system/host-memory.ts +29 -12
- package/src/system/load-better-sqlite3.ts +68 -0
- package/src/types.ts +5 -0
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -0,0 +1,1064 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecCliAdapter — bridges SpecDriver into the daemon's CliAdapter
|
|
3
|
+
* interface so an existing CliProviderInstance can drive a spec-backed
|
|
4
|
+
* provider without rewriting the surrounding session machinery.
|
|
5
|
+
*
|
|
6
|
+
* Translation:
|
|
7
|
+
* spec state.id 'approval' (with modal_buttons that produced a modal)
|
|
8
|
+
* → CliAdapterStatus.status='waiting_approval', activeModal={...}
|
|
9
|
+
* spec state.id 'busy' / any non-decision state with a 'busy' label
|
|
10
|
+
* → status='generating'
|
|
11
|
+
* spec state.id 'idle' or default
|
|
12
|
+
* → status='idle'
|
|
13
|
+
*
|
|
14
|
+
* Methods that the round-3 spec model doesn't have an opinion on
|
|
15
|
+
* (transcript reading, slash commands, history, runtime metadata
|
|
16
|
+
* surfacing) are minimal stubs. They satisfy the daemon's call sites
|
|
17
|
+
* without pretending to implement anything.
|
|
18
|
+
*/
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
import { FsmDriver, type DashboardEvent, type ISpecDriver } from './fsm-driver.js';
|
|
22
|
+
import { lastContiguousNumberedBlock } from './evaluator.js';
|
|
23
|
+
import { executeNativeHistory } from './native-history-executor.js';
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import type { NativeHistoryConfig, Control, ControlAction } from './types.js';
|
|
26
|
+
import type { CliAdapter, CliAdapterStatus } from '../../cli-adapter-types.js';
|
|
27
|
+
import type { ChatMessage } from '../../types.js';
|
|
28
|
+
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
29
|
+
import { LOG } from '../../logging/logger.js';
|
|
30
|
+
import {
|
|
31
|
+
buildClaudeInteractiveTuiAnswerSteps,
|
|
32
|
+
buildClaudeInteractiveToolResult,
|
|
33
|
+
detectClaudeAskUserQuestionPromptFromJson,
|
|
34
|
+
detectClaudeAskUserQuestionPromptFromTuiPages,
|
|
35
|
+
detectClaudeTuiMultiSelect,
|
|
36
|
+
readFocusedClaudeTuiQuestion,
|
|
37
|
+
type ClaudeInteractiveTuiPage,
|
|
38
|
+
type InteractivePrompt,
|
|
39
|
+
type InteractivePromptResponse,
|
|
40
|
+
} from '../types/interactive-prompt.js';
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
function stripAnsi(text: string): string {
|
|
44
|
+
// eslint-disable-next-line no-control-regex
|
|
45
|
+
return String(text || '')
|
|
46
|
+
.replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
|
|
47
|
+
.replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
|
|
48
|
+
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function delay(ms: number): Promise<void> {
|
|
52
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class SpecCliAdapter implements CliAdapter {
|
|
56
|
+
readonly cliType: string;
|
|
57
|
+
readonly cliName: string;
|
|
58
|
+
readonly workingDir: string;
|
|
59
|
+
/**
|
|
60
|
+
* Marker the daemon's finalization gate checks: `getStatus()` returns
|
|
61
|
+
* `messages: []` by design here (chat history lives in the daemon's
|
|
62
|
+
* native-history pipeline, not the adapter). Without this flag,
|
|
63
|
+
* cli-provider-instance's `missing_final_assistant` gate would stall
|
|
64
|
+
* every turn until the 30s safety timeout because it expects the
|
|
65
|
+
* adapter to surface the final assistant message.
|
|
66
|
+
*/
|
|
67
|
+
readonly chatMessagesOwnedExternally = true as const;
|
|
68
|
+
|
|
69
|
+
private driver: ISpecDriver;
|
|
70
|
+
/** Common spec fields the adapter reads, present in both v3 and v4. */
|
|
71
|
+
private spec: {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
control_bar?: Control[];
|
|
75
|
+
native_history?: NativeHistoryConfig;
|
|
76
|
+
};
|
|
77
|
+
private lastEvent: DashboardEvent | null = null;
|
|
78
|
+
private latestState: { id: string; label: string; title: string | null; status: 'idle' | 'generating' | 'approval' } | null = null;
|
|
79
|
+
private latestModal: { title: string | null; buttons: { index: number; label: string }[]; kind?: 'approval' | 'picker' | 'confirm' | null } | null = null;
|
|
80
|
+
private statusCallback: (() => void) | null = null;
|
|
81
|
+
private ptyDataCallback: ((data: string) => void) | null = null;
|
|
82
|
+
private partialResponse = '';
|
|
83
|
+
private activeInteractivePrompt: InteractivePrompt | null = null;
|
|
84
|
+
private interactivePromptTransport: 'stream-json' | 'tui' | null = null;
|
|
85
|
+
private claudeTuiPromptCaptureInFlight = false;
|
|
86
|
+
/**
|
|
87
|
+
* Wall clock of the first frame on which a held interactive prompt was
|
|
88
|
+
* observed to have left the screen. Mirrors the approval FSM's
|
|
89
|
+
* `modalLostAt` hysteresis (see cli-state-engine.ts): claude-cli's TUI
|
|
90
|
+
* repaints the choice picker as several PTY chunks, so a single frame
|
|
91
|
+
* with no "Enter to select" footer is not proof the prompt is gone — it
|
|
92
|
+
* may just be mid-repaint. We only clear the held prompt once it has
|
|
93
|
+
* been absent across a short grace window. Reset to null the moment the
|
|
94
|
+
* prompt footer reappears.
|
|
95
|
+
*
|
|
96
|
+
* Without this, a choice prompt resolved *directly in the terminal* (the
|
|
97
|
+
* user picked an option without going through ADHDev's
|
|
98
|
+
* setInteractivePromptResponse) was never cleared from
|
|
99
|
+
* `activeInteractivePrompt`, so getStatus() re-emitted the same prompt
|
|
100
|
+
* forever — the choice-resolve-stuck bug.
|
|
101
|
+
*/
|
|
102
|
+
private interactivePromptLostAt: number | null = null;
|
|
103
|
+
private jsonLineTail = '';
|
|
104
|
+
private exited = false;
|
|
105
|
+
private spawned = false;
|
|
106
|
+
private providerSessionId: string | undefined;
|
|
107
|
+
/** Wall clock at the moment spawn() ran. Used as the cutoff for
|
|
108
|
+
* native-history file selection so a prior session's transcript
|
|
109
|
+
* can't leak into this session before the agent has written its
|
|
110
|
+
* own records. */
|
|
111
|
+
private spawnedAtMs = 0;
|
|
112
|
+
/** Env vars the daemon set on the spawned child. Mesh coordinator
|
|
113
|
+
* points hermes at a per-coordinator HERMES_HOME so the dashboard's
|
|
114
|
+
* native-history reader needs that override to find the right
|
|
115
|
+
* state.db; without it the reader sees ~/.hermes/state.db which
|
|
116
|
+
* the coordinator-launched hermes never writes to. The choice to
|
|
117
|
+
* redirect HERMES_HOME is a workaround for an unresolved hermes
|
|
118
|
+
* upstream feature gap (see hermes-agent#23130 — runtime-supplied
|
|
119
|
+
* MCP config), so this routing keeps the dashboard honest until
|
|
120
|
+
* hermes ships a runtime MCP override. */
|
|
121
|
+
private spawnedEnv: Record<string, string> = {};
|
|
122
|
+
|
|
123
|
+
constructor(
|
|
124
|
+
specPath: string,
|
|
125
|
+
workingDir: string,
|
|
126
|
+
cliArgs: string[],
|
|
127
|
+
extraEnv: Record<string, string>,
|
|
128
|
+
transportFactory?: PtyTransportFactory,
|
|
129
|
+
) {
|
|
130
|
+
const raw = JSON.parse(fs.readFileSync(specPath, 'utf8'));
|
|
131
|
+
this.spec = {
|
|
132
|
+
id: raw.id,
|
|
133
|
+
name: raw.name,
|
|
134
|
+
control_bar: raw.control_bar,
|
|
135
|
+
native_history: raw.native_history,
|
|
136
|
+
};
|
|
137
|
+
this.cliType = this.spec.id;
|
|
138
|
+
this.cliName = this.spec.name;
|
|
139
|
+
this.workingDir = workingDir;
|
|
140
|
+
this.spawnedEnv = { ...extraEnv };
|
|
141
|
+
|
|
142
|
+
// cli-manager.ts allocates providerSessionId per launch and threads
|
|
143
|
+
// it through resume.newSessionArgs as additional cliArgs (e.g.
|
|
144
|
+
// ["--session-id", "<uuid>"]). We must hand those to the driver
|
|
145
|
+
// so the agent uses the daemon's id, otherwise (claude case) the
|
|
146
|
+
// agent generates its own id and the chat-history pipeline can't
|
|
147
|
+
// pair the on-disk transcript with the live session.
|
|
148
|
+
this.driver = new FsmDriver({
|
|
149
|
+
specPath,
|
|
150
|
+
workingDir,
|
|
151
|
+
extraEnv,
|
|
152
|
+
hotReload: true,
|
|
153
|
+
emitTrace: false,
|
|
154
|
+
transportFactory,
|
|
155
|
+
extraCliArgs: cliArgs,
|
|
156
|
+
});
|
|
157
|
+
this.driver.subscribe((ev) => this.handleEvent(ev));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async spawn(): Promise<void> {
|
|
161
|
+
if (this.spawned) return;
|
|
162
|
+
this.driver.start();
|
|
163
|
+
this.spawned = true;
|
|
164
|
+
this.spawnedAtMs = Date.now();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async sendMessage(text: string): Promise<void> {
|
|
168
|
+
// Content-free at info — the prompt body is user data.
|
|
169
|
+
LOG.info('SpecAdapter', `[${this.cliType}] sendMessage(len=${text.length})`);
|
|
170
|
+
LOG.debug('SpecAdapter', `[${this.cliType}] sendMessage body=${JSON.stringify(text.slice(0, 80))}${text.length > 80 ? '…' : ''}`);
|
|
171
|
+
this.driver.dispatch({ kind: 'send_message', text });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getStatus(): CliAdapterStatus {
|
|
175
|
+
const sessionFields = this.providerSessionId ? { providerSessionId: this.providerSessionId } : {};
|
|
176
|
+
if (this.exited) return { status: 'stopped', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
177
|
+
if (!this.spawned) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
178
|
+
|
|
179
|
+
// Refresh native history lazily — the watch_path is cheap to stat,
|
|
180
|
+
// but parsing a full session.jsonl every call would be wasteful.
|
|
181
|
+
this.maybeRefreshNativeHistory();
|
|
182
|
+
|
|
183
|
+
const state = this.latestState;
|
|
184
|
+
if (!state) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
185
|
+
|
|
186
|
+
// The FSM state is authoritative for status. We do NOT infer status from whether
|
|
187
|
+
// a modal was parsed this frame: a modal/approval state whose buttons briefly fail
|
|
188
|
+
// to parse (PTY repaint) must still report waiting_approval, not collapse to idle —
|
|
189
|
+
// that collapse fired false completions while a session sat at an approval prompt.
|
|
190
|
+
const modal = this.latestModal;
|
|
191
|
+
if (state.status === 'approval') {
|
|
192
|
+
return {
|
|
193
|
+
status: 'waiting_approval',
|
|
194
|
+
messages: [],
|
|
195
|
+
// Surface buttons when we have them; an approval state with no parsed
|
|
196
|
+
// modal this frame still stays waiting_approval (no activeModal yet).
|
|
197
|
+
// `kind` carries the semantic modal class through to the auto-approve
|
|
198
|
+
// gate so a /model picker (kind='picker') is never auto-answered.
|
|
199
|
+
activeModal: modal
|
|
200
|
+
? { message: modal.title ?? state.label, buttons: modal.buttons.map(b => b.label), kind: modal.kind ?? null }
|
|
201
|
+
: null,
|
|
202
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
203
|
+
...sessionFields,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
if (state.status === 'generating') {
|
|
207
|
+
return { status: 'generating', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
|
|
208
|
+
}
|
|
209
|
+
// fsmReadySeen lets CliProviderInstance re-arm the queue-claim agent:ready
|
|
210
|
+
// on the first genuine ready (prompt drawn), independent of the boot-time
|
|
211
|
+
// starting→idle one-shot that the provider-instance otherwise relies on.
|
|
212
|
+
// Surfaced only on idle so the provider-instance fires agent:ready exactly
|
|
213
|
+
// when the worker is actually ready to claim.
|
|
214
|
+
return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, fsmReadySeen: this.driver.hasSeenReady?.() ?? false, ...sessionFields };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private maybeRefreshNativeHistory(): void {
|
|
218
|
+
// Native history is now sourced by daemon's chat-history pipeline
|
|
219
|
+
// (which calls provider.scripts.readNativeHistory wired by
|
|
220
|
+
// provider-loader). SpecCliAdapter no longer polls or caches.
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getScriptParsedStatus(): unknown {
|
|
224
|
+
const providerSessionId = this.extractProviderSessionIdFromScreen();
|
|
225
|
+
if (providerSessionId) this.providerSessionId = providerSessionId;
|
|
226
|
+
const status = this.getStatus();
|
|
227
|
+
return {
|
|
228
|
+
...status,
|
|
229
|
+
messages: this.readClaudeScreenAssistantMessages(),
|
|
230
|
+
...(this.providerSessionId ? { providerSessionId: this.providerSessionId } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getPartialResponse(): string {
|
|
235
|
+
return this.partialResponse;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
shutdown(): void {
|
|
239
|
+
try { this.driver.dispatch({ kind: 'shutdown' }); } catch { /* ignore */ }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
cancel(): void {
|
|
243
|
+
try { this.driver.dispatch({ kind: 'cancel' }); } catch { /* ignore */ }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
isProcessing(): boolean {
|
|
247
|
+
return this.getStatus().status === 'generating';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
isReady(): boolean {
|
|
251
|
+
return this.spawned && !this.exited;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setOnStatusChange(cb: () => void): void {
|
|
255
|
+
this.statusCallback = cb;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
setOnPtyData(cb: (data: string) => void): void {
|
|
259
|
+
this.ptyDataCallback = cb;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
writeRaw(data: string): void {
|
|
263
|
+
// Raw pty input — typed characters, escape codes, arrow keys —
|
|
264
|
+
// goes straight to the underlying terminal. send_message would
|
|
265
|
+
// append submit_key after every chunk, which is why typing in
|
|
266
|
+
// the dashboard terminal felt like "enter on every keystroke".
|
|
267
|
+
this.driver.dispatch({ kind: 'pty_write', data });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
resize(cols: number, rows: number): void {
|
|
271
|
+
this.driver.dispatch({ kind: 'resize', cols, rows });
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
resolveModal(buttonIndex: number): void {
|
|
275
|
+
// CliAdapter buttonIndex is 0-based; spec buttons are 1-based.
|
|
276
|
+
this.driver.dispatch({ kind: 'click_modal_button', index: buttonIndex + 1 });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async resolveAction(data: unknown): Promise<void> {
|
|
280
|
+
const args = (data && typeof data === 'object') ? (data as any) : {};
|
|
281
|
+
const explicitIndex = typeof args.buttonIndex === 'number' ? args.buttonIndex : -1;
|
|
282
|
+
if (explicitIndex >= 0) { this.resolveModal(explicitIndex); return; }
|
|
283
|
+
const action = typeof args.action === 'string' ? args.action : 'approve';
|
|
284
|
+
const buttons = this.latestModal?.buttons ?? [];
|
|
285
|
+
if (buttons.length === 0) return;
|
|
286
|
+
let target = -1;
|
|
287
|
+
if (action === 'reject' || action === 'deny') {
|
|
288
|
+
target = buttons.findIndex(b => /^(no|deny|reject|cancel)\b/i.test(b.label));
|
|
289
|
+
if (target < 0) target = buttons.length - 1;
|
|
290
|
+
} else {
|
|
291
|
+
target = buttons.findIndex(b => /^(yes|allow|approve|accept|continue|proceed|update)\b/i.test(b.label));
|
|
292
|
+
if (target < 0) target = 0;
|
|
293
|
+
}
|
|
294
|
+
this.resolveModal(target);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async setInteractivePromptResponse(response: InteractivePromptResponse): Promise<void> {
|
|
298
|
+
const prompt = this.activeInteractivePrompt;
|
|
299
|
+
if (!prompt || prompt.promptId !== response.promptId) throw new Error('Interactive prompt response does not match active prompt');
|
|
300
|
+
if (this.cliType !== 'claude-cli') return;
|
|
301
|
+
if (this.interactivePromptTransport === 'tui') {
|
|
302
|
+
const steps = buildClaudeInteractiveTuiAnswerSteps(prompt, response);
|
|
303
|
+
for (const step of steps) {
|
|
304
|
+
this.driver.dispatch({ kind: 'pty_write', data: step });
|
|
305
|
+
await new Promise(resolve => setTimeout(resolve, 180));
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
this.driver.dispatch({ kind: 'pty_write', data: `${buildClaudeInteractiveToolResult(response)}\n` });
|
|
309
|
+
}
|
|
310
|
+
this.activeInteractivePrompt = null;
|
|
311
|
+
this.interactivePromptTransport = null;
|
|
312
|
+
this.statusCallback?.();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
isApprovalRecentlyResolved(): boolean { return false; }
|
|
316
|
+
clearHistory(): void { /* no transcript buffer yet */ }
|
|
317
|
+
updateRuntimeSettings(): void { /* no runtime settings in spec model yet */ }
|
|
318
|
+
setServerConn(): void { /* server conn unused by SpecDriver */ }
|
|
319
|
+
/**
|
|
320
|
+
* Map an invokeScript(name, args) call onto a control_bar entry.
|
|
321
|
+
*
|
|
322
|
+
* scriptName is matched against control.id. The control's action.type
|
|
323
|
+
* drives the dispatch:
|
|
324
|
+
*
|
|
325
|
+
* send_keys → click_control (e.g. stop)
|
|
326
|
+
* open_picker → two roles, driven by the screen, not a hardcoded list:
|
|
327
|
+
* - LIST (no choice arg): open the picker, wait for it
|
|
328
|
+
* to render, parse the on-screen options via
|
|
329
|
+
* `extract_choices`, and return them as
|
|
330
|
+
* `controlResult.options` (+ `currentValue`). This is
|
|
331
|
+
* how the dashboard's Model/Mode controls learn what is
|
|
332
|
+
* actually selectable in this CLI right now.
|
|
333
|
+
* - SELECT (args.choiceIndex / args.choiceLabel): drive
|
|
334
|
+
* the picker to that option using `submit_key`.
|
|
335
|
+
* attach_image → attach_image dispatch; expects args.blob (data url
|
|
336
|
+
* or base64) and args.mime
|
|
337
|
+
*
|
|
338
|
+
* Callers that pass an unknown control id get a { not_found } response.
|
|
339
|
+
* No control matched, no driver call — keeps the surface honest.
|
|
340
|
+
*/
|
|
341
|
+
invokeScript(scriptName: string, args?: Record<string, unknown>): Promise<unknown> {
|
|
342
|
+
const controls = this.spec.control_bar ?? [];
|
|
343
|
+
const ctl = controls.find(c => c.id === scriptName);
|
|
344
|
+
if (!ctl) {
|
|
345
|
+
return Promise.resolve({ ok: false, error: `unknown control: ${scriptName}` });
|
|
346
|
+
}
|
|
347
|
+
// Args may arrive as either { blob, mime } (direct invocation) or
|
|
348
|
+
// { params: { blob, mime } } (when the dashboard wraps script args
|
|
349
|
+
// in a params bag). Look at both.
|
|
350
|
+
const flat: Record<string, unknown> = { ...(args || {}) };
|
|
351
|
+
if (args && typeof args.params === 'object' && args.params) {
|
|
352
|
+
Object.assign(flat, args.params as Record<string, unknown>);
|
|
353
|
+
}
|
|
354
|
+
const action = ctl.action;
|
|
355
|
+
if (action.type === 'attach_image') {
|
|
356
|
+
const blob = typeof flat.blob === 'string' ? flat.blob : '';
|
|
357
|
+
const mime = typeof flat.mime === 'string' ? flat.mime : 'image/png';
|
|
358
|
+
if (!blob) return Promise.resolve({ ok: false, error: 'attach_image requires args.blob (base64 or data URL)' });
|
|
359
|
+
this.driver.dispatch({ kind: 'attach_image', blob, mime });
|
|
360
|
+
return Promise.resolve({ ok: true, effects: [{ type: 'attached_image', controlId: ctl.id }] });
|
|
361
|
+
}
|
|
362
|
+
if (action.type === 'open_picker') {
|
|
363
|
+
const choiceIndex = typeof flat.choiceIndex === 'number' ? flat.choiceIndex
|
|
364
|
+
: typeof flat.choiceIndex === 'string' && flat.choiceIndex.trim() ? Number(flat.choiceIndex)
|
|
365
|
+
: undefined;
|
|
366
|
+
// `value` is the arg the dashboard's generic value-control set path
|
|
367
|
+
// sends ({ value: <chosen option> }). control_bar pickers are
|
|
368
|
+
// surfaced to the dashboard as dynamic `select` controls whose
|
|
369
|
+
// option values are the screen-parsed labels, so a bare `value`
|
|
370
|
+
// is just a label to match against the live choices.
|
|
371
|
+
const choiceLabel = typeof flat.choiceLabel === 'string' ? flat.choiceLabel
|
|
372
|
+
: typeof flat.choice === 'string' ? flat.choice
|
|
373
|
+
: typeof flat.value === 'string' ? flat.value
|
|
374
|
+
: undefined;
|
|
375
|
+
if ((typeof choiceIndex === 'number' && Number.isFinite(choiceIndex)) || (choiceLabel && choiceLabel.trim())) {
|
|
376
|
+
return this.selectPickerChoice(ctl, action, choiceIndex, choiceLabel);
|
|
377
|
+
}
|
|
378
|
+
return this.openPickerAndListChoices(ctl, action);
|
|
379
|
+
}
|
|
380
|
+
// send_keys routes through click_control.
|
|
381
|
+
this.driver.dispatch({ kind: 'click_control', control_id: ctl.id, payload: flat });
|
|
382
|
+
return Promise.resolve({ ok: true, effects: [{ type: 'sent_keys', controlId: ctl.id }] });
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Open an `open_picker` control and return the options the CLI is showing,
|
|
387
|
+
* parsed live from the screen via `extract_choices`. Nothing is selected —
|
|
388
|
+
* the picker is left open so a follow-up SELECT invoke can commit a choice.
|
|
389
|
+
*/
|
|
390
|
+
private async openPickerAndListChoices(
|
|
391
|
+
ctl: Control,
|
|
392
|
+
action: Extract<ControlAction, { type: 'open_picker' }>,
|
|
393
|
+
): Promise<unknown> {
|
|
394
|
+
this.driver.dispatch({ kind: 'click_control', control_id: ctl.id });
|
|
395
|
+
const ready = await this.waitForPickerRendered(action);
|
|
396
|
+
const options = this.extractPickerChoices(action);
|
|
397
|
+
const currentValue = options.find(o => o.current)?.label;
|
|
398
|
+
return {
|
|
399
|
+
ok: true,
|
|
400
|
+
effects: [{ type: 'opened_picker', controlId: ctl.id }],
|
|
401
|
+
controlResult: {
|
|
402
|
+
options: options.map(o => ({ value: o.label, label: o.label, current: o.current })),
|
|
403
|
+
...(currentValue ? { currentValue } : {}),
|
|
404
|
+
source: 'screen-parse',
|
|
405
|
+
...(ready ? {} : { warning: 'picker_render_timeout' }),
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Drive an already-listable picker to a specific option. The option can be
|
|
412
|
+
* named (choiceLabel — matched against the parsed on-screen labels) or
|
|
413
|
+
* positional (choiceIndex — the on-screen number). The actual keystrokes
|
|
414
|
+
* come from the spec's `submit_key` with `{index}` substituted, so the spec
|
|
415
|
+
* — not this code — decides how a selection is keyed for each CLI.
|
|
416
|
+
*/
|
|
417
|
+
private async selectPickerChoice(
|
|
418
|
+
ctl: Control,
|
|
419
|
+
action: Extract<ControlAction, { type: 'open_picker' }>,
|
|
420
|
+
choiceIndex: number | undefined,
|
|
421
|
+
choiceLabel: string | undefined,
|
|
422
|
+
): Promise<unknown> {
|
|
423
|
+
// Open + wait so the choice list is on screen before we resolve the
|
|
424
|
+
// label → index mapping. The picker is normally ALREADY open here (a
|
|
425
|
+
// preceding list invoke leaves it rendered), so only send the trigger
|
|
426
|
+
// when it is not on screen. Re-sending the trigger to an open picker is
|
|
427
|
+
// NOT a harmless no-op on claude-cli: the trailing CR of `/model\r`
|
|
428
|
+
// lands as Enter on the cursor's current row and commits the wrong
|
|
429
|
+
// model before we navigate. De-dup the open to avoid that.
|
|
430
|
+
let options = this.extractPickerChoicesIfRendered(action);
|
|
431
|
+
if (!options) {
|
|
432
|
+
this.driver.dispatch({ kind: 'click_control', control_id: ctl.id });
|
|
433
|
+
await this.waitForPickerRendered(action);
|
|
434
|
+
options = this.extractPickerChoices(action);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let index = choiceIndex;
|
|
438
|
+
if ((index == null || !Number.isFinite(index)) && choiceLabel) {
|
|
439
|
+
const needle = choiceLabel.trim().toLowerCase();
|
|
440
|
+
const match = options.find(o => o.label.toLowerCase().includes(needle));
|
|
441
|
+
if (!match) {
|
|
442
|
+
return { ok: false, error: `choice not found on screen: ${choiceLabel}`, controlResult: { options: options.map(o => ({ value: o.label, label: o.label })) } };
|
|
443
|
+
}
|
|
444
|
+
index = match.index;
|
|
445
|
+
}
|
|
446
|
+
if (index == null || !Number.isFinite(index)) {
|
|
447
|
+
return { ok: false, error: 'choiceIndex or choiceLabel required to select' };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (action.select_mode === 'arrow_keys') {
|
|
451
|
+
// Cursor-list picker (claude-cli /model): number keys are ignored.
|
|
452
|
+
// The cursor starts on the active row (extract flags it `current`);
|
|
453
|
+
// step it to the target row with arrows, then confirm.
|
|
454
|
+
const current = options.find(o => o.current);
|
|
455
|
+
if (current == null) {
|
|
456
|
+
// Without a known cursor position a blind Enter would commit
|
|
457
|
+
// whatever row the cursor sits on — fail loud instead.
|
|
458
|
+
return {
|
|
459
|
+
ok: false,
|
|
460
|
+
error: 'arrow-nav picker: current cursor row not detected on screen',
|
|
461
|
+
controlResult: { options: options.map(o => ({ value: o.label, label: o.label, current: o.current })) },
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const up = action.cursor_keys?.up ?? '[A';
|
|
465
|
+
const down = action.cursor_keys?.down ?? '[B';
|
|
466
|
+
const delta = index - current.index;
|
|
467
|
+
const step = delta >= 0 ? down : up;
|
|
468
|
+
const nav = step.repeat(Math.abs(delta));
|
|
469
|
+
// Confirm key = submit_key with the (unused) {index} placeholder
|
|
470
|
+
// stripped — e.g. `{index}\r` → `\r`.
|
|
471
|
+
const confirm = (action.submit_key || '\r').replace(/\{index\}/g, '') || '\r';
|
|
472
|
+
if (nav) this.driver.dispatch({ kind: 'pty_write', data: nav });
|
|
473
|
+
this.driver.dispatch({ kind: 'pty_write', data: confirm });
|
|
474
|
+
} else {
|
|
475
|
+
const keys = (action.submit_key || '{index}\r').replace(/\{index\}/g, String(index));
|
|
476
|
+
this.driver.dispatch({ kind: 'pty_write', data: keys });
|
|
477
|
+
}
|
|
478
|
+
const selected = options.find(o => o.index === index);
|
|
479
|
+
return {
|
|
480
|
+
ok: true,
|
|
481
|
+
effects: [{ type: 'selected_choice', controlId: ctl.id }],
|
|
482
|
+
controlResult: {
|
|
483
|
+
ok: true,
|
|
484
|
+
...(selected ? { currentValue: selected.label } : {}),
|
|
485
|
+
selectedIndex: index,
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** Parse the picker choices only if the picker already appears rendered on
|
|
491
|
+
* the live screen (its `wait_for` condition currently matches and at least
|
|
492
|
+
* one choice parses). Returns the parsed choices when open, else null so
|
|
493
|
+
* the caller knows it must send the trigger to open it. Used to de-dup the
|
|
494
|
+
* picker open in {@link selectPickerChoice}. */
|
|
495
|
+
private extractPickerChoicesIfRendered(
|
|
496
|
+
action: Extract<ControlAction, { type: 'open_picker' }>,
|
|
497
|
+
): Array<{ index: number; label: string; current: boolean }> | null {
|
|
498
|
+
const wf = action.wait_for;
|
|
499
|
+
if (wf?.regex) {
|
|
500
|
+
const re = new RegExp(wf.regex, wf.flags ?? 'i');
|
|
501
|
+
if (!re.test(this.readScreenSectionText(wf.section))) return null;
|
|
502
|
+
}
|
|
503
|
+
const options = this.extractPickerChoices(action);
|
|
504
|
+
return options.length > 0 ? options : null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Poll the live screen until the picker's `wait_for` condition matches,
|
|
508
|
+
* up to a short budget. Returns true if it rendered, false on timeout. */
|
|
509
|
+
private async waitForPickerRendered(action: Extract<ControlAction, { type: 'open_picker' }>): Promise<boolean> {
|
|
510
|
+
const wf = action.wait_for;
|
|
511
|
+
if (!wf?.regex) { await delay(250); return true; }
|
|
512
|
+
const re = new RegExp(wf.regex, wf.flags ?? 'i');
|
|
513
|
+
const deadline = Date.now() + 2500;
|
|
514
|
+
while (Date.now() < deadline) {
|
|
515
|
+
await delay(120);
|
|
516
|
+
const hay = this.readScreenSectionText(wf.section);
|
|
517
|
+
if (re.test(hay)) return true;
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/** Parse the picker's `extract_choices` pattern against the live screen.
|
|
523
|
+
* Each match yields { index, label, current }. `current` is true for the
|
|
524
|
+
* line the CLI marks with its cursor glyph (❯ ›). Purely screen-driven —
|
|
525
|
+
* no model/mode names are baked in. */
|
|
526
|
+
private extractPickerChoices(action: Extract<ControlAction, { type: 'open_picker' }>): Array<{ index: number; label: string; current: boolean }> {
|
|
527
|
+
const ec = action.extract_choices;
|
|
528
|
+
if (!ec?.pattern) return [];
|
|
529
|
+
const text = this.readScreenSectionText(ec.section);
|
|
530
|
+
// Collect EVERY matching line in screen order with no top-down de-dup.
|
|
531
|
+
// The picker section can include conversation history above it (a stray
|
|
532
|
+
// "1./2./3." list, blockquote `>` lines); a `seen.has(idx)` first-wins
|
|
533
|
+
// scan would let those body lines claim the option indices and shadow
|
|
534
|
+
// the real choices — committing the wrong model under arrow-key nav.
|
|
535
|
+
const all: Array<{ index: number; label: string; current: boolean }> = [];
|
|
536
|
+
for (const rawLine of text.split('\n')) {
|
|
537
|
+
const line = rawLine.replace(/\r$/, '');
|
|
538
|
+
const m = new RegExp(ec.pattern, ec.flags ?? '').exec(line);
|
|
539
|
+
if (!m) continue;
|
|
540
|
+
const idx = Number(m[1]);
|
|
541
|
+
if (!Number.isFinite(idx) || idx <= 0) continue;
|
|
542
|
+
const label = (m[2] ?? '').replace(/\s+/g, ' ').trim();
|
|
543
|
+
if (!label) continue;
|
|
544
|
+
const current = /^\s*[❯›>]/.test(line) || /[✔✓●]\s*$/.test(label);
|
|
545
|
+
all.push({ index: idx, label, current });
|
|
546
|
+
}
|
|
547
|
+
// Real options are the bottom-most contiguous numbered block; this also
|
|
548
|
+
// confines the `current` cursor flag to that block so a body `>` line is
|
|
549
|
+
// never mistaken for the cursor row.
|
|
550
|
+
return lastContiguousNumberedBlock(all);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** Live text of a named screen section (or the whole screen when no
|
|
554
|
+
* section is named), resolved from the driver's current sections. */
|
|
555
|
+
private readScreenSectionText(sectionId?: string): string {
|
|
556
|
+
try {
|
|
557
|
+
const sections = this.driver.getSections();
|
|
558
|
+
if (sectionId && sections) {
|
|
559
|
+
const hit = sections.find(s => s.id === sectionId);
|
|
560
|
+
if (hit) return hit.text;
|
|
561
|
+
}
|
|
562
|
+
return this.driver.getScreen();
|
|
563
|
+
} catch {
|
|
564
|
+
return '';
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
getDebugSnapshot(): unknown {
|
|
568
|
+
let screen = '';
|
|
569
|
+
let sections: Record<string, string> | undefined;
|
|
570
|
+
try {
|
|
571
|
+
screen = this.driver.snapshot();
|
|
572
|
+
const driverSections = this.driver.getSections?.();
|
|
573
|
+
if (driverSections) {
|
|
574
|
+
sections = Object.fromEntries(driverSections.map(s => [s.id, s.text]));
|
|
575
|
+
} else {
|
|
576
|
+
sections = this.readCurrentScreenSections(screen);
|
|
577
|
+
}
|
|
578
|
+
} catch { /* best-effort */ }
|
|
579
|
+
// Read native transcript messages for the debug snapshot
|
|
580
|
+
let messages: any[] = [];
|
|
581
|
+
if (this.spec.native_history?.source) {
|
|
582
|
+
try {
|
|
583
|
+
const nhResult = executeNativeHistory(this.spec.native_history, {
|
|
584
|
+
agentType: this.cliType,
|
|
585
|
+
providerSessionId: this.providerSessionId,
|
|
586
|
+
sessionStartedAtMs: this.spawnedAtMs,
|
|
587
|
+
envOverrides: this.spawnedEnv,
|
|
588
|
+
workspace: this.workingDir,
|
|
589
|
+
});
|
|
590
|
+
if (nhResult && Array.isArray(nhResult.messages)) messages = nhResult.messages;
|
|
591
|
+
} catch { /* best-effort */ }
|
|
592
|
+
} else {
|
|
593
|
+
messages = this.readClaudeScreenAssistantMessages();
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
cliType: this.cliType,
|
|
597
|
+
spec_id: this.spec.id,
|
|
598
|
+
current_state: this.latestState,
|
|
599
|
+
current_modal: this.latestModal,
|
|
600
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
601
|
+
exited: this.exited,
|
|
602
|
+
screen,
|
|
603
|
+
sections,
|
|
604
|
+
stateHistory: this.driver.getStateHistory(),
|
|
605
|
+
idleHoldPending: this.driver.hasIdleHoldPending(),
|
|
606
|
+
lastBusyAt: this.driver.getLastBusyAt(),
|
|
607
|
+
specPath: this.driver.getSpecPath(),
|
|
608
|
+
cursorPosition: this.driver.getCursorPosition(),
|
|
609
|
+
completionIdleDebounce: this.driver.getCompletionIdleDebounceState(),
|
|
610
|
+
// v4 FSM live transition table (null for v3 specs). Every outgoing
|
|
611
|
+
// transition from the current state with its per-condition match
|
|
612
|
+
// result + countdown — the canonical "why isn't it moving" answer.
|
|
613
|
+
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
614
|
+
// v4 FSM transition snapshot history (null for v3 specs). The full
|
|
615
|
+
// pre-transition evaluation table captured at each transition —
|
|
616
|
+
// answers "why did this rule fire" after the fact, unlike the live
|
|
617
|
+
// `fsm` field which only reflects the current instant.
|
|
618
|
+
fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
|
|
619
|
+
// PTY input/output/resize/cursor event timeline (debug-only) so the
|
|
620
|
+
// snapshot shows what we typed / what the PTY printed around each
|
|
621
|
+
// status transition. Null for drivers without the timeline.
|
|
622
|
+
eventTimeline: this.driver.getEventTimeline?.() ?? null,
|
|
623
|
+
// Extended fields
|
|
624
|
+
name: this.cliName,
|
|
625
|
+
status: this.getStatus().status,
|
|
626
|
+
workingDir: this.workingDir,
|
|
627
|
+
spawnedAtMs: this.spawnedAtMs,
|
|
628
|
+
providerSessionId: this.providerSessionId ?? null,
|
|
629
|
+
messages,
|
|
630
|
+
committedMessages: messages,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
getRuntimeMetadata(): unknown {
|
|
634
|
+
return {
|
|
635
|
+
runtimeId: this.spec.id,
|
|
636
|
+
runtimeKey: this.spec.id,
|
|
637
|
+
displayName: this.spec.name,
|
|
638
|
+
spawnedAtMs: this.spawnedAtMs,
|
|
639
|
+
spawnedEnv: this.spawnedEnv,
|
|
640
|
+
...(this.providerSessionId ? { providerSessionId: this.providerSessionId } : {}),
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
updateRuntimeMeta(meta?: Record<string, unknown>): void {
|
|
644
|
+
if (!meta) return;
|
|
645
|
+
if (typeof meta.providerSessionId === 'string') {
|
|
646
|
+
this.providerSessionId = meta.providerSessionId;
|
|
647
|
+
}
|
|
648
|
+
// Forward the FULL meta (meshNodeId / meshNodeFor / workspaceLabel /
|
|
649
|
+
// lifecycle / …) down to the transport so it reaches the session
|
|
650
|
+
// registry. The legacy ProviderCliAdapter.updateRuntimeMeta does the
|
|
651
|
+
// same via ptyProcess.updateMeta; the spec path previously dropped
|
|
652
|
+
// everything but providerSessionId, leaving autoLaunch's meshNodeId
|
|
653
|
+
// stamp unbound on the record — the root of SESSION-ACCUMULATION-LEAK.
|
|
654
|
+
try { this.driver.updateMeta(meta); } catch { /* transport may not support meta */ }
|
|
655
|
+
}
|
|
656
|
+
refreshProviderDefinition(): void { /* hot reload handled by SpecDriver fs.watch */ }
|
|
657
|
+
|
|
658
|
+
private handleEvent(ev: DashboardEvent): void {
|
|
659
|
+
this.lastEvent = ev;
|
|
660
|
+
switch (ev.kind) {
|
|
661
|
+
case 'state_changed':
|
|
662
|
+
this.latestState = ev.state;
|
|
663
|
+
this.latestModal = ev.modal;
|
|
664
|
+
// info-level keeps only spec-defined identifiers (state.id /
|
|
665
|
+
// state.label / button count). The extracted title can carry
|
|
666
|
+
// user data — file paths, command text, ticket titles — so
|
|
667
|
+
// it stays at debug.
|
|
668
|
+
LOG.info('SpecAdapter', `[${this.cliType}] state=${ev.state.id} (${ev.state.label}) modal=${ev.modal ? `${ev.modal.buttons.length}-buttons` : 'none'}`);
|
|
669
|
+
if (ev.state.title) {
|
|
670
|
+
LOG.debug('SpecAdapter', `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
|
|
671
|
+
}
|
|
672
|
+
this.maybeClearResolvedClaudeTuiPrompt();
|
|
673
|
+
this.maybeCaptureClaudeTuiPrompt();
|
|
674
|
+
this.maybeUpgradeClaudeTuiMultiSelect();
|
|
675
|
+
this.statusCallback?.();
|
|
676
|
+
return;
|
|
677
|
+
case 'pty_data':
|
|
678
|
+
this.detectInteractivePromptFromPtyChunk(ev.chunk);
|
|
679
|
+
this.maybeClearResolvedClaudeTuiPrompt();
|
|
680
|
+
this.maybeCaptureClaudeTuiPrompt();
|
|
681
|
+
this.maybeUpgradeClaudeTuiMultiSelect();
|
|
682
|
+
try { this.ptyDataCallback?.(ev.chunk); } catch { /* ignore */ }
|
|
683
|
+
return;
|
|
684
|
+
case 'exit':
|
|
685
|
+
this.exited = true;
|
|
686
|
+
this.statusCallback?.();
|
|
687
|
+
return;
|
|
688
|
+
case 'spec_error':
|
|
689
|
+
LOG.warn('SpecAdapter', `[${this.cliType}] spec reload error: ${ev.errors.join('; ')}`);
|
|
690
|
+
return;
|
|
691
|
+
default:
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
private detectInteractivePromptFromPtyChunk(chunk: string): void {
|
|
697
|
+
if (this.cliType !== 'claude-cli' || !chunk) return;
|
|
698
|
+
this.jsonLineTail += chunk;
|
|
699
|
+
if (this.jsonLineTail.length > 64 * 1024) this.jsonLineTail = this.jsonLineTail.slice(-64 * 1024);
|
|
700
|
+
const lines = this.jsonLineTail.split(/\r?\n/);
|
|
701
|
+
this.jsonLineTail = lines.pop() || '';
|
|
702
|
+
for (const line of lines) {
|
|
703
|
+
const trimmed = line.trim();
|
|
704
|
+
if (!trimmed.startsWith('{') || !trimmed.includes('AskUserQuestion')) continue;
|
|
705
|
+
try {
|
|
706
|
+
const parsed = JSON.parse(trimmed);
|
|
707
|
+
const prompt = detectClaudeAskUserQuestionPromptFromJson(parsed, this.cliType);
|
|
708
|
+
if (!prompt) continue;
|
|
709
|
+
this.activeInteractivePrompt = prompt;
|
|
710
|
+
this.interactivePromptTransport = 'stream-json';
|
|
711
|
+
this.interactivePromptLostAt = null;
|
|
712
|
+
this.statusCallback?.();
|
|
713
|
+
} catch {
|
|
714
|
+
// PTY output is not guaranteed to be machine JSON.
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
private readCurrentScreenSections(_screenText: string): Record<string, string> {
|
|
720
|
+
try {
|
|
721
|
+
const sections = this.driver.getSections() ?? [];
|
|
722
|
+
return Object.fromEntries(sections.map(section => [section.id, section.text]));
|
|
723
|
+
} catch {
|
|
724
|
+
return {};
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private extractProviderSessionIdFromScreen(): string | undefined {
|
|
729
|
+
if (this.cliType !== 'codex-cli') return this.providerSessionId;
|
|
730
|
+
let screenText = '';
|
|
731
|
+
try {
|
|
732
|
+
screenText = this.driver.snapshot();
|
|
733
|
+
} catch {
|
|
734
|
+
return this.providerSessionId;
|
|
735
|
+
}
|
|
736
|
+
const clean = stripAnsi(screenText);
|
|
737
|
+
const match = clean.match(/(?:gpt-|o\d|codex-)[^\n·]*·[^\n·]*·\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
738
|
+
return match?.[1] || this.providerSessionId;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
private readClaudeScreenAssistantMessages(): ChatMessage[] {
|
|
742
|
+
if (this.cliType !== 'claude-cli') return [];
|
|
743
|
+
let screenText = '';
|
|
744
|
+
try {
|
|
745
|
+
screenText = this.driver.snapshot();
|
|
746
|
+
} catch {
|
|
747
|
+
return [];
|
|
748
|
+
}
|
|
749
|
+
const sections = this.readCurrentScreenSections(screenText);
|
|
750
|
+
const body = sections.body || screenText;
|
|
751
|
+
const messages: ChatMessage[] = [];
|
|
752
|
+
const seen = new Set<string>();
|
|
753
|
+
for (const line of body.split(/\r?\n/)) {
|
|
754
|
+
const match = line.match(/^\s*⏺\s+(.+?)\s*$/);
|
|
755
|
+
const content = match?.[1]?.trim();
|
|
756
|
+
if (!content || seen.has(content)) continue;
|
|
757
|
+
seen.add(content);
|
|
758
|
+
messages.push({
|
|
759
|
+
role: 'assistant',
|
|
760
|
+
kind: 'standard',
|
|
761
|
+
content,
|
|
762
|
+
source: 'assistant_text',
|
|
763
|
+
userFacing: true,
|
|
764
|
+
bubbleState: 'final',
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
return messages;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Grace window a held interactive prompt must be absent from the screen
|
|
772
|
+
* before we treat it as resolved-in-terminal and clear it. claude-cli
|
|
773
|
+
* repaints the picker across multiple PTY chunks, so a single
|
|
774
|
+
* footer-less frame is not proof the prompt is gone. Sized in the same
|
|
775
|
+
* spirit as the approval FSM's `approvalCooldown` modal-lost hysteresis.
|
|
776
|
+
*/
|
|
777
|
+
private static readonly INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Multi-question TUI capture: after Tabbing to a page, re-snapshot at this
|
|
781
|
+
* interval until its checkbox glyph column has settled, up to the timeout.
|
|
782
|
+
* The interval matches the legacy single fixed wait (120ms); the timeout
|
|
783
|
+
* bounds total capture time so a single-select page (which never shows
|
|
784
|
+
* glyphs) doesn't stall the capture indefinitely.
|
|
785
|
+
*/
|
|
786
|
+
private static readonly CLAUDE_TUI_PAGE_POLL_INTERVAL_MS = 120;
|
|
787
|
+
private static readonly CLAUDE_TUI_PAGE_SETTLE_TIMEOUT_MS = 600;
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Clear a held interactive prompt once the user has resolved it directly
|
|
791
|
+
* in the terminal (the choice picker leaves the screen without going
|
|
792
|
+
* through setInteractivePromptResponse). The approval path already does
|
|
793
|
+
* this via the FSM's modal-lost hysteresis; the interactive-prompt path
|
|
794
|
+
* had no equivalent, so a terminal-side answer left activeInteractivePrompt
|
|
795
|
+
* set and getStatus() re-emitted the same choice modal forever.
|
|
796
|
+
*
|
|
797
|
+
* Detection mirrors capture: the claude TUI picker is on-screen exactly
|
|
798
|
+
* while its "Enter to select" footer is rendered. When the footer is gone
|
|
799
|
+
* for INTERACTIVE_PROMPT_LOST_GRACE_MS the prompt is genuinely resolved.
|
|
800
|
+
*/
|
|
801
|
+
private maybeClearResolvedClaudeTuiPrompt(): void {
|
|
802
|
+
if (this.cliType !== 'claude-cli' || !this.activeInteractivePrompt) return;
|
|
803
|
+
// stream-json prompts are tracked by their tool-call lifecycle, not by
|
|
804
|
+
// screen footer, but claude renders the same TUI picker for both
|
|
805
|
+
// transports while awaiting an answer — so screen presence is a valid
|
|
806
|
+
// resolved-signal for either. (If the screen read fails, keep holding.)
|
|
807
|
+
let screenText = '';
|
|
808
|
+
try {
|
|
809
|
+
screenText = this.driver.snapshot();
|
|
810
|
+
} catch {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const stillOnScreen = screenText.includes('Enter to select');
|
|
814
|
+
if (stillOnScreen) {
|
|
815
|
+
// Prompt reappeared / never left — reset the hysteresis timer.
|
|
816
|
+
this.interactivePromptLostAt = null;
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const lostAt = this.interactivePromptLostAt ?? Date.now();
|
|
820
|
+
if (this.interactivePromptLostAt === null) this.interactivePromptLostAt = lostAt;
|
|
821
|
+
if (Date.now() - lostAt < SpecCliAdapter.INTERACTIVE_PROMPT_LOST_GRACE_MS) return;
|
|
822
|
+
// Resolved in the terminal — drop the held prompt so getStatus() stops
|
|
823
|
+
// re-emitting it.
|
|
824
|
+
this.activeInteractivePrompt = null;
|
|
825
|
+
this.interactivePromptTransport = null;
|
|
826
|
+
this.interactivePromptLostAt = null;
|
|
827
|
+
this.statusCallback?.();
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
private maybeCaptureClaudeTuiPrompt(): void {
|
|
831
|
+
if (this.cliType !== 'claude-cli'
|
|
832
|
+
|| this.activeInteractivePrompt
|
|
833
|
+
|| this.claudeTuiPromptCaptureInFlight) return;
|
|
834
|
+
const screenText = this.driver.snapshot();
|
|
835
|
+
const headers = this.readClaudeTuiHeaders(screenText);
|
|
836
|
+
if (!screenText.includes('Enter to select')) return;
|
|
837
|
+
if (headers.length === 0) {
|
|
838
|
+
const prompt = detectClaudeAskUserQuestionPromptFromTuiPages([{ screenText }], {
|
|
839
|
+
promptId: `ask-user-${this.providerSessionId || 'claude'}-${Date.now()}`,
|
|
840
|
+
providerType: this.cliType,
|
|
841
|
+
});
|
|
842
|
+
if (!prompt) return;
|
|
843
|
+
this.activeInteractivePrompt = prompt;
|
|
844
|
+
this.interactivePromptTransport = 'tui';
|
|
845
|
+
this.interactivePromptLostAt = null;
|
|
846
|
+
this.statusCallback?.();
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
this.claudeTuiPromptCaptureInFlight = true;
|
|
850
|
+
void this.captureClaudeTuiPrompt(screenText, headers).finally(() => {
|
|
851
|
+
this.claudeTuiPromptCaptureInFlight = false;
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* The TUI prompt is captured on the FIRST frame that renders the
|
|
857
|
+
* "Enter to select" footer. At that instant the option rows' checkbox
|
|
858
|
+
* column may not have drawn yet, so `detectClaudeTuiMultiSelect` returns
|
|
859
|
+
* false and the prompt is frozen as single-select — the dashboard then
|
|
860
|
+
* renders radio buttons even though the picker is multi-select.
|
|
861
|
+
*
|
|
862
|
+
* While the same TUI prompt is still on screen, re-check the live snapshot:
|
|
863
|
+
* if checkbox glyphs have since appeared, promote any single-select
|
|
864
|
+
* question to multi-select and re-emit status. Promotion is one-way
|
|
865
|
+
* (false→true only) — once a question is known multi-select we never demote
|
|
866
|
+
* it, since the glyph column can scroll out of view on later frames.
|
|
867
|
+
*
|
|
868
|
+
* For MULTI-question prompts the per-page Tab capture is the actual source
|
|
869
|
+
* of the bug: pages 2..N are snapshotted ~120ms after the Tab keypress,
|
|
870
|
+
* before their option-row glyph column has redrawn, so those questions
|
|
871
|
+
* freeze as single-select while page 1 (already settled) is correct. We
|
|
872
|
+
* cannot upgrade blindly — the live snapshot shows only ONE focused page —
|
|
873
|
+
* but we CAN read that page's question text/header and upgrade the matching
|
|
874
|
+
* question. As the user navigates the picker (or it settles), each page is
|
|
875
|
+
* eventually re-read and repaired.
|
|
876
|
+
*/
|
|
877
|
+
private maybeUpgradeClaudeTuiMultiSelect(): void {
|
|
878
|
+
if (this.cliType !== 'claude-cli'
|
|
879
|
+
|| this.interactivePromptTransport !== 'tui'
|
|
880
|
+
|| !this.activeInteractivePrompt) return;
|
|
881
|
+
const questions = this.activeInteractivePrompt.questions;
|
|
882
|
+
if (questions.every(q => q.multiSelect)) return;
|
|
883
|
+
let screenText = '';
|
|
884
|
+
try {
|
|
885
|
+
screenText = this.driver.snapshot();
|
|
886
|
+
} catch {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (!screenText.includes('Enter to select')) return;
|
|
890
|
+
|
|
891
|
+
if (questions.length === 1) {
|
|
892
|
+
if (questions[0].multiSelect) return;
|
|
893
|
+
if (!detectClaudeTuiMultiSelect(screenText)) return;
|
|
894
|
+
questions[0].multiSelect = true;
|
|
895
|
+
this.statusCallback?.();
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Multi-question: attribute the focused page's glyphs to its question by
|
|
900
|
+
// matching header (preferred) or question text, then upgrade just that
|
|
901
|
+
// one. Never demote — a settled non-multi page is left as captured.
|
|
902
|
+
const focused = readFocusedClaudeTuiQuestion(screenText);
|
|
903
|
+
if (!focused || !focused.multiSelect) return;
|
|
904
|
+
const match = questions.find(q =>
|
|
905
|
+
(focused.header && q.header && q.header === focused.header)
|
|
906
|
+
|| q.question === focused.question);
|
|
907
|
+
if (!match || match.multiSelect) return;
|
|
908
|
+
match.multiSelect = true;
|
|
909
|
+
this.statusCallback?.();
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
private readClaudeTuiHeaders(screenText: string): string[] {
|
|
913
|
+
const navLine = screenText.split(/\r?\n/).find(line => line.includes('✔ Submit') && /[☐☒]/.test(line));
|
|
914
|
+
if (!navLine) return [];
|
|
915
|
+
const headers: string[] = [];
|
|
916
|
+
for (const match of navLine.matchAll(/[☐☒]\s+(.+?)(?=\s+[☐☒]|\s+✔\s+Submit)/g)) {
|
|
917
|
+
const header = match[1]?.trim();
|
|
918
|
+
if (header) headers.push(header);
|
|
919
|
+
}
|
|
920
|
+
return headers;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Snapshot the currently-focused claude TUI page, polling until its
|
|
925
|
+
* option-row checkbox glyph column has settled (or a bounded timeout).
|
|
926
|
+
*
|
|
927
|
+
* Why poll: right after a Tab keypress the newly-focused page's glyph
|
|
928
|
+
* column has not redrawn yet, so an immediate snapshot shows the question +
|
|
929
|
+
* option labels but NO per-option checkbox markers — freezing that page as
|
|
930
|
+
* single-select. A fixed delay either races (too short) or is wasteful (too
|
|
931
|
+
* long). Instead we re-snapshot at a fixed interval and stop as soon as the
|
|
932
|
+
* frame shows multi-select glyphs, falling back to the last frame at the
|
|
933
|
+
* timeout. Single-select pages never show glyphs, so they always poll to the
|
|
934
|
+
* timeout — bounded small to keep capture snappy.
|
|
935
|
+
*/
|
|
936
|
+
private async snapshotSettledClaudeTuiPage(): Promise<string> {
|
|
937
|
+
let screenText = this.driver.snapshot();
|
|
938
|
+
const deadline = Date.now() + SpecCliAdapter.CLAUDE_TUI_PAGE_SETTLE_TIMEOUT_MS;
|
|
939
|
+
while (!detectClaudeTuiMultiSelect(screenText) && Date.now() < deadline) {
|
|
940
|
+
await new Promise(resolve => setTimeout(resolve, SpecCliAdapter.CLAUDE_TUI_PAGE_POLL_INTERVAL_MS));
|
|
941
|
+
screenText = this.driver.snapshot();
|
|
942
|
+
}
|
|
943
|
+
return screenText;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
private async captureClaudeTuiPrompt(firstScreen: string, headers: string[]): Promise<void> {
|
|
947
|
+
const pages: ClaudeInteractiveTuiPage[] = [{ screenText: firstScreen, header: headers[0] }];
|
|
948
|
+
// Forward pass: Tab to each page 2..N and snapshot once its glyph column
|
|
949
|
+
// has settled, so pages 2+ capture their checkbox markers (not a racy
|
|
950
|
+
// pre-redraw frame).
|
|
951
|
+
for (let index = 1; index < headers.length; index += 1) {
|
|
952
|
+
this.driver.dispatch({ kind: 'pty_write', data: '\t' });
|
|
953
|
+
await new Promise(resolve => setTimeout(resolve, SpecCliAdapter.CLAUDE_TUI_PAGE_POLL_INTERVAL_MS));
|
|
954
|
+
pages.push({ screenText: await this.snapshotSettledClaudeTuiPage(), header: headers[index] });
|
|
955
|
+
}
|
|
956
|
+
// Return pass: Shift-Tab back through pages N..2. As we land on each page
|
|
957
|
+
// again re-read it and OR-in any now-visible multi-select glyphs — a
|
|
958
|
+
// second chance to repair a page whose forward-pass frame was still racy.
|
|
959
|
+
for (let index = headers.length - 1; index > 0; index -= 1) {
|
|
960
|
+
this.driver.dispatch({ kind: 'pty_write', data: '\x1b[Z' });
|
|
961
|
+
await new Promise(resolve => setTimeout(resolve, SpecCliAdapter.CLAUDE_TUI_PAGE_POLL_INTERVAL_MS));
|
|
962
|
+
const reread = await this.snapshotSettledClaudeTuiPage();
|
|
963
|
+
// The page we just Shift-Tab'd ONTO is index-1 (we move backwards).
|
|
964
|
+
const landed = pages[index - 1];
|
|
965
|
+
if (landed
|
|
966
|
+
&& !detectClaudeTuiMultiSelect(landed.screenText)
|
|
967
|
+
&& detectClaudeTuiMultiSelect(reread)) {
|
|
968
|
+
landed.screenText = reread;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const prompt = detectClaudeAskUserQuestionPromptFromTuiPages(pages, {
|
|
973
|
+
promptId: `ask-user-${this.providerSessionId || 'claude'}-${Date.now()}`,
|
|
974
|
+
providerType: this.cliType,
|
|
975
|
+
});
|
|
976
|
+
if (!prompt) return;
|
|
977
|
+
this.activeInteractivePrompt = prompt;
|
|
978
|
+
this.interactivePromptTransport = 'tui';
|
|
979
|
+
this.interactivePromptLostAt = null;
|
|
980
|
+
this.statusCallback?.();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
getDebugState(): Record<string, any> {
|
|
984
|
+
const screen = this.driver.getScreen?.() ?? '';
|
|
985
|
+
const history = this.driver.getStateHistory();
|
|
986
|
+
const status = this.getStatus();
|
|
987
|
+
|
|
988
|
+
let messages: any[] = [];
|
|
989
|
+
if (this.spec.native_history?.source) {
|
|
990
|
+
try {
|
|
991
|
+
const result = executeNativeHistory(this.spec.native_history, {
|
|
992
|
+
agentType: this.cliType,
|
|
993
|
+
providerSessionId: this.providerSessionId,
|
|
994
|
+
sessionStartedAtMs: this.spawnedAtMs,
|
|
995
|
+
envOverrides: this.spawnedEnv,
|
|
996
|
+
workspace: this.workingDir,
|
|
997
|
+
});
|
|
998
|
+
if (result && Array.isArray(result.messages)) {
|
|
999
|
+
messages = result.messages;
|
|
1000
|
+
}
|
|
1001
|
+
} catch (e) {
|
|
1002
|
+
// Ignore native history read errors in debug state
|
|
1003
|
+
}
|
|
1004
|
+
} else {
|
|
1005
|
+
messages = this.readClaudeScreenAssistantMessages();
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
const latestState = this.latestState;
|
|
1009
|
+
const latestModal = this.latestModal;
|
|
1010
|
+
return {
|
|
1011
|
+
type: this.cliType,
|
|
1012
|
+
name: this.cliName,
|
|
1013
|
+
status: status.status,
|
|
1014
|
+
rawStatus: status.status,
|
|
1015
|
+
projectedStatus: status.status,
|
|
1016
|
+
ready: this.spawned,
|
|
1017
|
+
// Legacy snapshot-style fields for panels that read getDebugSnapshot shape
|
|
1018
|
+
spec_id: this.spec.id,
|
|
1019
|
+
current_state: latestState ?? null,
|
|
1020
|
+
current_modal: latestModal ?? null,
|
|
1021
|
+
exited: this.exited,
|
|
1022
|
+
idleHoldPending: this.driver.hasIdleHoldPending?.() ?? false,
|
|
1023
|
+
lastBusyAt: this.driver.getLastBusyAt?.() ?? 0,
|
|
1024
|
+
screen: screen,
|
|
1025
|
+
screenText: screen,
|
|
1026
|
+
workingDir: this.workingDir,
|
|
1027
|
+
spawnedAtMs: this.spawnedAtMs,
|
|
1028
|
+
providerSessionId: this.providerSessionId ?? null,
|
|
1029
|
+
sections: this.driver.getSections?.() ?? null,
|
|
1030
|
+
stateHistory: history,
|
|
1031
|
+
specPath: this.driver.getSpecPath?.() ?? null,
|
|
1032
|
+
// v4 FSM live transition table — present only for FsmDriver. Lets
|
|
1033
|
+
// the panel (and the daemon API) show, for the current instant,
|
|
1034
|
+
// every outgoing transition with its per-condition match result
|
|
1035
|
+
// and countdown. This is the canonical "why isn't it transitioning"
|
|
1036
|
+
// answer — no screenshots needed.
|
|
1037
|
+
fsm: this.driver.getFsmDebug?.() ?? null,
|
|
1038
|
+
// v4 FSM transition snapshot history — the captured pre-transition
|
|
1039
|
+
// evaluation table at each transition (null for v3 specs).
|
|
1040
|
+
fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
|
|
1041
|
+
// PTY input/output/resize/cursor event timeline (debug-only).
|
|
1042
|
+
eventTimeline: this.driver.getEventTimeline?.() ?? null,
|
|
1043
|
+
messages,
|
|
1044
|
+
committedMessages: messages,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
getTraceState(limit = 120): Record<string, any> {
|
|
1049
|
+
const history = this.driver.getStateHistory();
|
|
1050
|
+
return {
|
|
1051
|
+
status: this.getStatus().status,
|
|
1052
|
+
stateHistory: history.slice(-limit),
|
|
1053
|
+
screenText: this.driver.getScreen?.() ?? '',
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
getProviderResolutionMeta(): Record<string, any> {
|
|
1058
|
+
return {
|
|
1059
|
+
type: this.cliType,
|
|
1060
|
+
providerDir: null,
|
|
1061
|
+
resolvedVersion: null,
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
}
|