@adhdev/daemon-core 0.9.82-rc.16 → 0.9.82-rc.161

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 (209) hide show
  1. package/dist/boot/process-hardening.d.ts +50 -0
  2. package/dist/chat/source-machine.d.ts +166 -0
  3. package/dist/chat/source-resolver.d.ts +104 -0
  4. package/dist/chat/subscription-updates.d.ts +1 -0
  5. package/dist/cli-adapter-types.d.ts +19 -2
  6. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  7. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  8. package/dist/cli-adapters/provider-cli-adapter.d.ts +104 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +27 -0
  11. package/dist/commands/handler.d.ts +66 -0
  12. package/dist/commands/mesh-coordinator.d.ts +72 -1
  13. package/dist/commands/router.d.ts +22 -0
  14. package/dist/config/chat-history.d.ts +7 -0
  15. package/dist/config/config.d.ts +5 -0
  16. package/dist/config/mesh-config.d.ts +71 -1
  17. package/dist/git/git-commands.d.ts +5 -1
  18. package/dist/index.d.ts +31 -6
  19. package/dist/index.js +17210 -3575
  20. package/dist/index.js.map +1 -1
  21. package/dist/index.mjs +17127 -3551
  22. package/dist/index.mjs.map +1 -1
  23. package/dist/installer.d.ts +1 -4
  24. package/dist/ipc/local-ipc-server.d.ts +91 -0
  25. package/dist/launch.d.ts +1 -1
  26. package/dist/logging/async-batch-writer.d.ts +10 -0
  27. package/dist/mesh/beads-db.d.ts +72 -0
  28. package/dist/mesh/contracts.d.ts +164 -0
  29. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  30. package/dist/mesh/coordinator-registry.d.ts +59 -0
  31. package/dist/mesh/mesh-active-work.d.ts +90 -0
  32. package/dist/mesh/mesh-events.d.ts +77 -5
  33. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  34. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  35. package/dist/mesh/mesh-ledger.d.ts +58 -1
  36. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  37. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  38. package/dist/mesh/preview-freshness.d.ts +18 -0
  39. package/dist/mesh/refine-config.d.ts +193 -0
  40. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  41. package/dist/providers/approval-utils.d.ts +9 -0
  42. package/dist/providers/cli-provider-instance.d.ts +7 -2
  43. package/dist/providers/contracts.d.ts +98 -5
  44. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  45. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  46. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  47. package/dist/providers/native-history/dispatcher.d.ts +24 -0
  48. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  49. package/dist/providers/native-history/index.d.ts +13 -0
  50. package/dist/providers/provider-loader.d.ts +19 -1
  51. package/dist/providers/read-chat-contract.d.ts +29 -0
  52. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  53. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  54. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  55. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +64 -0
  56. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  57. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  58. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  59. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  60. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  61. package/dist/providers/sdk/v1/index.d.ts +30 -0
  62. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  63. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  64. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  65. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  66. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  67. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  68. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  69. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  70. package/dist/providers/spec/adapter.d.ts +56 -0
  71. package/dist/providers/spec/cli-adapter.d.ts +76 -0
  72. package/dist/providers/spec/driver.d.ts +148 -0
  73. package/dist/providers/spec/evaluator.d.ts +47 -0
  74. package/dist/providers/spec/loader.d.ts +14 -0
  75. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  76. package/dist/providers/spec/route.d.ts +4 -0
  77. package/dist/providers/spec/schema.gen.d.ts +507 -0
  78. package/dist/providers/spec/types.d.ts +211 -0
  79. package/dist/providers/transcript-v2.d.ts +176 -0
  80. package/dist/repo-mesh-types.d.ts +100 -1
  81. package/dist/sessions/registry.d.ts +3 -0
  82. package/dist/shared-types.d.ts +12 -0
  83. package/dist/status/reporter.d.ts +2 -0
  84. package/dist/status/snapshot.d.ts +1 -0
  85. package/dist/types.d.ts +5 -0
  86. package/package.json +7 -2
  87. package/src/boot/daemon-lifecycle.ts +17 -10
  88. package/src/boot/process-hardening.ts +89 -0
  89. package/src/chat/source-machine.ts +534 -0
  90. package/src/chat/source-resolver.ts +0 -0
  91. package/src/chat/subscription-updates.ts +14 -1
  92. package/src/cli-adapter-types.d.ts +1 -0
  93. package/src/cli-adapter-types.ts +18 -2
  94. package/src/cli-adapters/cli-script-runner.ts +421 -0
  95. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  96. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  97. package/src/cli-adapters/provider-cli-adapter.ts +664 -1137
  98. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  99. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  100. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  101. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  102. package/src/cli-adapters/provider-cli-shared.ts +57 -11
  103. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  104. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  105. package/src/commands/chat-commands.ts +1597 -60
  106. package/src/commands/cli-manager.ts +195 -3
  107. package/src/commands/handler.ts +547 -2
  108. package/src/commands/mesh-coordinator.ts +231 -127
  109. package/src/commands/router.ts +3547 -502
  110. package/src/config/chat-history.ts +87 -24
  111. package/src/config/config.ts +12 -0
  112. package/src/config/mesh-config.ts +264 -2
  113. package/src/config/recent-activity.ts +8 -2
  114. package/src/daemon/dev-cli-debug.ts +10 -1
  115. package/src/detection/ide-detector.ts +26 -16
  116. package/src/git/git-commands.ts +17 -5
  117. package/src/git/git-worktree.ts +8 -1
  118. package/src/index.ts +91 -5
  119. package/src/installer.d.ts +1 -1
  120. package/src/installer.ts +8 -6
  121. package/src/ipc/local-ipc-server.ts +278 -0
  122. package/src/launch.d.ts +1 -1
  123. package/src/launch.ts +37 -28
  124. package/src/logging/async-batch-writer.ts +55 -0
  125. package/src/logging/logger.ts +2 -1
  126. package/src/mesh/beads-db.ts +479 -0
  127. package/src/mesh/contracts.ts +329 -0
  128. package/src/mesh/coordinator-prompt.ts +204 -30
  129. package/src/mesh/coordinator-registry.ts +121 -0
  130. package/src/mesh/mesh-active-work.ts +437 -0
  131. package/src/mesh/mesh-events.ts +820 -61
  132. package/src/mesh/mesh-fast-forward.ts +430 -0
  133. package/src/mesh/mesh-host-ownership.ts +73 -0
  134. package/src/mesh/mesh-ledger.ts +457 -104
  135. package/src/mesh/mesh-refine-status.ts +144 -0
  136. package/src/mesh/mesh-work-queue.ts +216 -158
  137. package/src/mesh/preview-freshness.ts +118 -0
  138. package/src/mesh/refine-config.ts +366 -0
  139. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  140. package/src/providers/approval-utils.ts +39 -5
  141. package/src/providers/chat-message-normalization.ts +4 -11
  142. package/src/providers/cli-provider-instance.ts +388 -47
  143. package/src/providers/contracts.ts +105 -5
  144. package/src/providers/ide-provider-instance.ts +17 -3
  145. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  146. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  147. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  148. package/src/providers/native-history/dispatcher.ts +227 -0
  149. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  150. package/src/providers/native-history/index.ts +30 -0
  151. package/src/providers/provider-loader.ts +420 -31
  152. package/src/providers/provider-schema.ts +31 -13
  153. package/src/providers/read-chat-contract.ts +76 -16
  154. package/src/providers/sdk/README.md +49 -0
  155. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  156. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  157. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  158. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +245 -0
  159. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  160. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  161. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  162. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  163. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  164. package/src/providers/sdk/v1/index.ts +152 -0
  165. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  166. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  167. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  168. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +457 -0
  169. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  170. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  171. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  172. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  173. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  174. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  175. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  176. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  177. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +119 -0
  178. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  179. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  180. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  181. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  182. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  183. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  184. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  185. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  186. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  187. package/src/providers/sdk/v1/validators/index.ts +19 -0
  188. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  189. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  190. package/src/providers/spec/adapter.ts +153 -0
  191. package/src/providers/spec/cli-adapter.ts +318 -0
  192. package/src/providers/spec/driver.ts +498 -0
  193. package/src/providers/spec/evaluator.ts +268 -0
  194. package/src/providers/spec/loader.ts +130 -0
  195. package/src/providers/spec/native-history-executor.ts +612 -0
  196. package/src/providers/spec/route.ts +51 -0
  197. package/src/providers/spec/schema.gen.ts +507 -0
  198. package/src/providers/spec/schema.json +210 -0
  199. package/src/providers/spec/types.ts +230 -0
  200. package/src/providers/transcript-v2.ts +567 -0
  201. package/src/providers/version-archive.ts +38 -20
  202. package/src/repo-mesh-types.ts +110 -1
  203. package/src/sessions/registry.ts +3 -0
  204. package/src/shared-types.ts +9 -0
  205. package/src/status/builders.ts +23 -6
  206. package/src/status/reporter.ts +15 -0
  207. package/src/status/snapshot.ts +35 -11
  208. package/src/system/host-memory.ts +29 -12
  209. package/src/types.ts +5 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Thin generic terminal adapter.
3
+ *
4
+ * Spawns a PTY, feeds bytes into a headless xterm so a visible screen
5
+ * snapshot is always available, and exposes a small hook surface
6
+ * (init / tick / on_screen_changed / on_exit). Nothing in this module
7
+ * knows about agents, modals, or specs — it's just a terminal host.
8
+ *
9
+ * The Spec Driver is built on top of this adapter and consumes the
10
+ * hooks to drive its declarative state machine.
11
+ *
12
+ * Inbound from driver:
13
+ * send_keys(str) — raw bytes to PTY
14
+ * resize(c, r) — resize the PTY and the xterm
15
+ * kill() — terminate the child
16
+ *
17
+ * Outbound to driver:
18
+ * init({ pid })
19
+ * on_pty_data(chunk) — raw bytes for dashboard's terminal pane
20
+ * on_screen_changed(snapshot) — coalesced visible-screen change
21
+ * tick() — periodic timer (configurable interval)
22
+ * on_exit({ exitCode })
23
+ */
24
+ 'use strict';
25
+
26
+ import { Terminal } from '@xterm/headless';
27
+ import { NodePtyTransportFactory, type PtyRuntimeTransport, type PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
28
+
29
+ export interface TerminalAdapterOpts {
30
+ binary: string;
31
+ args?: string[];
32
+ cwd: string;
33
+ env?: Record<string, string>;
34
+ cols?: number;
35
+ rows?: number;
36
+ /** Coalesce screen snapshots: emit on_screen_changed at most this often. */
37
+ screenChangeDebounceMs?: number;
38
+ /** tick() period. 0 disables ticks. */
39
+ tickIntervalMs?: number;
40
+ /**
41
+ * Optional PTY transport factory. When the daemon supplies a
42
+ * SessionHostPtyTransportFactory (standalone runs the PTY inside the
43
+ * session-host so the runtime/<sid>/snapshot endpoint can serve it),
44
+ * forward it here. Without it the PTY spawns locally inside the
45
+ * daemon process and the dashboard's terminal pane reports
46
+ * "Runtime terminal unavailable: Unknown session".
47
+ */
48
+ transportFactory?: PtyTransportFactory;
49
+ }
50
+
51
+ export interface TerminalAdapterHandlers {
52
+ init?(info: { pid: number }): void;
53
+ on_pty_data?(chunk: string): void;
54
+ on_screen_changed?(snapshot: string): void;
55
+ tick?(): void;
56
+ on_exit?(info: { exitCode: number }): void;
57
+ }
58
+
59
+ export class TerminalAdapter {
60
+ private term: Terminal;
61
+ private pty: PtyRuntimeTransport | null = null;
62
+ private factory: PtyTransportFactory;
63
+ private cols: number;
64
+ private rows: number;
65
+ private screenDebounceMs: number;
66
+ private tickIntervalMs: number;
67
+ private screenTimer: ReturnType<typeof setTimeout> | null = null;
68
+ private tickTimer: ReturnType<typeof setInterval> | null = null;
69
+ private lastScreen = '';
70
+
71
+ constructor(
72
+ private readonly opts: TerminalAdapterOpts,
73
+ private readonly handlers: TerminalAdapterHandlers,
74
+ ) {
75
+ this.cols = opts.cols ?? 100;
76
+ this.rows = opts.rows ?? 30;
77
+ this.screenDebounceMs = opts.screenChangeDebounceMs ?? 80;
78
+ this.tickIntervalMs = opts.tickIntervalMs ?? 0;
79
+ this.factory = opts.transportFactory ?? new NodePtyTransportFactory();
80
+ this.term = new Terminal({ cols: this.cols, rows: this.rows, allowProposedApi: true, scrollback: 1000 });
81
+ }
82
+
83
+ start(): void {
84
+ this.pty = this.factory.spawn(this.opts.binary, this.opts.args ?? [], {
85
+ cwd: this.opts.cwd,
86
+ env: { ...process.env, ...(this.opts.env ?? {}) } as Record<string, string>,
87
+ cols: this.cols,
88
+ rows: this.rows,
89
+ });
90
+ this.handlers.init?.({ pid: this.pty.pid });
91
+ this.pty.onData((chunk) => this.onChunk(chunk));
92
+ this.pty.onExit((info) => {
93
+ this.stopTimers();
94
+ this.handlers.on_exit?.({ exitCode: typeof info.exitCode === 'number' ? info.exitCode : 0 });
95
+ this.pty = null;
96
+ });
97
+ if (this.tickIntervalMs > 0) {
98
+ this.tickTimer = setInterval(() => this.handlers.tick?.(), this.tickIntervalMs);
99
+ }
100
+ }
101
+
102
+ send_keys(s: string): void {
103
+ this.pty?.write(s);
104
+ }
105
+
106
+ resize(cols: number, rows: number): void {
107
+ this.cols = cols; this.rows = rows;
108
+ this.pty?.resize(cols, rows);
109
+ this.term.resize(cols, rows);
110
+ }
111
+
112
+ snapshot(): string {
113
+ return this.lastScreen || this.computeScreen();
114
+ }
115
+
116
+ kill(): void {
117
+ this.stopTimers();
118
+ try { this.pty?.kill(); } catch { /* ignore */ }
119
+ this.pty = null;
120
+ this.term.dispose();
121
+ }
122
+
123
+ private onChunk(chunk: string): void {
124
+ try { this.handlers.on_pty_data?.(chunk); } catch { /* user side */ }
125
+ this.term.write(chunk);
126
+ // Coalesce snapshot emission — rapid bursts shouldn't fire 200x.
127
+ if (this.screenTimer) return;
128
+ this.screenTimer = setTimeout(() => {
129
+ this.screenTimer = null;
130
+ const snap = this.computeScreen();
131
+ if (snap === this.lastScreen) return;
132
+ this.lastScreen = snap;
133
+ try { this.handlers.on_screen_changed?.(snap); } catch { /* user side */ }
134
+ }, this.screenDebounceMs);
135
+ }
136
+
137
+ private computeScreen(): string {
138
+ const buf = this.term.buffer.active;
139
+ const out: string[] = [];
140
+ for (let y = 0; y < buf.length; y += 1) {
141
+ const line = buf.getLine(y);
142
+ if (!line) continue;
143
+ out.push(line.translateToString(true));
144
+ }
145
+ while (out.length > 0 && out[out.length - 1].trim() === '') out.pop();
146
+ return out.join('\n');
147
+ }
148
+
149
+ private stopTimers(): void {
150
+ if (this.screenTimer) { clearTimeout(this.screenTimer); this.screenTimer = null; }
151
+ if (this.tickTimer) { clearInterval(this.tickTimer); this.tickTimer = null; }
152
+ }
153
+ }
@@ -0,0 +1,318 @@
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 { SpecDriver, type DashboardEvent } from './driver.js';
22
+ import { loadSpec } from './loader.js';
23
+ import type { CliSpec } from './types.js';
24
+ import type { CliAdapter, CliAdapterStatus } from '../../cli-adapter-types.js';
25
+ import type { ChatMessage } from '../../types.js';
26
+ import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
27
+ import { LOG } from '../../logging/logger.js';
28
+
29
+ export class SpecCliAdapter implements CliAdapter {
30
+ readonly cliType: string;
31
+ readonly cliName: string;
32
+ readonly workingDir: string;
33
+
34
+ private driver: SpecDriver;
35
+ private spec: CliSpec;
36
+ private lastEvent: DashboardEvent | null = null;
37
+ private latestState: { id: string; label: string; title: string | null } | null = null;
38
+ private latestModal: { title: string | null; buttons: { index: number; label: string }[] } | null = null;
39
+ private statusCallback: (() => void) | null = null;
40
+ private ptyDataCallback: ((data: string) => void) | null = null;
41
+ private partialResponse = '';
42
+ private exited = false;
43
+ private spawned = false;
44
+ private providerSessionId: string | undefined;
45
+ /** Wall clock at the moment spawn() ran. Used as the cutoff for
46
+ * native-history file selection so a prior session's transcript
47
+ * can't leak into this session before the agent has written its
48
+ * own records. */
49
+ private spawnedAtMs = 0;
50
+ /** Env vars the daemon set on the spawned child. Mesh coordinator
51
+ * points hermes at a per-coordinator HERMES_HOME so the dashboard's
52
+ * native-history reader needs that override to find the right
53
+ * state.db; without it the reader sees ~/.hermes/state.db which
54
+ * the coordinator-launched hermes never writes to. The choice to
55
+ * redirect HERMES_HOME is a workaround for an unresolved hermes
56
+ * upstream feature gap (see hermes-agent#23130 — runtime-supplied
57
+ * MCP config), so this routing keeps the dashboard honest until
58
+ * hermes ships a runtime MCP override. */
59
+ private spawnedEnv: Record<string, string> = {};
60
+
61
+ constructor(
62
+ specPath: string,
63
+ workingDir: string,
64
+ cliArgs: string[],
65
+ extraEnv: Record<string, string>,
66
+ transportFactory?: PtyTransportFactory,
67
+ ) {
68
+ const res = loadSpec(specPath);
69
+ if (!res.ok) throw new Error(`spec invalid (${specPath}): ${res.errors.join('; ')}`);
70
+ this.spec = res.spec;
71
+ this.cliType = this.spec.id;
72
+ this.cliName = this.spec.name;
73
+ this.workingDir = workingDir;
74
+ this.spawnedEnv = { ...extraEnv };
75
+
76
+ // cli-manager.ts allocates providerSessionId per launch and threads
77
+ // it through resume.newSessionArgs as additional cliArgs (e.g.
78
+ // ["--session-id", "<uuid>"]). We must hand those to SpecDriver
79
+ // so the agent uses the daemon's id, otherwise (claude case) the
80
+ // agent generates its own id and the chat-history pipeline can't
81
+ // pair the on-disk transcript with the live session.
82
+ this.driver = new SpecDriver({
83
+ specPath,
84
+ workingDir,
85
+ extraEnv,
86
+ hotReload: true,
87
+ emitTrace: false,
88
+ transportFactory,
89
+ extraCliArgs: cliArgs,
90
+ });
91
+ this.driver.subscribe((ev) => this.handleEvent(ev));
92
+ }
93
+
94
+ async spawn(): Promise<void> {
95
+ if (this.spawned) return;
96
+ this.driver.start();
97
+ this.spawned = true;
98
+ this.spawnedAtMs = Date.now();
99
+ }
100
+
101
+ async sendMessage(text: string): Promise<void> {
102
+ // Content-free at info — the prompt body is user data.
103
+ LOG.info('SpecAdapter', `[${this.cliType}] sendMessage(len=${text.length})`);
104
+ LOG.debug('SpecAdapter', `[${this.cliType}] sendMessage body=${JSON.stringify(text.slice(0, 80))}${text.length > 80 ? '…' : ''}`);
105
+ this.driver.dispatch({ kind: 'send_message', text });
106
+ }
107
+
108
+ getStatus(): CliAdapterStatus {
109
+ if (this.exited) return { status: 'stopped', messages: [], activeModal: null };
110
+ if (!this.spawned) return { status: 'starting', messages: [], activeModal: null };
111
+
112
+ // Refresh native history lazily — the watch_path is cheap to stat,
113
+ // but parsing a full session.jsonl every call would be wasteful.
114
+ this.maybeRefreshNativeHistory();
115
+
116
+ const state = this.latestState;
117
+ if (!state) return { status: 'starting', messages: [], activeModal: null };
118
+
119
+ const modal = this.latestModal;
120
+ const lc = state.id.toLowerCase();
121
+ if (modal) {
122
+ return {
123
+ status: 'waiting_approval',
124
+ messages: [],
125
+ activeModal: {
126
+ message: modal.title ?? state.label,
127
+ buttons: modal.buttons.map(b => b.label),
128
+ },
129
+ };
130
+ }
131
+ if (/busy|generating|working|running|thinking/i.test(lc + ' ' + state.label)) {
132
+ return { status: 'generating', messages: [], activeModal: null };
133
+ }
134
+ return { status: 'idle', messages: [], activeModal: null };
135
+ }
136
+
137
+ private maybeRefreshNativeHistory(): void {
138
+ // Native history is now sourced by daemon's chat-history pipeline
139
+ // (which calls provider.scripts.readNativeHistory wired by
140
+ // provider-loader). SpecCliAdapter no longer polls or caches.
141
+ }
142
+
143
+ getScriptParsedStatus(): unknown {
144
+ return this.getStatus();
145
+ }
146
+
147
+ getPartialResponse(): string {
148
+ return this.partialResponse;
149
+ }
150
+
151
+ shutdown(): void {
152
+ try { this.driver.dispatch({ kind: 'shutdown' }); } catch { /* ignore */ }
153
+ }
154
+
155
+ cancel(): void {
156
+ try { this.driver.dispatch({ kind: 'cancel' }); } catch { /* ignore */ }
157
+ }
158
+
159
+ isProcessing(): boolean {
160
+ return this.getStatus().status === 'generating';
161
+ }
162
+
163
+ isReady(): boolean {
164
+ return this.spawned && !this.exited;
165
+ }
166
+
167
+ setOnStatusChange(cb: () => void): void {
168
+ this.statusCallback = cb;
169
+ }
170
+
171
+ setOnPtyData(cb: (data: string) => void): void {
172
+ this.ptyDataCallback = cb;
173
+ }
174
+
175
+ writeRaw(data: string): void {
176
+ // Raw pty input — typed characters, escape codes, arrow keys —
177
+ // goes straight to the underlying terminal. send_message would
178
+ // append submit_key after every chunk, which is why typing in
179
+ // the dashboard terminal felt like "enter on every keystroke".
180
+ this.driver.dispatch({ kind: 'pty_write', data });
181
+ }
182
+
183
+ resize(cols: number, rows: number): void {
184
+ this.driver.dispatch({ kind: 'resize', cols, rows });
185
+ }
186
+
187
+ resolveModal(buttonIndex: number): void {
188
+ // CliAdapter buttonIndex is 0-based; spec buttons are 1-based.
189
+ this.driver.dispatch({ kind: 'click_modal_button', index: buttonIndex + 1 });
190
+ }
191
+
192
+ async resolveAction(data: unknown): Promise<void> {
193
+ const args = (data && typeof data === 'object') ? (data as any) : {};
194
+ const explicitIndex = typeof args.buttonIndex === 'number' ? args.buttonIndex : -1;
195
+ if (explicitIndex >= 0) { this.resolveModal(explicitIndex); return; }
196
+ const action = typeof args.action === 'string' ? args.action : 'approve';
197
+ const buttons = this.latestModal?.buttons ?? [];
198
+ if (buttons.length === 0) return;
199
+ let target = -1;
200
+ if (action === 'reject' || action === 'deny') {
201
+ target = buttons.findIndex(b => /^(no|deny|reject|cancel)\b/i.test(b.label));
202
+ if (target < 0) target = buttons.length - 1;
203
+ } else {
204
+ target = buttons.findIndex(b => /^(yes|allow|approve|accept|continue|proceed|update)\b/i.test(b.label));
205
+ if (target < 0) target = 0;
206
+ }
207
+ this.resolveModal(target);
208
+ }
209
+
210
+ isApprovalRecentlyResolved(): boolean { return false; }
211
+ clearHistory(): void { /* no transcript buffer yet */ }
212
+ updateRuntimeSettings(): void { /* no runtime settings in spec model yet */ }
213
+ setServerConn(): void { /* server conn unused by SpecDriver */ }
214
+ /**
215
+ * Map an invokeScript(name, args) call onto a control_bar entry.
216
+ *
217
+ * scriptName is matched against control.id. The control's action.type
218
+ * drives the dispatch:
219
+ *
220
+ * send_keys → click_control (e.g. stop)
221
+ * open_picker → click_control then resolve when extract_choices
222
+ * surface; choice index comes from args.choiceIndex
223
+ * or args.choice (string label match), defaulting to 0
224
+ * attach_image → attach_image dispatch; expects args.blob (data url
225
+ * or base64) and args.mime
226
+ *
227
+ * Callers that pass an unknown control id get a { not_found } response.
228
+ * No control matched, no driver call — keeps the surface honest.
229
+ */
230
+ invokeScript(scriptName: string, args?: Record<string, unknown>): Promise<unknown> {
231
+ const controls = this.spec.control_bar ?? [];
232
+ const ctl = controls.find(c => c.id === scriptName);
233
+ if (!ctl) {
234
+ return Promise.resolve({ ok: false, error: `unknown control: ${scriptName}` });
235
+ }
236
+ // Args may arrive as either { blob, mime } (direct invocation) or
237
+ // { params: { blob, mime } } (when the dashboard wraps script args
238
+ // in a params bag). Look at both.
239
+ const flat: Record<string, unknown> = { ...(args || {}) };
240
+ if (args && typeof args.params === 'object' && args.params) {
241
+ Object.assign(flat, args.params as Record<string, unknown>);
242
+ }
243
+ const action = ctl.action;
244
+ if (action.type === 'attach_image') {
245
+ const blob = typeof flat.blob === 'string' ? flat.blob : '';
246
+ const mime = typeof flat.mime === 'string' ? flat.mime : 'image/png';
247
+ if (!blob) return Promise.resolve({ ok: false, error: 'attach_image requires args.blob (base64 or data URL)' });
248
+ this.driver.dispatch({ kind: 'attach_image', blob, mime });
249
+ return Promise.resolve({ ok: true, effects: [{ type: 'attached_image', controlId: ctl.id }] });
250
+ }
251
+ // send_keys + open_picker both route through click_control. For
252
+ // open_picker the dashboard sees the picker modal arrive via a
253
+ // state_changed event, then submits the choice using
254
+ // resolveModal/click_modal_button. invokeScript's synchronous
255
+ // return is just an acknowledgement that the trigger fired.
256
+ this.driver.dispatch({ kind: 'click_control', control_id: ctl.id, payload: flat });
257
+ const effects: { type: string; controlId: string }[] = [];
258
+ if (action.type === 'open_picker') effects.push({ type: 'opened_picker', controlId: ctl.id });
259
+ else if (action.type === 'send_keys') effects.push({ type: 'sent_keys', controlId: ctl.id });
260
+ return Promise.resolve({ ok: true, effects });
261
+ }
262
+ getDebugSnapshot(): unknown {
263
+ return {
264
+ cliType: this.cliType,
265
+ spec_id: this.spec.id,
266
+ current_state: this.latestState,
267
+ current_modal: this.latestModal,
268
+ exited: this.exited,
269
+ };
270
+ }
271
+ getRuntimeMetadata(): unknown {
272
+ return {
273
+ runtimeId: this.spec.id,
274
+ runtimeKey: this.spec.id,
275
+ displayName: this.spec.name,
276
+ spawnedAtMs: this.spawnedAtMs,
277
+ spawnedEnv: this.spawnedEnv,
278
+ };
279
+ }
280
+ updateRuntimeMeta(meta?: Record<string, unknown>): void {
281
+ if (meta && typeof meta.providerSessionId === 'string') {
282
+ this.providerSessionId = meta.providerSessionId;
283
+ }
284
+ }
285
+ refreshProviderDefinition(): void { /* hot reload handled by SpecDriver fs.watch */ }
286
+
287
+ private handleEvent(ev: DashboardEvent): void {
288
+ this.lastEvent = ev;
289
+ switch (ev.kind) {
290
+ case 'state_changed':
291
+ this.latestState = ev.state;
292
+ this.latestModal = ev.modal;
293
+ // info-level keeps only spec-defined identifiers (state.id /
294
+ // state.label / button count). The extracted title can carry
295
+ // user data — file paths, command text, ticket titles — so
296
+ // it stays at debug.
297
+ LOG.info('SpecAdapter', `[${this.cliType}] state=${ev.state.id} (${ev.state.label}) modal=${ev.modal ? `${ev.modal.buttons.length}-buttons` : 'none'}`);
298
+ if (ev.state.title) {
299
+ LOG.debug('SpecAdapter', `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
300
+ }
301
+ this.statusCallback?.();
302
+ return;
303
+ case 'pty_data':
304
+ try { this.ptyDataCallback?.(ev.chunk); } catch { /* ignore */ }
305
+ return;
306
+ case 'exit':
307
+ this.exited = true;
308
+ this.statusCallback?.();
309
+ return;
310
+ case 'spec_error':
311
+ LOG.warn('SpecAdapter', `[${this.cliType}] spec reload error: ${ev.errors.join('; ')}`);
312
+ return;
313
+ default:
314
+ return;
315
+ }
316
+ }
317
+ }
318
+