@reticlehq/server 2.11.0 → 2.13.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 (513) hide show
  1. package/SKILL.md +93 -111
  2. package/dist/bridge/auth-failure-reason.d.ts +6 -2
  3. package/dist/bridge/auth-failure-reason.js +15 -8
  4. package/dist/bridge/bridge.d.ts +18 -14
  5. package/dist/bridge/bridge.js +248 -165
  6. package/dist/bridge/bridge.test-harness.d.ts +2 -0
  7. package/dist/bridge/bridge.test-harness.js +8 -1
  8. package/dist/bridge/pairing-token.d.ts +5 -0
  9. package/dist/bridge/pairing-token.js +28 -0
  10. package/dist/bridge/protocol-skew.d.ts +37 -0
  11. package/dist/bridge/protocol-skew.js +45 -0
  12. package/dist/capsule/capsule-store.d.ts +8 -7
  13. package/dist/capsule/capsule-store.js +1 -1
  14. package/dist/capsule/causal-summary.d.ts +2 -2
  15. package/dist/capsule/causal-summary.js +7 -2
  16. package/dist/chromium-launch-options.d.ts +28 -0
  17. package/dist/chromium-launch-options.js +29 -0
  18. package/dist/cli/cli-doctor.js +39 -1
  19. package/dist/cli/cli-flow-commands.d.ts +3 -1
  20. package/dist/cli/cli-flow-commands.js +34 -4
  21. package/dist/cli/cli-kill.d.ts +2 -1
  22. package/dist/cli/cli-launch.d.ts +11 -1
  23. package/dist/cli/cli-launch.js +59 -10
  24. package/dist/cli/cli-parse.d.ts +17 -1
  25. package/dist/cli/cli-parse.js +238 -12
  26. package/dist/cli/cli-update-commands.js +20 -1
  27. package/dist/cli/cli-verify.d.ts +2 -1
  28. package/dist/cli/cli-verify.js +5 -4
  29. package/dist/cli/cloud-cli.js +428 -253
  30. package/dist/cli/cloud-keystore.d.ts +37 -0
  31. package/dist/cli/cloud-keystore.js +97 -0
  32. package/dist/cli/cloud-kit.d.ts +37 -0
  33. package/dist/cli/cloud-kit.js +121 -0
  34. package/dist/cli/cloud-login.d.ts +17 -0
  35. package/dist/cli/cloud-login.js +183 -0
  36. package/dist/cli/cloud-session.d.ts +48 -0
  37. package/dist/cli/cloud-session.js +53 -0
  38. package/dist/cli/config-discovery.d.ts +2 -1
  39. package/dist/cli/config-discovery.js +1 -0
  40. package/dist/cli/daemon-start-options.d.ts +32 -0
  41. package/dist/cli/daemon-start-options.js +38 -0
  42. package/dist/cli/device-flow.d.ts +76 -0
  43. package/dist/cli/device-flow.js +56 -0
  44. package/dist/cli/doctor-rows.d.ts +12 -1
  45. package/dist/cli/doctor-rows.js +12 -1
  46. package/dist/cli/drive-attach.d.ts +13 -0
  47. package/dist/cli/drive-attach.js +18 -0
  48. package/dist/cli/drive-command.js +7 -3
  49. package/dist/cli/gate-exit.d.ts +21 -0
  50. package/dist/cli/gate-exit.js +21 -0
  51. package/dist/cli/gate-hook-message.d.ts +33 -0
  52. package/dist/cli/gate-hook-message.js +46 -0
  53. package/dist/cli/mcp-command.d.ts +25 -0
  54. package/dist/cli/mcp-command.js +174 -0
  55. package/dist/cli/project-name.d.ts +30 -0
  56. package/dist/cli/project-name.js +44 -0
  57. package/dist/cli/sibling-ports.d.ts +37 -0
  58. package/dist/cli/sibling-ports.js +56 -0
  59. package/dist/cli/status-next-action.d.ts +14 -0
  60. package/dist/cli/status-next-action.js +4 -4
  61. package/dist/cli.d.ts +1 -0
  62. package/dist/cli.js +167 -186
  63. package/dist/cloud/cloud-config.d.ts +24 -2
  64. package/dist/cloud/cloud-config.js +64 -5
  65. package/dist/cloud/cloud-sync.d.ts +16 -5
  66. package/dist/cloud/cloud-sync.js +64 -1
  67. package/dist/cloud/sync-cycle.d.ts +110 -0
  68. package/dist/cloud/sync-cycle.js +270 -0
  69. package/dist/cloud/sync-daemon.d.ts +69 -0
  70. package/dist/cloud/sync-daemon.js +260 -0
  71. package/dist/cloud/sync-disk.d.ts +9 -0
  72. package/dist/cloud/sync-disk.js +191 -0
  73. package/dist/cloud/sync-hash.d.ts +2 -0
  74. package/dist/cloud/sync-hash.js +35 -0
  75. package/dist/crawl/crawl-tools.js +5 -3
  76. package/dist/crawl/crawl.d.ts +12 -13
  77. package/dist/crawl/crawl.js +25 -3
  78. package/dist/daemon/daemon-resolve.d.ts +122 -0
  79. package/dist/daemon/daemon-resolve.js +187 -0
  80. package/dist/daemon/dev-servers.d.ts +15 -0
  81. package/dist/daemon/dev-servers.js +68 -0
  82. package/dist/daemon/free-port.d.ts +29 -0
  83. package/dist/daemon/free-port.js +55 -0
  84. package/dist/daemon/heartbeat.d.ts +0 -4
  85. package/dist/daemon/heartbeat.js +2 -2
  86. package/dist/daemon/wake-decision.d.ts +32 -0
  87. package/dist/daemon/wake-decision.js +38 -0
  88. package/dist/dev/stale-issue-guard.d.ts +0 -2
  89. package/dist/dev/stale-issue-guard.js +1 -1
  90. package/dist/domain/domain-tools.js +2 -2
  91. package/dist/events/contradictions.d.ts +59 -0
  92. package/dist/events/contradictions.js +253 -27
  93. package/dist/events/declared.d.ts +17 -0
  94. package/dist/events/declared.js +43 -1
  95. package/dist/events/echo-mismatch.js +65 -1
  96. package/dist/events/event-filters.d.ts +8 -0
  97. package/dist/events/event-filters.js +19 -2
  98. package/dist/events/observed-in-window.d.ts +12 -0
  99. package/dist/events/observed-in-window.js +17 -0
  100. package/dist/events/predicate-asks.d.ts +13 -0
  101. package/dist/events/predicate-asks.js +22 -0
  102. package/dist/events/predicate-eval.js +117 -27
  103. package/dist/events/predicate-parse.d.ts +0 -6
  104. package/dist/events/predicate-parse.js +43 -3
  105. package/dist/events/predicate-precheck.d.ts +8 -0
  106. package/dist/events/predicate-precheck.js +41 -0
  107. package/dist/events/predicate-route.d.ts +14 -0
  108. package/dist/events/predicate-route.js +36 -8
  109. package/dist/events/predicate-schema.d.ts +44 -0
  110. package/dist/events/predicate-schema.js +126 -23
  111. package/dist/events/predicate.d.ts +20 -0
  112. package/dist/events/predicate.js +77 -6
  113. package/dist/events/split-text-miss.d.ts +30 -0
  114. package/dist/events/split-text-miss.js +44 -0
  115. package/dist/flows/coverage.d.ts +0 -8
  116. package/dist/flows/coverage.js +1 -1
  117. package/dist/flows/decision.d.ts +5 -1
  118. package/dist/flows/decision.js +33 -7
  119. package/dist/flows/flake.d.ts +6 -8
  120. package/dist/flows/flake.js +1 -1
  121. package/dist/flows/flow-intent.d.ts +52 -0
  122. package/dist/flows/flow-intent.js +123 -0
  123. package/dist/flows/flow-memory-consult.d.ts +55 -0
  124. package/dist/flows/flow-memory-consult.js +38 -0
  125. package/dist/flows/flow-replay-run.d.ts +18 -7
  126. package/dist/flows/flow-replay-run.js +263 -23
  127. package/dist/flows/flow-replay.js +11 -2
  128. package/dist/flows/flow-step-runners.d.ts +8 -0
  129. package/dist/flows/flow-step-runners.js +8 -2
  130. package/dist/flows/flow-store-for-session.d.ts +31 -0
  131. package/dist/flows/flow-store-for-session.js +33 -0
  132. package/dist/flows/flow-success.js +15 -3
  133. package/dist/flows/flow-tools.js +87 -33
  134. package/dist/flows/flows.d.ts +14 -1
  135. package/dist/flows/flows.js +502 -24
  136. package/dist/flows/heal-run.js +5 -2
  137. package/dist/flows/predicate-to-expect.d.ts +17 -12
  138. package/dist/flows/predicate-to-expect.js +36 -15
  139. package/dist/flows/recordings.d.ts +13 -1
  140. package/dist/flows/recordings.js +17 -2
  141. package/dist/flows/replay.d.ts +22 -2
  142. package/dist/flows/replay.js +28 -2
  143. package/dist/flows/server-verify.js +16 -2
  144. package/dist/flows/verify-change-tools.js +9 -3
  145. package/dist/honesty/blind-spots.d.ts +51 -15
  146. package/dist/honesty/blind-spots.js +64 -4
  147. package/dist/honesty/feature-capture.d.ts +150 -0
  148. package/dist/honesty/feature-capture.js +233 -0
  149. package/dist/honesty/honesty.d.ts +24 -0
  150. package/dist/honesty/honesty.js +25 -1
  151. package/dist/honesty/instrumentation-gaps.d.ts +39 -6
  152. package/dist/honesty/instrumentation-gaps.js +88 -5
  153. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  154. package/dist/honesty/tool-hit-rate.js +90 -0
  155. package/dist/honesty/undeclared-change.d.ts +51 -0
  156. package/dist/honesty/undeclared-change.js +56 -0
  157. package/dist/honesty/verified.d.ts +16 -0
  158. package/dist/honesty/verified.js +84 -5
  159. package/dist/http-server.d.ts +0 -1
  160. package/dist/http-server.js +13 -1
  161. package/dist/hunt/hunt-report.d.ts +0 -2
  162. package/dist/hunt/hunt-report.js +1 -1
  163. package/dist/impact/impact-recorder.d.ts +31 -8
  164. package/dist/impact/impact-recorder.js +104 -21
  165. package/dist/impact/impact-store.d.ts +13 -4
  166. package/dist/impact/impact-store.js +44 -3
  167. package/dist/index.d.ts +4 -1
  168. package/dist/index.js +113 -7
  169. package/dist/init/agent-rules.d.ts +30 -6
  170. package/dist/init/agent-rules.js +54 -13
  171. package/dist/init/astro-patch.d.ts +16 -0
  172. package/dist/init/astro-patch.js +28 -0
  173. package/dist/init/capabilities.d.ts +4 -3
  174. package/dist/init/capabilities.js +23 -3
  175. package/dist/init/closing-hint.d.ts +36 -0
  176. package/dist/init/closing-hint.js +120 -0
  177. package/dist/init/confirm.d.ts +27 -28
  178. package/dist/init/confirm.js +51 -11
  179. package/dist/init/cra.d.ts +34 -5
  180. package/dist/init/cra.js +96 -25
  181. package/dist/init/csp-check.js +17 -2
  182. package/dist/init/csp-doctor.d.ts +8 -1
  183. package/dist/init/csp-doctor.js +8 -3
  184. package/dist/init/dev-script.d.ts +50 -0
  185. package/dist/init/dev-script.js +66 -0
  186. package/dist/init/format-generated.d.ts +18 -0
  187. package/dist/init/format-generated.js +39 -0
  188. package/dist/init/install-hint.d.ts +9 -0
  189. package/dist/init/install-hint.js +29 -0
  190. package/dist/init/mcp-clients.d.ts +7 -0
  191. package/dist/init/mcp-clients.js +7 -0
  192. package/dist/init/mcp.d.ts +17 -6
  193. package/dist/init/mcp.js +27 -11
  194. package/dist/init/node-io.js +11 -1
  195. package/dist/init/plan-framework.d.ts +0 -5
  196. package/dist/init/plan-framework.js +147 -37
  197. package/dist/init/plan.d.ts +30 -37
  198. package/dist/init/plan.js +50 -60
  199. package/dist/init/preflight.d.ts +28 -0
  200. package/dist/init/preflight.js +38 -0
  201. package/dist/init/refresh-rules.d.ts +39 -0
  202. package/dist/init/refresh-rules.js +51 -0
  203. package/dist/init/run.d.ts +32 -35
  204. package/dist/init/run.js +151 -202
  205. package/dist/init/slash-command.d.ts +1 -1
  206. package/dist/init/snippets.js +90 -31
  207. package/dist/init/vite-config.d.ts +1 -1
  208. package/dist/init/vite-config.js +39 -25
  209. package/dist/init/workspace-apps.d.ts +17 -10
  210. package/dist/init/workspace-apps.js +110 -0
  211. package/dist/input/network-mock-tools.js +3 -6
  212. package/dist/input/real-input.d.ts +2 -0
  213. package/dist/input/real-input.js +28 -7
  214. package/dist/input/scroll-find.d.ts +7 -4
  215. package/dist/input/scroll-find.js +63 -25
  216. package/dist/input/scroll-tools.js +4 -6
  217. package/dist/input/viewport-tools.js +5 -4
  218. package/dist/intent/inline-intent.d.ts +37 -0
  219. package/dist/intent/inline-intent.js +193 -0
  220. package/dist/intent/intent-shard-store.d.ts +72 -0
  221. package/dist/intent/intent-shard-store.js +203 -0
  222. package/dist/intent/intent-shard.d.ts +411 -0
  223. package/dist/intent/intent-shard.js +176 -0
  224. package/dist/intent/intent-store.d.ts +49 -0
  225. package/dist/intent/intent-store.js +133 -0
  226. package/dist/intent/intent-subject.d.ts +45 -0
  227. package/dist/intent/intent-subject.js +177 -0
  228. package/dist/intent/intent-tools.d.ts +2 -0
  229. package/dist/intent/intent-tools.js +143 -0
  230. package/dist/intent/open-intents.d.ts +42 -0
  231. package/dist/intent/open-intents.js +40 -0
  232. package/dist/journal/envelope.d.ts +26 -26
  233. package/dist/journal/journal-recorder.d.ts +23 -0
  234. package/dist/journal/journal-recorder.js +35 -0
  235. package/dist/license/license-env.d.ts +29 -0
  236. package/dist/license/license-env.js +148 -0
  237. package/dist/loopback-agent.d.ts +31 -0
  238. package/dist/loopback-agent.js +57 -0
  239. package/dist/mcp/attach-memory.d.ts +83 -0
  240. package/dist/mcp/attach-memory.js +185 -0
  241. package/dist/mcp/client-identity.d.ts +31 -0
  242. package/dist/mcp/client-identity.js +51 -0
  243. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  244. package/dist/mcp/mcp-post-transport.js +109 -0
  245. package/dist/mcp/mcp-proxy.d.ts +2 -8
  246. package/dist/mcp/mcp-proxy.js +64 -62
  247. package/dist/mcp/mcp.d.ts +2 -0
  248. package/dist/mcp/mcp.js +52 -24
  249. package/dist/mcp/proxy-backoff.d.ts +11 -0
  250. package/dist/mcp/proxy-backoff.js +13 -0
  251. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  252. package/dist/mcp/proxy-daemon-probe.js +7 -1
  253. package/dist/mcp/proxy-handshake.d.ts +23 -0
  254. package/dist/mcp/proxy-handshake.js +31 -0
  255. package/dist/mcp/server-instructions.js +45 -3
  256. package/dist/mcp/tool-catalog-cache.d.ts +16 -0
  257. package/dist/mcp/tool-catalog-cache.js +22 -0
  258. package/dist/memory/memory-tools.d.ts +2 -0
  259. package/dist/memory/memory-tools.js +82 -0
  260. package/dist/memory/project-memory.d.ts +34 -0
  261. package/dist/memory/project-memory.js +96 -0
  262. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  263. package/dist/oracles/flow-instrument-gaps.js +4 -5
  264. package/dist/oracles/propose-consequences.js +12 -3
  265. package/dist/oracles/self-instrument.d.ts +9 -9
  266. package/dist/oracles/self-instrument.js +19 -13
  267. package/dist/pool/browser-pool.d.ts +75 -5
  268. package/dist/pool/browser-pool.js +144 -5
  269. package/dist/pool/playwright-launcher.js +12 -1
  270. package/dist/project/fs-port.d.ts +3 -0
  271. package/dist/project/fs-port.js +3 -2
  272. package/dist/project/memory-fs.js +6 -1
  273. package/dist/project/project-tools.js +6 -35
  274. package/dist/project/reticle-dir.d.ts +17 -2
  275. package/dist/project/reticle-dir.js +22 -4
  276. package/dist/runs/context-tools.d.ts +2 -0
  277. package/dist/runs/context-tools.js +89 -0
  278. package/dist/runs/run-context.d.ts +40 -0
  279. package/dist/runs/run-context.js +158 -0
  280. package/dist/runs/run-diff.d.ts +2 -1
  281. package/dist/runs/run-store.d.ts +10 -0
  282. package/dist/runs/run-store.js +10 -0
  283. package/dist/runs/verification-sync.d.ts +0 -11
  284. package/dist/runs/verification-sync.js +24 -3
  285. package/dist/runs/verify-server.d.ts +8 -1
  286. package/dist/runs/verify-server.js +14 -2
  287. package/dist/session/command-timeout.d.ts +5 -2
  288. package/dist/session/command-timeout.js +5 -4
  289. package/dist/session/dev-server-probe.d.ts +0 -8
  290. package/dist/session/dev-server-probe.js +1 -1
  291. package/dist/session/last-act.d.ts +7 -1
  292. package/dist/session/last-act.js +2 -2
  293. package/dist/session/lease-visibility.d.ts +67 -0
  294. package/dist/session/lease-visibility.js +76 -0
  295. package/dist/session/live-control-tools.js +1 -42
  296. package/dist/session/no-session-diagnosis.d.ts +33 -0
  297. package/dist/session/no-session-diagnosis.js +82 -24
  298. package/dist/session/no-session-next-action.d.ts +12 -0
  299. package/dist/session/no-session-next-action.js +77 -6
  300. package/dist/session/no-session-watch.d.ts +18 -6
  301. package/dist/session/no-session-watch.js +125 -75
  302. package/dist/session/output-budget.js +4 -1
  303. package/dist/session/pending-commands.d.ts +11 -1
  304. package/dist/session/pending-commands.js +12 -2
  305. package/dist/session/resolve-within.d.ts +34 -0
  306. package/dist/session/resolve-within.js +53 -0
  307. package/dist/session/session-health.d.ts +9 -0
  308. package/dist/session/session-health.js +23 -0
  309. package/dist/session/session-info.d.ts +18 -0
  310. package/dist/session/session-info.js +11 -0
  311. package/dist/session/session-manager.d.ts +14 -0
  312. package/dist/session/session-manager.js +111 -4
  313. package/dist/session/session-mirror.d.ts +16 -0
  314. package/dist/session/session-mirror.js +22 -0
  315. package/dist/session/session-recommendation.d.ts +19 -5
  316. package/dist/session/session-recommendation.js +25 -9
  317. package/dist/session/session-successor.d.ts +37 -0
  318. package/dist/session/session-successor.js +113 -0
  319. package/dist/session/session-tools.js +2 -2
  320. package/dist/session/session.d.ts +53 -3
  321. package/dist/session/session.js +162 -9
  322. package/dist/session/wall-clock.d.ts +12 -0
  323. package/dist/session/wall-clock.js +14 -0
  324. package/dist/setup/agent-configs.d.ts +96 -0
  325. package/dist/setup/agent-configs.js +333 -0
  326. package/dist/setup/agent-io.d.ts +9 -0
  327. package/dist/setup/agent-io.js +16 -0
  328. package/dist/setup/agent-writer.d.ts +51 -0
  329. package/dist/setup/agent-writer.js +115 -0
  330. package/dist/setup/approval-migration.d.ts +47 -0
  331. package/dist/setup/approval-migration.js +68 -0
  332. package/dist/setup/auto-approve.d.ts +85 -0
  333. package/dist/setup/auto-approve.js +150 -0
  334. package/dist/setup/bridge-port.d.ts +15 -0
  335. package/dist/setup/bridge-port.js +24 -0
  336. package/dist/setup/desktop-shape.d.ts +57 -0
  337. package/dist/setup/desktop-shape.js +63 -0
  338. package/dist/setup/dev-server-wait.d.ts +60 -0
  339. package/dist/setup/dev-server-wait.js +65 -0
  340. package/dist/setup/drive-agent.d.ts +71 -0
  341. package/dist/setup/drive-agent.js +171 -0
  342. package/dist/setup/drive-plan.d.ts +68 -0
  343. package/dist/setup/drive-plan.js +130 -0
  344. package/dist/setup/ensure-daemon.d.ts +29 -0
  345. package/dist/setup/ensure-daemon.js +58 -0
  346. package/dist/setup/init-runtime.d.ts +47 -0
  347. package/dist/setup/init-runtime.js +160 -0
  348. package/dist/setup/interrupt.d.ts +25 -0
  349. package/dist/setup/interrupt.js +40 -0
  350. package/dist/setup/license-key.d.ts +39 -0
  351. package/dist/setup/license-key.js +72 -0
  352. package/dist/setup/listeners.d.ts +37 -0
  353. package/dist/setup/listeners.js +85 -0
  354. package/dist/setup/node-effects.d.ts +60 -0
  355. package/dist/setup/node-effects.js +210 -0
  356. package/dist/setup/page-probe.d.ts +39 -0
  357. package/dist/setup/page-probe.js +49 -0
  358. package/dist/setup/relaunch.d.ts +36 -0
  359. package/dist/setup/relaunch.js +57 -0
  360. package/dist/setup/remaining-steps.d.ts +19 -0
  361. package/dist/setup/remaining-steps.js +31 -0
  362. package/dist/setup/run-setup.d.ts +99 -0
  363. package/dist/setup/run-setup.js +187 -0
  364. package/dist/setup/session-pick.d.ts +47 -0
  365. package/dist/setup/session-pick.js +51 -0
  366. package/dist/setup/setup-command.d.ts +45 -0
  367. package/dist/setup/setup-command.js +199 -0
  368. package/dist/setup/setup-options.d.ts +74 -0
  369. package/dist/setup/setup-options.js +67 -0
  370. package/dist/setup/transcripts.d.ts +16 -0
  371. package/dist/setup/transcripts.js +43 -0
  372. package/dist/status-payload.d.ts +14 -1
  373. package/dist/status-payload.js +23 -1
  374. package/dist/telemetry/automation-hint.d.ts +2 -1
  375. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  376. package/dist/telemetry/daemon-telemetry.js +52 -5
  377. package/dist/telemetry/error-class.js +1 -1
  378. package/dist/telemetry/feedback-context.d.ts +9 -1
  379. package/dist/telemetry/feedback-context.js +60 -29
  380. package/dist/telemetry/feedback-local.d.ts +15 -0
  381. package/dist/telemetry/feedback-local.js +56 -0
  382. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  383. package/dist/telemetry/feedback.d.ts +11 -0
  384. package/dist/telemetry/feedback.js +19 -6
  385. package/dist/telemetry/init-telemetry.d.ts +2 -0
  386. package/dist/telemetry/init-telemetry.js +2 -0
  387. package/dist/telemetry/install-source.d.ts +14 -0
  388. package/dist/telemetry/install-source.js +30 -0
  389. package/dist/telemetry/license-activation.d.ts +7 -1
  390. package/dist/telemetry/license-activation.js +9 -5
  391. package/dist/telemetry/mcp-connection.js +7 -0
  392. package/dist/telemetry/project-profile.d.ts +12 -9
  393. package/dist/telemetry/project-profile.js +12 -7
  394. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  395. package/dist/telemetry/proxy-telemetry.js +21 -0
  396. package/dist/telemetry/run-telemetry.d.ts +0 -2
  397. package/dist/telemetry/run-telemetry.js +1 -1
  398. package/dist/telemetry/session-metrics.d.ts +12 -0
  399. package/dist/telemetry/session-metrics.js +35 -1
  400. package/dist/telemetry/telemetry.js +17 -2
  401. package/dist/telemetry/tool-refused.d.ts +3 -1
  402. package/dist/telemetry/tool-refused.js +34 -2
  403. package/dist/telemetry/verification-of.js +2 -2
  404. package/dist/tools/act-capsule.d.ts +10 -0
  405. package/dist/tools/act-capsule.js +8 -2
  406. package/dist/tools/act-danger.d.ts +2 -1
  407. package/dist/tools/act-danger.js +11 -3
  408. package/dist/tools/act-observation.d.ts +41 -0
  409. package/dist/tools/act-observation.js +36 -0
  410. package/dist/tools/act-preflight.d.ts +20 -0
  411. package/dist/tools/act-preflight.js +98 -0
  412. package/dist/tools/act-sequence-retry.d.ts +45 -0
  413. package/dist/tools/act-sequence-retry.js +58 -0
  414. package/dist/tools/act-sequence-tool.d.ts +13 -0
  415. package/dist/tools/act-sequence-tool.js +135 -0
  416. package/dist/tools/act-target.d.ts +14 -0
  417. package/dist/tools/act-target.js +47 -0
  418. package/dist/tools/act-tools.d.ts +21 -1
  419. package/dist/tools/act-tools.js +196 -204
  420. package/dist/tools/assert-source.d.ts +22 -0
  421. package/dist/tools/assert-source.js +53 -0
  422. package/dist/tools/assert-verdict.d.ts +20 -3
  423. package/dist/tools/assert-verdict.js +58 -20
  424. package/dist/tools/browser-tools.js +6 -22
  425. package/dist/tools/complete-snapshot.d.ts +48 -0
  426. package/dist/tools/complete-snapshot.js +75 -0
  427. package/dist/tools/coverage-tools.d.ts +10 -1
  428. package/dist/tools/coverage-tools.js +123 -69
  429. package/dist/tools/dynamic-tools.js +53 -13
  430. package/dist/tools/error-recovery.d.ts +25 -14
  431. package/dist/tools/error-recovery.js +78 -9
  432. package/dist/tools/feedback-tools.d.ts +1 -1
  433. package/dist/tools/feedback-tools.js +2 -2
  434. package/dist/tools/invoke-tool.js +121 -11
  435. package/dist/tools/lease-hint.d.ts +10 -0
  436. package/dist/tools/lease-hint.js +53 -4
  437. package/dist/tools/lease-tools.d.ts +35 -5
  438. package/dist/tools/lease-tools.js +191 -13
  439. package/dist/tools/merged-name-redirect.d.ts +13 -0
  440. package/dist/tools/merged-name-redirect.js +22 -0
  441. package/dist/tools/navigate-arrival.d.ts +0 -2
  442. package/dist/tools/navigate-arrival.js +1 -1
  443. package/dist/tools/numeric-bounds.d.ts +100 -0
  444. package/dist/tools/numeric-bounds.js +125 -0
  445. package/dist/tools/observe-tools.js +109 -65
  446. package/dist/tools/read-tools.js +31 -8
  447. package/dist/tools/real-input-attempt.d.ts +32 -0
  448. package/dist/tools/real-input-attempt.js +212 -2
  449. package/dist/tools/reconcile-tools.js +22 -8
  450. package/dist/tools/resolve-target.d.ts +0 -7
  451. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  452. package/dist/tools/sdk-marker-probe.js +57 -0
  453. package/dist/tools/snapshot-delta.d.ts +1 -1
  454. package/dist/tools/tool-kit.d.ts +24 -0
  455. package/dist/tools/tool-kit.js +21 -0
  456. package/dist/tools/tool-names.d.ts +5 -0
  457. package/dist/tools/tool-names.js +5 -0
  458. package/dist/tools/tool-surface.d.ts +85 -6
  459. package/dist/tools/tool-surface.js +122 -2
  460. package/dist/tools/tools-helpers.d.ts +19 -0
  461. package/dist/tools/tools-helpers.js +46 -0
  462. package/dist/tools/tools.d.ts +2 -0
  463. package/dist/tools/tools.js +78 -12
  464. package/dist/tools/unadvertised-help.js +4 -4
  465. package/dist/tools/verify-next-baton.d.ts +53 -0
  466. package/dist/tools/verify-next-baton.js +52 -0
  467. package/dist/update/reticle-deps.d.ts +9 -0
  468. package/dist/update/reticle-deps.js +27 -0
  469. package/dist/update/sdk-sync.d.ts +1 -2
  470. package/dist/update/sdk-sync.js +1 -21
  471. package/dist/update/update-nudge.d.ts +25 -10
  472. package/dist/update/update-nudge.js +32 -3
  473. package/dist/update/updater.d.ts +1 -1
  474. package/dist/version/peer-announce.js +4 -2
  475. package/dist/version/sdk-fix.d.ts +47 -0
  476. package/dist/version/sdk-fix.js +127 -0
  477. package/dist/version/version-nudge.d.ts +14 -0
  478. package/dist/version/version-nudge.js +21 -0
  479. package/dist/version/version-skew.d.ts +38 -3
  480. package/dist/version/version-skew.js +83 -7
  481. package/dist/visual/visual-store.d.ts +6 -6
  482. package/dist/visual/visual-store.js +18 -16
  483. package/dist/visual/visual-tools.js +57 -12
  484. package/docs/README.md +2 -1
  485. package/docs/cli/cloud.mdx +1 -1
  486. package/docs/cli/doctor.mdx +2 -0
  487. package/docs/cli/serve.mdx +4 -2
  488. package/docs/cli/verify.mdx +4 -5
  489. package/docs/cli.mdx +7 -3
  490. package/docs/desktop.mdx +1 -1
  491. package/docs/docs.json +2 -2
  492. package/docs/gates.md +1 -1
  493. package/docs/getting-started.md +19 -3
  494. package/docs/index.mdx +9 -6
  495. package/docs/install-agentic.mdx +38 -12
  496. package/docs/install-manual.mdx +1 -1
  497. package/docs/packages/browser.mdx +2 -2
  498. package/docs/packages/server.mdx +1 -1
  499. package/docs/packages.mdx +8 -6
  500. package/docs/platform-integration.md +17 -0
  501. package/docs/predicates.mdx +8 -0
  502. package/docs/reticle-manual.md +106 -0
  503. package/docs/skill-file.mdx +3 -3
  504. package/docs/telemetry-contract.md +108 -4
  505. package/docs/tools-act-sequence.mdx +16 -0
  506. package/docs/tools-tools-and-run.mdx +2 -4
  507. package/docs/troubleshooting.mdx +9 -0
  508. package/docs/usage.md +4 -3
  509. package/package.json +4 -3
  510. package/dist/init/cursor.d.ts +0 -24
  511. package/dist/init/cursor.js +0 -80
  512. package/dist/session/session-readiness.d.ts +0 -34
  513. package/dist/session/session-readiness.js +0 -36
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Automatic sync, for the session nobody thinks about.
3
+ *
4
+ * Somebody runs Reticle all morning. Every tool call rewrites the impact record; every verification
5
+ * writes a run. If sync is a command a human types, the dashboard is stale for exactly as long as
6
+ * they are busy — which is precisely when the numbers on it would have been worth looking at.
7
+ *
8
+ * So the daemon does it. Not a new process, not a thing to remember: the daemon is already running
9
+ * for the whole session (it is what the agent talks to), so it is the only place where "sync while
10
+ * work is happening" costs nobody anything.
11
+ *
12
+ * ── THE THREE RULES THAT KEEP IT INVISIBLE ────────────────────────────────────────────────────
13
+ * QUIET WHEN IDLE. A cycle on an unchanged session is one small GET and nothing else. That is what
14
+ * makes a one-minute timer affordable; it is also why the timer is not adaptive — a scheme that
15
+ * "backs off when idle" is complexity paid for a cost that is already near zero.
16
+ *
17
+ * NEVER IN THE WAY. Nothing here is awaited by a tool call. A sync that can slow down or fail a
18
+ * verification would be a sync worth switching off, and the local record is authoritative anyway.
19
+ *
20
+ * SILENT UNLESS IT MATTERS. A failed cycle is logged once per NEW error, not once per minute: a
21
+ * laptop on a train would otherwise fill the log with the same line four hundred times and teach
22
+ * everyone to ignore it.
23
+ */
24
+ import { log } from '../log.js';
25
+ import { describeSync, runSyncCycle } from './sync-cycle.js';
26
+ import { diskSink, diskSource, readCloudState } from './sync-disk.js';
27
+ /** How often the daemon cycles. See the note above on why this is a constant and not a curve. */
28
+ export const DAEMON_SYNC_INTERVAL_MS = 60_000;
29
+ /**
30
+ * The cadence while something is actually happening.
31
+ *
32
+ * A fixed minute is the wrong shape for both states it has to cover. During a drive the ledger
33
+ * changes on every tool call and a minute of lag is what makes a dashboard feel dead; idle, a minute
34
+ * is already more often than nothing has changed deserves.
35
+ *
36
+ * So the interval follows the work: a cycle that MOVED something earns the fast rate, a cycle that
37
+ * moved nothing backs off to the slow one. No session plumbing and no new configuration — activity
38
+ * is inferred from what the last cycle actually sent, which is the only honest evidence available.
39
+ *
40
+ * Deliberately not per-tool-call. A push in the tool path would put the network in the agent's inner
41
+ * loop, and verification working with the network down is a promise this product makes.
42
+ */
43
+ export const DAEMON_SYNC_ACTIVE_INTERVAL_MS = 5_000;
44
+ /** Given to the first cycle so a freshly-started daemon does not race the session that woke it. */
45
+ const FIRST_CYCLE_DELAY_MS = 5_000;
46
+ /**
47
+ * How long a nudge waits before cycling.
48
+ *
49
+ * Long enough to COALESCE — a battery that writes six runs in a second must cause one cycle, not
50
+ * six — and short enough that finishing a verification and looking at the dashboard feels like one
51
+ * action. Not zero: a run artifact is renamed into place and the sibling records it implies are
52
+ * written just after, so cycling on the very first byte would ship a bundle that is missing them.
53
+ */
54
+ const NUDGE_DELAY_MS = 1_500;
55
+ const defaultRequest = async (url, init) => {
56
+ const res = await fetch(url, init);
57
+ return { status: res.status, text: await res.text() };
58
+ };
59
+ /**
60
+ * Start the loop. Safe to call for an UNLINKED project: it resolves the link on every tick and does
61
+ * nothing until one appears, which is what lets `reticle link` take effect without a restart.
62
+ */
63
+ export function startSyncDaemon(deps) {
64
+ const intervalMs = deps.intervalMs ?? DAEMON_SYNC_INTERVAL_MS;
65
+ // Never slower than the idle rate: a deployment that lengthens the interval means "sync less", and
66
+ // an active burst must not quietly re-introduce the cost it was lowering.
67
+ const activeIntervalMs = Math.min(DAEMON_SYNC_ACTIVE_INTERVAL_MS, intervalMs);
68
+ const now = deps.now ?? (() => Date.now());
69
+ const request = deps.request ?? defaultRequest;
70
+ let timer;
71
+ let stopped = false;
72
+ let running = false;
73
+ /** The last error already reported. Repeats are swallowed so an offline laptop stays quiet. */
74
+ let reportedError;
75
+ /**
76
+ * Whether the last cycle found a link — undefined until the first one has looked.
77
+ *
78
+ * An unlinked project used to sync nothing and say nothing, which is indistinguishable from a
79
+ * working sync until somebody notices the dashboard is hours behind. Observed for real: the
80
+ * daemon was started from a sibling repo that had never been linked, so every call for an entire
81
+ * session was recorded into THAT repo's ledger and pushed nowhere, and the first evidence was a
82
+ * stale number on a dashboard nobody had reason to distrust.
83
+ *
84
+ * Announced on the FIRST cycle and on every change after it, so `reticle link` mid-session says
85
+ * so too. Once per transition, never per tick — a line every minute is a line people stop reading.
86
+ */
87
+ let wasLinked;
88
+ /** One root's bundle. The state, source and sink are already per-root; only the caller was not. */
89
+ const pushRoot = async (root, cloud) => {
90
+ if (null === cloud.config)
91
+ return undefined;
92
+ const full = diskSource(root);
93
+ return runSyncCycle({
94
+ config: cloud.config,
95
+ source: {
96
+ runs: () => (cloud.policy.runs ? full.runs() : []),
97
+ flows: () => (cloud.policy.flows ? full.flows() : []),
98
+ derived: (kind) => (cloud.policy.memory ? full.derived(kind) : undefined),
99
+ },
100
+ sink: diskSink(root),
101
+ state: readCloudState(root),
102
+ now,
103
+ request,
104
+ });
105
+ };
106
+ /**
107
+ * Push every other linked root this machine knows about.
108
+ *
109
+ * Isolated per root on purpose. A revoked credential, a deleted directory or an unreachable
110
+ * self-hosted server in ONE repo is a fact about that repo; letting it throw would take the
111
+ * daemon's own push down with it and turn one broken link into a machine-wide outage.
112
+ */
113
+ const syncOtherRoots = async () => {
114
+ const roots = deps.otherRoots;
115
+ const cloudFor = deps.cloudFor;
116
+ if (roots === undefined || cloudFor === undefined)
117
+ return;
118
+ let list = [];
119
+ try {
120
+ list = await roots();
121
+ }
122
+ catch {
123
+ // Enumeration is best-effort: a registry that cannot be read must not stop this daemon
124
+ // syncing the root it is standing in.
125
+ return;
126
+ }
127
+ for (const root of list) {
128
+ if (root === deps.reticleRoot)
129
+ continue;
130
+ try {
131
+ const cloud = await cloudFor(root);
132
+ if (null === cloud.config)
133
+ continue;
134
+ const report = await pushRoot(root, cloud);
135
+ if (report === undefined)
136
+ continue;
137
+ if (report.error !== undefined) {
138
+ log('reticle_cloud_sync_failed', { root, error: report.error });
139
+ continue;
140
+ }
141
+ const moved = report.runsSent > 0 ||
142
+ report.flowsSent > 0 ||
143
+ report.derivedSent.length > 0 ||
144
+ report.pulled > 0;
145
+ // The root is NAMED here and not in the single-root log below, because with several repos
146
+ // reporting, "synced 3 runs" without a directory is not an answer to "synced from where".
147
+ if (moved)
148
+ log('reticle_cloud_synced', { root, summary: describeSync(report) });
149
+ }
150
+ catch (error) {
151
+ log('reticle_cloud_sync_failed', {
152
+ root,
153
+ error: error instanceof Error ? error.message : String(error),
154
+ });
155
+ }
156
+ }
157
+ };
158
+ const cycle = async () => {
159
+ // Overlap guard: a slow cycle must not have a second one started on top of it, or two bundles
160
+ // race and the cursor written by the loser silently rewinds the winner's progress.
161
+ if (running)
162
+ return undefined;
163
+ running = true;
164
+ try {
165
+ const cloud = await deps.cloud();
166
+ const linked = null !== cloud.config;
167
+ if (linked !== wasLinked) {
168
+ wasLinked = linked;
169
+ if (linked)
170
+ log('reticle_cloud_linked', {
171
+ projectId: cloud.projectId,
172
+ root: deps.reticleRoot,
173
+ });
174
+ // Names the ROOT, because the answer is almost always "the daemon is not where you think it
175
+ // is" — and a message that omits the directory sends people to check their key instead.
176
+ else
177
+ log('reticle_cloud_unlinked', {
178
+ root: deps.reticleRoot,
179
+ fix: 'run `reticle link` in this directory, or start the daemon in the linked one — nothing is being synced from here',
180
+ });
181
+ }
182
+ // Every OTHER linked root, first. Their failures are logged and never abort this daemon's own
183
+ // push: one repo whose credential was revoked must not silence the rest of the machine.
184
+ await syncOtherRoots();
185
+ if (null === cloud.config)
186
+ return undefined;
187
+ const report = await pushRoot(deps.reticleRoot, cloud);
188
+ // `pushRoot` returns undefined only for an unlinked root, which the guard above has ruled out.
189
+ if (report === undefined)
190
+ return undefined;
191
+ if (report.error !== undefined) {
192
+ if (report.error !== reportedError) {
193
+ reportedError = report.error;
194
+ log('reticle_cloud_sync_failed', { error: report.error });
195
+ }
196
+ }
197
+ else {
198
+ reportedError = undefined;
199
+ // Only when something actually moved. A per-minute "nothing to send" is noise that trains
200
+ // people to stop reading the log.
201
+ const moved = report.runsSent > 0 ||
202
+ report.flowsSent > 0 ||
203
+ report.derivedSent.length > 0 ||
204
+ report.pulled > 0;
205
+ if (moved)
206
+ log('reticle_cloud_synced', { summary: describeSync(report) });
207
+ /*
208
+ * A cycle that moved something means a drive is in progress, so the next one comes sooner;
209
+ * one that moved nothing means the machine is idle, so it backs straight off. The rate
210
+ * follows the work without needing to be told about sessions, and an idle laptop settles at
211
+ * the same cost it had before this existed.
212
+ */
213
+ nextDelay = moved ? activeIntervalMs : intervalMs;
214
+ }
215
+ return report;
216
+ }
217
+ catch (error) {
218
+ // Belt and braces: runSyncCycle already swallows, and a throw here would kill the timer.
219
+ const message = error instanceof Error ? error.message : String(error);
220
+ if (message !== reportedError) {
221
+ reportedError = message;
222
+ log('reticle_cloud_sync_failed', { error: message });
223
+ }
224
+ return undefined;
225
+ }
226
+ finally {
227
+ running = false;
228
+ }
229
+ };
230
+ /** The delay for the NEXT cycle: fast while the last one moved something, slow once it stops. */
231
+ let nextDelay = intervalMs;
232
+ const schedule = (delay) => {
233
+ if (stopped)
234
+ return;
235
+ // Replace, never stack. `nudge` and the interval both schedule, and leaving the old timer armed
236
+ // would let every nudge add a permanent extra cycle per minute for the life of the process.
237
+ if (timer !== undefined)
238
+ clearTimeout(timer);
239
+ timer = setTimeout(() => {
240
+ void cycle().finally(() => schedule(nextDelay));
241
+ }, delay);
242
+ // Unref'd: a pending sync must never be the reason a process refuses to exit.
243
+ timer.unref?.();
244
+ };
245
+ schedule(FIRST_CYCLE_DELAY_MS);
246
+ return {
247
+ syncNow: cycle,
248
+ nudge: () => {
249
+ // A cycle already in flight will not pick this up, so the nudge is still scheduled behind it
250
+ // rather than dropped — otherwise the run that arrived during a slow cycle waits a full tick.
251
+ schedule(NUDGE_DELAY_MS);
252
+ },
253
+ stop: () => {
254
+ stopped = true;
255
+ if (timer !== undefined)
256
+ clearTimeout(timer);
257
+ timer = undefined;
258
+ },
259
+ };
260
+ }
@@ -0,0 +1,9 @@
1
+ import type { CloudSyncState, PulledIssues, SyncSink, SyncSource } from './sync-cycle.js';
2
+ /** What the cycle reads, backed by a real `.reticle` directory. */
3
+ export declare function diskSource(reticleRoot: string): SyncSource;
4
+ /** What the cycle writes back into `.reticle/`. */
5
+ export declare function diskSink(reticleRoot: string): SyncSink;
6
+ /** This machine's side of the conversation, or a blank one on first run. */
7
+ export declare function readCloudState(reticleRoot: string): CloudSyncState;
8
+ /** The decisions pulled back from the dashboard, for the HUD and for the next run to read. */
9
+ export declare function readCloudIssues(reticleRoot: string): PulledIssues;
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The filesystem half of a sync cycle: what `.reticle/` looks like to the protocol.
3
+ *
4
+ * Kept apart from `sync-cycle.ts` on purpose. The cycle is the interesting part — what to send, what
5
+ * to skip, what to do when the network is down — and it stays provable only because it never touches
6
+ * a disk. This is the boring part, and boring is what it should be: read a file, tolerate its
7
+ * absence, never throw.
8
+ *
9
+ * EVERY READ IS FORGIVING. A half-written impact.json from a process that died mid-flush must cost
10
+ * one unsynced record, not a crashed sync. There is no state here worth defending against a parse
11
+ * error — the file will be rewritten on the next tool call anyway.
12
+ */
13
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync, } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { ReticleDir } from '@reticlehq/core';
16
+ import { subjectFor } from '../intent/intent-subject.js';
17
+ const JSON_SUFFIX = '.json';
18
+ /** Read + parse, or undefined for absent/unreadable/malformed. Never throws. */
19
+ function readJson(path) {
20
+ try {
21
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : undefined;
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ /**
28
+ * Write atomically. tmp + rename, because two daemons can serve one repo at once and a half-written
29
+ * cursor file is one the next read discards — which would silently re-pull the whole decision
30
+ * history, or worse, skip it.
31
+ */
32
+ function writeJson(path, value) {
33
+ try {
34
+ mkdirSync(dirname(path), { recursive: true });
35
+ const tmp = `${path}.${String(process.pid)}.tmp`;
36
+ writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
37
+ renameSync(tmp, path);
38
+ }
39
+ catch {
40
+ // Bookkeeping that cannot be written must never break a sync. The next cycle re-derives it; the
41
+ // worst case is one redundant upload, which the server dedupes anyway.
42
+ }
43
+ }
44
+ /** Every `*.json` directly inside a directory, parsed. Absent directory reads as empty. */
45
+ function readJsonDir(dir) {
46
+ try {
47
+ if (!existsSync(dir))
48
+ return [];
49
+ return readdirSync(dir)
50
+ .filter((f) => f.endsWith(JSON_SUFFIX))
51
+ .map((f) => readJson(join(dir, f)))
52
+ .filter((v) => v !== undefined);
53
+ }
54
+ catch {
55
+ return [];
56
+ }
57
+ }
58
+ /** Flows live one directory deeper, under the app's own project id. */
59
+ function readFlows(root) {
60
+ const dir = join(root, ReticleDir.FLOWS_SUBDIR);
61
+ try {
62
+ if (!existsSync(dir))
63
+ return [];
64
+ return readdirSync(dir).flatMap((scope) => readJsonDir(join(dir, scope)));
65
+ }
66
+ catch {
67
+ return [];
68
+ }
69
+ }
70
+ const DERIVED_FILE = {
71
+ impact: ReticleDir.IMPACT_FILE,
72
+ flake: ReticleDir.FLAKE_FILE,
73
+ intent: ReticleDir.INTENT_FILE,
74
+ };
75
+ /** The directory the sharded intent store writes into, beside the legacy flat file. */
76
+ const INTENT_SUBDIR = 'intent';
77
+ const INTENT_INDEX = 'index.json';
78
+ /**
79
+ * Every intent this project holds, flat file and shards merged.
80
+ *
81
+ * The store was split into one file per subject so that writing one record stops rewriting all of
82
+ * them. Sync was not told, and kept reading `intent.json` alone — which the migration deliberately
83
+ * does not delete. The result was the worst shape a sync bug can take: not an error, but a corpus
84
+ * that looked healthy on the dashboard and had silently stopped moving, because every intent
85
+ * captured after the migration stayed on the engineer's laptop.
86
+ *
87
+ * Shards WIN on a collision, matching the store's own rule: an id present in both has been migrated
88
+ * and possibly edited since, and letting the flat copy overwrite that would revert the edit on every
89
+ * cycle. `index.json` is derived from the shards and is skipped — sending it would duplicate every
90
+ * record as a summary of itself.
91
+ *
92
+ * Returns undefined when there is no memory at all, so a project with nothing to say sends nothing
93
+ * rather than an empty envelope the server has to store.
94
+ */
95
+ function readIntent(reticleRoot) {
96
+ const legacy = readJson(join(reticleRoot, ReticleDir.INTENT_FILE));
97
+ const merged = {
98
+ ...(legacy?.intents ?? {}),
99
+ };
100
+ let shards = 0;
101
+ for (const file of safeReaddir(join(reticleRoot, INTENT_SUBDIR))) {
102
+ if (!file.endsWith(JSON_SUFFIX) || INTENT_INDEX === file)
103
+ continue;
104
+ const shard = readJson(join(reticleRoot, INTENT_SUBDIR, file));
105
+ // A hand-edited or half-written shard costs one subject, never the whole sync.
106
+ if (shard?.intents === undefined)
107
+ continue;
108
+ shards++;
109
+ for (const [id, record] of Object.entries(shard.intents))
110
+ merged[id] = record;
111
+ }
112
+ if (legacy === undefined && 0 === shards)
113
+ return undefined;
114
+ /*
115
+ * Stamp the SUBJECT before sending, on anything that does not already carry one.
116
+ *
117
+ * The subject ladder lives here, in the engine, and knows how to read a flow, a route and a
118
+ * binding predicate. The server had no access to it and had reimplemented a far weaker version —
119
+ * flow-or-nothing — so records this ladder files correctly arrived at the dashboard as
120
+ * `unsorted`. Measured on the real corpus: 105 of 163 records the engine can place were landing
121
+ * in the bucket of last resort, which is what made a coverage map read as one pile with six
122
+ * labels.
123
+ *
124
+ * Computed HERE rather than duplicated there, because two rules for one question is how the
125
+ * answers start disagreeing — and the one with the evidence should be the one that decides.
126
+ * Records that already name a subject keep it: an agent's explicit choice outranks inference.
127
+ */
128
+ const placed = Object.fromEntries(Object.entries(merged).map(([id, record]) => {
129
+ const held = record;
130
+ if ('string' === typeof held.subject && held.subject.length > 0)
131
+ return [id, record];
132
+ const subject = subjectFor({
133
+ surface: held.surface,
134
+ binding: held.binding,
135
+ });
136
+ return [id, { ...held, subject }];
137
+ }));
138
+ return { version: 1, intents: placed };
139
+ }
140
+ /** Directory listing that tolerates absence — an unmigrated project has no `intent/`. */
141
+ function safeReaddir(dir) {
142
+ try {
143
+ return readdirSync(dir);
144
+ }
145
+ catch {
146
+ return [];
147
+ }
148
+ }
149
+ /** What the cycle reads, backed by a real `.reticle` directory. */
150
+ export function diskSource(reticleRoot) {
151
+ return {
152
+ runs: () => readJsonDir(join(reticleRoot, ReticleDir.RUNS_SUBDIR))
153
+ .map((payload) => {
154
+ const id = payload?.runId;
155
+ return 'string' === typeof id ? { runId: id, payload } : undefined;
156
+ })
157
+ // A run artifact with no id cannot be diffed against the server's list, so sending it would
158
+ // mean re-sending it every cycle forever. Dropped rather than uploaded repeatedly.
159
+ .filter((r) => r !== undefined),
160
+ flows: () => readFlows(reticleRoot),
161
+ // Intent is the only derived record that is no longer one file. See readIntent.
162
+ derived: (kind) => 'intent' === kind ? readIntent(reticleRoot) : readJson(join(reticleRoot, DERIVED_FILE[kind])),
163
+ };
164
+ }
165
+ /** What the cycle writes back into `.reticle/`. */
166
+ export function diskSink(reticleRoot) {
167
+ return {
168
+ writeIssues: (issues) => {
169
+ // MERGED, not replaced. A pull returns only what changed since the cursor, so overwriting
170
+ // would drop every earlier decision the moment one new decision arrived.
171
+ const held = readCloudIssues(reticleRoot);
172
+ writeJson(join(reticleRoot, ReticleDir.ISSUES_FILE), {
173
+ triage: { ...held.triage, ...issues.triage },
174
+ });
175
+ },
176
+ writeState: (state) => {
177
+ writeJson(join(reticleRoot, ReticleDir.CLOUD_STATE_FILE), state);
178
+ },
179
+ };
180
+ }
181
+ /** This machine's side of the conversation, or a blank one on first run. */
182
+ export function readCloudState(reticleRoot) {
183
+ const raw = readJson(join(reticleRoot, ReticleDir.CLOUD_STATE_FILE));
184
+ return 'object' === typeof raw && null !== raw ? raw : {};
185
+ }
186
+ /** The decisions pulled back from the dashboard, for the HUD and for the next run to read. */
187
+ export function readCloudIssues(reticleRoot) {
188
+ const raw = readJson(join(reticleRoot, ReticleDir.ISSUES_FILE));
189
+ const triage = 'object' === typeof raw && null !== raw ? raw.triage : undefined;
190
+ return { triage: 'object' === typeof triage && null !== triage ? triage : {} };
191
+ }
@@ -0,0 +1,2 @@
1
+ /** A stable content hash of one derived record. Must equal the server's for the same input. */
2
+ export declare function hashPayload(payload: unknown): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The content hash both halves of sync compare on.
3
+ *
4
+ * The server answers "here is a hash per record you have already sent me"; the machine hashes what
5
+ * it holds and sends only what differs. That only works if the two sides compute the SAME number
6
+ * from the same record, so this function is a CONTRACT, not a utility — it has to stay byte-for-byte
7
+ * equivalent to the server's, and it is separated out and tested against fixed vectors for exactly
8
+ * that reason.
9
+ *
10
+ * Keys are sorted before serialising because two structurally identical records must hash the same
11
+ * however they happened to be written out. Without that, a machine whose JSON key order differed
12
+ * from the server's would re-upload the same unchanged record on every cycle, forever, and the whole
13
+ * point of hashing — not paying for what has not moved — would be lost silently.
14
+ */
15
+ import { createHash } from 'node:crypto';
16
+ /** How much of the digest is kept. Long enough that a collision is not a practical concern here. */
17
+ const HASH_LENGTH = 32;
18
+ /** Recursively sort object keys so serialisation is canonical. Arrays keep their order — it is data. */
19
+ function canonical(value) {
20
+ if (Array.isArray(value))
21
+ return value.map(canonical);
22
+ if (value !== null && 'object' === typeof value) {
23
+ return Object.fromEntries(Object.entries(value)
24
+ .sort(([a], [b]) => a.localeCompare(b))
25
+ .map(([k, v]) => [k, canonical(v)]));
26
+ }
27
+ return value;
28
+ }
29
+ /** A stable content hash of one derived record. Must equal the server's for the same input. */
30
+ export function hashPayload(payload) {
31
+ return createHash('sha256')
32
+ .update(JSON.stringify(canonical(payload)))
33
+ .digest('hex')
34
+ .slice(0, HASH_LENGTH);
35
+ }
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { ReticleTool } from '../tools/tool-names.js';
3
3
  import { asNumber, asString } from '../tools/tools-helpers.js';
4
+ import { stepCountSchema, timeoutMsSchema } from '../tools/numeric-bounds.js';
4
5
  import { crawl } from './crawl.js';
5
6
  import { routeFromUrl, routesFromEvents } from '../project/learned-routes.js';
6
7
  const nodeSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -14,9 +15,10 @@ export const CRAWL_TOOLS = [
14
15
  name: ReticleTool.CRAWL,
15
16
  description: 'Autonomously click every reachable interactive control (bounded by maxSteps, default 25) and report anomalies WITHOUT a script. Two classes: single-channel faults (console errors, failed requests ≥400, DEAD controls that dispatched but did nothing) and CONTRADICTIONS — two channels disagreeing about the same click, e.g. the UI advanced while its write failed, a success signal fired over a failed request, a write succeeded and nothing changed, the same write fired twice, or the UI moved on over an in-flight request. Contradictions are the false greens a human cannot see, because a human watches one channel (the screen) and it looks correct. DESTRUCTIVE — it really clicks (may navigate/mutate state); use reticle_explore first for a non-destructive list. Returns { interactiveFound, stepsRun, anomalies[{kind,ref,desc,detail}], counts, visited, truncated }.',
16
17
  inputSchema: {
17
- maxSteps: z.number().optional().describe('Maximum number of controls to click. Default: 25.'),
18
- settleMs: z
19
- .number()
18
+ maxSteps: stepCountSchema
19
+ .optional()
20
+ .describe('Maximum number of controls to click. Default: 25.'),
21
+ settleMs: timeoutMsSchema
20
22
  .optional()
21
23
  .describe('Milliseconds to wait after each click for the app to react. Default: 500.'),
22
24
  scope: z
@@ -4,6 +4,18 @@ export interface CrawlSession {
4
4
  command(name: string, args?: Record<string, unknown>): Promise<CommandResult>;
5
5
  elapsed(): number;
6
6
  eventsSince(cursor: number): ReticleEvent[];
7
+ /**
8
+ * Which document is on screen now, so a finding is never drawn from a page the crawl has already
9
+ * navigated away from. Optional for the same reason `beginAction` is: a test fake supplies a
10
+ * minimal session, and its absence leaves the scoping inert rather than broken.
11
+ */
12
+ readonly currentDocumentId?: string | undefined;
13
+ /** Which round of source edits is in force, so a finding drawn entirely from pre-edit evidence
14
+ * says so. Optional for the same reason `currentDocumentId` is. */
15
+ readonly currentEditEpoch?: number | undefined;
16
+ /** The page under test, so a third-party beacon cannot be reported against a control. Optional
17
+ * for the same reason `currentDocumentId` is. */
18
+ readonly url?: string | undefined;
7
19
  /**
8
20
  * Attribution window around each click. Optional so a caller can supply a minimal session, but a real
9
21
  * session MUST provide it: without a window the click's own effects carry no actionId, and an
@@ -44,19 +56,6 @@ export interface CrawlAnomaly {
44
56
  }
45
57
  /** Said out loud when the page was larger than one snapshot, so a clean sweep cannot mean "all clear". */
46
58
  export declare const CAPPED_SNAPSHOT_NOTE = "the page exceeded one snapshot, so controls past the cap were never seen \u2014 interactiveFound is a floor, not a total; re-run with a narrower `scope` to cover the rest";
47
- /**
48
- * Said out loud when the crawl's OWN clicks put controls on the page that it then never visited.
49
- *
50
- * The enumeration happens once, before the first click, so a control that only exists after a login,
51
- * a modal or a tab switch is invisible to this run. Measured on the bench app: a crawl of the login
52
- * screen visited three controls, its click on "Sign in" produced seven more, and it returned
53
- * `truncated: false` with an empty anomaly list and nine of twelve steps unused. Read as a sweep,
54
- * that is a clean bill of health for an app the crawl never entered.
55
- *
56
- * The same words as the capped-snapshot case on purpose — it is the same idea (interactiveFound is a
57
- * floor) reached by a different route, and a second vocabulary for it would just be more to learn.
58
- */
59
- export declare function revealedControlsNote(count: number): string;
60
59
  export interface CrawlReport {
61
60
  interactiveFound: number;
62
61
  stepsRun: number;
@@ -18,7 +18,7 @@ export const CAPPED_SNAPSHOT_NOTE = 'the page exceeded one snapshot, so controls
18
18
  * The same words as the capped-snapshot case on purpose — it is the same idea (interactiveFound is a
19
19
  * floor) reached by a different route, and a second vocabulary for it would just be more to learn.
20
20
  */
21
- export function revealedControlsNote(count) {
21
+ function revealedControlsNote(count) {
22
22
  return (`the page changed as you clicked: ${String(count)} control(s) that did not exist when this crawl ` +
23
23
  'started were never visited — interactiveFound is a floor, not a total; re-run the crawl from ' +
24
24
  'the state you are now in to cover them');
@@ -65,7 +65,19 @@ export function legitimatelyInert(desc) {
65
65
  return true;
66
66
  if (desc.includes('[checked]'))
67
67
  return true;
68
- return /\b(textbox|searchbox|combobox|spinbutton)\b/.test(desc);
68
+ // Inputs, and the ARIA LIVE-REGION roles.
69
+ //
70
+ // `alert`, `status`, `log`, `timer` and `marquee` exist to be READ — a screen reader announces
71
+ // them and nothing is meant to happen when one is clicked. Clicking twice and seeing nothing is
72
+ // therefore correct behaviour, and reporting it as `dead-control` accuses a working app.
73
+ //
74
+ // Found by driving a real app: the demo reported `dead-control — alert "Invalid email or
75
+ // password"`, which is a login form correctly telling somebody their password was wrong. That is
76
+ // the most expensive kind of false positive there is — the strongest claim the crawler makes,
77
+ // about an app that was working, and on the path a new user meets first.
78
+ //
79
+ // `alertdialog` is deliberately NOT here: it is interactive, and a word boundary keeps it out.
80
+ return /\b(textbox|searchbox|combobox|spinbutton|alert|status|log|timer|marquee)\b/.test(desc);
69
81
  }
70
82
  /**
71
83
  * How many interactive controls exist now that did not exist when the crawl enumerated.
@@ -199,7 +211,17 @@ export async function crawl(session, opts, sleep) {
199
211
  }
200
212
  // CONTRADICTIONS: the channels disagree about what this click did. Reported per control, so a
201
213
  // crawl of an unknown app surfaces its false greens without anyone knowing where to look.
202
- for (const c of findContradictions(events)) {
214
+ // A crawl clicks controls that navigate, so its windows are the ones most likely to straddle a
215
+ // document boundary — and a finding here is reported against a specific control by name.
216
+ for (const c of findContradictions(events, {
217
+ currentDocumentId: session.currentDocumentId,
218
+ currentEditEpoch: session.currentEditEpoch,
219
+ appOrigin: session.url,
220
+ // The crawl's window IS one control's click, so it can name the floor the consequence rules
221
+ // need — without it a crawl would report nothing about the UI moving, which is most of what a
222
+ // crawl is for.
223
+ actionSince: since,
224
+ })) {
203
225
  counts.contradictions += 1;
204
226
  anomalies.push({
205
227
  kind: c.kind,