@adhdev/daemon-core 0.9.82-rc.20 → 0.9.82-rc.201

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 (241) hide show
  1. package/dist/boot/daemon-lifecycle.d.ts +1 -0
  2. package/dist/boot/process-hardening.d.ts +50 -0
  3. package/dist/chat/source-machine.d.ts +166 -0
  4. package/dist/chat/source-resolver.d.ts +104 -0
  5. package/dist/chat/subscription-updates.d.ts +1 -0
  6. package/dist/cli-adapter-types.d.ts +23 -2
  7. package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
  8. package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
  9. package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
  10. package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
  11. package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
  12. package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
  13. package/dist/commands/cli-manager.d.ts +2 -1
  14. package/dist/commands/handler.d.ts +110 -0
  15. package/dist/commands/mesh-coordinator.d.ts +85 -1
  16. package/dist/commands/router.d.ts +26 -1
  17. package/dist/config/chat-history.d.ts +9 -0
  18. package/dist/config/config.d.ts +5 -0
  19. package/dist/config/mesh-config.d.ts +72 -1
  20. package/dist/git/git-commands.d.ts +7 -1
  21. package/dist/git/git-types.d.ts +2 -0
  22. package/dist/index.d.ts +39 -10
  23. package/dist/index.js +25488 -8333
  24. package/dist/index.js.map +1 -1
  25. package/dist/index.mjs +29124 -12045
  26. package/dist/index.mjs.map +1 -1
  27. package/dist/installer.d.ts +1 -4
  28. package/dist/ipc/local-ipc-server.d.ts +91 -0
  29. package/dist/launch.d.ts +1 -1
  30. package/dist/logging/async-batch-writer.d.ts +10 -0
  31. package/dist/mesh/contracts.d.ts +164 -0
  32. package/dist/mesh/coordinator-prompt.d.ts +30 -0
  33. package/dist/mesh/coordinator-registry.d.ts +59 -0
  34. package/dist/mesh/mesh-active-work.d.ts +90 -0
  35. package/dist/mesh/mesh-delivery-policy.d.ts +126 -0
  36. package/dist/mesh/mesh-events.d.ts +164 -6
  37. package/dist/mesh/mesh-fast-forward.d.ts +41 -0
  38. package/dist/mesh/mesh-host-ownership.d.ts +9 -0
  39. package/dist/mesh/mesh-ledger.d.ts +58 -1
  40. package/dist/mesh/mesh-refine-status.d.ts +26 -0
  41. package/dist/mesh/mesh-runtime-store.d.ts +149 -0
  42. package/dist/mesh/mesh-work-queue.d.ts +55 -6
  43. package/dist/mesh/preview-freshness.d.ts +18 -0
  44. package/dist/mesh/refine-config.d.ts +193 -0
  45. package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
  46. package/dist/providers/approval-utils.d.ts +13 -0
  47. package/dist/providers/cli-provider-instance.d.ts +39 -3
  48. package/dist/providers/contracts.d.ts +130 -6
  49. package/dist/providers/external-sources.d.ts +71 -0
  50. package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
  51. package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
  52. package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
  53. package/dist/providers/native-history/dispatcher.d.ts +27 -0
  54. package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
  55. package/dist/providers/native-history/index.d.ts +13 -0
  56. package/dist/providers/provider-instance-manager.d.ts +12 -0
  57. package/dist/providers/provider-instance.d.ts +13 -1
  58. package/dist/providers/provider-loader.d.ts +26 -4
  59. package/dist/providers/provider-trust.d.ts +31 -0
  60. package/dist/providers/read-chat-contract.d.ts +29 -0
  61. package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
  62. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
  63. package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
  64. package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
  65. package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
  66. package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
  67. package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
  68. package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
  69. package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
  70. package/dist/providers/sdk/v1/index.d.ts +30 -0
  71. package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
  72. package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
  73. package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
  74. package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
  75. package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
  76. package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
  77. package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
  78. package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
  79. package/dist/providers/spec/adapter.d.ts +60 -0
  80. package/dist/providers/spec/cli-adapter.d.ts +101 -0
  81. package/dist/providers/spec/driver.d.ts +195 -0
  82. package/dist/providers/spec/evaluator.d.ts +55 -0
  83. package/dist/providers/spec/loader.d.ts +14 -0
  84. package/dist/providers/spec/native-history-executor.d.ts +41 -0
  85. package/dist/providers/spec/route.d.ts +4 -0
  86. package/dist/providers/spec/schema.gen.d.ts +599 -0
  87. package/dist/providers/spec/types.d.ts +282 -0
  88. package/dist/providers/transcript-v2.d.ts +176 -0
  89. package/dist/providers/types/interactive-prompt.d.ts +48 -0
  90. package/dist/repo-mesh-types.d.ts +108 -1
  91. package/dist/sessions/registry.d.ts +3 -0
  92. package/dist/shared-types-extra.d.ts +1 -1
  93. package/dist/shared-types.d.ts +42 -1
  94. package/dist/status/normalize.d.ts +1 -1
  95. package/dist/status/normalize.js +1 -0
  96. package/dist/status/normalize.js.map +1 -1
  97. package/dist/status/normalize.mjs +1 -0
  98. package/dist/status/normalize.mjs.map +1 -1
  99. package/dist/status/reporter.d.ts +2 -0
  100. package/dist/status/snapshot.d.ts +1 -0
  101. package/dist/types.d.ts +5 -0
  102. package/package.json +7 -2
  103. package/src/agent-stream/poller.ts +2 -3
  104. package/src/boot/daemon-lifecycle.ts +19 -10
  105. package/src/boot/process-hardening.ts +89 -0
  106. package/src/chat/source-machine.ts +534 -0
  107. package/src/chat/source-resolver.ts +0 -0
  108. package/src/chat/subscription-updates.ts +20 -1
  109. package/src/cli-adapter-types.d.ts +1 -0
  110. package/src/cli-adapter-types.ts +22 -2
  111. package/src/cli-adapters/cli-script-runner.ts +421 -0
  112. package/src/cli-adapters/cli-state-engine.ts +1138 -0
  113. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  114. package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
  115. package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
  116. package/src/cli-adapters/provider-cli-parse.ts +13 -0
  117. package/src/cli-adapters/provider-cli-runtime.ts +3 -1
  118. package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
  119. package/src/cli-adapters/provider-cli-shared.ts +59 -11
  120. package/src/cli-adapters/raw-terminal-io.ts +252 -0
  121. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
  122. package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
  123. package/src/commands/chat-commands.ts +1787 -60
  124. package/src/commands/cli-manager.ts +283 -14
  125. package/src/commands/handler.ts +809 -2
  126. package/src/commands/mesh-coordinator.ts +331 -122
  127. package/src/commands/router.ts +3628 -515
  128. package/src/config/chat-history.ts +95 -24
  129. package/src/config/config.ts +12 -0
  130. package/src/config/mesh-config.ts +280 -2
  131. package/src/config/recent-activity.ts +8 -2
  132. package/src/daemon/dev-cli-debug.ts +10 -1
  133. package/src/detection/ide-detector.ts +26 -16
  134. package/src/git/git-commands.ts +37 -7
  135. package/src/git/git-status.ts +35 -6
  136. package/src/git/git-types.ts +2 -0
  137. package/src/git/git-worktree.ts +8 -1
  138. package/src/index.ts +119 -9
  139. package/src/installer.d.ts +1 -1
  140. package/src/installer.ts +8 -6
  141. package/src/ipc/local-ipc-server.ts +278 -0
  142. package/src/launch.d.ts +1 -1
  143. package/src/launch.ts +37 -28
  144. package/src/logging/async-batch-writer.ts +55 -0
  145. package/src/logging/logger.ts +2 -1
  146. package/src/mesh/contracts.ts +329 -0
  147. package/src/mesh/coordinator-prompt.ts +204 -30
  148. package/src/mesh/coordinator-registry.ts +121 -0
  149. package/src/mesh/mesh-active-work.ts +437 -0
  150. package/src/mesh/mesh-delivery-policy.ts +298 -0
  151. package/src/mesh/mesh-events.ts +1341 -109
  152. package/src/mesh/mesh-fast-forward.ts +438 -0
  153. package/src/mesh/mesh-host-ownership.ts +73 -0
  154. package/src/mesh/mesh-ledger.ts +457 -104
  155. package/src/mesh/mesh-refine-status.ts +144 -0
  156. package/src/mesh/mesh-runtime-store.ts +769 -0
  157. package/src/mesh/mesh-work-queue.ts +267 -159
  158. package/src/mesh/preview-freshness.ts +118 -0
  159. package/src/mesh/refine-config.ts +366 -0
  160. package/src/mesh/worktree-bootstrap-config.ts +247 -0
  161. package/src/providers/approval-utils.d.ts +4 -0
  162. package/src/providers/approval-utils.ts +47 -5
  163. package/src/providers/chat-message-normalization.ts +4 -11
  164. package/src/providers/cli-provider-instance.ts +806 -64
  165. package/src/providers/contracts.d.ts +55 -0
  166. package/src/providers/contracts.ts +141 -6
  167. package/src/providers/external-sources.ts +218 -0
  168. package/src/providers/ide-provider-instance.ts +19 -5
  169. package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
  170. package/src/providers/native-history/claude-cli-transcript.ts +396 -0
  171. package/src/providers/native-history/codex-cli-transcript.ts +419 -0
  172. package/src/providers/native-history/dispatcher.ts +340 -0
  173. package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
  174. package/src/providers/native-history/index.ts +30 -0
  175. package/src/providers/provider-instance-manager.ts +30 -0
  176. package/src/providers/provider-instance.ts +10 -1
  177. package/src/providers/provider-loader.ts +582 -50
  178. package/src/providers/provider-schema.ts +87 -14
  179. package/src/providers/provider-trust.ts +114 -0
  180. package/src/providers/read-chat-contract.ts +76 -16
  181. package/src/providers/sdk/README.md +49 -0
  182. package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
  183. package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
  184. package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
  185. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
  186. package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
  187. package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
  188. package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
  189. package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
  190. package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
  191. package/src/providers/sdk/v1/index.ts +152 -0
  192. package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
  193. package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
  194. package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
  195. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
  196. package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
  197. package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
  198. package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
  199. package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
  200. package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
  201. package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
  202. package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
  203. package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
  204. package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
  205. package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
  206. package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
  207. package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
  208. package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
  209. package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
  210. package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
  211. package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
  212. package/src/providers/sdk/v1/types/cli/index.ts +365 -0
  213. package/src/providers/sdk/v1/types/common/index.ts +229 -0
  214. package/src/providers/sdk/v1/validators/index.ts +19 -0
  215. package/src/providers/sdk/v1/validators/manifest.ts +110 -0
  216. package/src/providers/sdk/v1/validators/taint.ts +309 -0
  217. package/src/providers/spec/adapter.ts +176 -0
  218. package/src/providers/spec/cli-adapter.ts +573 -0
  219. package/src/providers/spec/driver.ts +733 -0
  220. package/src/providers/spec/evaluator.ts +373 -0
  221. package/src/providers/spec/loader.ts +130 -0
  222. package/src/providers/spec/native-history-executor.ts +939 -0
  223. package/src/providers/spec/route.ts +51 -0
  224. package/src/providers/spec/schema.gen.ts +559 -0
  225. package/src/providers/spec/schema.json +237 -0
  226. package/src/providers/spec/types.ts +301 -0
  227. package/src/providers/transcript-v2.ts +567 -0
  228. package/src/providers/types/interactive-prompt.ts +425 -0
  229. package/src/providers/version-archive.ts +38 -20
  230. package/src/repo-mesh-types.ts +118 -1
  231. package/src/sessions/registry.ts +3 -0
  232. package/src/shared-types-extra.ts +1 -1
  233. package/src/shared-types.ts +45 -1
  234. package/src/status/builders.ts +24 -6
  235. package/src/status/normalize.ts +2 -0
  236. package/src/status/reporter.ts +15 -0
  237. package/src/status/snapshot.ts +84 -25
  238. package/src/system/host-memory.ts +29 -12
  239. package/src/types.ts +5 -0
  240. package/dist/mesh/mesh-sync.d.ts +0 -53
  241. package/src/mesh/mesh-sync.ts +0 -111
@@ -0,0 +1,373 @@
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.anchor_regex !== undefined) {
88
+ try {
89
+ const re = new RegExp(sec.anchor_regex, sec.anchor_flags ?? '');
90
+ const prevRe = sec.anchor_context?.prev !== undefined
91
+ ? new RegExp(sec.anchor_context.prev, sec.anchor_context.prev_flags ?? '') : null;
92
+ const nextRe = sec.anchor_context?.next !== undefined
93
+ ? new RegExp(sec.anchor_context.next, sec.anchor_context.next_flags ?? '') : null;
94
+ const matches = (i: number) => re.test(lines[i])
95
+ && (prevRe === null || (i > 0 && prevRe.test(lines[i - 1])))
96
+ && (nextRe === null || (i < total - 1 && nextRe.test(lines[i + 1])));
97
+ let idx = -1;
98
+ if (sec.anchor_last) {
99
+ for (let i = total - 1; i >= 0; i--) { if (matches(i)) { idx = i; break; } }
100
+ } else {
101
+ for (let i = 0; i < total; i++) { if (matches(i)) { idx = i; break; } }
102
+ }
103
+ if (idx !== -1) {
104
+ from = idx;
105
+ to = total;
106
+ if (sec.until_regex !== undefined) {
107
+ try {
108
+ const ure = new RegExp(sec.until_regex, sec.until_regex_flags ?? '');
109
+ const end = lines.findIndex((l, i) => i > idx && ure.test(l));
110
+ if (end !== -1) to = end;
111
+ } catch { /* bad until_regex — extend to end */ }
112
+ } else if (sec.lines !== undefined) {
113
+ to = Math.min(total, from + sec.lines);
114
+ }
115
+ }
116
+ } catch { /* bad anchor_regex — fall through to defaults */ }
117
+ } else if (sec.from_top !== undefined) {
118
+ from = resolveSize(sec.from_top, total);
119
+ } else if (sec.from_bottom !== undefined) {
120
+ const sz = resolveSize(sec.from_bottom, total);
121
+ from = total - sz;
122
+ to = total;
123
+ }
124
+ anchored.set(sec.id, { fromLine: from, toLine: to });
125
+ }
126
+
127
+ // Second pass: apply `until` references.
128
+ for (const sec of spec.layout.sections) {
129
+ let { fromLine, toLine } = anchored.get(sec.id)!;
130
+ if (sec.until) {
131
+ const target = anchored.get(sec.until.section);
132
+ if (target) toLine = target.fromLine;
133
+ }
134
+ if (toLine < fromLine) toLine = fromLine;
135
+ const text = lines.slice(fromLine, toLine).join('\n');
136
+ resolved.push({ id: sec.id, fromLine, toLine, text });
137
+ }
138
+
139
+ return resolved;
140
+ }
141
+
142
+ function sectionText(sections: ResolvedSection[], sectionId: string | undefined, fullScreen: string): string {
143
+ if (!sectionId) return fullScreen;
144
+ const found = sections.find(s => s.id === sectionId);
145
+ return found ? found.text : '';
146
+ }
147
+
148
+ function compileRegex(ref: { regex: string; flags?: string }): RegExp {
149
+ return new RegExp(ref.regex, ref.flags ?? 'i');
150
+ }
151
+
152
+ function compilePattern(ref: { pattern: string; flags?: string }): RegExp {
153
+ const flags = ref.flags ?? 'gm';
154
+ return new RegExp(ref.pattern, flags.includes('g') ? flags : flags + 'g');
155
+ }
156
+
157
+ function compileLinePattern(ref: { pattern: string; flags?: string }): RegExp {
158
+ const flags = (ref.flags ?? 'm').replace(/g/g, '');
159
+ return new RegExp(ref.pattern, flags);
160
+ }
161
+
162
+ // ────────────────────────────────────────────────────────────────────────────
163
+ // State matching
164
+ // ────────────────────────────────────────────────────────────────────────────
165
+
166
+ function matchState(
167
+ state: SpecState,
168
+ sections: ResolvedSection[],
169
+ fullScreen: string,
170
+ trace: TraceEntry[],
171
+ cursor?: { row: number; col: number },
172
+ ): { matched: boolean; title: string | null } {
173
+ const haystack = sectionText(sections, state.when.section, fullScreen);
174
+ const re = compileRegex(state.when);
175
+ if (!re.test(haystack)) {
176
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] when ${state.when.section ?? '*'}~/${state.when.regex}/ no match` });
177
+ return { matched: false, title: null };
178
+ }
179
+
180
+ // Cursor-position guards: check row/col bounds when the state declares them.
181
+ // Guards are skipped entirely when the caller did not supply a cursor position
182
+ // (cursor === undefined) so existing pure-text evaluation is unaffected.
183
+ if (cursor !== undefined) {
184
+ const w = state.when;
185
+ if (w.cursor_row_min !== undefined && cursor.row < w.cursor_row_min) {
186
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor row ${cursor.row} < cursor_row_min ${w.cursor_row_min}` });
187
+ return { matched: false, title: null };
188
+ }
189
+ if (w.cursor_row_max !== undefined && cursor.row > w.cursor_row_max) {
190
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor row ${cursor.row} > cursor_row_max ${w.cursor_row_max}` });
191
+ return { matched: false, title: null };
192
+ }
193
+ if (w.cursor_col_min !== undefined && cursor.col < w.cursor_col_min) {
194
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor col ${cursor.col} < cursor_col_min ${w.cursor_col_min}` });
195
+ return { matched: false, title: null };
196
+ }
197
+ if (w.cursor_col_max !== undefined && cursor.col > w.cursor_col_max) {
198
+ trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor col ${cursor.col} > cursor_col_max ${w.cursor_col_max}` });
199
+ return { matched: false, title: null };
200
+ }
201
+ }
202
+
203
+ trace.push({ kind: 'state_match', text: `state[${state.id}] matched via ${state.when.section ?? '*'}~/${state.when.regex}/${cursor !== undefined ? ` cursor=(${cursor.row},${cursor.col})` : ''}` });
204
+
205
+ let title: string | null = null;
206
+ if (state.extract_title) {
207
+ const titleHay = sectionText(sections, state.extract_title.section, fullScreen);
208
+ const m = compileRegex(state.extract_title).exec(titleHay);
209
+ if (m) title = (m[1] ?? m[0]).trim();
210
+ trace.push({ kind: 'state_match', text: `state[${state.id}] extract_title → ${title ?? '(none)'}` });
211
+ }
212
+ return { matched: true, title };
213
+ }
214
+
215
+ function extractModal(
216
+ state: SpecState,
217
+ sections: ResolvedSection[],
218
+ fullScreen: string,
219
+ title: string | null,
220
+ trace: TraceEntry[],
221
+ ): ModalSnapshot | null {
222
+ if (!state.modal_buttons) return null;
223
+ const hay = sectionText(sections, state.modal_buttons.section, fullScreen);
224
+ const buttons: { index: number; label: string; key: string }[] = [];
225
+ if (state.modal_buttons.continuation_lines) {
226
+ const re = compileLinePattern(state.modal_buttons);
227
+ const lines = hay.split('\n');
228
+ for (let i = 0; i < lines.length; i += 1) {
229
+ const m = re.exec(lines[i]);
230
+ if (!m) continue;
231
+ const idx = Number(m[1]);
232
+ let label = String(m[2] ?? '').trim();
233
+ if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
234
+ let j = i + 1;
235
+ while (j < lines.length) {
236
+ const next = lines[j];
237
+ if (!next.trim()) break;
238
+ if (re.test(next)) break;
239
+ if (!/^\s+/.test(next)) break;
240
+ label += ' ' + next.trim();
241
+ j += 1;
242
+ }
243
+ if (buttons.some(b => b.index === idx)) continue;
244
+ const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
245
+ buttons.push({ index: idx, label, key });
246
+ i = j - 1;
247
+ }
248
+ } else {
249
+ const re = compilePattern(state.modal_buttons);
250
+ let m: RegExpExecArray | null;
251
+ while ((m = re.exec(hay)) !== null) {
252
+ const idx = Number(m[1]);
253
+ const label = String(m[2] ?? '').trim();
254
+ if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
255
+ if (buttons.some(b => b.index === idx)) continue;
256
+ const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
257
+ buttons.push({ index: idx, label, key });
258
+ }
259
+ }
260
+ buttons.sort((a, b) => a.index - b.index);
261
+ const minCount = state.modal_buttons.min_count ?? 2;
262
+ if (buttons.length < minCount) {
263
+ trace.push({ kind: 'modal', text: `modal_buttons matched ${buttons.length}/${minCount} required — discarded` });
264
+ return null;
265
+ }
266
+ trace.push({ kind: 'modal', text: `modal_buttons matched ${buttons.length} choices` });
267
+ return { title, buttons };
268
+ }
269
+
270
+ // ────────────────────────────────────────────────────────────────────────────
271
+ // Public evaluator
272
+ // ────────────────────────────────────────────────────────────────────────────
273
+
274
+ export function evaluate(
275
+ spec: CliSpec,
276
+ screenText: string,
277
+ /** Optional cursor position (0-based row and col). When supplied, states
278
+ * with cursor_row_min/max or cursor_col_min/max predicates are filtered.
279
+ * When omitted, cursor predicates are ignored and evaluation is text-only
280
+ * (backward-compatible with all existing specs and call sites). */
281
+ cursor?: { row: number; col: number },
282
+ ): SpecEvaluation {
283
+ const trace: TraceEntry[] = [];
284
+ const lines = screenText.split('\n');
285
+ const sections = resolveSections(spec, lines);
286
+ for (const s of sections) {
287
+ trace.push({ kind: 'section', text: `section[${s.id}] lines [${s.fromLine}, ${s.toLine}) (${s.toLine - s.fromLine} lines)` });
288
+ }
289
+ if (cursor !== undefined) {
290
+ trace.push({ kind: 'section', text: `cursor (${cursor.row}, ${cursor.col})` });
291
+ }
292
+
293
+ let activeState: { id: string; label: string; title: string | null } | null = null;
294
+ let modal: ModalSnapshot | null = null;
295
+
296
+ for (const st of spec.states) {
297
+ const { matched, title } = matchState(st, sections, screenText, trace, cursor);
298
+ if (!matched) continue;
299
+ const extractedModal = extractModal(st, sections, screenText, title, trace);
300
+ // If the state declares modal_buttons but extraction failed (button
301
+ // count below min_count, or text-was-mistaken-for-modal), do not
302
+ // promote the state. Otherwise we would surface a phantom approval
303
+ // built from arbitrary screen text — see claude-cli numbered-list
304
+ // false-positive on 2026-06-07.
305
+ if (st.modal_buttons && !extractedModal) {
306
+ trace.push({ kind: 'state_skip', text: `state[${st.id}] matched but modal_buttons extraction failed — not promoting` });
307
+ continue;
308
+ }
309
+ activeState = { id: st.id, label: st.label, title };
310
+ modal = extractedModal;
311
+ break;
312
+ }
313
+
314
+ if (!activeState) {
315
+ const def = spec.states.find(s => s.id === spec.default_state);
316
+ if (def) {
317
+ activeState = { id: def.id, label: def.label, title: null };
318
+ trace.push({ kind: 'state_match', text: `(no state matched — fallback to default_state ${def.id})` });
319
+ } else {
320
+ activeState = { id: spec.default_state, label: spec.default_state, title: null };
321
+ trace.push({ kind: 'state_match', text: `(no state matched, no default_state defined — using id "${spec.default_state}")` });
322
+ }
323
+ }
324
+
325
+ const controls: VisibleControl[] = [];
326
+ for (const c of spec.control_bar ?? []) {
327
+ const visible = !c.visible_when_state || c.visible_when_state.includes(activeState.id);
328
+ trace.push({ kind: 'control', text: `control[${c.id}] visible=${visible}${c.visible_when_state ? ` (when_state ${c.visible_when_state.join('|')})` : ''}` });
329
+ if (visible) controls.push({ id: c.id, label: c.label, actionType: c.action.type });
330
+ }
331
+
332
+ const notifications: FiredNotification[] = [];
333
+ for (const n of spec.notifications ?? []) {
334
+ if (n.when_state !== activeState.id) continue;
335
+ const body = interpolate(n.body ?? '', activeState, sections);
336
+ notifications.push({ id: n.id, title: n.title, body });
337
+ trace.push({ kind: 'notification', text: `notification[${n.id}] fired (when_state=${n.when_state})` });
338
+ }
339
+
340
+ const delegates: FiredDelegate[] = [];
341
+ for (const d of spec.delegate ?? []) {
342
+ if (d.when_state !== activeState.id) continue;
343
+ // after_duration_ms is enforced by the driver, not the evaluator
344
+ // (the evaluator is pure / stateless). The driver bundles a
345
+ // timer per delegate trigger and emits when it fires.
346
+ delegates.push({ id: d.id, task: interpolate(d.task_template, activeState, sections) });
347
+ }
348
+
349
+ return {
350
+ state: activeState,
351
+ modal,
352
+ controls,
353
+ notifications,
354
+ delegates,
355
+ sections,
356
+ trace,
357
+ };
358
+ }
359
+
360
+ function interpolate(
361
+ template: string,
362
+ state: { id: string; label: string; title: string | null },
363
+ sections: ResolvedSection[],
364
+ ): string {
365
+ return template
366
+ .replace(/\{state\.label\}/g, state.label)
367
+ .replace(/\{state\.title\}/g, state.title ?? '')
368
+ .replace(/\{state\.id\}/g, state.id)
369
+ .replace(/\{screen\.([a-z][a-z0-9_]*)\}/g, (_, id: string) => {
370
+ const s = sections.find(s => s.id === id);
371
+ return s ? s.text : '';
372
+ });
373
+ }
@@ -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
+ }