@adhdev/daemon-core 0.9.82-rc.17 → 0.9.82-rc.170

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 (221) 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 +22 -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 +106 -63
  9. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  10. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  11. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  12. package/dist/commands/handler.d.ts +110 -0
  13. package/dist/commands/mesh-coordinator.d.ts +72 -1
  14. package/dist/commands/router.d.ts +22 -0
  15. package/dist/config/chat-history.d.ts +7 -0
  16. package/dist/config/config.d.ts +5 -0
  17. package/dist/config/mesh-config.d.ts +71 -1
  18. package/dist/git/git-commands.d.ts +5 -1
  19. package/dist/index.d.ts +35 -6
  20. package/dist/index.js +18621 -3760
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +18525 -3732
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/installer.d.ts +1 -4
  25. package/dist/ipc/local-ipc-server.d.ts +91 -0
  26. package/dist/launch.d.ts +1 -1
  27. package/dist/logging/async-batch-writer.d.ts +10 -0
  28. package/dist/mesh/beads-db.d.ts +72 -0
  29. package/dist/mesh/contracts.d.ts +164 -0
  30. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  31. package/dist/mesh/coordinator-registry.d.ts +59 -0
  32. package/dist/mesh/mesh-active-work.d.ts +90 -0
  33. package/dist/mesh/mesh-events.d.ts +96 -5
  34. package/dist/mesh/mesh-fast-forward.d.ts +39 -0
  35. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  36. package/dist/mesh/mesh-ledger.d.ts +58 -1
  37. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  38. package/dist/mesh/mesh-work-queue.d.ts +44 -5
  39. package/dist/mesh/preview-freshness.d.ts +18 -0
  40. package/dist/mesh/refine-config.d.ts +193 -0
  41. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  42. package/dist/providers/approval-utils.d.ts +13 -0
  43. package/dist/providers/cli-provider-instance.d.ts +9 -3
  44. package/dist/providers/contracts.d.ts +98 -5
  45. package/dist/providers/external-sources.d.ts +71 -0
  46. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  47. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  48. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  49. package/dist/providers/native-history/dispatcher.d.ts +24 -0
  50. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  51. package/dist/providers/native-history/index.d.ts +13 -0
  52. package/dist/providers/provider-instance.d.ts +3 -0
  53. package/dist/providers/provider-loader.d.ts +26 -4
  54. package/dist/providers/provider-trust.d.ts +31 -0
  55. package/dist/providers/read-chat-contract.d.ts +29 -0
  56. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  57. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  58. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  59. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  60. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  61. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  62. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  63. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  64. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  65. package/dist/providers/sdk/v1/index.d.ts +30 -0
  66. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  67. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  68. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  69. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  70. package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
  71. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  72. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  73. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  74. package/dist/providers/spec/adapter.d.ts +56 -0
  75. package/dist/providers/spec/cli-adapter.d.ts +95 -0
  76. package/dist/providers/spec/driver.d.ts +149 -0
  77. package/dist/providers/spec/evaluator.d.ts +47 -0
  78. package/dist/providers/spec/loader.d.ts +14 -0
  79. package/dist/providers/spec/native-history-executor.d.ts +39 -0
  80. package/dist/providers/spec/route.d.ts +4 -0
  81. package/dist/providers/spec/schema.gen.d.ts +511 -0
  82. package/dist/providers/spec/types.d.ts +212 -0
  83. package/dist/providers/transcript-v2.d.ts +176 -0
  84. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  85. package/dist/repo-mesh-types.d.ts +100 -1
  86. package/dist/sessions/registry.d.ts +3 -0
  87. package/dist/shared-types.d.ts +42 -1
  88. package/dist/status/reporter.d.ts +2 -0
  89. package/dist/status/snapshot.d.ts +1 -0
  90. package/dist/types.d.ts +5 -0
  91. package/package.json +7 -2
  92. package/src/agent-stream/poller.ts +2 -3
  93. package/src/boot/daemon-lifecycle.ts +17 -10
  94. package/src/boot/process-hardening.ts +89 -0
  95. package/src/chat/source-machine.ts +534 -0
  96. package/src/chat/source-resolver.ts +0 -0
  97. package/src/chat/subscription-updates.ts +14 -1
  98. package/src/cli-adapter-types.d.ts +1 -0
  99. package/src/cli-adapter-types.ts +21 -2
  100. package/src/cli-adapters/cli-script-runner.ts +421 -0
  101. package/src/cli-adapters/cli-state-engine.ts +1086 -0
  102. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  103. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  104. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  105. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  106. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  107. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  108. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  109. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  110. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  111. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  112. package/src/commands/chat-commands.ts +1597 -60
  113. package/src/commands/cli-manager.ts +214 -3
  114. package/src/commands/handler.ts +809 -2
  115. package/src/commands/mesh-coordinator.ts +231 -127
  116. package/src/commands/router.ts +3559 -502
  117. package/src/config/chat-history.ts +87 -24
  118. package/src/config/config.ts +12 -0
  119. package/src/config/mesh-config.ts +264 -2
  120. package/src/config/recent-activity.ts +8 -2
  121. package/src/daemon/dev-cli-debug.ts +10 -1
  122. package/src/detection/ide-detector.ts +26 -16
  123. package/src/git/git-commands.ts +17 -5
  124. package/src/git/git-worktree.ts +8 -1
  125. package/src/index.ts +113 -5
  126. package/src/installer.d.ts +1 -1
  127. package/src/installer.ts +8 -6
  128. package/src/ipc/local-ipc-server.ts +278 -0
  129. package/src/launch.d.ts +1 -1
  130. package/src/launch.ts +37 -28
  131. package/src/logging/async-batch-writer.ts +55 -0
  132. package/src/logging/logger.ts +2 -1
  133. package/src/mesh/beads-db.ts +479 -0
  134. package/src/mesh/contracts.ts +329 -0
  135. package/src/mesh/coordinator-prompt.ts +204 -30
  136. package/src/mesh/coordinator-registry.ts +121 -0
  137. package/src/mesh/mesh-active-work.ts +437 -0
  138. package/src/mesh/mesh-events.ts +879 -61
  139. package/src/mesh/mesh-fast-forward.ts +430 -0
  140. package/src/mesh/mesh-host-ownership.ts +73 -0
  141. package/src/mesh/mesh-ledger.ts +457 -104
  142. package/src/mesh/mesh-refine-status.ts +144 -0
  143. package/src/mesh/mesh-work-queue.ts +216 -158
  144. package/src/mesh/preview-freshness.ts +118 -0
  145. package/src/mesh/refine-config.ts +366 -0
  146. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  147. package/src/providers/approval-utils.d.ts +4 -0
  148. package/src/providers/approval-utils.ts +47 -5
  149. package/src/providers/chat-message-normalization.ts +4 -11
  150. package/src/providers/cli-provider-instance.ts +452 -54
  151. package/src/providers/contracts.ts +105 -5
  152. package/src/providers/external-sources.ts +218 -0
  153. package/src/providers/ide-provider-instance.ts +19 -5
  154. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  155. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  156. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  157. package/src/providers/native-history/dispatcher.ts +227 -0
  158. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  159. package/src/providers/native-history/index.ts +30 -0
  160. package/src/providers/provider-instance.ts +3 -0
  161. package/src/providers/provider-loader.ts +585 -50
  162. package/src/providers/provider-schema.ts +31 -13
  163. package/src/providers/provider-trust.ts +114 -0
  164. package/src/providers/read-chat-contract.ts +76 -16
  165. package/src/providers/sdk/README.md +49 -0
  166. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  167. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  168. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  169. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  170. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  171. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  172. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  173. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  174. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  175. package/src/providers/sdk/v1/index.ts +152 -0
  176. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  177. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  178. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  179. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +458 -0
  180. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  181. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  182. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  183. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  184. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  185. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  186. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  187. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  188. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  189. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  190. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  191. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  192. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  193. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  194. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  195. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  196. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  197. package/src/providers/sdk/v1/types/common/index.ts +210 -0
  198. package/src/providers/sdk/v1/validators/index.ts +19 -0
  199. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  200. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  201. package/src/providers/spec/adapter.ts +168 -0
  202. package/src/providers/spec/cli-adapter.ts +430 -0
  203. package/src/providers/spec/driver.ts +502 -0
  204. package/src/providers/spec/evaluator.ts +298 -0
  205. package/src/providers/spec/loader.ts +130 -0
  206. package/src/providers/spec/native-history-executor.ts +623 -0
  207. package/src/providers/spec/route.ts +51 -0
  208. package/src/providers/spec/schema.gen.ts +511 -0
  209. package/src/providers/spec/schema.json +211 -0
  210. package/src/providers/spec/types.ts +231 -0
  211. package/src/providers/transcript-v2.ts +567 -0
  212. package/src/providers/types/interactive-prompt.ts +297 -0
  213. package/src/providers/version-archive.ts +38 -20
  214. package/src/repo-mesh-types.ts +110 -1
  215. package/src/sessions/registry.ts +3 -0
  216. package/src/shared-types.ts +45 -1
  217. package/src/status/builders.ts +24 -6
  218. package/src/status/reporter.ts +15 -0
  219. package/src/status/snapshot.ts +84 -25
  220. package/src/system/host-memory.ts +29 -12
  221. package/src/types.ts +5 -0
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Spec evaluator — pure function.
3
+ *
4
+ * Given a visible screen text and a CliSpec, returns:
5
+ * - which sections cover which line ranges
6
+ * - which state matched and why
7
+ * - extracted modal title + buttons (if any)
8
+ * - which control_bar entries are visible
9
+ * - which notifications/delegates this evaluation activates
10
+ * - a trace object that explains every decision (for the inspector)
11
+ *
12
+ * No I/O, no state.
13
+ */
14
+ 'use strict';
15
+
16
+ import type {
17
+ CliSpec, Section, SectionRegex, SectionPattern, SpecState,
18
+ ControlAction, NotificationRule, DelegateTrigger,
19
+ } from './types.js';
20
+
21
+ export interface ResolvedSection {
22
+ id: string;
23
+ fromLine: number; // inclusive
24
+ toLine: number; // exclusive
25
+ text: string;
26
+ }
27
+
28
+ export interface ModalSnapshot {
29
+ title: string | null;
30
+ buttons: { index: number; label: string; key: string }[];
31
+ }
32
+
33
+ export interface VisibleControl {
34
+ id: string;
35
+ label: string;
36
+ actionType: 'send_keys' | 'open_picker' | 'attach_image';
37
+ }
38
+
39
+ export interface FiredNotification {
40
+ id: string;
41
+ title: string;
42
+ body: string;
43
+ }
44
+
45
+ export interface FiredDelegate {
46
+ id: string;
47
+ task: string;
48
+ }
49
+
50
+ export interface TraceEntry {
51
+ kind: 'section' | 'state_match' | 'state_skip' | 'modal' | 'control' | 'notification' | 'delegate';
52
+ text: string;
53
+ }
54
+
55
+ export interface SpecEvaluation {
56
+ state: { id: string; label: string; title: string | null };
57
+ modal: ModalSnapshot | null;
58
+ controls: VisibleControl[];
59
+ notifications: FiredNotification[];
60
+ delegates: FiredDelegate[];
61
+ sections: ResolvedSection[];
62
+ trace: TraceEntry[];
63
+ }
64
+
65
+ // ────────────────────────────────────────────────────────────────────────────
66
+ // Layout
67
+ // ────────────────────────────────────────────────────────────────────────────
68
+
69
+ function resolveSize(size: number | string | undefined, total: number): number {
70
+ if (size === undefined) return 0;
71
+ if (typeof size === 'number') return Math.max(0, Math.min(total, size));
72
+ const m = /^(\d+(?:\.\d+)?)%$/.exec(size);
73
+ if (!m) return 0;
74
+ const pct = Number(m[1]);
75
+ return Math.max(0, Math.min(total, Math.round((total * pct) / 100)));
76
+ }
77
+
78
+ function resolveSections(spec: CliSpec, lines: string[]): ResolvedSection[] {
79
+ const total = lines.length;
80
+ const resolved: ResolvedSection[] = [];
81
+ // Two-pass: first resolve from_top / from_bottom without `until`.
82
+ const anchored = new Map<string, { fromLine: number; toLine: number }>();
83
+
84
+ for (const sec of spec.layout.sections) {
85
+ let from = 0;
86
+ let to = total;
87
+ if (sec.from_top !== undefined) {
88
+ from = resolveSize(sec.from_top, total);
89
+ }
90
+ if (sec.from_bottom !== undefined) {
91
+ const sz = resolveSize(sec.from_bottom, total);
92
+ from = total - sz;
93
+ to = total;
94
+ }
95
+ anchored.set(sec.id, { fromLine: from, toLine: to });
96
+ }
97
+
98
+ // Second pass: apply `until` references.
99
+ for (const sec of spec.layout.sections) {
100
+ let { fromLine, toLine } = anchored.get(sec.id)!;
101
+ if (sec.until) {
102
+ const target = anchored.get(sec.until.section);
103
+ if (target) toLine = target.fromLine;
104
+ }
105
+ if (toLine < fromLine) toLine = fromLine;
106
+ const text = lines.slice(fromLine, toLine).join('\n');
107
+ resolved.push({ id: sec.id, fromLine, toLine, text });
108
+ }
109
+
110
+ return resolved;
111
+ }
112
+
113
+ function sectionText(sections: ResolvedSection[], sectionId: string | undefined, fullScreen: string): string {
114
+ if (!sectionId) return fullScreen;
115
+ const found = sections.find(s => s.id === sectionId);
116
+ return found ? found.text : '';
117
+ }
118
+
119
+ function compileRegex(ref: { regex: string; flags?: string }): RegExp {
120
+ return new RegExp(ref.regex, ref.flags ?? 'i');
121
+ }
122
+
123
+ function compilePattern(ref: { pattern: string; flags?: string }): RegExp {
124
+ const flags = ref.flags ?? 'gm';
125
+ return new RegExp(ref.pattern, flags.includes('g') ? flags : flags + 'g');
126
+ }
127
+
128
+ function compileLinePattern(ref: { pattern: string; flags?: string }): RegExp {
129
+ const flags = (ref.flags ?? 'm').replace(/g/g, '');
130
+ return new RegExp(ref.pattern, flags);
131
+ }
132
+
133
+ // ────────────────────────────────────────────────────────────────────────────
134
+ // State matching
135
+ // ────────────────────────────────────────────────────────────────────────────
136
+
137
+ function matchState(
138
+ state: SpecState,
139
+ sections: ResolvedSection[],
140
+ fullScreen: string,
141
+ trace: TraceEntry[],
142
+ ): { matched: boolean; title: string | null } {
143
+ const haystack = sectionText(sections, state.when.section, fullScreen);
144
+ const re = compileRegex(state.when);
145
+ if (!re.test(haystack)) {
146
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] when ${state.when.section ?? '*'}~/${state.when.regex}/ no match` });
147
+ return { matched: false, title: null };
148
+ }
149
+ trace.push({ kind: 'state_match', text: `state[${state.id}] matched via ${state.when.section ?? '*'}~/${state.when.regex}/` });
150
+
151
+ let title: string | null = null;
152
+ if (state.extract_title) {
153
+ const titleHay = sectionText(sections, state.extract_title.section, fullScreen);
154
+ const m = compileRegex(state.extract_title).exec(titleHay);
155
+ if (m) title = (m[1] ?? m[0]).trim();
156
+ trace.push({ kind: 'state_match', text: `state[${state.id}] extract_title → ${title ?? '(none)'}` });
157
+ }
158
+ return { matched: true, title };
159
+ }
160
+
161
+ function extractModal(
162
+ state: SpecState,
163
+ sections: ResolvedSection[],
164
+ fullScreen: string,
165
+ title: string | null,
166
+ trace: TraceEntry[],
167
+ ): ModalSnapshot | null {
168
+ if (!state.modal_buttons) return null;
169
+ const hay = sectionText(sections, state.modal_buttons.section, fullScreen);
170
+ const buttons: { index: number; label: string; key: string }[] = [];
171
+ if (state.modal_buttons.continuation_lines) {
172
+ const re = compileLinePattern(state.modal_buttons);
173
+ const lines = hay.split('\n');
174
+ for (let i = 0; i < lines.length; i += 1) {
175
+ const m = re.exec(lines[i]);
176
+ if (!m) continue;
177
+ const idx = Number(m[1]);
178
+ let label = String(m[2] ?? '').trim();
179
+ if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
180
+ let j = i + 1;
181
+ while (j < lines.length) {
182
+ const next = lines[j];
183
+ if (!next.trim()) break;
184
+ if (re.test(next)) break;
185
+ if (!/^\s+/.test(next)) break;
186
+ label += ' ' + next.trim();
187
+ j += 1;
188
+ }
189
+ if (buttons.some(b => b.index === idx)) continue;
190
+ const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
191
+ buttons.push({ index: idx, label, key });
192
+ i = j - 1;
193
+ }
194
+ } else {
195
+ const re = compilePattern(state.modal_buttons);
196
+ let m: RegExpExecArray | null;
197
+ while ((m = re.exec(hay)) !== null) {
198
+ const idx = Number(m[1]);
199
+ const label = String(m[2] ?? '').trim();
200
+ if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
201
+ if (buttons.some(b => b.index === idx)) continue;
202
+ const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
203
+ buttons.push({ index: idx, label, key });
204
+ }
205
+ }
206
+ buttons.sort((a, b) => a.index - b.index);
207
+ const minCount = state.modal_buttons.min_count ?? 2;
208
+ if (buttons.length < minCount) {
209
+ trace.push({ kind: 'modal', text: `modal_buttons matched ${buttons.length}/${minCount} required — discarded` });
210
+ return null;
211
+ }
212
+ trace.push({ kind: 'modal', text: `modal_buttons matched ${buttons.length} choices` });
213
+ return { title, buttons };
214
+ }
215
+
216
+ // ────────────────────────────────────────────────────────────────────────────
217
+ // Public evaluator
218
+ // ────────────────────────────────────────────────────────────────────────────
219
+
220
+ export function evaluate(spec: CliSpec, screenText: string): SpecEvaluation {
221
+ const trace: TraceEntry[] = [];
222
+ const lines = screenText.split('\n');
223
+ const sections = resolveSections(spec, lines);
224
+ for (const s of sections) {
225
+ trace.push({ kind: 'section', text: `section[${s.id}] lines [${s.fromLine}, ${s.toLine}) (${s.toLine - s.fromLine} lines)` });
226
+ }
227
+
228
+ let activeState: { id: string; label: string; title: string | null } | null = null;
229
+ let modal: ModalSnapshot | null = null;
230
+
231
+ for (const st of spec.states) {
232
+ const { matched, title } = matchState(st, sections, screenText, trace);
233
+ if (!matched) continue;
234
+ activeState = { id: st.id, label: st.label, title };
235
+ modal = extractModal(st, sections, screenText, title, trace);
236
+ break;
237
+ }
238
+
239
+ if (!activeState) {
240
+ const def = spec.states.find(s => s.id === spec.default_state);
241
+ if (def) {
242
+ activeState = { id: def.id, label: def.label, title: null };
243
+ trace.push({ kind: 'state_match', text: `(no state matched — fallback to default_state ${def.id})` });
244
+ } else {
245
+ activeState = { id: spec.default_state, label: spec.default_state, title: null };
246
+ trace.push({ kind: 'state_match', text: `(no state matched, no default_state defined — using id "${spec.default_state}")` });
247
+ }
248
+ }
249
+
250
+ const controls: VisibleControl[] = [];
251
+ for (const c of spec.control_bar ?? []) {
252
+ const visible = !c.visible_when_state || c.visible_when_state.includes(activeState.id);
253
+ trace.push({ kind: 'control', text: `control[${c.id}] visible=${visible}${c.visible_when_state ? ` (when_state ${c.visible_when_state.join('|')})` : ''}` });
254
+ if (visible) controls.push({ id: c.id, label: c.label, actionType: c.action.type });
255
+ }
256
+
257
+ const notifications: FiredNotification[] = [];
258
+ for (const n of spec.notifications ?? []) {
259
+ if (n.when_state !== activeState.id) continue;
260
+ const body = interpolate(n.body ?? '', activeState, sections);
261
+ notifications.push({ id: n.id, title: n.title, body });
262
+ trace.push({ kind: 'notification', text: `notification[${n.id}] fired (when_state=${n.when_state})` });
263
+ }
264
+
265
+ const delegates: FiredDelegate[] = [];
266
+ for (const d of spec.delegate ?? []) {
267
+ if (d.when_state !== activeState.id) continue;
268
+ // after_duration_ms is enforced by the driver, not the evaluator
269
+ // (the evaluator is pure / stateless). The driver bundles a
270
+ // timer per delegate trigger and emits when it fires.
271
+ delegates.push({ id: d.id, task: interpolate(d.task_template, activeState, sections) });
272
+ }
273
+
274
+ return {
275
+ state: activeState,
276
+ modal,
277
+ controls,
278
+ notifications,
279
+ delegates,
280
+ sections,
281
+ trace,
282
+ };
283
+ }
284
+
285
+ function interpolate(
286
+ template: string,
287
+ state: { id: string; label: string; title: string | null },
288
+ sections: ResolvedSection[],
289
+ ): string {
290
+ return template
291
+ .replace(/\{state\.label\}/g, state.label)
292
+ .replace(/\{state\.title\}/g, state.title ?? '')
293
+ .replace(/\{state\.id\}/g, state.id)
294
+ .replace(/\{screen\.([a-z][a-z0-9_]*)\}/g, (_, id: string) => {
295
+ const s = sections.find(s => s.id === id);
296
+ return s ? s.text : '';
297
+ });
298
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * spec.json loader + strict validator. Rejects spec files that don't
3
+ * conform to schema.json — unknown fields, missing required fields,
4
+ * wrong types, regex strings that don't compile, state references
5
+ * that don't exist, etc.
6
+ *
7
+ * Validation runs once at load. Hot reload re-runs validation.
8
+ */
9
+ 'use strict';
10
+
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import Ajv from 'ajv';
14
+ import type { CliSpec } from './types.js';
15
+ import { SCHEMA as schema } from './schema.gen.js';
16
+
17
+ const ajv = new Ajv({ allErrors: true, strict: false });
18
+ const validate = ajv.compile<CliSpec>(schema);
19
+
20
+ export interface SpecLoadResult {
21
+ ok: true;
22
+ spec: CliSpec;
23
+ sourcePath: string;
24
+ }
25
+
26
+ export interface SpecLoadError {
27
+ ok: false;
28
+ errors: string[];
29
+ sourcePath: string;
30
+ }
31
+
32
+ export function loadSpec(sourcePath: string): SpecLoadResult | SpecLoadError {
33
+ let raw: unknown;
34
+ try {
35
+ const text = fs.readFileSync(sourcePath, 'utf8');
36
+ raw = JSON.parse(text);
37
+ } catch (err) {
38
+ return { ok: false, errors: [`Failed to read spec: ${(err as Error).message}`], sourcePath };
39
+ }
40
+
41
+ if (!validate(raw)) {
42
+ const errors = (validate.errors || []).map(e => `${e.instancePath || '(root)'} ${e.message ?? 'invalid'}`);
43
+ return { ok: false, errors, sourcePath };
44
+ }
45
+
46
+ const spec = raw as CliSpec;
47
+ const extra = validateRefs(spec);
48
+ if (extra.length > 0) return { ok: false, errors: extra, sourcePath };
49
+
50
+ return { ok: true, spec, sourcePath };
51
+ }
52
+
53
+ /**
54
+ * Cross-field validation that JSON Schema can't express on its own:
55
+ * - section references actually point to a defined section
56
+ * - state references in control_bar/notifications/delegate exist
57
+ * - default_state exists
58
+ * - regex / pattern strings compile
59
+ */
60
+ function validateRefs(spec: CliSpec): string[] {
61
+ const errs: string[] = [];
62
+ const sectionIds = new Set(spec.layout.sections.map(s => s.id));
63
+ const stateIds = new Set(spec.states.map(s => s.id));
64
+
65
+ if (!stateIds.has(spec.default_state)) {
66
+ errs.push(`default_state "${spec.default_state}" is not defined in states[]`);
67
+ }
68
+
69
+ for (const s of spec.states) {
70
+ checkSectionRef(s.when, `states[${s.id}].when`, sectionIds, errs);
71
+ compileRegex(s.when.regex, s.when.flags, `states[${s.id}].when.regex`, errs);
72
+ if (s.extract_title) {
73
+ checkSectionRef(s.extract_title, `states[${s.id}].extract_title`, sectionIds, errs);
74
+ compileRegex(s.extract_title.regex, s.extract_title.flags, `states[${s.id}].extract_title.regex`, errs);
75
+ }
76
+ if (s.modal_buttons) {
77
+ if (s.modal_buttons.section && !sectionIds.has(s.modal_buttons.section)) {
78
+ errs.push(`states[${s.id}].modal_buttons.section "${s.modal_buttons.section}" unknown`);
79
+ }
80
+ compileRegex(s.modal_buttons.pattern, s.modal_buttons.flags ?? 'm', `states[${s.id}].modal_buttons.pattern`, errs);
81
+ }
82
+ }
83
+
84
+ for (const c of spec.control_bar ?? []) {
85
+ for (const stId of c.visible_when_state ?? []) {
86
+ if (!stateIds.has(stId)) errs.push(`control_bar[${c.id}].visible_when_state references unknown state "${stId}"`);
87
+ }
88
+ if (c.action.type === 'open_picker') {
89
+ checkSectionRef(c.action.wait_for, `control_bar[${c.id}].action.wait_for`, sectionIds, errs);
90
+ compileRegex(c.action.wait_for.regex, c.action.wait_for.flags, `control_bar[${c.id}].action.wait_for.regex`, errs);
91
+ if (c.action.extract_choices.section && !sectionIds.has(c.action.extract_choices.section)) {
92
+ errs.push(`control_bar[${c.id}].action.extract_choices.section "${c.action.extract_choices.section}" unknown`);
93
+ }
94
+ compileRegex(c.action.extract_choices.pattern, c.action.extract_choices.flags ?? 'm', `control_bar[${c.id}].action.extract_choices.pattern`, errs);
95
+ }
96
+ }
97
+
98
+ for (const n of spec.notifications ?? []) {
99
+ if (!stateIds.has(n.when_state)) errs.push(`notifications[${n.id}].when_state "${n.when_state}" unknown`);
100
+ }
101
+ for (const d of spec.delegate ?? []) {
102
+ if (!stateIds.has(d.when_state)) errs.push(`delegate[${d.id}].when_state "${d.when_state}" unknown`);
103
+ }
104
+
105
+ // native_history: exactly one of {reader, source, override_path}.
106
+ const nh = spec.native_history;
107
+ if (nh) {
108
+ const modes = (['reader', 'source', 'override_path'] as const).filter(k => (nh as Record<string, unknown>)[k] !== undefined);
109
+ if (modes.length === 0) {
110
+ errs.push('native_history must set exactly one of {reader, source, override_path}');
111
+ } else if (modes.length > 1) {
112
+ errs.push(`native_history sets ${modes.length} modes (${modes.join(', ')}); pick exactly one`);
113
+ }
114
+ }
115
+
116
+ return errs;
117
+ }
118
+
119
+ function checkSectionRef(ref: { section?: string }, where: string, valid: Set<string>, errs: string[]): void {
120
+ if (ref.section && !valid.has(ref.section)) errs.push(`${where}.section "${ref.section}" unknown`);
121
+ }
122
+
123
+ function compileRegex(source: string, flags: string | undefined, where: string, errs: string[]): void {
124
+ try { new RegExp(source, flags ?? ''); } catch (err) { errs.push(`${where} regex invalid: ${(err as Error).message}`); }
125
+ }
126
+
127
+ /** Convenience: look up a provider's spec.json next to its provider dir. */
128
+ export function resolveSpecPath(providerDir: string): string {
129
+ return path.join(providerDir, 'spec.json');
130
+ }