@reticlehq/server 2.7.0 → 2.9.0

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 (357) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +130 -778
  3. package/dist/bridge/bridge.d.ts +23 -0
  4. package/dist/bridge/bridge.js +80 -2
  5. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  6. package/dist/bridge/bridge.test-harness.js +6 -4
  7. package/dist/capsule/causal-summary.d.ts +36 -1
  8. package/dist/capsule/causal-summary.js +33 -7
  9. package/dist/capsule/divergence.js +7 -1
  10. package/dist/cli/chromium-hint.d.ts +33 -5
  11. package/dist/cli/chromium-hint.js +82 -12
  12. package/dist/cli/cli-doctor.js +65 -34
  13. package/dist/cli/cli-flow-commands.js +8 -8
  14. package/dist/cli/cli-kill.d.ts +58 -0
  15. package/dist/cli/cli-kill.js +162 -0
  16. package/dist/cli/cli-parse.d.ts +16 -1
  17. package/dist/cli/cli-parse.js +32 -9
  18. package/dist/cli/cli-port.d.ts +32 -0
  19. package/dist/cli/cli-port.js +82 -35
  20. package/dist/cli/cli-verify.d.ts +21 -0
  21. package/dist/cli/cli-verify.js +47 -3
  22. package/dist/cli/config-discovery.d.ts +35 -0
  23. package/dist/cli/config-discovery.js +163 -0
  24. package/dist/cli/doctor-daemon-line.js +4 -1
  25. package/dist/cli/doctor-project-line.d.ts +37 -0
  26. package/dist/cli/doctor-project-line.js +35 -0
  27. package/dist/cli/doctor-rows.d.ts +51 -0
  28. package/dist/cli/doctor-rows.js +54 -0
  29. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  30. package/dist/cli/doctor-sessions-line.js +69 -0
  31. package/dist/cli/drive-attach.d.ts +99 -0
  32. package/dist/cli/drive-attach.js +203 -0
  33. package/dist/cli/drive-command.d.ts +13 -0
  34. package/dist/cli/drive-command.js +103 -0
  35. package/dist/cli/open-note.d.ts +20 -0
  36. package/dist/cli/open-note.js +37 -0
  37. package/dist/cli/port-holder.d.ts +10 -0
  38. package/dist/cli/port-holder.js +26 -2
  39. package/dist/cli/status-next-action.d.ts +37 -0
  40. package/dist/cli/status-next-action.js +61 -0
  41. package/dist/cli.js +119 -39
  42. package/dist/crawl/crawl-tools.js +12 -2
  43. package/dist/crawl/crawl.d.ts +13 -0
  44. package/dist/crawl/crawl.js +50 -2
  45. package/dist/events/contradictions.d.ts +46 -0
  46. package/dist/events/contradictions.js +35 -22
  47. package/dist/events/declared.d.ts +45 -0
  48. package/dist/events/declared.js +73 -0
  49. package/dist/events/echo-mismatch.d.ts +8 -2
  50. package/dist/events/echo-mismatch.js +33 -5
  51. package/dist/events/event-filters.d.ts +1 -1
  52. package/dist/events/event-filters.js +7 -4
  53. package/dist/events/predicate-eval.d.ts +17 -72
  54. package/dist/events/predicate-eval.js +117 -236
  55. package/dist/events/predicate-parse.js +31 -3
  56. package/dist/events/predicate-route.d.ts +32 -0
  57. package/dist/events/predicate-route.js +102 -0
  58. package/dist/events/predicate-schema.d.ts +99 -0
  59. package/dist/events/predicate-schema.js +309 -0
  60. package/dist/events/predicate.d.ts +8 -0
  61. package/dist/events/predicate.js +296 -22
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-classify.js +1 -1
  69. package/dist/flows/flow-replay-run.js +8 -1
  70. package/dist/flows/flow-tools.js +21 -5
  71. package/dist/flows/heal-run.js +1 -1
  72. package/dist/flows/replay.js +3 -0
  73. package/dist/honesty/accepted-write.d.ts +3 -0
  74. package/dist/honesty/accepted-write.js +8 -3
  75. package/dist/honesty/already-true.js +15 -0
  76. package/dist/honesty/blind-spots.js +10 -2
  77. package/dist/honesty/honesty.d.ts +11 -0
  78. package/dist/honesty/honesty.js +1 -0
  79. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  80. package/dist/honesty/uncaptured-bodies.js +9 -2
  81. package/dist/honesty/unread-outcome.d.ts +8 -0
  82. package/dist/honesty/unread-outcome.js +36 -9
  83. package/dist/honesty/unsettled.d.ts +49 -0
  84. package/dist/honesty/unsettled.js +82 -0
  85. package/dist/honesty/verified.d.ts +36 -3
  86. package/dist/honesty/verified.js +54 -11
  87. package/dist/http-server.d.ts +19 -0
  88. package/dist/http-server.js +202 -4
  89. package/dist/impact/impact-recorder.d.ts +23 -0
  90. package/dist/impact/impact-recorder.js +76 -0
  91. package/dist/impact/impact-store.d.ts +47 -0
  92. package/dist/impact/impact-store.js +171 -0
  93. package/dist/index.d.ts +7 -0
  94. package/dist/index.js +47 -10
  95. package/dist/init/agent-rules.d.ts +36 -0
  96. package/dist/init/agent-rules.js +132 -22
  97. package/dist/init/astro-patch.d.ts +2 -1
  98. package/dist/init/astro-patch.js +38 -6
  99. package/dist/init/capabilities.d.ts +22 -0
  100. package/dist/init/capabilities.js +76 -0
  101. package/dist/init/confirm.d.ts +114 -0
  102. package/dist/init/confirm.js +165 -0
  103. package/dist/init/connect-steps.d.ts +2 -0
  104. package/dist/init/connect-steps.js +50 -0
  105. package/dist/init/cra.js +3 -2
  106. package/dist/init/csp-check.d.ts +38 -0
  107. package/dist/init/csp-check.js +92 -0
  108. package/dist/init/csp-doctor.d.ts +25 -0
  109. package/dist/init/csp-doctor.js +49 -0
  110. package/dist/init/cursor.js +6 -9
  111. package/dist/init/detect.d.ts +8 -0
  112. package/dist/init/detect.js +33 -3
  113. package/dist/init/mcp-clients.d.ts +1 -0
  114. package/dist/init/mcp-clients.js +51 -7
  115. package/dist/init/mcp.d.ts +15 -0
  116. package/dist/init/mcp.js +19 -0
  117. package/dist/init/non-js-project.js +29 -5
  118. package/dist/init/plan-framework.d.ts +10 -0
  119. package/dist/init/plan-framework.js +78 -16
  120. package/dist/init/plan.d.ts +47 -6
  121. package/dist/init/plan.js +228 -69
  122. package/dist/init/run.d.ts +24 -2
  123. package/dist/init/run.js +157 -45
  124. package/dist/init/slash-command.d.ts +1 -1
  125. package/dist/init/slash-command.js +54 -6
  126. package/dist/init/snippets.d.ts +45 -5
  127. package/dist/init/snippets.js +194 -42
  128. package/dist/init/vite-config.js +53 -5
  129. package/dist/input/network-mock-tools.d.ts +19 -0
  130. package/dist/input/network-mock-tools.js +37 -5
  131. package/dist/input/scroll-find.d.ts +6 -0
  132. package/dist/input/scroll-find.js +17 -2
  133. package/dist/input/scroll-tools.js +4 -0
  134. package/dist/journal/attach-journal.d.ts +9 -1
  135. package/dist/journal/attach-journal.js +13 -2
  136. package/dist/journal/journal-query.d.ts +13 -0
  137. package/dist/journal/journal-query.js +28 -3
  138. package/dist/journal/workspace-gitignore.d.ts +10 -0
  139. package/dist/journal/workspace-gitignore.js +59 -0
  140. package/dist/license/license.js +1 -1
  141. package/dist/mcp/mcp-proxy.d.ts +35 -30
  142. package/dist/mcp/mcp-proxy.js +306 -104
  143. package/dist/mcp/mcp.d.ts +10 -1
  144. package/dist/mcp/mcp.js +70 -34
  145. package/dist/mcp/proxy-handshake.d.ts +82 -1
  146. package/dist/mcp/proxy-handshake.js +109 -2
  147. package/dist/mcp/proxy-lifecycle.d.ts +11 -2
  148. package/dist/mcp/proxy-lifecycle.js +12 -3
  149. package/dist/mcp/proxy-log.d.ts +63 -0
  150. package/dist/mcp/proxy-log.js +132 -0
  151. package/dist/mcp/server-instructions.d.ts +17 -0
  152. package/dist/mcp/server-instructions.js +56 -0
  153. package/dist/mcp/shared-params.d.ts +27 -0
  154. package/dist/mcp/shared-params.js +36 -0
  155. package/dist/pool/browser-pool.d.ts +22 -2
  156. package/dist/pool/browser-pool.js +44 -4
  157. package/dist/project/learned-routes.d.ts +24 -0
  158. package/dist/project/learned-routes.js +63 -0
  159. package/dist/project/project-store.d.ts +6 -0
  160. package/dist/project/project-store.js +25 -1
  161. package/dist/project/reticle-dir.d.ts +2 -0
  162. package/dist/project/reticle-dir.js +1 -0
  163. package/dist/session/connection-memory.d.ts +59 -0
  164. package/dist/session/connection-memory.js +108 -0
  165. package/dist/session/dev-command.d.ts +26 -0
  166. package/dist/session/dev-command.js +89 -0
  167. package/dist/session/dev-server-probe.d.ts +68 -0
  168. package/dist/session/dev-server-probe.js +102 -7
  169. package/dist/session/last-act.d.ts +26 -0
  170. package/dist/session/last-act.js +35 -1
  171. package/dist/session/live-control-tools.js +42 -5
  172. package/dist/session/no-session-diagnosis.d.ts +67 -2
  173. package/dist/session/no-session-diagnosis.js +236 -27
  174. package/dist/session/no-session-next-action.d.ts +46 -0
  175. package/dist/session/no-session-next-action.js +85 -0
  176. package/dist/session/no-session-watch.d.ts +40 -1
  177. package/dist/session/no-session-watch.js +174 -8
  178. package/dist/session/pending-commands.d.ts +12 -0
  179. package/dist/session/pending-commands.js +13 -1
  180. package/dist/session/ref-provenance.d.ts +54 -0
  181. package/dist/session/ref-provenance.js +61 -0
  182. package/dist/session/session-lease.js +1 -1
  183. package/dist/session/session-manager.d.ts +4 -0
  184. package/dist/session/session-manager.js +35 -5
  185. package/dist/session/session.d.ts +10 -1
  186. package/dist/session/session.js +146 -5
  187. package/dist/telemetry/app-instrumented.d.ts +8 -0
  188. package/dist/telemetry/app-instrumented.js +10 -0
  189. package/dist/telemetry/automation-hint.d.ts +17 -0
  190. package/dist/telemetry/automation-hint.js +71 -0
  191. package/dist/telemetry/bug-found.d.ts +15 -17
  192. package/dist/telemetry/bug-found.js +52 -21
  193. package/dist/telemetry/cli-telemetry.js +7 -1
  194. package/dist/telemetry/daemon-telemetry.js +24 -0
  195. package/dist/telemetry/feedback.js +1 -1
  196. package/dist/telemetry/init-telemetry.js +9 -1
  197. package/dist/telemetry/install-source.d.ts +56 -0
  198. package/dist/telemetry/install-source.js +74 -0
  199. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  200. package/dist/telemetry/instrumentation-stall.js +90 -0
  201. package/dist/telemetry/run-telemetry.js +4 -1
  202. package/dist/telemetry/session-metrics.d.ts +8 -0
  203. package/dist/telemetry/session-metrics.js +15 -0
  204. package/dist/telemetry/telemetry.d.ts +7 -1
  205. package/dist/telemetry/telemetry.js +26 -2
  206. package/dist/telemetry/tool-refused.d.ts +19 -0
  207. package/dist/telemetry/tool-refused.js +55 -0
  208. package/dist/telemetry/verification-of.js +46 -7
  209. package/dist/temp-dir.d.ts +27 -0
  210. package/dist/temp-dir.js +46 -0
  211. package/dist/tools/act-tools.js +209 -28
  212. package/dist/tools/assert-grade.d.ts +1 -1
  213. package/dist/tools/assert-grade.js +27 -5
  214. package/dist/tools/assert-verdict.js +49 -7
  215. package/dist/tools/browser-tools.js +10 -0
  216. package/dist/tools/contract-tools.js +4 -0
  217. package/dist/tools/dynamic-tools.js +16 -7
  218. package/dist/tools/error-recovery.d.ts +10 -0
  219. package/dist/tools/error-recovery.js +73 -9
  220. package/dist/tools/feedback-invite.js +1 -1
  221. package/dist/tools/invoke-tool.js +159 -26
  222. package/dist/tools/lease-hint.d.ts +30 -8
  223. package/dist/tools/lease-hint.js +65 -14
  224. package/dist/tools/lease-tools.d.ts +40 -1
  225. package/dist/tools/lease-tools.js +181 -83
  226. package/dist/tools/navigate-result.js +1 -1
  227. package/dist/tools/observe-tools.js +10 -4
  228. package/dist/tools/observed-nothing.js +1 -1
  229. package/dist/tools/react-grace.d.ts +52 -0
  230. package/dist/tools/react-grace.js +90 -0
  231. package/dist/tools/read-tools.js +67 -5
  232. package/dist/tools/resolve-target.d.ts +33 -0
  233. package/dist/tools/resolve-target.js +53 -0
  234. package/dist/tools/settle-in-flight.d.ts +94 -0
  235. package/dist/tools/settle-in-flight.js +146 -0
  236. package/dist/tools/snapshot-delta.d.ts +10 -0
  237. package/dist/tools/snapshot-delta.js +194 -9
  238. package/dist/tools/tool-kit.d.ts +8 -0
  239. package/dist/tools/tool-surface.d.ts +60 -0
  240. package/dist/tools/tool-surface.js +86 -1
  241. package/dist/tools/tools.js +61 -8
  242. package/dist/tools/unadvertised-help.js +9 -6
  243. package/docs/README.md +55 -11
  244. package/docs/actions.mdx +120 -0
  245. package/docs/agent-cheatsheet.md +95 -29
  246. package/docs/architecture.md +52 -35
  247. package/docs/benchmarks.md +127 -75
  248. package/docs/best-practices.mdx +216 -0
  249. package/docs/capabilities.mdx +534 -0
  250. package/docs/cli/affected.mdx +53 -0
  251. package/docs/cli/capsules.mdx +70 -0
  252. package/docs/cli/cloud.mdx +153 -0
  253. package/docs/cli/doctor.mdx +109 -0
  254. package/docs/cli/drive.mdx +74 -0
  255. package/docs/cli/feedback.mdx +67 -0
  256. package/docs/cli/gate.mdx +64 -0
  257. package/docs/cli/help.mdx +94 -0
  258. package/docs/cli/hunt.mdx +73 -0
  259. package/docs/cli/identify.mdx +76 -0
  260. package/docs/cli/init.mdx +121 -0
  261. package/docs/cli/kill.mdx +91 -0
  262. package/docs/cli/license.mdx +48 -0
  263. package/docs/cli/mcp.mdx +61 -0
  264. package/docs/cli/open.mdx +74 -0
  265. package/docs/cli/restart.mdx +60 -0
  266. package/docs/cli/rollback.mdx +35 -0
  267. package/docs/cli/serve.mdx +69 -0
  268. package/docs/cli/status.mdx +88 -0
  269. package/docs/cli/stop.mdx +64 -0
  270. package/docs/cli/telemetry.mdx +57 -0
  271. package/docs/cli/update.mdx +52 -0
  272. package/docs/cli/verify.mdx +76 -0
  273. package/docs/cli/version.mdx +39 -0
  274. package/docs/cli/watch.mdx +62 -0
  275. package/docs/cli.mdx +241 -0
  276. package/docs/debugging.md +16 -10
  277. package/docs/deploy-checks.md +45 -12
  278. package/docs/desktop.mdx +384 -0
  279. package/docs/docs.json +364 -0
  280. package/docs/enterprise.md +22 -16
  281. package/docs/faq.mdx +102 -0
  282. package/docs/fixtures.md +13 -7
  283. package/docs/flows.md +29 -23
  284. package/docs/for-agents.md +71 -0
  285. package/docs/frameworks.mdx +238 -0
  286. package/docs/gate-plan.md +48 -42
  287. package/docs/gates.md +28 -22
  288. package/docs/getting-started.md +129 -77
  289. package/docs/human-control.md +24 -18
  290. package/docs/index.mdx +143 -0
  291. package/docs/install-agentic.mdx +224 -0
  292. package/docs/install-manual.mdx +196 -0
  293. package/docs/instrumentation.mdx +238 -0
  294. package/docs/integration-patterns.md +41 -35
  295. package/docs/local-registry.md +19 -13
  296. package/docs/multi-agent-testing.md +19 -13
  297. package/docs/packages/babel-plugin.mdx +79 -0
  298. package/docs/packages/browser.mdx +111 -0
  299. package/docs/packages/core.mdx +116 -0
  300. package/docs/packages/electron.mdx +85 -0
  301. package/docs/packages/eslint-plugin.mdx +86 -0
  302. package/docs/packages/next.mdx +89 -0
  303. package/docs/packages/react.mdx +91 -0
  304. package/docs/packages/server.mdx +98 -0
  305. package/docs/packages/tauri.mdx +63 -0
  306. package/docs/packages/test.mdx +119 -0
  307. package/docs/packages/vite-plugin.mdx +94 -0
  308. package/docs/packages.mdx +218 -0
  309. package/docs/platform-integration.md +35 -26
  310. package/docs/predicates.mdx +248 -0
  311. package/docs/quickstart.mdx +257 -0
  312. package/docs/recipes.mdx +282 -0
  313. package/docs/skill-file.mdx +107 -0
  314. package/docs/state-management.mdx +199 -0
  315. package/docs/style.css +14 -0
  316. package/docs/system-map.md +50 -33
  317. package/docs/telemetry-contract.md +106 -52
  318. package/docs/telemetry-events.mdx +475 -0
  319. package/docs/telemetry.md +63 -55
  320. package/docs/testing.md +44 -6
  321. package/docs/token-efficiency.md +37 -27
  322. package/docs/tools-act-and-wait.mdx +173 -0
  323. package/docs/tools-act-sequence.mdx +137 -0
  324. package/docs/tools-act.mdx +95 -0
  325. package/docs/tools-assert.mdx +129 -0
  326. package/docs/tools-console.mdx +63 -0
  327. package/docs/tools-inspect.mdx +88 -0
  328. package/docs/tools-navigate.mdx +99 -0
  329. package/docs/tools-network.mdx +119 -0
  330. package/docs/tools-observe.mdx +145 -0
  331. package/docs/tools-overview.mdx +160 -0
  332. package/docs/tools-query.mdx +126 -0
  333. package/docs/tools-session-and-feedback.mdx +112 -0
  334. package/docs/tools-sessions.mdx +94 -0
  335. package/docs/tools-snapshot.mdx +129 -0
  336. package/docs/tools-state.mdx +104 -0
  337. package/docs/tools-tools-and-run.mdx +156 -0
  338. package/docs/tools-wait-for.mdx +85 -0
  339. package/docs/troubleshooting.mdx +263 -0
  340. package/docs/usage.md +209 -165
  341. package/docs/vs-chrome-devtools-mcp.mdx +123 -0
  342. package/docs/vs-playwright-mcp.mdx +125 -0
  343. package/docs/vs-screenshots.mdx +137 -0
  344. package/docs/what-is-reticle.mdx +63 -0
  345. package/docs/why-reticle.mdx +109 -0
  346. package/package.json +5 -5
  347. package/docs/desktop-apps.md +0 -256
  348. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  349. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  350. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  351. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  352. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  353. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  354. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  355. package/docs/matrix/MATRIX.md +0 -26
  356. package/docs/matrix/README.md +0 -59
  357. package/docs/mint.json +0 -61
@@ -12,7 +12,152 @@
12
12
  * invalidates the prior snapshot (a diff across pages would be meaningless), so the next snapshot
13
13
  * returns full — never a misleading cross-page delta.
14
14
  */
15
- import { normalizeLines, diffLines } from '../project/baselines.js';
15
+ /**
16
+ * Identity for comparison, ref for action.
17
+ *
18
+ * The delta path used `normalizeLines` from the baseline layer, which strips the ref marker on
19
+ * purpose — a ref stored in a baseline would make every later diff noisy. Correct there, wrong here:
20
+ * every delta line came back as `- button "Row actions"` with no ref, and acting needs a ref, so the
21
+ * full snapshot had to be taken anyway. The diff call cost tokens and bought nothing. One helper, two
22
+ * callers, opposite requirements — so this path keeps the ref-stripped line as the KEY and emits the
23
+ * ORIGINAL line with its ref.
24
+ *
25
+ * Focus is stripped from the key too. It rides in the line as `[focused]`, alone or comma-joined with
26
+ * other states, so a focus move used to surface as one line removed and its twin added — structural
27
+ * change reported where nothing had been added at all. Focus is a property OF a line, so it gets its
28
+ * own field. Only focus is exempt: `disabled` moving is a real change and must still show.
29
+ */
30
+ /** The ref marker, as `formatLine` writes it. Bounded quantifier: `\s*` backtracks polynomially. */
31
+ const REF_MARKER = /\s?\(ref=e\d+\)/;
32
+ /** The state bracket — ` [disabled,checked,expanded,focused]` — and the one state that is not structural. */
33
+ const STATE_BRACKET = /\s\[([a-z,]+)\]$/;
34
+ const FOCUSED_STATE = 'focused';
35
+ /** True when this line carries focus. */
36
+ function isFocused(line) {
37
+ return true === STATE_BRACKET.exec(line)?.[1]?.split(',').includes(FOCUSED_STATE);
38
+ }
39
+ /** The same line with focus removed — and the bracket dropped when focus was all it held. */
40
+ function withoutFocus(line) {
41
+ const match = STATE_BRACKET.exec(line);
42
+ const states = match?.[1];
43
+ if (null === match || undefined === match || undefined === states)
44
+ return line;
45
+ const rest = states.split(',').filter((s) => s !== FOCUSED_STATE);
46
+ const head = line.slice(0, match.index);
47
+ return 0 === rest.length ? head : `${head} [${rest.join(',')}]`;
48
+ }
49
+ /** What makes two lines "the same element" for diffing: no ref, no focus, no surrounding space. */
50
+ function deltaKey(line) {
51
+ return withoutFocus(line.replace(REF_MARKER, '')).trim();
52
+ }
53
+ function deltaLines(tree) {
54
+ return tree
55
+ .split('\n')
56
+ .map((line) => ({ key: deltaKey(line), text: withoutFocus(line).trim() }))
57
+ .filter((line) => line.key.length > 0);
58
+ }
59
+ /**
60
+ * Multiset diff on the key, emitting the original text.
61
+ *
62
+ * Multiset rather than set because duplicate labels are the norm — twelve identical `Row actions`
63
+ * buttons — and the count is the only thing that says how many left. Emitting the ORIGINAL text is
64
+ * what finally tells them apart: with refs, "which twelve" has an answer.
65
+ */
66
+ /** Extract the ref marker value from a text line, or undefined if absent. */
67
+ function refOf(text) {
68
+ return REF_MARKER.exec(text)?.[0]?.trim();
69
+ }
70
+ /** Extract the role (first word after the bullet) from a snapshot line — the structural type that must match. */
71
+ function roleOf(text) {
72
+ return (text
73
+ .trimStart()
74
+ .replace(/^-\s*/, '')
75
+ .split(/[\s"(]/)[0] ?? '');
76
+ }
77
+ function diffKeyed(prev, next) {
78
+ const bucket = (lines) => {
79
+ const map = new Map();
80
+ for (const line of lines) {
81
+ const existing = map.get(line.key);
82
+ if (existing === undefined)
83
+ map.set(line.key, [line.text]);
84
+ else
85
+ existing.push(line.text);
86
+ }
87
+ return map;
88
+ };
89
+ const before = bucket(prev);
90
+ const after = bucket(next);
91
+ const removed = [];
92
+ const added = [];
93
+ for (const key of new Set([...before.keys(), ...after.keys()])) {
94
+ const was = before.get(key) ?? [];
95
+ const now = after.get(key) ?? [];
96
+ // Pair the SAME REF first. Twelve identical `Row actions` buttons are one key, and the ref is the
97
+ // only thing that says which of them left — an end-of-list surplus would name an arbitrary one.
98
+ const survived = new Set(now);
99
+ const wentOrChanged = was.filter((text) => !survived.has(text));
100
+ const arrivedOrChanged = now.filter((text) => !new Set(was).has(text));
101
+ // Whatever is left over on both sides is the SAME element with a re-minted ref (a re-render, or a
102
+ // navigation that restarts the sequence). Pairing those off is what stops a ref change reading as
103
+ // a removal plus an addition; only the count difference is a real structural change.
104
+ const paired = Math.min(wentOrChanged.length, arrivedOrChanged.length);
105
+ removed.push(...wentOrChanged.slice(paired));
106
+ added.push(...arrivedOrChanged.slice(paired));
107
+ }
108
+ return { removed, added };
109
+ }
110
+ /**
111
+ * Same ref AND same role in both added and removed = value change, not a structural
112
+ * arrival/departure. Refs alone are not stable identities — they are re-minted on re-render — so a
113
+ * removed button and an added textbox sharing a ref must stay in their respective lists.
114
+ */
115
+ function extractChanged(added, removed) {
116
+ const addedByRef = new Map();
117
+ for (const [i, text] of added.entries()) {
118
+ const ref = refOf(text);
119
+ if (ref !== undefined)
120
+ addedByRef.set(ref, { idx: i, text, role: roleOf(text) });
121
+ }
122
+ const changedIndicesInAdded = new Set();
123
+ const changedIndicesInRemoved = new Set();
124
+ const changed = [];
125
+ for (const [i, text] of removed.entries()) {
126
+ const ref = refOf(text);
127
+ if (ref === undefined)
128
+ continue;
129
+ const match = addedByRef.get(ref);
130
+ if (match !== undefined && match.role === roleOf(text)) {
131
+ changed.push(match.text);
132
+ changedIndicesInAdded.add(match.idx);
133
+ changedIndicesInRemoved.add(i);
134
+ addedByRef.delete(ref);
135
+ }
136
+ }
137
+ return {
138
+ added: added.filter((_, i) => !changedIndicesInAdded.has(i)),
139
+ removed: removed.filter((_, i) => !changedIndicesInRemoved.has(i)),
140
+ changed,
141
+ };
142
+ }
143
+ /**
144
+ * Where focus sits now: the line to show, plus the identity to compare it BY.
145
+ *
146
+ * These have to be two different things. Comparing the rendered lines said focus had moved whenever
147
+ * the focused element merely changed — typing into a focused textbox rewrites its `[value=...]`, so
148
+ * `from` and `to` came back naming the same element with different text. Focus identity is the
149
+ * ELEMENT, so the comparison is on the ref, and the line is only what gets displayed. Where there is
150
+ * no ref to compare (a non-interactive line), the ref-stripped key stands in.
151
+ *
152
+ * Read from the raw tree so a line the diff filters out cannot silently swallow the focus report.
153
+ */
154
+ function focusedLine(tree) {
155
+ const focused = tree.split('\n').find((line) => isFocused(line));
156
+ if (focused === undefined)
157
+ return undefined;
158
+ const text = withoutFocus(focused).trim();
159
+ return { id: REF_MARKER.exec(focused)?.[0]?.trim() ?? deltaKey(focused), text };
160
+ }
16
161
  export const SnapshotDeltaMode = {
17
162
  FULL: 'full',
18
163
  DELTA: 'delta',
@@ -22,12 +167,32 @@ export const SnapshotDeltaMode = {
22
167
  export function snapshotDelta(prevTree, nextTree) {
23
168
  if (prevTree === undefined)
24
169
  return { mode: SnapshotDeltaMode.FULL };
25
- const { added, removed } = diffLines(normalizeLines(prevTree), normalizeLines(nextTree));
26
- if (0 === added.length && 0 === removed.length)
27
- return { mode: SnapshotDeltaMode.UNCHANGED };
170
+ const raw = diffKeyed(deltaLines(prevTree), deltaLines(nextTree));
171
+ const { added, removed, changed } = extractChanged(raw.added, raw.removed);
172
+ const before = focusedLine(prevTree);
173
+ const now = focusedLine(nextTree);
174
+ // Reported only on a MOVE. Repeating "focus is still here" every turn is the noise the delta exists
175
+ // to remove, and an absent field is how the agent knows nothing happened.
176
+ const focusChanged = before?.id === now?.id
177
+ ? undefined
178
+ : {
179
+ ...(before === undefined ? {} : { from: before.text }),
180
+ ...(now === undefined ? {} : { to: now.text }),
181
+ };
182
+ const moved = focusChanged === undefined ? {} : { focusChanged };
183
+ if (0 === added.length && 0 === removed.length && 0 === changed.length) {
184
+ return { mode: SnapshotDeltaMode.UNCHANGED, ...moved };
185
+ }
28
186
  return {
29
187
  mode: SnapshotDeltaMode.DELTA,
30
- delta: { added, removed, addedCount: added.length, removedCount: removed.length },
188
+ delta: {
189
+ added,
190
+ removed,
191
+ changed,
192
+ addedCount: added.length,
193
+ removedCount: removed.length,
194
+ },
195
+ ...moved,
31
196
  };
32
197
  }
33
198
  const DEFAULT_MAX_ENTRIES = 50;
@@ -38,6 +203,10 @@ export class SnapshotCache {
38
203
  constructor(max = DEFAULT_MAX_ENTRIES) {
39
204
  this.#max = max;
40
205
  }
206
+ /** True when an entry exists for this key (regardless of route match). */
207
+ has(key) {
208
+ return this.#map.has(key);
209
+ }
41
210
  /** Last tree for this key IF the route still matches; undefined when absent or route changed. */
42
211
  recall(key, route) {
43
212
  const entry = this.#map.get(key);
@@ -96,17 +265,33 @@ export function applySnapshotDelta(raw, opts, cache) {
96
265
  return raw;
97
266
  }
98
267
  const prev = cache.recall(key, route);
268
+ const hadEntry = cache.has(key);
99
269
  cache.remember(key, route, tree);
100
270
  const decision = snapshotDelta(prev, tree);
101
- if (decision.mode === SnapshotDeltaMode.FULL)
102
- return raw;
271
+ if (decision.mode === SnapshotDeltaMode.FULL) {
272
+ const reason = hadEntry ? 'route changed' : 'first snapshot for this route';
273
+ return { ...r, mode: SnapshotDeltaMode.FULL, reason };
274
+ }
103
275
  // The walk stops at a node cap and returns a DOCUMENT-ORDER PREFIX, so two capped snapshots of a
104
276
  // large page are identical whenever the change happened past the cap — and "unchanged" is then a
105
277
  // statement about the cap, not about the page. Carried through on both branches: a delta computed
106
278
  // over a prefix is real but not exhaustive either.
107
279
  const capped = true === r['truncated'] ? { truncated: true, note: CAPPED_DIFF_NOTE } : {};
280
+ // Spread onto BOTH branches. A value computed correctly and declared nowhere on the way out is the
281
+ // defect shape that hit this release five separate times: nothing throws, no test reddens, the
282
+ // field is simply absent forever.
283
+ const moved = decision.focusChanged === undefined ? {} : { focusChanged: decision.focusChanged };
108
284
  if (decision.mode === SnapshotDeltaMode.UNCHANGED) {
109
- return { mode: SnapshotDeltaMode.UNCHANGED, status: r['status'], ...capped };
285
+ return { mode: SnapshotDeltaMode.UNCHANGED, status: r['status'], ...moved, ...capped };
110
286
  }
111
- return { mode: SnapshotDeltaMode.DELTA, delta: decision.delta, status: r['status'], ...capped };
287
+ const { changed, ...structuralDelta } = decision.delta;
288
+ const changedField = changed.length > 0 ? { changed, changedCount: changed.length } : {};
289
+ return {
290
+ mode: SnapshotDeltaMode.DELTA,
291
+ delta: structuralDelta,
292
+ ...changedField,
293
+ status: r['status'],
294
+ ...moved,
295
+ ...capped,
296
+ };
112
297
  }
@@ -15,10 +15,18 @@ import type { FlowStore } from '../flows/flows.js';
15
15
  import type { ProjectStore } from '../project/project-store.js';
16
16
  import type { AnnotationStore } from '../flows/annotation-store.js';
17
17
  import type { BrowserPool } from '../pool/browser-pool.js';
18
+ import type { ChromiumProbe } from '../cli/chromium-hint.js';
18
19
  export interface ToolDeps {
19
20
  sessions: SessionManager;
20
21
  /** shared one-browser/N-context pool for headless leases. undefined ⇒ lease tools report unavailable. */
21
22
  pool?: BrowserPool;
23
+ /**
24
+ * Probe for a launchable Chromium, so the lease path can refuse at the FIRST call with the install
25
+ * fix rather than failing several calls in with a message that reads as "the app is not running".
26
+ * Optional: absent ⇒ no preflight — every existing test construction of ToolDeps keeps working, and
27
+ * the fake-pool lease tests are deliberately runnable without a real Chromium.
28
+ */
29
+ browserProbe?: () => Promise<ChromiumProbe>;
22
30
  baselines: BaselineStore;
23
31
  recordings: RecordingStore;
24
32
  /** on-disk anchored-flow store (.reticle/flows/). */
@@ -40,6 +40,36 @@ export declare const TOOL_SURFACE: {
40
40
  readonly DEFAULT: "default";
41
41
  /** Every tool advertised directly, WITH output schemas. A verification switch — see above. */
42
42
  readonly ALL: "all";
43
+ /**
44
+ * The smallest surface that can still produce a VERDICT. Not a profile — a cost switch.
45
+ *
46
+ * Measured on the wire: a verification that names its own target costs one `act_and_wait` call,
47
+ * and 5,480 of its 5,909 tokens are the advertised surface re-sent for that single turn. The
48
+ * answers cost 430. So on the path where the caller already knows what to assert, almost the
49
+ * entire bill is the menu, and the menu is the thing to cut.
50
+ *
51
+ * Deliberately NOT the default, and now MEASURED rather than inherited. Run over the same 30-bug
52
+ * set that the default surface scores 23/27 detection and 2/29 false alarms on:
53
+ *
54
+ * detection 24/28 (held)
55
+ * FALSE ALARMS 7/30 (was 2/29 — TRIPLED)
56
+ * tokens 113,599 per run (was 179,959 — 37% cheaper)
57
+ *
58
+ * The five new false alarms are not scattered. They are `mutation-leak` and
59
+ * `generate-blast-filter` (state), `kpi-deploys-tamper` (business-logic), `debounce-broken`
60
+ * (timing) and `route-stuck-deployments` (routing) — precisely the classes whose evidence lives in
61
+ * `reticle_state`, `reticle_network` and `reticle_observe`, which this surface does not advertise.
62
+ * Strip the observation tools and the model stops observing: it reaches for the verdict without
63
+ * the evidence and calls a healthy build broken.
64
+ *
65
+ * So the retired `dynamic` finding is confirmed, with a sharper mechanism than "accuracy drops".
66
+ * 37% of the tokens is not worth trading for the one metric this product actually wins on, and
67
+ * this must not become the default on the strength of the token number alone.
68
+ *
69
+ * It remains correct for a caller who has ALREADY decided what to assert — there the assertion
70
+ * supplies the evidence the surface would otherwise have to go and find.
71
+ */
72
+ readonly VERIFY: "verify";
43
73
  };
44
74
  export type ToolSurface = (typeof TOOL_SURFACE)[keyof typeof TOOL_SURFACE];
45
75
  /**
@@ -49,6 +79,8 @@ export type ToolSurface = (typeof TOOL_SURFACE)[keyof typeof TOOL_SURFACE];
49
79
  * users to shop among alternatives that did not meaningfully differ.
50
80
  */
51
81
  export declare const ADVERTISE_ALL_ENV = "RETICLE_ADVERTISE_ALL_TOOLS";
82
+ /** Opt into the smallest verdict-capable surface. Read by the DAEMON at startup, like the others. */
83
+ export declare const VERIFY_SURFACE_ENV = "RETICLE_VERIFY_SURFACE";
52
84
  /**
53
85
  * The retired setting, still read so nobody's shell profile breaks.
54
86
  *
@@ -58,6 +90,34 @@ export declare const ADVERTISE_ALL_ENV = "RETICLE_ADVERTISE_ALL_TOOLS";
58
90
  */
59
91
  export declare const TOOL_PROFILE_ENV = "RETICLE_TOOL_PROFILE";
60
92
  export declare const CORE_TOOL_NAMES: ReadonlySet<string>;
93
+ /**
94
+ * The extended surface: what `all` advertises BEYOND the default set.
95
+ *
96
+ * `all` used to mean "every tool in the registry", and that is no longer allowed to be true. Cursor
97
+ * enforces a limit of 40 tools across every connected MCP server COMBINED, so a server advertising
98
+ * 48 by itself can push a user's other servers out or be dropped wholesale. The budget is a count,
99
+ * so no amount of trimming parameter prose buys anything back — only advertising fewer names does.
100
+ *
101
+ * Everything omitted stays in the registry, stays catalogued by `reticle_tools`, and stays callable
102
+ * by name through `reticle_run { tool, args }`. The cost is one discovery hop on the cold tail, which
103
+ * is the same trade the default surface has always made, applied one level further out.
104
+ *
105
+ * What it costs that is NOT free: `all` is the only surface that carries `outputSchema`, which is
106
+ * what makes the MCP layer validate tool OUTPUT — the check that once caught `reticle_verify_change`
107
+ * returning a payload its own schema rejected. The tools left off this list no longer get that
108
+ * validation from the wire. That defect class is now uncovered for them, and it is written down here
109
+ * rather than discovered later.
110
+ *
111
+ * Chosen as the capabilities an agent plausibly reaches for once the verify loop is not enough:
112
+ * orientation, the record/replay flow loop, visual evidence, and the three fault-injection controls.
113
+ */
114
+ export declare const EXTENDED_TOOL_NAMES: ReadonlySet<string>;
115
+ /**
116
+ * The verify surface: one acting tool that returns a verdict, plus the two meta-tools that reach
117
+ * everything else. `act_and_wait` can resolve its own target, so no query tool is needed to name an
118
+ * element — that round trip was half the token cost of a verification.
119
+ */
120
+ export declare const VERIFY_TOOL_NAMES: ReadonlySet<string>;
61
121
  /**
62
122
  * Which surface is live.
63
123
  *
@@ -40,6 +40,36 @@ export const TOOL_SURFACE = {
40
40
  DEFAULT: 'default',
41
41
  /** Every tool advertised directly, WITH output schemas. A verification switch — see above. */
42
42
  ALL: 'all',
43
+ /**
44
+ * The smallest surface that can still produce a VERDICT. Not a profile — a cost switch.
45
+ *
46
+ * Measured on the wire: a verification that names its own target costs one `act_and_wait` call,
47
+ * and 5,480 of its 5,909 tokens are the advertised surface re-sent for that single turn. The
48
+ * answers cost 430. So on the path where the caller already knows what to assert, almost the
49
+ * entire bill is the menu, and the menu is the thing to cut.
50
+ *
51
+ * Deliberately NOT the default, and now MEASURED rather than inherited. Run over the same 30-bug
52
+ * set that the default surface scores 23/27 detection and 2/29 false alarms on:
53
+ *
54
+ * detection 24/28 (held)
55
+ * FALSE ALARMS 7/30 (was 2/29 — TRIPLED)
56
+ * tokens 113,599 per run (was 179,959 — 37% cheaper)
57
+ *
58
+ * The five new false alarms are not scattered. They are `mutation-leak` and
59
+ * `generate-blast-filter` (state), `kpi-deploys-tamper` (business-logic), `debounce-broken`
60
+ * (timing) and `route-stuck-deployments` (routing) — precisely the classes whose evidence lives in
61
+ * `reticle_state`, `reticle_network` and `reticle_observe`, which this surface does not advertise.
62
+ * Strip the observation tools and the model stops observing: it reaches for the verdict without
63
+ * the evidence and calls a healthy build broken.
64
+ *
65
+ * So the retired `dynamic` finding is confirmed, with a sharper mechanism than "accuracy drops".
66
+ * 37% of the tokens is not worth trading for the one metric this product actually wins on, and
67
+ * this must not become the default on the strength of the token number alone.
68
+ *
69
+ * It remains correct for a caller who has ALREADY decided what to assert — there the assertion
70
+ * supplies the evidence the surface would otherwise have to go and find.
71
+ */
72
+ VERIFY: 'verify',
43
73
  };
44
74
  /**
45
75
  * The switch that turns on the full, output-schema-carrying surface. A boolean, not a menu.
@@ -48,6 +78,8 @@ export const TOOL_SURFACE = {
48
78
  * users to shop among alternatives that did not meaningfully differ.
49
79
  */
50
80
  export const ADVERTISE_ALL_ENV = 'RETICLE_ADVERTISE_ALL_TOOLS';
81
+ /** Opt into the smallest verdict-capable surface. Read by the DAEMON at startup, like the others. */
82
+ export const VERIFY_SURFACE_ENV = 'RETICLE_VERIFY_SURFACE';
51
83
  /**
52
84
  * The retired setting, still read so nobody's shell profile breaks.
53
85
  *
@@ -145,6 +177,51 @@ export const CORE_TOOL_NAMES = new Set([
145
177
  // because there is nowhere else those two calls are ever named.
146
178
  ReticleTool.SESSION,
147
179
  ]);
180
+ /**
181
+ * The extended surface: what `all` advertises BEYOND the default set.
182
+ *
183
+ * `all` used to mean "every tool in the registry", and that is no longer allowed to be true. Cursor
184
+ * enforces a limit of 40 tools across every connected MCP server COMBINED, so a server advertising
185
+ * 48 by itself can push a user's other servers out or be dropped wholesale. The budget is a count,
186
+ * so no amount of trimming parameter prose buys anything back — only advertising fewer names does.
187
+ *
188
+ * Everything omitted stays in the registry, stays catalogued by `reticle_tools`, and stays callable
189
+ * by name through `reticle_run { tool, args }`. The cost is one discovery hop on the cold tail, which
190
+ * is the same trade the default surface has always made, applied one level further out.
191
+ *
192
+ * What it costs that is NOT free: `all` is the only surface that carries `outputSchema`, which is
193
+ * what makes the MCP layer validate tool OUTPUT — the check that once caught `reticle_verify_change`
194
+ * returning a payload its own schema rejected. The tools left off this list no longer get that
195
+ * validation from the wire. That defect class is now uncovered for them, and it is written down here
196
+ * rather than discovered later.
197
+ *
198
+ * Chosen as the capabilities an agent plausibly reaches for once the verify loop is not enough:
199
+ * orientation, the record/replay flow loop, visual evidence, and the three fault-injection controls.
200
+ */
201
+ export const EXTENDED_TOOL_NAMES = new Set([
202
+ // Demoted from the default set to make room under the cap. It was added as an explicit bet that
203
+ // orientation replaces exploratory snapshots, and the measurement that would have settled that
204
+ // bet was never run — so when the budget became a hard count, the unproven entry is the one that
205
+ // gives way. Still one `reticle_run` hop from any agent that wants it.
206
+ ReticleTool.CAPABILITIES,
207
+ ReticleTool.FLOW_SAVE,
208
+ ReticleTool.FLOW_REPLAY,
209
+ ReticleTool.FLOW_VERIFY,
210
+ ReticleTool.RECORD,
211
+ ReticleTool.SCREENSHOT,
212
+ ReticleTool.VISUAL_DIFF,
213
+ ReticleTool.CLOCK,
214
+ ReticleTool.NETWORK_MOCK,
215
+ ReticleTool.STORAGE,
216
+ ReticleTool.VERIFY_CHANGE,
217
+ ReticleTool.CRAWL,
218
+ ]);
219
+ /**
220
+ * The verify surface: one acting tool that returns a verdict, plus the two meta-tools that reach
221
+ * everything else. `act_and_wait` can resolve its own target, so no query tool is needed to name an
222
+ * element — that round trip was half the token cost of a verification.
223
+ */
224
+ export const VERIFY_TOOL_NAMES = new Set([ReticleTool.ACT_AND_WAIT]);
148
225
  /** Is the truthy form of a boolean env var set? `1`, `true`, `yes` — anything else is off. */
149
226
  function envFlagOn(raw) {
150
227
  if (raw === undefined)
@@ -159,6 +236,8 @@ function envFlagOn(raw) {
159
236
  * switch decides, and the retired setting is honoured last so an old shell profile still works.
160
237
  */
161
238
  export function resolveToolSurface(explicit) {
239
+ if (explicit === TOOL_SURFACE.VERIFY)
240
+ return TOOL_SURFACE.VERIFY;
162
241
  if (explicit === TOOL_SURFACE.ALL)
163
242
  return TOOL_SURFACE.ALL;
164
243
  if (explicit === TOOL_SURFACE.DEFAULT)
@@ -166,6 +245,8 @@ export function resolveToolSurface(explicit) {
166
245
  const retiredExplicit = explicit === undefined ? undefined : RETIRED_PROFILE_VALUES[explicit];
167
246
  if (retiredExplicit !== undefined)
168
247
  return retiredExplicit;
248
+ if (envFlagOn(process.env[VERIFY_SURFACE_ENV]))
249
+ return TOOL_SURFACE.VERIFY;
169
250
  if (envFlagOn(process.env[ADVERTISE_ALL_ENV]))
170
251
  return TOOL_SURFACE.ALL;
171
252
  const retiredEnv = RETIRED_PROFILE_VALUES[process.env[TOOL_PROFILE_ENV] ?? ''];
@@ -211,7 +292,11 @@ export function describeToolSurface(active, requested) {
211
292
  export function filterTools(tools, surface) {
212
293
  // CORE_TOOL_NAMES is what it always really was: the set advertised directly. It was never the
213
294
  // interesting thing about the `core` PROFILE, whose only distinction was a second name for this.
295
+ if (surface === TOOL_SURFACE.VERIFY)
296
+ return tools.filter((t) => VERIFY_TOOL_NAMES.has(t.name));
214
297
  if (surface === TOOL_SURFACE.DEFAULT)
215
298
  return tools.filter((t) => CORE_TOOL_NAMES.has(t.name));
216
- return tools;
299
+ // `all` is the extended surface, not the whole registry — the cap is a hard budget shared with
300
+ // every other MCP server the user has connected. surface-sizes.test.ts enforces it.
301
+ return tools.filter((t) => CORE_TOOL_NAMES.has(t.name) || EXTENDED_TOOL_NAMES.has(t.name));
217
302
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { QueryBy, ReticleCommand, SnapshotMode } from '@reticlehq/core';
2
+ import { NoSessionAction, QueryBy, ReticleCommand, SnapshotMode } from '@reticlehq/core';
3
3
  import { ReticleTool } from './tool-names.js';
4
4
  import { withSizeCost } from '../session/output-budget.js';
5
5
  import { applySnapshotDelta, SnapshotCache } from './snapshot-delta.js';
@@ -82,6 +82,15 @@ const RAW_TOOLS = [
82
82
  .string()
83
83
  .optional()
84
84
  .describe('Present ONLY when `sessions` is empty: why nothing is connected, and the next action that fixes it. An empty list is never the end of the road — read this before concluding the app cannot be driven.'),
85
+ next_action: z
86
+ .object({
87
+ action: z.nativeEnum(NoSessionAction),
88
+ command: z.string().optional(),
89
+ port: z.number().optional(),
90
+ reason: z.string(),
91
+ })
92
+ .optional()
93
+ .describe("Present ONLY when `sessions` is empty: the same answer as `why`, executable. `command` is the LITERAL command to run, sourced from this project's own package.json scripts and lockfile — it is absent, never guessed, when the project declares no dev script. `action` is one of start_dev_server | run_init | open_app | reopen_app."),
85
94
  },
86
95
  handler: async (deps) => {
87
96
  const provider = deps.realInput;
@@ -97,7 +106,14 @@ const RAW_TOOLS = [
97
106
  // rather than only when a later tool fails for want of a session.
98
107
  if (0 === sessions.length) {
99
108
  const why = deps.sessions.noSessionHint();
100
- return { sessions, ...(why === undefined ? {} : { why }) };
109
+ // The executable half. `why` is for the human reading the transcript; this is the one the
110
+ // agent acts on, so it never has to parse a paragraph to find a command inside it.
111
+ const next = deps.sessions.noSessionNextAction();
112
+ return {
113
+ sessions,
114
+ ...(why === undefined ? {} : { why }),
115
+ ...(next === undefined ? {} : { next_action: next }),
116
+ };
101
117
  }
102
118
  return { sessions };
103
119
  },
@@ -126,11 +142,18 @@ const RAW_TOOLS = [
126
142
  .string()
127
143
  .optional()
128
144
  .describe('Indented ARIA tree of every element on the page (or the scoped subtree).'),
129
- status: z.object({ route: z.string(), title: z.string().optional() }).optional(),
145
+ status: z
146
+ .object({
147
+ route: z.string(),
148
+ title: z.string().optional(),
149
+ visibleDialogs: z.array(z.string()).optional(),
150
+ overlayHidingPage: z.string().optional(),
151
+ })
152
+ .optional(),
130
153
  mode: z
131
154
  .string()
132
155
  .optional()
133
- .describe('delta | unchanged when diff:true returned a change set.'),
156
+ .describe('full | delta | unchanged — which kind of diff response this is.'),
134
157
  delta: z
135
158
  .object({
136
159
  added: z.array(z.string()),
@@ -140,6 +163,15 @@ const RAW_TOOLS = [
140
163
  })
141
164
  .optional()
142
165
  .describe('Only present on a diff:true call that found changes.'),
166
+ changed: z
167
+ .array(z.string())
168
+ .optional()
169
+ .describe('Elements whose value changed in place (same ref, different content) — not a structural add/remove.'),
170
+ changedCount: z.number().optional().describe('Number of elements in `changed`.'),
171
+ reason: z
172
+ .string()
173
+ .optional()
174
+ .describe('Why mode is "full": first snapshot for this route, or route changed.'),
143
175
  cost: z
144
176
  .object({ bytes: z.number(), tokens: z.number() })
145
177
  .optional()
@@ -358,16 +390,37 @@ const RAW_TOOLS = [
358
390
  })
359
391
  .partial()
360
392
  .optional(),
361
- // Theme compliance vs the app's design tokens: { colorToken, backgroundToken (null = off-palette),
362
- // offTheme, tokenCount }. Kept as unknown — the structured-content serializer can truncate a
363
- // large inspect payload's fields to strings, which a strict shape would reject; the full object
364
- // is always present in the text content the agent reads.
393
+ scroll: z
394
+ .object({
395
+ scrollTop: z.number(),
396
+ scrollHeight: z.number(),
397
+ clientHeight: z.number(),
398
+ overflowY: z.string(),
399
+ })
400
+ .optional(),
401
+ // Theme compliance vs the app's design tokens: { colorToken, colorTokens, backgroundToken,
402
+ // backgroundTokens, offTheme, tokenCount, themeScope }. The plural fields carry EVERY token
403
+ // matching the resolved colour; the singular ones abstain (null) when several tokens share it,
404
+ // because returning an arbitrary winner was the defect (#313). `themeScope` names the theme
405
+ // that was active at capture, so two inspects minutes apart are comparable at all.
406
+ // Kept as unknown — the structured-content serializer can truncate a large inspect payload's
407
+ // fields to strings, which a strict shape would reject; the full object is always present in
408
+ // the text content the agent reads.
365
409
  theme: z.unknown().optional(),
366
410
  /**
367
411
  * Where this element is written, as `file:line`. Present when the app is built with the
368
412
  * Reticle build plugin in dev; absent in production builds.
369
413
  */
370
414
  source: z.string().optional(),
415
+ /**
416
+ * Why `source` is missing, when it is.
417
+ *
418
+ * Distinguishes "this element has no stamp" from "nothing on this page has one, so the
419
+ * stamping loader is not running" — the second means `file:line` is unavailable for the
420
+ * whole session and the fix is a build-config change, which used to be discoverable only by
421
+ * reading the adapter's own source.
422
+ */
423
+ sourceUnavailable: z.string().optional(),
371
424
  /**
372
425
  * Component identity from the framework adapter (@reticlehq/react).
373
426
  *
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * What to say when an agent calls a real Reticle tool that this profile does not advertise.
3
3
  *
4
- * The default profile advertises 16 of 46 tools. That is deliberate — every advertised tool's schema
5
- * is re-sent on every turn, and the trim is most of why `hybrid` is cheap — and the other 30 stay
6
- * fully callable through `reticle_run`, which `profile-reachability.test.ts` guards.
4
+ * The default surface advertises 18 of 48 tools, and the extended one 30. That is deliberate — every
5
+ * advertised tool's schema is re-sent on every turn, and editors budget MCP tools as a COUNT shared
6
+ * across every connected server — and everything omitted stays fully callable through `reticle_run`,
7
+ * which `surface-reachability.test.ts` guards.
7
8
  *
8
9
  * The gap was in the ERROR. The MCP SDK answers `Tool <name> not found`, which is indistinguishable
9
10
  * from "this tool does not exist", so an agent that trusts it stops trying. Reported from a real
@@ -14,7 +15,7 @@
14
15
  * and the switch that makes it stop being necessary.
15
16
  */
16
17
  import { ReticleTool } from './tool-names.js';
17
- import { TOOL_SURFACE, TOOL_PROFILE_ENV } from './tool-surface.js';
18
+ import { ADVERTISE_ALL_ENV } from './tool-surface.js';
18
19
  import { mergedNameRedirect, mergedNameMessage } from './merged-name-redirect.js';
19
20
  /**
20
21
  * Guidance for `name`, or undefined when there is nothing useful to add — the tool IS advertised (so
@@ -34,6 +35,8 @@ export function unadvertisedToolHelp(name, advertised, known) {
34
35
  `away. It is NOT missing: invoke it with ` +
35
36
  `${ReticleTool.RUN} { tool: "${name}", args: { ... } }. ` +
36
37
  `Call ${ReticleTool.TOOLS} { names: ["${name}"] } for its parameters. ` +
37
- `If you need it repeatedly, set ${TOOL_PROFILE_ENV}=${TOOL_SURFACE.ALL} to advertise ` +
38
- `every tool directly.`);
38
+ `If you need it repeatedly, start the daemon with ${ADVERTISE_ALL_ENV}=1 for the extended ` +
39
+ `surface. That is read by the DAEMON at startup, so it takes effect on the next daemon, and it ` +
40
+ `advertises MORE tools rather than all of them — editors budget MCP tools as a shared count, so ` +
41
+ `${ReticleTool.RUN} stays the way to reach the rest.`);
39
42
  }