@adhdev/daemon-core 0.9.82-rc.29 → 0.9.82-rc.290

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.
Files changed (281) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +8 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/build-info.d.ts +37 -0
  4. package/dist/chat/source-machine.d.ts +166 -0
  5. package/dist/chat/source-resolver.d.ts +104 -0
  6. package/dist/chat/subscription-updates.d.ts +1 -0
  7. package/dist/cli-adapter-types.d.ts +23 -2
  8. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  9. package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
  10. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  11. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  12. package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
  13. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  14. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
  15. package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
  16. package/dist/cli-adapters/terminal-screen.d.ts +4 -11
  17. package/dist/commands/cli-manager.d.ts +2 -1
  18. package/dist/commands/handler.d.ts +110 -0
  19. package/dist/commands/mesh-coordinator.d.ts +85 -1
  20. package/dist/commands/router.d.ts +160 -1
  21. package/dist/config/chat-history.d.ts +9 -0
  22. package/dist/config/config.d.ts +5 -0
  23. package/dist/config/mesh-config.d.ts +72 -1
  24. package/dist/git/git-commands.d.ts +9 -1
  25. package/dist/git/git-diff.d.ts +6 -0
  26. package/dist/git/git-status.d.ts +7 -0
  27. package/dist/git/git-types.d.ts +2 -50
  28. package/dist/index.d.ts +51 -12
  29. package/dist/index.js +34510 -12948
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.mjs +38172 -16711
  32. package/dist/index.mjs.map +1 -1
  33. package/dist/installer.d.ts +1 -4
  34. package/dist/ipc/local-ipc-server.d.ts +91 -0
  35. package/dist/launch.d.ts +1 -1
  36. package/dist/logging/async-batch-writer.d.ts +10 -0
  37. package/dist/mesh/contracts.d.ts +164 -0
  38. package/dist/mesh/coordinator-prompt.d.ts +36 -0
  39. package/dist/mesh/coordinator-registry.d.ts +59 -0
  40. package/dist/mesh/mesh-active-work.d.ts +108 -0
  41. package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
  42. package/dist/mesh/mesh-events-coordinator.d.ts +133 -0
  43. package/dist/mesh/mesh-events-pending.d.ts +45 -0
  44. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  45. package/dist/mesh/mesh-events-utils.d.ts +47 -0
  46. package/dist/mesh/mesh-events.d.ts +6 -49
  47. package/dist/mesh/mesh-fast-forward.d.ts +81 -0
  48. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  49. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  50. package/dist/mesh/mesh-ledger.d.ts +77 -1
  51. package/dist/mesh/mesh-missions.d.ts +68 -0
  52. package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
  53. package/dist/mesh/mesh-refine-batch.d.ts +68 -0
  54. package/dist/mesh/mesh-refine-status.d.ts +62 -0
  55. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  56. package/dist/mesh/mesh-routing.d.ts +70 -0
  57. package/dist/mesh/mesh-runtime-store.d.ts +345 -0
  58. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  59. package/dist/mesh/mesh-work-queue.d.ts +166 -5
  60. package/dist/mesh/preview-freshness.d.ts +18 -0
  61. package/dist/mesh/refine-config.d.ts +215 -0
  62. package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
  63. package/dist/providers/acp-provider-instance.d.ts +2 -0
  64. package/dist/providers/approval-utils.d.ts +13 -0
  65. package/dist/providers/cli-provider-instance.d.ts +61 -3
  66. package/dist/providers/contracts.d.ts +130 -6
  67. package/dist/providers/external-sources.d.ts +71 -0
  68. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  69. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  70. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  71. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  72. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  73. package/dist/providers/native-history/index.d.ts +13 -0
  74. package/dist/providers/provider-instance-manager.d.ts +13 -0
  75. package/dist/providers/provider-instance.d.ts +13 -1
  76. package/dist/providers/provider-loader.d.ts +26 -4
  77. package/dist/providers/provider-trust.d.ts +31 -0
  78. package/dist/providers/read-chat-contract.d.ts +29 -0
  79. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  80. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  81. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  82. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  83. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  84. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  85. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  86. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  87. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  88. package/dist/providers/sdk/v1/index.d.ts +30 -0
  89. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  90. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  91. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  92. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  93. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  94. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  95. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  96. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  97. package/dist/providers/spec/adapter.d.ts +59 -0
  98. package/dist/providers/spec/cli-adapter.d.ts +140 -0
  99. package/dist/providers/spec/evaluator.d.ts +23 -0
  100. package/dist/providers/spec/fsm-driver.d.ts +278 -0
  101. package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
  102. package/dist/providers/spec/fsm-loader.d.ts +14 -0
  103. package/dist/providers/spec/fsm-types.d.ts +128 -0
  104. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  105. package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
  106. package/dist/providers/spec/route.d.ts +4 -0
  107. package/dist/providers/spec/types.d.ts +144 -0
  108. package/dist/providers/transcript-v2.d.ts +176 -0
  109. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  110. package/dist/repo-mesh-types.d.ts +148 -13
  111. package/dist/sessions/registry.d.ts +6 -0
  112. package/dist/shared-types-extra.d.ts +2 -4
  113. package/dist/shared-types.d.ts +42 -1
  114. package/dist/status/normalize.d.ts +1 -1
  115. package/dist/status/normalize.js +1 -0
  116. package/dist/status/normalize.js.map +1 -1
  117. package/dist/status/normalize.mjs +1 -0
  118. package/dist/status/normalize.mjs.map +1 -1
  119. package/dist/status/reporter.d.ts +2 -0
  120. package/dist/status/snapshot.d.ts +1 -0
  121. package/dist/types.d.ts +5 -0
  122. package/package.json +7 -3
  123. package/src/agent-stream/poller.ts +2 -3
  124. package/src/boot/daemon-lifecycle.ts +55 -12
  125. package/src/boot/process-hardening.ts +89 -0
  126. package/src/build-info.ts +73 -0
  127. package/src/chat/source-machine.ts +534 -0
  128. package/src/chat/source-resolver.ts +0 -0
  129. package/src/chat/subscription-updates.ts +20 -1
  130. package/src/cli-adapter-types.d.ts +1 -0
  131. package/src/cli-adapter-types.ts +22 -2
  132. package/src/cli-adapters/cli-script-runner.ts +421 -0
  133. package/src/cli-adapters/cli-state-engine.ts +1215 -0
  134. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  135. package/src/cli-adapters/provider-cli-adapter.ts +674 -1139
  136. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  137. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  138. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  139. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  140. package/src/cli-adapters/provider-cli-shared.ts +68 -11
  141. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  142. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
  143. package/src/cli-adapters/terminal-backends/types.ts +10 -3
  144. package/src/cli-adapters/terminal-screen.ts +16 -81
  145. package/src/commands/chat-commands.ts +1813 -60
  146. package/src/commands/cli-manager.ts +286 -14
  147. package/src/commands/handler.ts +809 -2
  148. package/src/commands/mesh-coordinator.ts +332 -122
  149. package/src/commands/router.ts +5721 -546
  150. package/src/config/chat-history.ts +229 -24
  151. package/src/config/config.ts +12 -0
  152. package/src/config/mesh-config.ts +303 -3
  153. package/src/config/recent-activity.ts +8 -2
  154. package/src/daemon/dev-auto-implement.ts +3 -2
  155. package/src/daemon/dev-cli-debug.ts +10 -1
  156. package/src/detection/ide-detector.ts +26 -16
  157. package/src/git/git-commands.ts +42 -12
  158. package/src/git/git-diff.ts +53 -0
  159. package/src/git/git-status.ts +198 -7
  160. package/src/git/git-types.ts +14 -62
  161. package/src/git/git-worktree.ts +8 -1
  162. package/src/index.ts +131 -11
  163. package/src/installer.d.ts +1 -1
  164. package/src/installer.ts +8 -6
  165. package/src/ipc/local-ipc-server.ts +278 -0
  166. package/src/launch.d.ts +1 -1
  167. package/src/launch.ts +37 -28
  168. package/src/logging/async-batch-writer.ts +55 -0
  169. package/src/logging/logger.ts +2 -1
  170. package/src/mesh/contracts.ts +329 -0
  171. package/src/mesh/coordinator-prompt.ts +219 -31
  172. package/src/mesh/coordinator-registry.ts +121 -0
  173. package/src/mesh/mesh-active-work.ts +468 -0
  174. package/src/mesh/mesh-delivery-policy.ts +315 -0
  175. package/src/mesh/mesh-events-coordinator.ts +1679 -0
  176. package/src/mesh/mesh-events-pending.ts +508 -0
  177. package/src/mesh/mesh-events-stale.ts +283 -0
  178. package/src/mesh/mesh-events-utils.ts +254 -0
  179. package/src/mesh/mesh-events.ts +30 -1035
  180. package/src/mesh/mesh-fast-forward.ts +839 -0
  181. package/src/mesh/mesh-host-ownership.ts +73 -0
  182. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  183. package/src/mesh/mesh-ledger.ts +589 -102
  184. package/src/mesh/mesh-missions.ts +172 -0
  185. package/src/mesh/mesh-reconcile-loop.ts +411 -0
  186. package/src/mesh/mesh-refine-batch.ts +197 -0
  187. package/src/mesh/mesh-refine-status.ts +231 -0
  188. package/src/mesh/mesh-review-inbox.ts +307 -0
  189. package/src/mesh/mesh-routing.ts +275 -0
  190. package/src/mesh/mesh-runtime-store.ts +1399 -0
  191. package/src/mesh/mesh-task-stats.ts +154 -0
  192. package/src/mesh/mesh-work-queue.ts +586 -145
  193. package/src/mesh/preview-freshness.ts +118 -0
  194. package/src/mesh/refine-config.ts +403 -0
  195. package/src/mesh/worktree-bootstrap-config.ts +326 -0
  196. package/src/providers/acp-provider-instance.ts +15 -1
  197. package/src/providers/approval-utils.d.ts +4 -0
  198. package/src/providers/approval-utils.ts +47 -5
  199. package/src/providers/chat-message-normalization.ts +4 -11
  200. package/src/providers/cli-provider-instance.ts +920 -81
  201. package/src/providers/contracts.d.ts +55 -0
  202. package/src/providers/contracts.ts +141 -6
  203. package/src/providers/external-sources.ts +218 -0
  204. package/src/providers/ide-provider-instance.ts +19 -5
  205. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  206. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  207. package/src/providers/native-history/codex-cli-transcript.ts +479 -0
  208. package/src/providers/native-history/dispatcher.ts +340 -0
  209. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  210. package/src/providers/native-history/index.ts +30 -0
  211. package/src/providers/provider-instance-manager.ts +30 -0
  212. package/src/providers/provider-instance.ts +10 -1
  213. package/src/providers/provider-loader.ts +584 -50
  214. package/src/providers/provider-schema.ts +87 -14
  215. package/src/providers/provider-trust.ts +114 -0
  216. package/src/providers/read-chat-contract.ts +76 -16
  217. package/src/providers/sdk/README.md +49 -0
  218. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  219. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  220. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  221. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  222. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  223. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  224. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  225. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  226. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  227. package/src/providers/sdk/v1/index.ts +152 -0
  228. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  229. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  230. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  231. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  232. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  233. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  234. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  235. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  236. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  237. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  238. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  239. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  240. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  241. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  242. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  243. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  244. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  245. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  246. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  247. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  248. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  249. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  250. package/src/providers/sdk/v1/validators/index.ts +19 -0
  251. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  252. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  253. package/src/providers/spec/adapter.ts +155 -0
  254. package/src/providers/spec/cli-adapter.ts +730 -0
  255. package/src/providers/spec/evaluator.ts +345 -0
  256. package/src/providers/spec/fsm-driver.ts +875 -0
  257. package/src/providers/spec/fsm-evaluator.ts +255 -0
  258. package/src/providers/spec/fsm-loader.ts +112 -0
  259. package/src/providers/spec/fsm-types.ts +222 -0
  260. package/src/providers/spec/native-history-executor.ts +943 -0
  261. package/src/providers/spec/pre-launch-trust.ts +104 -0
  262. package/src/providers/spec/route.ts +51 -0
  263. package/src/providers/spec/types.ts +189 -0
  264. package/src/providers/transcript-v2.ts +567 -0
  265. package/src/providers/types/interactive-prompt.ts +488 -0
  266. package/src/providers/version-archive.ts +38 -20
  267. package/src/repo-mesh-types.ts +177 -13
  268. package/src/sessions/registry.ts +6 -0
  269. package/src/shared-types-extra.ts +2 -4
  270. package/src/shared-types.ts +45 -1
  271. package/src/status/builders.ts +26 -6
  272. package/src/status/normalize.ts +2 -0
  273. package/src/status/reporter.ts +15 -0
  274. package/src/status/snapshot.ts +84 -25
  275. package/src/system/host-memory.ts +29 -12
  276. package/src/types.ts +5 -0
  277. package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
  278. package/dist/mesh/mesh-sync.d.ts +0 -53
  279. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
  280. package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
  281. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,730 @@
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 { executeNativeHistory } from './native-history-executor.js';
23
+ import * as fs from 'node:fs';
24
+ import type { NativeHistoryConfig, Control } from './types.js';
25
+ import type { CliAdapter, CliAdapterStatus } from '../../cli-adapter-types.js';
26
+ import type { ChatMessage } from '../../types.js';
27
+ import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
28
+ import { LOG } from '../../logging/logger.js';
29
+ import {
30
+ buildClaudeInteractiveTuiAnswerSteps,
31
+ buildClaudeInteractiveToolResult,
32
+ detectClaudeAskUserQuestionPromptFromJson,
33
+ detectClaudeAskUserQuestionPromptFromTuiPages,
34
+ type ClaudeInteractiveTuiPage,
35
+ type InteractivePrompt,
36
+ type InteractivePromptResponse,
37
+ } from '../types/interactive-prompt.js';
38
+
39
+
40
+ function stripAnsi(text: string): string {
41
+ // eslint-disable-next-line no-control-regex
42
+ return String(text || '')
43
+ .replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
44
+ .replace(/\x1B[P^_X][\s\S]*?(?:\x07|\x1B\\)/g, '')
45
+ .replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
46
+ }
47
+
48
+ export class SpecCliAdapter implements CliAdapter {
49
+ readonly cliType: string;
50
+ readonly cliName: string;
51
+ readonly workingDir: string;
52
+ /**
53
+ * Marker the daemon's finalization gate checks: `getStatus()` returns
54
+ * `messages: []` by design here (chat history lives in the daemon's
55
+ * native-history pipeline, not the adapter). Without this flag,
56
+ * cli-provider-instance's `missing_final_assistant` gate would stall
57
+ * every turn until the 30s safety timeout because it expects the
58
+ * adapter to surface the final assistant message.
59
+ */
60
+ readonly chatMessagesOwnedExternally = true as const;
61
+
62
+ private driver: ISpecDriver;
63
+ /** Common spec fields the adapter reads, present in both v3 and v4. */
64
+ private spec: {
65
+ id: string;
66
+ name: string;
67
+ control_bar?: Control[];
68
+ native_history?: NativeHistoryConfig;
69
+ };
70
+ private lastEvent: DashboardEvent | null = null;
71
+ private latestState: { id: string; label: string; title: string | null; status: 'idle' | 'generating' | 'approval' } | null = null;
72
+ private latestModal: { title: string | null; buttons: { index: number; label: string }[] } | null = null;
73
+ private statusCallback: (() => void) | null = null;
74
+ private ptyDataCallback: ((data: string) => void) | null = null;
75
+ private partialResponse = '';
76
+ private activeInteractivePrompt: InteractivePrompt | null = null;
77
+ private interactivePromptTransport: 'stream-json' | 'tui' | null = null;
78
+ private claudeTuiPromptCaptureInFlight = false;
79
+ /**
80
+ * Wall clock of the first frame on which a held interactive prompt was
81
+ * observed to have left the screen. Mirrors the approval FSM's
82
+ * `modalLostAt` hysteresis (see cli-state-engine.ts): claude-cli's TUI
83
+ * repaints the choice picker as several PTY chunks, so a single frame
84
+ * with no "Enter to select" footer is not proof the prompt is gone — it
85
+ * may just be mid-repaint. We only clear the held prompt once it has
86
+ * been absent across a short grace window. Reset to null the moment the
87
+ * prompt footer reappears.
88
+ *
89
+ * Without this, a choice prompt resolved *directly in the terminal* (the
90
+ * user picked an option without going through ADHDev's
91
+ * setInteractivePromptResponse) was never cleared from
92
+ * `activeInteractivePrompt`, so getStatus() re-emitted the same prompt
93
+ * forever — the choice-resolve-stuck bug.
94
+ */
95
+ private interactivePromptLostAt: number | null = null;
96
+ private jsonLineTail = '';
97
+ private exited = false;
98
+ private spawned = false;
99
+ private providerSessionId: string | undefined;
100
+ /** Wall clock at the moment spawn() ran. Used as the cutoff for
101
+ * native-history file selection so a prior session's transcript
102
+ * can't leak into this session before the agent has written its
103
+ * own records. */
104
+ private spawnedAtMs = 0;
105
+ /** Env vars the daemon set on the spawned child. Mesh coordinator
106
+ * points hermes at a per-coordinator HERMES_HOME so the dashboard's
107
+ * native-history reader needs that override to find the right
108
+ * state.db; without it the reader sees ~/.hermes/state.db which
109
+ * the coordinator-launched hermes never writes to. The choice to
110
+ * redirect HERMES_HOME is a workaround for an unresolved hermes
111
+ * upstream feature gap (see hermes-agent#23130 — runtime-supplied
112
+ * MCP config), so this routing keeps the dashboard honest until
113
+ * hermes ships a runtime MCP override. */
114
+ private spawnedEnv: Record<string, string> = {};
115
+
116
+ constructor(
117
+ specPath: string,
118
+ workingDir: string,
119
+ cliArgs: string[],
120
+ extraEnv: Record<string, string>,
121
+ transportFactory?: PtyTransportFactory,
122
+ ) {
123
+ const raw = JSON.parse(fs.readFileSync(specPath, 'utf8'));
124
+ this.spec = {
125
+ id: raw.id,
126
+ name: raw.name,
127
+ control_bar: raw.control_bar,
128
+ native_history: raw.native_history,
129
+ };
130
+ this.cliType = this.spec.id;
131
+ this.cliName = this.spec.name;
132
+ this.workingDir = workingDir;
133
+ this.spawnedEnv = { ...extraEnv };
134
+
135
+ // cli-manager.ts allocates providerSessionId per launch and threads
136
+ // it through resume.newSessionArgs as additional cliArgs (e.g.
137
+ // ["--session-id", "<uuid>"]). We must hand those to the driver
138
+ // so the agent uses the daemon's id, otherwise (claude case) the
139
+ // agent generates its own id and the chat-history pipeline can't
140
+ // pair the on-disk transcript with the live session.
141
+ this.driver = new FsmDriver({
142
+ specPath,
143
+ workingDir,
144
+ extraEnv,
145
+ hotReload: true,
146
+ emitTrace: false,
147
+ transportFactory,
148
+ extraCliArgs: cliArgs,
149
+ });
150
+ this.driver.subscribe((ev) => this.handleEvent(ev));
151
+ }
152
+
153
+ async spawn(): Promise<void> {
154
+ if (this.spawned) return;
155
+ this.driver.start();
156
+ this.spawned = true;
157
+ this.spawnedAtMs = Date.now();
158
+ }
159
+
160
+ async sendMessage(text: string): Promise<void> {
161
+ // Content-free at info — the prompt body is user data.
162
+ LOG.info('SpecAdapter', `[${this.cliType}] sendMessage(len=${text.length})`);
163
+ LOG.debug('SpecAdapter', `[${this.cliType}] sendMessage body=${JSON.stringify(text.slice(0, 80))}${text.length > 80 ? '…' : ''}`);
164
+ this.driver.dispatch({ kind: 'send_message', text });
165
+ }
166
+
167
+ getStatus(): CliAdapterStatus {
168
+ const sessionFields = this.providerSessionId ? { providerSessionId: this.providerSessionId } : {};
169
+ if (this.exited) return { status: 'stopped', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
170
+ if (!this.spawned) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
171
+
172
+ // Refresh native history lazily — the watch_path is cheap to stat,
173
+ // but parsing a full session.jsonl every call would be wasteful.
174
+ this.maybeRefreshNativeHistory();
175
+
176
+ const state = this.latestState;
177
+ if (!state) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
178
+
179
+ // The FSM state is authoritative for status. We do NOT infer status from whether
180
+ // a modal was parsed this frame: a modal/approval state whose buttons briefly fail
181
+ // to parse (PTY repaint) must still report waiting_approval, not collapse to idle —
182
+ // that collapse fired false completions while a session sat at an approval prompt.
183
+ const modal = this.latestModal;
184
+ if (state.status === 'approval') {
185
+ return {
186
+ status: 'waiting_approval',
187
+ messages: [],
188
+ // Surface buttons when we have them; an approval state with no parsed
189
+ // modal this frame still stays waiting_approval (no activeModal yet).
190
+ activeModal: modal
191
+ ? { message: modal.title ?? state.label, buttons: modal.buttons.map(b => b.label) }
192
+ : null,
193
+ activeInteractivePrompt: this.activeInteractivePrompt,
194
+ ...sessionFields,
195
+ };
196
+ }
197
+ if (state.status === 'generating') {
198
+ return { status: 'generating', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
199
+ }
200
+ return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt, ...sessionFields };
201
+ }
202
+
203
+ private maybeRefreshNativeHistory(): void {
204
+ // Native history is now sourced by daemon's chat-history pipeline
205
+ // (which calls provider.scripts.readNativeHistory wired by
206
+ // provider-loader). SpecCliAdapter no longer polls or caches.
207
+ }
208
+
209
+ getScriptParsedStatus(): unknown {
210
+ const providerSessionId = this.extractProviderSessionIdFromScreen();
211
+ if (providerSessionId) this.providerSessionId = providerSessionId;
212
+ const status = this.getStatus();
213
+ return {
214
+ ...status,
215
+ messages: this.readClaudeScreenAssistantMessages(),
216
+ ...(this.providerSessionId ? { providerSessionId: this.providerSessionId } : {}),
217
+ };
218
+ }
219
+
220
+ getPartialResponse(): string {
221
+ return this.partialResponse;
222
+ }
223
+
224
+ shutdown(): void {
225
+ try { this.driver.dispatch({ kind: 'shutdown' }); } catch { /* ignore */ }
226
+ }
227
+
228
+ cancel(): void {
229
+ try { this.driver.dispatch({ kind: 'cancel' }); } catch { /* ignore */ }
230
+ }
231
+
232
+ isProcessing(): boolean {
233
+ return this.getStatus().status === 'generating';
234
+ }
235
+
236
+ isReady(): boolean {
237
+ return this.spawned && !this.exited;
238
+ }
239
+
240
+ setOnStatusChange(cb: () => void): void {
241
+ this.statusCallback = cb;
242
+ }
243
+
244
+ setOnPtyData(cb: (data: string) => void): void {
245
+ this.ptyDataCallback = cb;
246
+ }
247
+
248
+ writeRaw(data: string): void {
249
+ // Raw pty input — typed characters, escape codes, arrow keys —
250
+ // goes straight to the underlying terminal. send_message would
251
+ // append submit_key after every chunk, which is why typing in
252
+ // the dashboard terminal felt like "enter on every keystroke".
253
+ this.driver.dispatch({ kind: 'pty_write', data });
254
+ }
255
+
256
+ resize(cols: number, rows: number): void {
257
+ this.driver.dispatch({ kind: 'resize', cols, rows });
258
+ }
259
+
260
+ resolveModal(buttonIndex: number): void {
261
+ // CliAdapter buttonIndex is 0-based; spec buttons are 1-based.
262
+ this.driver.dispatch({ kind: 'click_modal_button', index: buttonIndex + 1 });
263
+ }
264
+
265
+ async resolveAction(data: unknown): Promise<void> {
266
+ const args = (data && typeof data === 'object') ? (data as any) : {};
267
+ const explicitIndex = typeof args.buttonIndex === 'number' ? args.buttonIndex : -1;
268
+ if (explicitIndex >= 0) { this.resolveModal(explicitIndex); return; }
269
+ const action = typeof args.action === 'string' ? args.action : 'approve';
270
+ const buttons = this.latestModal?.buttons ?? [];
271
+ if (buttons.length === 0) return;
272
+ let target = -1;
273
+ if (action === 'reject' || action === 'deny') {
274
+ target = buttons.findIndex(b => /^(no|deny|reject|cancel)\b/i.test(b.label));
275
+ if (target < 0) target = buttons.length - 1;
276
+ } else {
277
+ target = buttons.findIndex(b => /^(yes|allow|approve|accept|continue|proceed|update)\b/i.test(b.label));
278
+ if (target < 0) target = 0;
279
+ }
280
+ this.resolveModal(target);
281
+ }
282
+
283
+ async setInteractivePromptResponse(response: InteractivePromptResponse): Promise<void> {
284
+ const prompt = this.activeInteractivePrompt;
285
+ if (!prompt || prompt.promptId !== response.promptId) throw new Error('Interactive prompt response does not match active prompt');
286
+ if (this.cliType !== 'claude-cli') return;
287
+ if (this.interactivePromptTransport === 'tui') {
288
+ const steps = buildClaudeInteractiveTuiAnswerSteps(prompt, response);
289
+ for (const step of steps) {
290
+ this.driver.dispatch({ kind: 'pty_write', data: step });
291
+ await new Promise(resolve => setTimeout(resolve, 180));
292
+ }
293
+ } else {
294
+ this.driver.dispatch({ kind: 'pty_write', data: `${buildClaudeInteractiveToolResult(response)}\n` });
295
+ }
296
+ this.activeInteractivePrompt = null;
297
+ this.interactivePromptTransport = null;
298
+ this.statusCallback?.();
299
+ }
300
+
301
+ isApprovalRecentlyResolved(): boolean { return false; }
302
+ clearHistory(): void { /* no transcript buffer yet */ }
303
+ updateRuntimeSettings(): void { /* no runtime settings in spec model yet */ }
304
+ setServerConn(): void { /* server conn unused by SpecDriver */ }
305
+ /**
306
+ * Map an invokeScript(name, args) call onto a control_bar entry.
307
+ *
308
+ * scriptName is matched against control.id. The control's action.type
309
+ * drives the dispatch:
310
+ *
311
+ * send_keys → click_control (e.g. stop)
312
+ * open_picker → click_control then resolve when extract_choices
313
+ * surface; choice index comes from args.choiceIndex
314
+ * or args.choice (string label match), defaulting to 0
315
+ * attach_image → attach_image dispatch; expects args.blob (data url
316
+ * or base64) and args.mime
317
+ *
318
+ * Callers that pass an unknown control id get a { not_found } response.
319
+ * No control matched, no driver call — keeps the surface honest.
320
+ */
321
+ invokeScript(scriptName: string, args?: Record<string, unknown>): Promise<unknown> {
322
+ const controls = this.spec.control_bar ?? [];
323
+ const ctl = controls.find(c => c.id === scriptName);
324
+ if (!ctl) {
325
+ return Promise.resolve({ ok: false, error: `unknown control: ${scriptName}` });
326
+ }
327
+ // Args may arrive as either { blob, mime } (direct invocation) or
328
+ // { params: { blob, mime } } (when the dashboard wraps script args
329
+ // in a params bag). Look at both.
330
+ const flat: Record<string, unknown> = { ...(args || {}) };
331
+ if (args && typeof args.params === 'object' && args.params) {
332
+ Object.assign(flat, args.params as Record<string, unknown>);
333
+ }
334
+ const action = ctl.action;
335
+ if (action.type === 'attach_image') {
336
+ const blob = typeof flat.blob === 'string' ? flat.blob : '';
337
+ const mime = typeof flat.mime === 'string' ? flat.mime : 'image/png';
338
+ if (!blob) return Promise.resolve({ ok: false, error: 'attach_image requires args.blob (base64 or data URL)' });
339
+ this.driver.dispatch({ kind: 'attach_image', blob, mime });
340
+ return Promise.resolve({ ok: true, effects: [{ type: 'attached_image', controlId: ctl.id }] });
341
+ }
342
+ // send_keys + open_picker both route through click_control. For
343
+ // open_picker the dashboard sees the picker modal arrive via a
344
+ // state_changed event, then submits the choice using
345
+ // resolveModal/click_modal_button. invokeScript's synchronous
346
+ // return is just an acknowledgement that the trigger fired.
347
+ this.driver.dispatch({ kind: 'click_control', control_id: ctl.id, payload: flat });
348
+ const effects: { type: string; controlId: string }[] = [];
349
+ if (action.type === 'open_picker') effects.push({ type: 'opened_picker', controlId: ctl.id });
350
+ else if (action.type === 'send_keys') effects.push({ type: 'sent_keys', controlId: ctl.id });
351
+ return Promise.resolve({ ok: true, effects });
352
+ }
353
+ getDebugSnapshot(): unknown {
354
+ let screen = '';
355
+ let sections: Record<string, string> | undefined;
356
+ try {
357
+ screen = this.driver.snapshot();
358
+ const driverSections = this.driver.getSections?.();
359
+ if (driverSections) {
360
+ sections = Object.fromEntries(driverSections.map(s => [s.id, s.text]));
361
+ } else {
362
+ sections = this.readCurrentScreenSections(screen);
363
+ }
364
+ } catch { /* best-effort */ }
365
+ // Read native transcript messages for the debug snapshot
366
+ let messages: any[] = [];
367
+ if (this.spec.native_history?.source) {
368
+ try {
369
+ const nhResult = executeNativeHistory(this.spec.native_history, {
370
+ agentType: this.cliType,
371
+ providerSessionId: this.providerSessionId,
372
+ sessionStartedAtMs: this.spawnedAtMs,
373
+ envOverrides: this.spawnedEnv,
374
+ workspace: this.workingDir,
375
+ });
376
+ if (nhResult && Array.isArray(nhResult.messages)) messages = nhResult.messages;
377
+ } catch { /* best-effort */ }
378
+ } else {
379
+ messages = this.readClaudeScreenAssistantMessages();
380
+ }
381
+ return {
382
+ cliType: this.cliType,
383
+ spec_id: this.spec.id,
384
+ current_state: this.latestState,
385
+ current_modal: this.latestModal,
386
+ activeInteractivePrompt: this.activeInteractivePrompt,
387
+ exited: this.exited,
388
+ screen,
389
+ sections,
390
+ stateHistory: this.driver.getStateHistory(),
391
+ idleHoldPending: this.driver.hasIdleHoldPending(),
392
+ lastBusyAt: this.driver.getLastBusyAt(),
393
+ specPath: this.driver.getSpecPath(),
394
+ cursorPosition: this.driver.getCursorPosition(),
395
+ completionIdleDebounce: this.driver.getCompletionIdleDebounceState(),
396
+ // v4 FSM live transition table (null for v3 specs). Every outgoing
397
+ // transition from the current state with its per-condition match
398
+ // result + countdown — the canonical "why isn't it moving" answer.
399
+ fsm: this.driver.getFsmDebug?.() ?? null,
400
+ // v4 FSM transition snapshot history (null for v3 specs). The full
401
+ // pre-transition evaluation table captured at each transition —
402
+ // answers "why did this rule fire" after the fact, unlike the live
403
+ // `fsm` field which only reflects the current instant.
404
+ fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
405
+ // Extended fields
406
+ name: this.cliName,
407
+ status: this.getStatus().status,
408
+ workingDir: this.workingDir,
409
+ spawnedAtMs: this.spawnedAtMs,
410
+ providerSessionId: this.providerSessionId ?? null,
411
+ messages,
412
+ committedMessages: messages,
413
+ };
414
+ }
415
+ getRuntimeMetadata(): unknown {
416
+ return {
417
+ runtimeId: this.spec.id,
418
+ runtimeKey: this.spec.id,
419
+ displayName: this.spec.name,
420
+ spawnedAtMs: this.spawnedAtMs,
421
+ spawnedEnv: this.spawnedEnv,
422
+ ...(this.providerSessionId ? { providerSessionId: this.providerSessionId } : {}),
423
+ };
424
+ }
425
+ updateRuntimeMeta(meta?: Record<string, unknown>): void {
426
+ if (meta && typeof meta.providerSessionId === 'string') {
427
+ this.providerSessionId = meta.providerSessionId;
428
+ }
429
+ }
430
+ refreshProviderDefinition(): void { /* hot reload handled by SpecDriver fs.watch */ }
431
+
432
+ private handleEvent(ev: DashboardEvent): void {
433
+ this.lastEvent = ev;
434
+ switch (ev.kind) {
435
+ case 'state_changed':
436
+ this.latestState = ev.state;
437
+ this.latestModal = ev.modal;
438
+ // info-level keeps only spec-defined identifiers (state.id /
439
+ // state.label / button count). The extracted title can carry
440
+ // user data — file paths, command text, ticket titles — so
441
+ // it stays at debug.
442
+ LOG.info('SpecAdapter', `[${this.cliType}] state=${ev.state.id} (${ev.state.label}) modal=${ev.modal ? `${ev.modal.buttons.length}-buttons` : 'none'}`);
443
+ if (ev.state.title) {
444
+ LOG.debug('SpecAdapter', `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
445
+ }
446
+ this.maybeClearResolvedClaudeTuiPrompt();
447
+ this.maybeCaptureClaudeTuiPrompt();
448
+ this.statusCallback?.();
449
+ return;
450
+ case 'pty_data':
451
+ this.detectInteractivePromptFromPtyChunk(ev.chunk);
452
+ this.maybeClearResolvedClaudeTuiPrompt();
453
+ this.maybeCaptureClaudeTuiPrompt();
454
+ try { this.ptyDataCallback?.(ev.chunk); } catch { /* ignore */ }
455
+ return;
456
+ case 'exit':
457
+ this.exited = true;
458
+ this.statusCallback?.();
459
+ return;
460
+ case 'spec_error':
461
+ LOG.warn('SpecAdapter', `[${this.cliType}] spec reload error: ${ev.errors.join('; ')}`);
462
+ return;
463
+ default:
464
+ return;
465
+ }
466
+ }
467
+
468
+ private detectInteractivePromptFromPtyChunk(chunk: string): void {
469
+ if (this.cliType !== 'claude-cli' || !chunk) return;
470
+ this.jsonLineTail += chunk;
471
+ if (this.jsonLineTail.length > 64 * 1024) this.jsonLineTail = this.jsonLineTail.slice(-64 * 1024);
472
+ const lines = this.jsonLineTail.split(/\r?\n/);
473
+ this.jsonLineTail = lines.pop() || '';
474
+ for (const line of lines) {
475
+ const trimmed = line.trim();
476
+ if (!trimmed.startsWith('{') || !trimmed.includes('AskUserQuestion')) continue;
477
+ try {
478
+ const parsed = JSON.parse(trimmed);
479
+ const prompt = detectClaudeAskUserQuestionPromptFromJson(parsed, this.cliType);
480
+ if (!prompt) continue;
481
+ this.activeInteractivePrompt = prompt;
482
+ this.interactivePromptTransport = 'stream-json';
483
+ this.interactivePromptLostAt = null;
484
+ this.statusCallback?.();
485
+ } catch {
486
+ // PTY output is not guaranteed to be machine JSON.
487
+ }
488
+ }
489
+ }
490
+
491
+ private readCurrentScreenSections(_screenText: string): Record<string, string> {
492
+ try {
493
+ const sections = this.driver.getSections() ?? [];
494
+ return Object.fromEntries(sections.map(section => [section.id, section.text]));
495
+ } catch {
496
+ return {};
497
+ }
498
+ }
499
+
500
+ private extractProviderSessionIdFromScreen(): string | undefined {
501
+ if (this.cliType !== 'codex-cli') return this.providerSessionId;
502
+ let screenText = '';
503
+ try {
504
+ screenText = this.driver.snapshot();
505
+ } catch {
506
+ return this.providerSessionId;
507
+ }
508
+ const clean = stripAnsi(screenText);
509
+ 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);
510
+ return match?.[1] || this.providerSessionId;
511
+ }
512
+
513
+ private readClaudeScreenAssistantMessages(): ChatMessage[] {
514
+ if (this.cliType !== 'claude-cli') return [];
515
+ let screenText = '';
516
+ try {
517
+ screenText = this.driver.snapshot();
518
+ } catch {
519
+ return [];
520
+ }
521
+ const sections = this.readCurrentScreenSections(screenText);
522
+ const body = sections.body || screenText;
523
+ const messages: ChatMessage[] = [];
524
+ const seen = new Set<string>();
525
+ for (const line of body.split(/\r?\n/)) {
526
+ const match = line.match(/^\s*⏺\s+(.+?)\s*$/);
527
+ const content = match?.[1]?.trim();
528
+ if (!content || seen.has(content)) continue;
529
+ seen.add(content);
530
+ messages.push({
531
+ role: 'assistant',
532
+ kind: 'standard',
533
+ content,
534
+ source: 'assistant_text',
535
+ userFacing: true,
536
+ bubbleState: 'final',
537
+ });
538
+ }
539
+ return messages;
540
+ }
541
+
542
+ /**
543
+ * Grace window a held interactive prompt must be absent from the screen
544
+ * before we treat it as resolved-in-terminal and clear it. claude-cli
545
+ * repaints the picker across multiple PTY chunks, so a single
546
+ * footer-less frame is not proof the prompt is gone. Sized in the same
547
+ * spirit as the approval FSM's `approvalCooldown` modal-lost hysteresis.
548
+ */
549
+ private static readonly INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
550
+
551
+ /**
552
+ * Clear a held interactive prompt once the user has resolved it directly
553
+ * in the terminal (the choice picker leaves the screen without going
554
+ * through setInteractivePromptResponse). The approval path already does
555
+ * this via the FSM's modal-lost hysteresis; the interactive-prompt path
556
+ * had no equivalent, so a terminal-side answer left activeInteractivePrompt
557
+ * set and getStatus() re-emitted the same choice modal forever.
558
+ *
559
+ * Detection mirrors capture: the claude TUI picker is on-screen exactly
560
+ * while its "Enter to select" footer is rendered. When the footer is gone
561
+ * for INTERACTIVE_PROMPT_LOST_GRACE_MS the prompt is genuinely resolved.
562
+ */
563
+ private maybeClearResolvedClaudeTuiPrompt(): void {
564
+ if (this.cliType !== 'claude-cli' || !this.activeInteractivePrompt) return;
565
+ // stream-json prompts are tracked by their tool-call lifecycle, not by
566
+ // screen footer, but claude renders the same TUI picker for both
567
+ // transports while awaiting an answer — so screen presence is a valid
568
+ // resolved-signal for either. (If the screen read fails, keep holding.)
569
+ let screenText = '';
570
+ try {
571
+ screenText = this.driver.snapshot();
572
+ } catch {
573
+ return;
574
+ }
575
+ const stillOnScreen = screenText.includes('Enter to select');
576
+ if (stillOnScreen) {
577
+ // Prompt reappeared / never left — reset the hysteresis timer.
578
+ this.interactivePromptLostAt = null;
579
+ return;
580
+ }
581
+ const lostAt = this.interactivePromptLostAt ?? Date.now();
582
+ if (this.interactivePromptLostAt === null) this.interactivePromptLostAt = lostAt;
583
+ if (Date.now() - lostAt < SpecCliAdapter.INTERACTIVE_PROMPT_LOST_GRACE_MS) return;
584
+ // Resolved in the terminal — drop the held prompt so getStatus() stops
585
+ // re-emitting it.
586
+ this.activeInteractivePrompt = null;
587
+ this.interactivePromptTransport = null;
588
+ this.interactivePromptLostAt = null;
589
+ this.statusCallback?.();
590
+ }
591
+
592
+ private maybeCaptureClaudeTuiPrompt(): void {
593
+ if (this.cliType !== 'claude-cli'
594
+ || this.activeInteractivePrompt
595
+ || this.claudeTuiPromptCaptureInFlight) return;
596
+ const screenText = this.driver.snapshot();
597
+ const headers = this.readClaudeTuiHeaders(screenText);
598
+ if (!screenText.includes('Enter to select')) return;
599
+ if (headers.length === 0) {
600
+ const prompt = detectClaudeAskUserQuestionPromptFromTuiPages([{ screenText }], {
601
+ promptId: `ask-user-${this.providerSessionId || 'claude'}-${Date.now()}`,
602
+ providerType: this.cliType,
603
+ });
604
+ if (!prompt) return;
605
+ this.activeInteractivePrompt = prompt;
606
+ this.interactivePromptTransport = 'tui';
607
+ this.interactivePromptLostAt = null;
608
+ this.statusCallback?.();
609
+ return;
610
+ }
611
+ this.claudeTuiPromptCaptureInFlight = true;
612
+ void this.captureClaudeTuiPrompt(screenText, headers).finally(() => {
613
+ this.claudeTuiPromptCaptureInFlight = false;
614
+ });
615
+ }
616
+
617
+ private readClaudeTuiHeaders(screenText: string): string[] {
618
+ const navLine = screenText.split(/\r?\n/).find(line => line.includes('✔ Submit') && /[☐☒]/.test(line));
619
+ if (!navLine) return [];
620
+ const headers: string[] = [];
621
+ for (const match of navLine.matchAll(/[☐☒]\s+(.+?)(?=\s+[☐☒]|\s+✔\s+Submit)/g)) {
622
+ const header = match[1]?.trim();
623
+ if (header) headers.push(header);
624
+ }
625
+ return headers;
626
+ }
627
+
628
+ private async captureClaudeTuiPrompt(firstScreen: string, headers: string[]): Promise<void> {
629
+ const pages: ClaudeInteractiveTuiPage[] = [{ screenText: firstScreen, header: headers[0] }];
630
+ for (let index = 1; index < headers.length; index += 1) {
631
+ this.driver.dispatch({ kind: 'pty_write', data: '\t' });
632
+ await new Promise(resolve => setTimeout(resolve, 120));
633
+ pages.push({ screenText: this.driver.snapshot(), header: headers[index] });
634
+ }
635
+ for (let index = headers.length - 1; index > 0; index -= 1) {
636
+ this.driver.dispatch({ kind: 'pty_write', data: '\x1b[Z' });
637
+ await new Promise(resolve => setTimeout(resolve, 80));
638
+ }
639
+
640
+ const prompt = detectClaudeAskUserQuestionPromptFromTuiPages(pages, {
641
+ promptId: `ask-user-${this.providerSessionId || 'claude'}-${Date.now()}`,
642
+ providerType: this.cliType,
643
+ });
644
+ if (!prompt) return;
645
+ this.activeInteractivePrompt = prompt;
646
+ this.interactivePromptTransport = 'tui';
647
+ this.interactivePromptLostAt = null;
648
+ this.statusCallback?.();
649
+ }
650
+
651
+ getDebugState(): Record<string, any> {
652
+ const screen = this.driver.getScreen?.() ?? '';
653
+ const history = this.driver.getStateHistory();
654
+ const status = this.getStatus();
655
+
656
+ let messages: any[] = [];
657
+ if (this.spec.native_history?.source) {
658
+ try {
659
+ const result = executeNativeHistory(this.spec.native_history, {
660
+ agentType: this.cliType,
661
+ providerSessionId: this.providerSessionId,
662
+ sessionStartedAtMs: this.spawnedAtMs,
663
+ envOverrides: this.spawnedEnv,
664
+ workspace: this.workingDir,
665
+ });
666
+ if (result && Array.isArray(result.messages)) {
667
+ messages = result.messages;
668
+ }
669
+ } catch (e) {
670
+ // Ignore native history read errors in debug state
671
+ }
672
+ } else {
673
+ messages = this.readClaudeScreenAssistantMessages();
674
+ }
675
+
676
+ const latestState = this.latestState;
677
+ const latestModal = this.latestModal;
678
+ return {
679
+ type: this.cliType,
680
+ name: this.cliName,
681
+ status: status.status,
682
+ rawStatus: status.status,
683
+ projectedStatus: status.status,
684
+ ready: this.spawned,
685
+ // Legacy snapshot-style fields for panels that read getDebugSnapshot shape
686
+ spec_id: this.spec.id,
687
+ current_state: latestState ?? null,
688
+ current_modal: latestModal ?? null,
689
+ exited: this.exited,
690
+ idleHoldPending: this.driver.hasIdleHoldPending?.() ?? false,
691
+ lastBusyAt: this.driver.getLastBusyAt?.() ?? 0,
692
+ screen: screen,
693
+ screenText: screen,
694
+ workingDir: this.workingDir,
695
+ spawnedAtMs: this.spawnedAtMs,
696
+ providerSessionId: this.providerSessionId ?? null,
697
+ sections: this.driver.getSections?.() ?? null,
698
+ stateHistory: history,
699
+ specPath: this.driver.getSpecPath?.() ?? null,
700
+ // v4 FSM live transition table — present only for FsmDriver. Lets
701
+ // the panel (and the daemon API) show, for the current instant,
702
+ // every outgoing transition with its per-condition match result
703
+ // and countdown. This is the canonical "why isn't it transitioning"
704
+ // answer — no screenshots needed.
705
+ fsm: this.driver.getFsmDebug?.() ?? null,
706
+ // v4 FSM transition snapshot history — the captured pre-transition
707
+ // evaluation table at each transition (null for v3 specs).
708
+ fsmHistory: this.driver.getFsmSnapshotHistory?.() ?? null,
709
+ messages,
710
+ committedMessages: messages,
711
+ };
712
+ }
713
+
714
+ getTraceState(limit = 120): Record<string, any> {
715
+ const history = this.driver.getStateHistory();
716
+ return {
717
+ status: this.getStatus().status,
718
+ stateHistory: history.slice(-limit),
719
+ screenText: this.driver.getScreen?.() ?? '',
720
+ };
721
+ }
722
+
723
+ getProviderResolutionMeta(): Record<string, any> {
724
+ return {
725
+ type: this.cliType,
726
+ providerDir: null,
727
+ resolvedVersion: null,
728
+ };
729
+ }
730
+ }