@reticlehq/server 2.12.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 (386) hide show
  1. package/SKILL.md +67 -109
  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 -1
  5. package/dist/bridge/bridge.js +247 -164
  6. package/dist/bridge/pairing-token.d.ts +5 -0
  7. package/dist/bridge/pairing-token.js +28 -0
  8. package/dist/bridge/protocol-skew.d.ts +37 -0
  9. package/dist/bridge/protocol-skew.js +45 -0
  10. package/dist/capsule/capsule-store.d.ts +6 -6
  11. package/dist/capsule/causal-summary.js +7 -2
  12. package/dist/cli/cli-doctor.js +31 -1
  13. package/dist/cli/cli-launch.d.ts +11 -1
  14. package/dist/cli/cli-launch.js +59 -10
  15. package/dist/cli/cli-parse.d.ts +15 -1
  16. package/dist/cli/cli-parse.js +212 -8
  17. package/dist/cli/cli-verify.d.ts +1 -0
  18. package/dist/cli/cli-verify.js +3 -2
  19. package/dist/cli/cloud-cli.js +428 -259
  20. package/dist/cli/cloud-keystore.d.ts +37 -0
  21. package/dist/cli/cloud-keystore.js +97 -0
  22. package/dist/cli/cloud-kit.d.ts +37 -0
  23. package/dist/cli/cloud-kit.js +121 -0
  24. package/dist/cli/cloud-login.d.ts +17 -0
  25. package/dist/cli/cloud-login.js +183 -0
  26. package/dist/cli/cloud-session.d.ts +48 -0
  27. package/dist/cli/cloud-session.js +53 -0
  28. package/dist/cli/config-discovery.js +1 -0
  29. package/dist/cli/daemon-start-options.d.ts +32 -0
  30. package/dist/cli/daemon-start-options.js +38 -0
  31. package/dist/cli/device-flow.d.ts +76 -0
  32. package/dist/cli/device-flow.js +56 -0
  33. package/dist/cli/doctor-rows.d.ts +9 -0
  34. package/dist/cli/doctor-rows.js +9 -0
  35. package/dist/cli/drive-attach.d.ts +13 -0
  36. package/dist/cli/drive-attach.js +18 -0
  37. package/dist/cli/drive-command.js +7 -3
  38. package/dist/cli/mcp-command.js +51 -20
  39. package/dist/cli/project-name.d.ts +30 -0
  40. package/dist/cli/project-name.js +44 -0
  41. package/dist/cli/status-next-action.d.ts +14 -0
  42. package/dist/cli/status-next-action.js +4 -4
  43. package/dist/cli.d.ts +1 -0
  44. package/dist/cli.js +134 -80
  45. package/dist/cloud/cloud-config.d.ts +22 -1
  46. package/dist/cloud/cloud-config.js +64 -5
  47. package/dist/cloud/cloud-sync.d.ts +5 -5
  48. package/dist/cloud/cloud-sync.js +33 -2
  49. package/dist/cloud/sync-cycle.d.ts +110 -0
  50. package/dist/cloud/sync-cycle.js +270 -0
  51. package/dist/cloud/sync-daemon.d.ts +69 -0
  52. package/dist/cloud/sync-daemon.js +260 -0
  53. package/dist/cloud/sync-disk.d.ts +9 -0
  54. package/dist/cloud/sync-disk.js +191 -0
  55. package/dist/cloud/sync-hash.d.ts +2 -0
  56. package/dist/cloud/sync-hash.js +35 -0
  57. package/dist/crawl/crawl-tools.js +5 -3
  58. package/dist/crawl/crawl.d.ts +3 -0
  59. package/dist/crawl/crawl.js +5 -0
  60. package/dist/daemon/daemon-resolve.d.ts +43 -0
  61. package/dist/daemon/daemon-resolve.js +69 -0
  62. package/dist/daemon/dev-servers.d.ts +15 -0
  63. package/dist/daemon/dev-servers.js +68 -0
  64. package/dist/daemon/wake-decision.d.ts +32 -0
  65. package/dist/daemon/wake-decision.js +38 -0
  66. package/dist/domain/domain-tools.js +2 -2
  67. package/dist/events/contradictions.d.ts +18 -0
  68. package/dist/events/contradictions.js +134 -21
  69. package/dist/events/declared.d.ts +7 -4
  70. package/dist/events/declared.js +15 -6
  71. package/dist/events/echo-mismatch.js +64 -14
  72. package/dist/events/event-filters.d.ts +8 -0
  73. package/dist/events/event-filters.js +19 -2
  74. package/dist/events/predicate-eval.js +53 -5
  75. package/dist/events/predicate-precheck.d.ts +8 -0
  76. package/dist/events/predicate-precheck.js +41 -0
  77. package/dist/events/predicate-route.d.ts +14 -0
  78. package/dist/events/predicate-route.js +36 -8
  79. package/dist/events/predicate.d.ts +5 -0
  80. package/dist/events/predicate.js +37 -5
  81. package/dist/events/split-text-miss.d.ts +30 -0
  82. package/dist/events/split-text-miss.js +44 -0
  83. package/dist/flows/flake.d.ts +6 -6
  84. package/dist/flows/flow-memory-consult.d.ts +55 -0
  85. package/dist/flows/flow-memory-consult.js +38 -0
  86. package/dist/flows/flow-replay-run.d.ts +18 -7
  87. package/dist/flows/flow-replay-run.js +239 -22
  88. package/dist/flows/flow-replay.js +11 -2
  89. package/dist/flows/flow-step-runners.d.ts +8 -0
  90. package/dist/flows/flow-step-runners.js +8 -2
  91. package/dist/flows/flow-store-for-session.d.ts +31 -0
  92. package/dist/flows/flow-store-for-session.js +33 -0
  93. package/dist/flows/flow-tools.js +58 -32
  94. package/dist/flows/flows.d.ts +8 -0
  95. package/dist/flows/flows.js +468 -2
  96. package/dist/flows/heal-run.js +5 -2
  97. package/dist/flows/predicate-to-expect.d.ts +17 -12
  98. package/dist/flows/predicate-to-expect.js +30 -12
  99. package/dist/flows/recordings.d.ts +13 -1
  100. package/dist/flows/recordings.js +17 -2
  101. package/dist/flows/replay.d.ts +22 -2
  102. package/dist/flows/replay.js +28 -2
  103. package/dist/flows/verify-change-tools.js +5 -2
  104. package/dist/honesty/blind-spots.d.ts +55 -2
  105. package/dist/honesty/blind-spots.js +63 -3
  106. package/dist/honesty/honesty.d.ts +24 -0
  107. package/dist/honesty/honesty.js +25 -1
  108. package/dist/honesty/instrumentation-gaps.d.ts +9 -0
  109. package/dist/honesty/instrumentation-gaps.js +29 -2
  110. package/dist/honesty/verified.d.ts +2 -1
  111. package/dist/honesty/verified.js +16 -1
  112. package/dist/http-server.js +12 -0
  113. package/dist/impact/impact-recorder.d.ts +31 -8
  114. package/dist/impact/impact-recorder.js +104 -21
  115. package/dist/impact/impact-store.d.ts +15 -1
  116. package/dist/impact/impact-store.js +43 -2
  117. package/dist/index.js +113 -7
  118. package/dist/init/agent-rules.d.ts +13 -6
  119. package/dist/init/agent-rules.js +23 -12
  120. package/dist/init/astro-patch.d.ts +16 -0
  121. package/dist/init/astro-patch.js +28 -0
  122. package/dist/init/capabilities.d.ts +4 -1
  123. package/dist/init/capabilities.js +22 -2
  124. package/dist/init/confirm.d.ts +31 -7
  125. package/dist/init/confirm.js +48 -8
  126. package/dist/init/cra.d.ts +20 -4
  127. package/dist/init/cra.js +57 -17
  128. package/dist/init/csp-check.js +17 -2
  129. package/dist/init/csp-doctor.d.ts +8 -1
  130. package/dist/init/csp-doctor.js +8 -3
  131. package/dist/init/dev-script.d.ts +3 -1
  132. package/dist/init/dev-script.js +8 -5
  133. package/dist/init/format-generated.d.ts +18 -0
  134. package/dist/init/format-generated.js +39 -0
  135. package/dist/init/install-hint.d.ts +9 -0
  136. package/dist/init/install-hint.js +29 -0
  137. package/dist/init/mcp-clients.d.ts +7 -0
  138. package/dist/init/mcp-clients.js +7 -0
  139. package/dist/init/mcp.d.ts +1 -1
  140. package/dist/init/mcp.js +1 -1
  141. package/dist/init/node-io.js +11 -1
  142. package/dist/init/plan-framework.js +81 -45
  143. package/dist/init/plan.d.ts +20 -7
  144. package/dist/init/plan.js +20 -45
  145. package/dist/init/preflight.d.ts +28 -0
  146. package/dist/init/preflight.js +38 -0
  147. package/dist/init/run.d.ts +32 -9
  148. package/dist/init/run.js +141 -119
  149. package/dist/init/slash-command.d.ts +1 -1
  150. package/dist/init/snippets.js +41 -25
  151. package/dist/init/vite-config.d.ts +1 -1
  152. package/dist/init/vite-config.js +39 -25
  153. package/dist/init/workspace-apps.d.ts +17 -10
  154. package/dist/init/workspace-apps.js +110 -0
  155. package/dist/input/network-mock-tools.js +3 -6
  156. package/dist/input/real-input.js +25 -5
  157. package/dist/input/scroll-tools.js +4 -6
  158. package/dist/input/viewport-tools.js +5 -4
  159. package/dist/intent/inline-intent.d.ts +10 -19
  160. package/dist/intent/inline-intent.js +91 -1
  161. package/dist/intent/intent-shard-store.d.ts +72 -0
  162. package/dist/intent/intent-shard-store.js +203 -0
  163. package/dist/intent/intent-shard.d.ts +411 -0
  164. package/dist/intent/intent-shard.js +176 -0
  165. package/dist/intent/intent-store.d.ts +12 -0
  166. package/dist/intent/intent-store.js +21 -0
  167. package/dist/intent/intent-subject.d.ts +45 -0
  168. package/dist/intent/intent-subject.js +177 -0
  169. package/dist/intent/intent-tools.js +65 -2
  170. package/dist/intent/open-intents.d.ts +21 -0
  171. package/dist/intent/open-intents.js +15 -0
  172. package/dist/license/license-env.d.ts +29 -0
  173. package/dist/license/license-env.js +148 -0
  174. package/dist/loopback-agent.d.ts +31 -0
  175. package/dist/loopback-agent.js +57 -0
  176. package/dist/mcp/attach-memory.d.ts +18 -2
  177. package/dist/mcp/attach-memory.js +50 -7
  178. package/dist/mcp/client-identity.d.ts +31 -0
  179. package/dist/mcp/client-identity.js +51 -0
  180. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  181. package/dist/mcp/mcp-post-transport.js +109 -0
  182. package/dist/mcp/mcp-proxy.d.ts +2 -1
  183. package/dist/mcp/mcp-proxy.js +52 -62
  184. package/dist/mcp/mcp.js +7 -1
  185. package/dist/mcp/proxy-backoff.d.ts +11 -0
  186. package/dist/mcp/proxy-backoff.js +13 -0
  187. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  188. package/dist/mcp/proxy-daemon-probe.js +7 -1
  189. package/dist/mcp/proxy-handshake.d.ts +23 -0
  190. package/dist/mcp/proxy-handshake.js +31 -0
  191. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  192. package/dist/mcp/tool-catalog-cache.js +12 -0
  193. package/dist/memory/memory-tools.d.ts +2 -0
  194. package/dist/memory/memory-tools.js +82 -0
  195. package/dist/memory/project-memory.d.ts +34 -0
  196. package/dist/memory/project-memory.js +96 -0
  197. package/dist/oracles/propose-consequences.js +12 -3
  198. package/dist/pool/browser-pool.d.ts +63 -4
  199. package/dist/pool/browser-pool.js +113 -4
  200. package/dist/pool/playwright-launcher.js +9 -0
  201. package/dist/project/project-tools.js +3 -3
  202. package/dist/project/reticle-dir.d.ts +15 -2
  203. package/dist/project/reticle-dir.js +21 -4
  204. package/dist/runs/run-store.d.ts +10 -0
  205. package/dist/runs/run-store.js +10 -0
  206. package/dist/runs/verification-sync.d.ts +0 -11
  207. package/dist/runs/verification-sync.js +22 -1
  208. package/dist/runs/verify-server.d.ts +8 -1
  209. package/dist/runs/verify-server.js +14 -2
  210. package/dist/session/command-timeout.d.ts +5 -2
  211. package/dist/session/command-timeout.js +5 -4
  212. package/dist/session/lease-visibility.d.ts +67 -0
  213. package/dist/session/lease-visibility.js +76 -0
  214. package/dist/session/no-session-diagnosis.d.ts +23 -0
  215. package/dist/session/no-session-diagnosis.js +58 -17
  216. package/dist/session/no-session-next-action.d.ts +7 -0
  217. package/dist/session/no-session-next-action.js +42 -3
  218. package/dist/session/no-session-watch.d.ts +10 -6
  219. package/dist/session/no-session-watch.js +51 -19
  220. package/dist/session/pending-commands.d.ts +11 -1
  221. package/dist/session/pending-commands.js +12 -2
  222. package/dist/session/resolve-within.d.ts +34 -0
  223. package/dist/session/resolve-within.js +53 -0
  224. package/dist/session/session-info.d.ts +18 -0
  225. package/dist/session/session-info.js +11 -0
  226. package/dist/session/session-manager.d.ts +14 -0
  227. package/dist/session/session-manager.js +61 -3
  228. package/dist/session/session-mirror.d.ts +16 -0
  229. package/dist/session/session-mirror.js +22 -0
  230. package/dist/session/session-recommendation.d.ts +19 -5
  231. package/dist/session/session-recommendation.js +25 -9
  232. package/dist/session/session-successor.js +36 -1
  233. package/dist/session/session-tools.js +2 -2
  234. package/dist/session/session.d.ts +22 -2
  235. package/dist/session/session.js +87 -7
  236. package/dist/session/wall-clock.d.ts +12 -0
  237. package/dist/session/wall-clock.js +14 -0
  238. package/dist/setup/agent-configs.d.ts +96 -0
  239. package/dist/setup/agent-configs.js +333 -0
  240. package/dist/setup/agent-io.d.ts +9 -0
  241. package/dist/setup/agent-io.js +16 -0
  242. package/dist/setup/agent-writer.d.ts +51 -0
  243. package/dist/setup/agent-writer.js +115 -0
  244. package/dist/setup/approval-migration.d.ts +47 -0
  245. package/dist/setup/approval-migration.js +68 -0
  246. package/dist/setup/auto-approve.d.ts +85 -0
  247. package/dist/setup/auto-approve.js +150 -0
  248. package/dist/setup/bridge-port.d.ts +15 -0
  249. package/dist/setup/bridge-port.js +24 -0
  250. package/dist/setup/desktop-shape.d.ts +57 -0
  251. package/dist/setup/desktop-shape.js +63 -0
  252. package/dist/setup/dev-server-wait.d.ts +60 -0
  253. package/dist/setup/dev-server-wait.js +65 -0
  254. package/dist/setup/drive-agent.d.ts +71 -0
  255. package/dist/setup/drive-agent.js +171 -0
  256. package/dist/setup/drive-plan.d.ts +68 -0
  257. package/dist/setup/drive-plan.js +130 -0
  258. package/dist/setup/ensure-daemon.d.ts +29 -0
  259. package/dist/setup/ensure-daemon.js +58 -0
  260. package/dist/setup/init-runtime.d.ts +47 -0
  261. package/dist/setup/init-runtime.js +160 -0
  262. package/dist/setup/interrupt.d.ts +25 -0
  263. package/dist/setup/interrupt.js +40 -0
  264. package/dist/setup/license-key.d.ts +39 -0
  265. package/dist/setup/license-key.js +72 -0
  266. package/dist/setup/listeners.d.ts +37 -0
  267. package/dist/setup/listeners.js +85 -0
  268. package/dist/setup/node-effects.d.ts +60 -0
  269. package/dist/setup/node-effects.js +210 -0
  270. package/dist/setup/page-probe.d.ts +39 -0
  271. package/dist/setup/page-probe.js +49 -0
  272. package/dist/setup/relaunch.d.ts +36 -0
  273. package/dist/setup/relaunch.js +57 -0
  274. package/dist/setup/remaining-steps.d.ts +19 -0
  275. package/dist/setup/remaining-steps.js +31 -0
  276. package/dist/setup/run-setup.d.ts +99 -0
  277. package/dist/setup/run-setup.js +187 -0
  278. package/dist/setup/session-pick.d.ts +47 -0
  279. package/dist/setup/session-pick.js +51 -0
  280. package/dist/setup/setup-command.d.ts +45 -0
  281. package/dist/setup/setup-command.js +199 -0
  282. package/dist/setup/setup-options.d.ts +74 -0
  283. package/dist/setup/setup-options.js +67 -0
  284. package/dist/setup/transcripts.d.ts +16 -0
  285. package/dist/setup/transcripts.js +43 -0
  286. package/dist/status-payload.d.ts +14 -1
  287. package/dist/status-payload.js +23 -1
  288. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  289. package/dist/telemetry/daemon-telemetry.js +52 -5
  290. package/dist/telemetry/error-class.js +1 -1
  291. package/dist/telemetry/feedback-context.d.ts +9 -1
  292. package/dist/telemetry/feedback-context.js +60 -29
  293. package/dist/telemetry/feedback-local.d.ts +15 -0
  294. package/dist/telemetry/feedback-local.js +56 -0
  295. package/dist/telemetry/feedback.d.ts +11 -0
  296. package/dist/telemetry/feedback.js +19 -6
  297. package/dist/telemetry/init-telemetry.d.ts +2 -0
  298. package/dist/telemetry/init-telemetry.js +2 -0
  299. package/dist/telemetry/install-source.d.ts +14 -0
  300. package/dist/telemetry/install-source.js +30 -0
  301. package/dist/telemetry/license-activation.d.ts +7 -1
  302. package/dist/telemetry/license-activation.js +9 -5
  303. package/dist/telemetry/mcp-connection.js +7 -0
  304. package/dist/telemetry/project-profile.d.ts +12 -1
  305. package/dist/telemetry/project-profile.js +11 -6
  306. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  307. package/dist/telemetry/proxy-telemetry.js +21 -0
  308. package/dist/telemetry/session-metrics.d.ts +12 -0
  309. package/dist/telemetry/session-metrics.js +35 -1
  310. package/dist/telemetry/telemetry.js +17 -2
  311. package/dist/telemetry/tool-refused.d.ts +3 -1
  312. package/dist/telemetry/tool-refused.js +34 -2
  313. package/dist/tools/act-capsule.d.ts +10 -0
  314. package/dist/tools/act-capsule.js +8 -2
  315. package/dist/tools/act-danger.d.ts +2 -1
  316. package/dist/tools/act-danger.js +11 -3
  317. package/dist/tools/act-observation.d.ts +41 -0
  318. package/dist/tools/act-observation.js +36 -0
  319. package/dist/tools/act-preflight.d.ts +20 -0
  320. package/dist/tools/act-preflight.js +98 -0
  321. package/dist/tools/act-sequence-retry.d.ts +45 -0
  322. package/dist/tools/act-sequence-retry.js +58 -0
  323. package/dist/tools/act-sequence-tool.d.ts +13 -0
  324. package/dist/tools/act-sequence-tool.js +135 -0
  325. package/dist/tools/act-target.d.ts +14 -0
  326. package/dist/tools/act-target.js +47 -0
  327. package/dist/tools/act-tools.d.ts +21 -1
  328. package/dist/tools/act-tools.js +100 -213
  329. package/dist/tools/assert-verdict.js +22 -16
  330. package/dist/tools/browser-tools.js +6 -2
  331. package/dist/tools/dynamic-tools.js +24 -5
  332. package/dist/tools/error-recovery.d.ts +1 -0
  333. package/dist/tools/error-recovery.js +8 -0
  334. package/dist/tools/invoke-tool.js +59 -9
  335. package/dist/tools/lease-tools.d.ts +30 -4
  336. package/dist/tools/lease-tools.js +169 -5
  337. package/dist/tools/merged-name-redirect.d.ts +13 -0
  338. package/dist/tools/merged-name-redirect.js +22 -0
  339. package/dist/tools/numeric-bounds.d.ts +100 -0
  340. package/dist/tools/numeric-bounds.js +125 -0
  341. package/dist/tools/observe-tools.js +47 -40
  342. package/dist/tools/read-tools.js +28 -7
  343. package/dist/tools/real-input-attempt.d.ts +10 -0
  344. package/dist/tools/real-input-attempt.js +59 -1
  345. package/dist/tools/reconcile-tools.js +4 -4
  346. package/dist/tools/tool-kit.d.ts +12 -0
  347. package/dist/tools/tool-kit.js +6 -1
  348. package/dist/tools/tool-names.d.ts +2 -0
  349. package/dist/tools/tool-names.js +2 -0
  350. package/dist/tools/tools-helpers.d.ts +19 -0
  351. package/dist/tools/tools-helpers.js +46 -0
  352. package/dist/tools/tools.d.ts +2 -0
  353. package/dist/tools/tools.js +56 -8
  354. package/dist/update/reticle-deps.d.ts +9 -0
  355. package/dist/update/reticle-deps.js +27 -0
  356. package/dist/update/sdk-sync.d.ts +1 -2
  357. package/dist/update/sdk-sync.js +1 -21
  358. package/dist/update/update-nudge.d.ts +18 -0
  359. package/dist/update/update-nudge.js +20 -0
  360. package/dist/version/peer-announce.js +4 -2
  361. package/dist/version/sdk-fix.d.ts +47 -0
  362. package/dist/version/sdk-fix.js +127 -0
  363. package/dist/version/version-nudge.d.ts +14 -0
  364. package/dist/version/version-nudge.js +21 -0
  365. package/dist/version/version-skew.d.ts +38 -3
  366. package/dist/version/version-skew.js +83 -7
  367. package/dist/visual/visual-store.d.ts +6 -6
  368. package/dist/visual/visual-store.js +18 -16
  369. package/dist/visual/visual-tools.js +57 -12
  370. package/docs/cli/doctor.mdx +1 -0
  371. package/docs/cli/serve.mdx +4 -2
  372. package/docs/cli.mdx +7 -3
  373. package/docs/desktop.mdx +1 -1
  374. package/docs/docs.json +1 -1
  375. package/docs/getting-started.md +19 -3
  376. package/docs/index.mdx +9 -6
  377. package/docs/install-agentic.mdx +38 -12
  378. package/docs/install-manual.mdx +1 -1
  379. package/docs/packages.mdx +1 -1
  380. package/docs/skill-file.mdx +3 -3
  381. package/docs/telemetry-contract.md +108 -4
  382. package/docs/tools-act-sequence.mdx +16 -0
  383. package/docs/usage.md +1 -1
  384. package/package.json +4 -3
  385. package/dist/init/cursor.d.ts +0 -24
  386. package/dist/init/cursor.js +0 -80
@@ -8,30 +8,38 @@ import { PatchKind } from './patch-kind.js';
8
8
  export const VITE_IMPORT = "import { reticle } from '@reticlehq/vite-plugin';";
9
9
  const RETICLE_MARKER = '@reticlehq/vite-plugin';
10
10
  /**
11
- * The `reticle(...)` call — the bridge port so the injected connect targets it, and body capture.
11
+ * The `reticle(...)` call — the bridge port so the injected connect targets it.
12
12
  *
13
- * `captureNetworkBodies` is here rather than in the SDK's defaults, deliberately. Without it a write
14
- * that answers 2xx with a body nobody recorded grades `unknown / outcome_unread`, because a 200
15
- * describes the transport and not the result — so the single bug class this product exists to catch is
16
- * unreachable on a default install. Measured on a real payments UI: a refund posted rupees into a
17
- * paise field, the server answered 200 having refunded a hundredth of it, the page rendered the amount
18
- * the user had typed rather than the amount that came back, and every DOM-level check passed. An agent
19
- * asked to verify that flow had to edit the app's own vite config mid-task to see the payload, and
20
- * then tell its human to undo the edit.
13
+ * **`captureNetworkBodies` is OPT-IN.** It was written in by default, and the argument for that was
14
+ * a good one: without a body, a write that answers 2xx grades `unknown / outcome_unread`, because a
15
+ * 200 describes the transport and not the result. Measured on a real payments UI — a refund posted
16
+ * rupees into a paise field, the server answered 200 having refunded a hundredth of it, the page
17
+ * rendered the amount the user had typed, and every DOM-level check passed.
21
18
  *
22
- * Written into the USER'S config, not switched on inside the SDK, and the difference is the point. A
23
- * body is the one part of a request that routinely carries personal data: the credential classes are
24
- * redacted before anything is journalled — tokens, cookies, card numbers, cvv, ssn — but an address or
25
- * an email is not, and nothing here should decide that for someone silently. In the config it is one
26
- * visible line they can read, keep, or delete, and an SDK that updates underneath them never starts
27
- * recording more than it did yesterday.
19
+ * That argument justifies the CAPABILITY. It does not justify the default, and the distinction is
20
+ * what #705 is about. A healthcare workspace proxying authenticated API traffic through Vite ran
21
+ * `init`, and on the first drive their login tokens and patient payloads were in the daemon's
22
+ * buffer. The old comment here conceded the exact gap — "the credential classes are redacted ... but
23
+ * an address or an email is not, and nothing here should decide that for someone silently" — and
24
+ * then decided it for them, because writing the line into their config IS deciding. `init` is run
25
+ * unattended by an agent; the person who knows the data is sensitive is not in the room.
26
+ *
27
+ * So the default is off and the ways in are all deliberate:
28
+ * - `reticle init --capture-bodies` writes the line, for someone who has decided
29
+ * - `VITE_RETICLE_CAPTURE_BODIES=1` turns it on for ONE dev-server run, no config edit
30
+ * - adding the option by hand, which is what the tools tell you to do
31
+ *
32
+ * Nothing goes quiet in exchange. Every tool that needs a body already says so when it is missing
33
+ * and names the option — see honesty/uncaptured-bodies.ts, honesty/verified.ts, predicate-eval.ts
34
+ * and reconcile-tools.ts — so the capability is discovered at the moment it is wanted, by the person
35
+ * who wanted it, instead of being switched on before anyone has asked.
28
36
  */
29
- function reticlePluginCall(port) {
37
+ function reticlePluginCall(port, captureBodies) {
30
38
  const options = [
31
39
  ...(port === undefined ? [] : [`port: ${String(port)}`]),
32
- 'captureNetworkBodies: true',
40
+ ...(captureBodies ? ['captureNetworkBodies: true'] : []),
33
41
  ];
34
- return `reticle({ ${options.join(', ')} })`;
42
+ return 0 === options.length ? 'reticle()' : `reticle({ ${options.join(', ')} })`;
35
43
  }
36
44
  /** Matches the start of a `plugins: [` array literal. */
37
45
  const PLUGINS_ARRAY = /plugins\s*:\s*\[/;
@@ -66,35 +74,41 @@ function insertImport(source) {
66
74
  * what a formatter rewrites, turning a one-line install into a diff against the user's own style. A
67
75
  * single-line array needs the space, or the result reads `[reticle(),react()]`.
68
76
  */
69
- function insertPlugin(source, port) {
77
+ function insertPlugin(source, port, captureBodies) {
70
78
  return source.replace(PLUGINS_ARRAY, (match, _g, offset) => {
71
79
  const next = source[offset + match.length] ?? '';
72
80
  const separator = '' === next || /\s/.test(next) ? '' : ' ';
73
- return `${match}${reticlePluginCall(port)},${separator}`;
81
+ return `${match}${reticlePluginCall(port, captureBodies)},${separator}`;
74
82
  });
75
83
  }
76
84
  /**
77
85
  * Add a whole `plugins: [reticle()]` key to a config object that has none, matching the layout of
78
86
  * the object it lands in: a multi-line object gets its own indented line, a one-liner stays inline.
79
87
  */
80
- function insertPluginsKey(source, port) {
88
+ function insertPluginsKey(source, port, captureBodies) {
81
89
  return source.replace(CONFIG_OBJECT, (_match, prefix, offset) => {
82
90
  const rest = source.slice(offset + _match.length);
83
91
  const multiline = /^\s*\n/.test(rest);
84
92
  const indent = /^\s*\n(\s*)\S/.exec(rest)?.[1] ?? ' ';
85
- const key = `plugins: [${reticlePluginCall(port)}],`;
93
+ const key = `plugins: [${reticlePluginCall(port, captureBodies)}],`;
86
94
  return multiline ? `${prefix}{\n${indent}${key}` : `${prefix}{ ${key}`;
87
95
  });
88
96
  }
89
- export function patchViteConfig(source, port) {
97
+ export function patchViteConfig(source, port, captureBodies = false) {
90
98
  if (source.includes(RETICLE_MARKER)) {
91
99
  return { kind: VitePatchKind.ALREADY };
92
100
  }
93
101
  if (PLUGINS_ARRAY.test(source)) {
94
- return { kind: VitePatchKind.APPLY, code: insertImport(insertPlugin(source, port)) };
102
+ return {
103
+ kind: VitePatchKind.APPLY,
104
+ code: insertImport(insertPlugin(source, port, captureBodies)),
105
+ };
95
106
  }
96
107
  if (CONFIG_OBJECT.test(source)) {
97
- return { kind: VitePatchKind.APPLY, code: insertImport(insertPluginsKey(source, port)) };
108
+ return {
109
+ kind: VitePatchKind.APPLY,
110
+ code: insertImport(insertPluginsKey(source, port, captureBodies)),
111
+ };
98
112
  }
99
113
  return { kind: VitePatchKind.MANUAL, reason: NO_PLUGINS_REASON };
100
114
  }
@@ -1,16 +1,14 @@
1
+ import type { InitIo } from './run.js';
1
2
  /**
2
- * Which directories to look in for the app, when `reticle init` runs at a monorepo root.
3
+ * The file names that say what a directory IS, shared by everything that has to ask.
3
4
  *
4
- * This used to be the literal list `['apps', 'packages']`. Measured on a real repo with three Next
5
- * apps at `web/`, `admin/` and `space/`, it found none — so the redirect never fired, init ran
6
- * against the root, and it reported ✓ for writing `app/reticle-dev.tsx` into a directory Next never
7
- * compiles. A ⚠ tells a human to act; a ✓ tells them it is handled.
8
- *
9
- * A workspace DECLARES its packages, so that declaration is used first and directory names are not
10
- * guessed at all. Where nothing is declared, every top-level directory is a better candidate than two
11
- * hardcoded ones — filtered by the usual non-source suspects, and still subject to the `looksLikeApp`
12
- * check that follows, so a wrong guess here costs a `package.json` read.
5
+ * They lived in run.ts and are read from here now because `findWorkspaceApps` moved and needs them —
6
+ * one definition, imported in both directions of the question ("is this an app?" and "which app?").
13
7
  */
8
+ export declare const PACKAGE_JSON = "package.json";
9
+ export declare const PNPM_WORKSPACE = "pnpm-workspace.yaml";
10
+ export declare const VITE_CONFIG_CANDIDATES: string[];
11
+ export declare const NEXT_CONFIG_CANDIDATES: string[];
14
12
  interface WorkspaceSources {
15
13
  /** Raw contents of pnpm-workspace.yaml, when present. */
16
14
  pnpmWorkspace?: string;
@@ -20,4 +18,13 @@ interface WorkspaceSources {
20
18
  topLevelDirs?: readonly string[];
21
19
  }
22
20
  export declare function workspaceParents(sources: WorkspaceSources): string[];
21
+ /**
22
+ * App directories under a workspace root.
23
+ *
24
+ * Running `reticle init` at the repo root is what people actually do, and in a monorepo the app is a
25
+ * directory down — so init detected "no framework", printed a wall of manual HTML instructions, and
26
+ * would have installed the SDK into the ROOT package.json. It already walks UP for the lockfile, so
27
+ * it knows it is in a workspace; this is the matching walk DOWN.
28
+ */
29
+ export declare function findWorkspaceApps(io: Pick<InitIo, 'exists' | 'readFile' | 'listDirs'>): string[];
23
30
  export {};
@@ -1,3 +1,24 @@
1
+ import { DEV_SCRIPT_NAMES } from './dev-script.js';
2
+ /**
3
+ * The file names that say what a directory IS, shared by everything that has to ask.
4
+ *
5
+ * They lived in run.ts and are read from here now because `findWorkspaceApps` moved and needs them —
6
+ * one definition, imported in both directions of the question ("is this an app?" and "which app?").
7
+ */
8
+ export const PACKAGE_JSON = 'package.json';
9
+ export const PNPM_WORKSPACE = 'pnpm-workspace.yaml';
10
+ export const VITE_CONFIG_CANDIDATES = [
11
+ 'vite.config.ts',
12
+ 'vite.config.js',
13
+ 'vite.config.mjs',
14
+ 'vite.config.mts',
15
+ ];
16
+ export const NEXT_CONFIG_CANDIDATES = [
17
+ 'next.config.mjs',
18
+ 'next.config.js',
19
+ 'next.config.ts',
20
+ 'next.config.cjs',
21
+ ];
1
22
  /**
2
23
  * Which directories to look in for the app, when `reticle init` runs at a monorepo root.
3
24
  *
@@ -66,3 +87,92 @@ export function workspaceParents(sources) {
66
87
  : (sources.topLevelDirs ?? []).filter((d) => !d.startsWith('.') && !NEVER_A_PACKAGE.has(d));
67
88
  return [...new Set(candidates)];
68
89
  }
90
+ /**
91
+ * Which directories under this workspace are runnable apps.
92
+ *
93
+ * Moved here from run.ts, which was over the 1000-line cap and had carried a note saying it was at
94
+ * its cohesion limit. `library-path-boundary.test.ts` had already written down that this function
95
+ * "squats in init/ for historical reasons" — it is a general-purpose question about a repository's
96
+ * shape, next to `workspaceParents`, which answers the other half of it.
97
+ */
98
+ /** Deps that mark a directory as a runnable web app even when it has no bundler config file. */
99
+ const APP_DEPS = ['next', 'vite'];
100
+ function hasDevScript(pkgRaw) {
101
+ try {
102
+ const parsed = JSON.parse(pkgRaw);
103
+ const scripts = 'object' === typeof parsed && parsed !== null
104
+ ? parsed.scripts
105
+ : undefined;
106
+ if ('object' !== typeof scripts || null === scripts)
107
+ return false;
108
+ const named = scripts;
109
+ return DEV_SCRIPT_NAMES.some((n) => 'string' === typeof named[n] && '' !== named[n]);
110
+ }
111
+ catch {
112
+ // A manifest that will not parse says nothing either way, and this is a filter, not a verdict.
113
+ return false;
114
+ }
115
+ }
116
+ function looksLikeApp(dir, io) {
117
+ const pkgRaw = io.readFile(`${dir}/${PACKAGE_JSON}`);
118
+ if (null === pkgRaw)
119
+ return false;
120
+ const configs = [...VITE_CONFIG_CANDIDATES, ...NEXT_CONFIG_CANDIDATES];
121
+ if (configs.some((c) => io.exists(`${dir}/${c}`)))
122
+ return true;
123
+ // `next.config` is optional in Next, so the dependency list is the other half of the signal.
124
+ if (APP_DEPS.some((d) => pkgRaw.includes(`"${d}"`)))
125
+ return true;
126
+ // And the honest third: an app somebody can SERVE. Asking only for a bundler missed every app
127
+ // built on anything else — Remix, Astro, a plain node server — and in a monorepo missing it means
128
+ // init never redirects, wires the ROOT, and reports ✓ for files nothing compiles.
129
+ //
130
+ // A monorepo root has no dev script by design, and a package that can only be BUILT is not the app
131
+ // somebody is working in, so this stays a filter rather than matching every directory.
132
+ return hasDevScript(pkgRaw);
133
+ }
134
+ /**
135
+ * App directories under a workspace root.
136
+ *
137
+ * Running `reticle init` at the repo root is what people actually do, and in a monorepo the app is a
138
+ * directory down — so init detected "no framework", printed a wall of manual HTML instructions, and
139
+ * would have installed the SDK into the ROOT package.json. It already walks UP for the lockfile, so
140
+ * it knows it is in a workspace; this is the matching walk DOWN.
141
+ */
142
+ export function findWorkspaceApps(io) {
143
+ const found = [];
144
+ // A workspace DECLARES its packages; `['apps','packages']` was a guess that missed a real repo
145
+ // with three Next apps at web/, admin/ and space/ — and missing them meant init ran against the
146
+ // root and reported ✓ for a file Next never compiles. See workspace-apps.
147
+ const pkgRaw = io.readFile(PACKAGE_JSON);
148
+ let pkgWorkspaces;
149
+ try {
150
+ const parsed = null === pkgRaw ? undefined : JSON.parse(pkgRaw);
151
+ pkgWorkspaces =
152
+ 'object' === typeof parsed && parsed !== null
153
+ ? parsed.workspaces
154
+ : undefined;
155
+ }
156
+ catch {
157
+ pkgWorkspaces = undefined;
158
+ }
159
+ const parents = workspaceParents({
160
+ ...(null === io.readFile(PNPM_WORKSPACE)
161
+ ? {}
162
+ : { pnpmWorkspace: io.readFile(PNPM_WORKSPACE) ?? '' }),
163
+ ...(pkgWorkspaces === undefined ? {} : { pkgWorkspaces }),
164
+ topLevelDirs: io.listDirs('.'),
165
+ });
166
+ // A declared parent can itself BE the app (`workspaces: ["web"]`), so check both the directory and
167
+ // its children rather than assuming one level of nesting.
168
+ for (const parent of parents) {
169
+ if (looksLikeApp(parent, io))
170
+ found.push(parent);
171
+ for (const name of io.listDirs(parent)) {
172
+ const dir = `${parent}/${name}`;
173
+ if (looksLikeApp(dir, io))
174
+ found.push(dir);
175
+ }
176
+ }
177
+ return [...new Set(found)];
178
+ }
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { CDP_NO_PROVIDER_REASON, CDP_NO_PROVIDER_RECOMMENDATION } from '@reticlehq/core';
3
3
  import { ReticleTool } from '../tools/tool-names.js';
4
4
  import { sessionIdShape } from '../tools/tool-kit.js';
5
+ import { delayMsSchema, httpStatusSchema } from '../tools/numeric-bounds.js';
5
6
  import { asString } from '../tools/tools-helpers.js';
6
7
  function mockProvider(deps) {
7
8
  const p = deps.realInput;
@@ -13,14 +14,10 @@ const ruleShape = z.object({
13
14
  .min(1)
14
15
  .describe('Substring the request URL must contain, e.g. "/api/pay".'),
15
16
  method: z.string().optional().describe('Optional method filter (GET/POST/…), case-insensitive.'),
16
- status: z.number().int().optional().describe('Fulfill with this HTTP status (default 200).'),
17
+ status: httpStatusSchema.optional().describe('Fulfill with this HTTP status (default 200).'),
17
18
  body: z.string().optional().describe('Response body to fulfill with.'),
18
19
  contentType: z.string().optional().describe('Response content type (default application/json).'),
19
- delayMs: z
20
- .number()
21
- .int()
22
- .optional()
23
- .describe('Delay (ms) before fulfilling — simulate a slow API.'),
20
+ delayMs: delayMsSchema.optional().describe('Delay (ms) before fulfilling — simulate a slow API.'),
24
21
  abort: z
25
22
  .boolean()
26
23
  .optional()
@@ -302,8 +302,28 @@ export class LaunchedRealInputProvider {
302
302
  // No SDK on the page (or it connected already) — the no-session guard in verify reports it.
303
303
  }
304
304
  }
305
- isAvailableFor(sessionUrl) {
305
+ /**
306
+ * The page we own, or undefined once it is gone.
307
+ *
308
+ * The handle is cached for the life of the provider and Playwright keeps answering `url()` after
309
+ * the page has closed, so a closed window read as AVAILABLE and every method below threw
310
+ * "Target page, context or browser has been closed" — the raw Playwright message, surfaced to the
311
+ * agent as a tool error, on every call for the rest of the run. A dead page is the same fact as no
312
+ * page, and every caller already handles that. Only an EXPLICIT `true` drops it, matching
313
+ * `CdpRealInputProvider`'s `isConnected` check: a test fake without the method is assumed live.
314
+ */
315
+ #livePage() {
306
316
  const page = this.#page;
317
+ if (page === undefined)
318
+ return undefined;
319
+ if (true === page.isClosed?.()) {
320
+ this.#page = undefined; // never ask a corpse twice
321
+ return undefined;
322
+ }
323
+ return page;
324
+ }
325
+ isAvailableFor(sessionUrl) {
326
+ const page = this.#livePage();
307
327
  if (page === undefined)
308
328
  return Promise.resolve(false);
309
329
  if (page.url() === sessionUrl)
@@ -311,14 +331,14 @@ export class LaunchedRealInputProvider {
311
331
  return Promise.resolve(stripVolatile(page.url()) === stripVolatile(sessionUrl));
312
332
  }
313
333
  perform(_sessionUrl, action, box, args) {
314
- const page = this.#page;
334
+ const page = this.#livePage();
315
335
  if (page === undefined)
316
336
  return Promise.resolve({ performed: false, center: boxCenter(box) });
317
337
  return performGesture(page, action, box, args, this.#sleep);
318
338
  }
319
339
  /** PNG of the owned page, or undefined before navigate / after dispose. */
320
340
  screenshot(_sessionUrl, opts) {
321
- const page = this.#page;
341
+ const page = this.#livePage();
322
342
  if (page === undefined)
323
343
  return Promise.resolve(undefined);
324
344
  return capturePage(page, opts);
@@ -332,7 +352,7 @@ export class LaunchedRealInputProvider {
332
352
  * screenshots the whole time; only these two methods were missing.
333
353
  */
334
354
  async setViewport(_sessionUrl, size) {
335
- const page = this.#page;
355
+ const page = this.#livePage();
336
356
  if (page === undefined)
337
357
  return false;
338
358
  await page.setViewportSize({ width: size.width, height: size.height });
@@ -340,7 +360,7 @@ export class LaunchedRealInputProvider {
340
360
  }
341
361
  /** Apply network-mock rules to the owned page; false before navigate / after dispose. */
342
362
  async setMocks(_sessionUrl, rules) {
343
- const page = this.#page;
363
+ const page = this.#livePage();
344
364
  if (page === undefined)
345
365
  return false;
346
366
  await installNetworkMocks(page, rules);
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { QueryBy } from '@reticlehq/core';
3
3
  import { ReticleTool } from '../tools/tool-names.js';
4
4
  import { asNumber, asString } from '../tools/tools-helpers.js';
5
+ import { listIndexSchema, scrollCountSchema } from '../tools/numeric-bounds.js';
5
6
  import { scrollToFind } from './scroll-find.js';
6
7
  /**
7
8
  * reticle_scroll_to — the windowed/virtualized-list escape hatch. A plain reticle_query
@@ -30,16 +31,13 @@ export const SCROLL_TOOLS = [
30
31
  .string()
31
32
  .optional()
32
33
  .describe('Element ref for the scrollable container. Omit to scroll the document.'),
33
- maxScrolls: z
34
- .number()
34
+ maxScrolls: scrollCountSchema
35
35
  .optional()
36
36
  .describe('Maximum number of scroll steps before giving up. Default: 20.'),
37
- targetIndex: z
38
- .number()
37
+ targetIndex: listIndexSchema
39
38
  .optional()
40
39
  .describe('Known row index of the target in the list. Combine with totalCount for bisection — jumps directly to the estimated offset.'),
41
- totalCount: z
42
- .number()
40
+ totalCount: listIndexSchema
43
41
  .optional()
44
42
  .describe('Total item count in the virtualized list. Required for bisection with targetIndex.'),
45
43
  sessionId: z
@@ -2,10 +2,11 @@ import { z } from 'zod';
2
2
  import { CDP_NO_PROVIDER_REASON, CDP_NO_PROVIDER_RECOMMENDATION } from '@reticlehq/core';
3
3
  import { ReticleTool } from '../tools/tool-names.js';
4
4
  import { sessionIdShape } from '../tools/tool-kit.js';
5
+ import { MAX_VIEWPORT_PX, MIN_VIEWPORT_PX, viewportPxSchema } from '../tools/numeric-bounds.js';
5
6
  import { asString } from '../tools/tools-helpers.js';
6
7
  /** Bounds so a viewport request stays sane (and a typo can't ask for a 1px or 100k-px window). */
7
- const MIN_DIM = 64;
8
- const MAX_DIM = 10000;
8
+ const MIN_DIM = MIN_VIEWPORT_PX;
9
+ const MAX_DIM = MAX_VIEWPORT_PX;
9
10
  function viewportProvider(deps) {
10
11
  const p = deps.realInput;
11
12
  return p !== undefined && 'function' === typeof p.setViewport
@@ -20,8 +21,8 @@ export const VIEWPORT_TOOLS = [
20
21
  'reticle_visual_diff `masks` and a frozen clock (reticle_clock). Set it once before reticle_screenshot / ' +
21
22
  'reticle_visual_diff. Returns { applied, width, height } or the no-provider recommendation.',
22
23
  inputSchema: {
23
- width: z.number().int().describe('Viewport width in CSS px (e.g. 1280).'),
24
- height: z.number().int().describe('Viewport height in CSS px (e.g. 800).'),
24
+ width: viewportPxSchema.describe('Viewport width in CSS px (e.g. 1280).'),
25
+ height: viewportPxSchema.describe('Viewport height in CSS px (e.g. 800).'),
25
26
  ...sessionIdShape,
26
27
  },
27
28
  outputSchema: {
@@ -11,24 +11,13 @@ import type { ToolDeps } from '../tools/tool-kit.js';
11
11
  export declare function inlineIntentId(statement: string): string;
12
12
  /** What `provenBy` records: which tool drew the verdict, and when. */
13
13
  export declare function inlineVerdictId(tool: string, at: number): string;
14
- /**
15
- * Put an inline intent in the ledger and attach the predicate about to be evaluated. Returns the id
16
- * the verdict may later discharge, or undefined when there was no intent to record.
17
- *
18
- * `intent` carries EITHER prose OR the id of an intent already in the ledger. One field rather than
19
- * two because the surface is the scarce thing here and the two cases are told apart by the ledger
20
- * itself: a string that names an existing row is a reference, anything else is prose. A statement
21
- * that happens to equal an existing id is the same intent said twice, so pointing at the row is the
22
- * right answer there too, not a collision.
23
- *
24
- * A referenced intent is never re-declared, which would overwrite the agent's own words with an id,
25
- * and its binding is left alone when it already has one — a predicate bound deliberately through
26
- * `reticle_intent { action: "bind" }` is a stronger statement than whatever this one call asserts,
27
- * and quietly replacing it is exactly the narrowing the ledger exists to make visible.
28
- *
29
- * Best-effort by construction: a ledger that cannot be written is a small problem, and a verdict
30
- * that fails to return because of one is a large problem.
31
- */
14
+ export declare function surfaceForInlineIntent(url: string | undefined, flow: string | undefined,
15
+ /** `file` or `file:line` for the element the verdict acted on, when it had one. */
16
+ sourceLabel?: string): {
17
+ route?: string;
18
+ flow?: string;
19
+ files?: string[];
20
+ } | undefined;
32
21
  export declare function linkInlineIntent(deps: ToolDeps, sessionId: string | undefined, intent: string | undefined,
33
22
  /** The predicate this verdict will evaluate, or undefined when it proves nothing (a bare settle). */
34
23
  binding: unknown): Promise<string | undefined>;
@@ -43,4 +32,6 @@ export declare function dischargeInlineIntent(deps: ToolDeps, sessionId: string
43
32
  verdictId: string;
44
33
  grade: string;
45
34
  at: number;
46
- }): Promise<void>;
35
+ },
36
+ /** `file:line` of the element this verdict acted on, when there was one. */
37
+ sourceLabel?: string): Promise<void>;
@@ -64,6 +64,81 @@ export function inlineVerdictId(tool, at) {
64
64
  * Best-effort by construction: a ledger that cannot be written is a small problem, and a verdict
65
65
  * that fails to return because of one is a large problem.
66
66
  */
67
+ /**
68
+ * The surface an inline intent was captured on, so the store can FILE it.
69
+ *
70
+ * Measured on a real corpus: 167 of 173 things a project knew landed in `unsorted`, because
71
+ * `act_and_wait({ intent })` declared a statement and nothing else. The subject ladder had no flow,
72
+ * no route and no explicit subject to work from, so every record fell to the bucket of last resort
73
+ * — and a coverage map that is one pile with six labels tells a manager the team knows nothing,
74
+ * when the truth is that it knows a great deal and none of it is filed.
75
+ *
76
+ * The ROUTE is what makes this work at all, because it is always available: an agent asserting
77
+ * something is always somewhere. A flow name is better when there is one, and `subjectFor` already
78
+ * prefers it — this only has to supply both and let that ladder decide.
79
+ *
80
+ * Query and hash are dropped. `/issues?category=severe` and `/issues` are the same subject seen
81
+ * twice; keeping the query would shard one area of the product across every filter anybody used.
82
+ *
83
+ * Returns undefined rather than an empty object when there is nothing to record. A surface that
84
+ * says nothing reads as a capture that looked for a location and found none, rather than one that
85
+ * never had one — and the second is the truth.
86
+ */
87
+ /**
88
+ * Strip a trailing `:line` from a `file:line` label.
89
+ *
90
+ * Anchored to the END and to digits, because a Windows path carries its own colon: splitting on the
91
+ * first one turns `C:\\app\\src\\cart.tsx:12` into `C`, which is a path to nothing and would be
92
+ * filed as though it were real.
93
+ */
94
+ const SOURCE_LINE_SUFFIX = /:\d+$/;
95
+ export function surfaceForInlineIntent(url, flow,
96
+ /** `file` or `file:line` for the element the verdict acted on, when it had one. */
97
+ sourceLabel) {
98
+ let route;
99
+ if (url !== undefined) {
100
+ try {
101
+ const path = new URL(url).pathname;
102
+ // A bare origin has no path worth filing under: `/` would put every record in one bucket,
103
+ // which is the problem this exists to fix rather than a solution to it.
104
+ if (path.length > 1)
105
+ route = path;
106
+ }
107
+ catch {
108
+ route = undefined;
109
+ }
110
+ }
111
+ /*
112
+ * The file, without its line.
113
+ *
114
+ * A record is about a FILE; the line is where one verdict happened to touch it, and keeping it
115
+ * would make the same rule look like a different one every time the file was edited above it.
116
+ */
117
+ const file = sourceLabel === undefined || 0 === sourceLabel.length
118
+ ? undefined
119
+ : sourceLabel.replace(SOURCE_LINE_SUFFIX, '');
120
+ if (route === undefined && flow === undefined && file === undefined)
121
+ return undefined;
122
+ return {
123
+ ...(route === undefined ? {} : { route }),
124
+ ...(flow === undefined ? {} : { flow }),
125
+ ...(file === undefined ? {} : { files: [file] }),
126
+ };
127
+ }
128
+ /**
129
+ * The session's live URL, or undefined when no session can be named.
130
+ *
131
+ * `sessions.resolve` throws for every "cannot tell which tab" case, and none of them is a reason to
132
+ * fail the caller's verdict — a missing surface costs a filing, not a result.
133
+ */
134
+ function sessionUrl(deps, sessionId) {
135
+ try {
136
+ return deps.sessions.resolve(sessionId).url;
137
+ }
138
+ catch {
139
+ return undefined;
140
+ }
141
+ }
67
142
  export async function linkInlineIntent(deps, sessionId, intent,
68
143
  /** The predicate this verdict will evaluate, or undefined when it proves nothing (a bare settle). */
69
144
  binding) {
@@ -73,6 +148,8 @@ binding) {
73
148
  const store = new IntentStore(deps.fs, sessionRoot(deps, sessionId), { now: deps.now });
74
149
  const existing = (await store.read()).find((row) => row.id === intent);
75
150
  const id = existing?.id ?? inlineIntentId(intent);
151
+ // Declared WITHOUT a surface on purpose: see dischargeInlineIntent for why the route that
152
+ // describes this record only exists after the action it is about.
76
153
  if (existing === undefined)
77
154
  await store.declare([{ id, statement: intent }]);
78
155
  if (binding !== undefined && existing?.binding === undefined)
@@ -90,11 +167,24 @@ binding) {
90
167
  * intent declared inline on a wait that asserted no consequence stays open and honest instead of
91
168
  * collecting a proof nothing earned.
92
169
  */
93
- export async function dischargeInlineIntent(deps, sessionId, id, proof) {
170
+ export async function dischargeInlineIntent(deps, sessionId, id, proof,
171
+ /** `file:line` of the element this verdict acted on, when there was one. */
172
+ sourceLabel) {
94
173
  if (id === undefined)
95
174
  return;
96
175
  try {
97
176
  const store = new IntentStore(deps.fs, sessionRoot(deps, sessionId), { now: deps.now });
177
+ /*
178
+ * File it HERE, not at declaration.
179
+ *
180
+ * The intent is declared before the action so a verdict can see it open, and at that instant the
181
+ * agent is still on the page it is leaving — clicking through to /issues from / would have filed
182
+ * the record under the page it left, or under nothing at all. The route that describes what the
183
+ * intent is about is the one that exists once the consequence has landed.
184
+ */
185
+ const surface = surfaceForInlineIntent(sessionUrl(deps, sessionId), undefined, sourceLabel);
186
+ if (surface !== undefined)
187
+ await store.place(id, surface);
98
188
  await store.discharge(id, proof);
99
189
  }
100
190
  catch {
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Reading and writing the sharded intent store, and migrating the flat one into it.
3
+ *
4
+ * The operations an agent actually performs, and what each is allowed to cost:
5
+ *
6
+ * index() every id + one line, always affordable — the session-start read
7
+ * subject(name) the full records for one subject, when it is about to work on that
8
+ * get(id) one record, via the index rather than by scanning shards
9
+ * record()/update() a write that touches ONE shard file plus the index
10
+ *
11
+ * The last one is the reason for the whole design. Under the flat file, changing one intent rewrote
12
+ * all 141, so two sessions touching unrelated subjects raced each other and the loser's write
13
+ * vanished. Sharded, they only collide when they genuinely edit the same subject.
14
+ *
15
+ * ## Migration is automatic, and it does not delete
16
+ *
17
+ * `.reticle/intent.json` is read on every load and folded in when the sharded copy has nothing for
18
+ * an id. That makes the move invisible: an agent on the new store sees old intents immediately, and
19
+ * an older build still reading the flat file keeps working, because nothing removes it. Deleting it
20
+ * is a separate decision somebody can take once they believe the migration.
21
+ */
22
+ import { type Intent } from '@reticlehq/core';
23
+ import type { FileSystemPort } from '../project/fs-port.js';
24
+ import { IntentStatus, type IntentIndex, type IntentRecord } from './intent-shard.js';
25
+ export interface Clock {
26
+ now: () => number;
27
+ }
28
+ /** What a caller supplies to write an intent. Everything optional is genuinely optional. */
29
+ export interface IntentInput {
30
+ id: string;
31
+ statement: string;
32
+ subject?: string | undefined;
33
+ why?: string | undefined;
34
+ source?: string | undefined;
35
+ status?: IntentStatus | undefined;
36
+ surface?: Intent['surface'];
37
+ binding?: unknown;
38
+ }
39
+ export declare class IntentShardStore {
40
+ #private;
41
+ constructor(fs: FileSystemPort, root: string, clock: Clock);
42
+ /**
43
+ * The cheap read: one line per intent, and the call an agent makes at the start of a session.
44
+ *
45
+ * Includes anything still only in the legacy flat file, so the very first call on an unmigrated
46
+ * project already answers completely — which is what makes the migration invisible rather than a
47
+ * step somebody has to remember to run.
48
+ */
49
+ index(): Promise<IntentIndex>;
50
+ /** Every record for one subject — the working read, once an agent knows what it is touching. */
51
+ subject(name: string): Promise<IntentRecord[]>;
52
+ /** One record by id, or null. */
53
+ get(id: string): Promise<IntentRecord | null>;
54
+ /**
55
+ * Write one intent, creating or updating it.
56
+ *
57
+ * Merges onto whatever is already stored, so a caller adding a `why` to a migrated record does not
58
+ * have to restate its binding — and cannot silently drop it by omission, which is the failure mode
59
+ * that makes agents afraid to touch a store.
60
+ */
61
+ record(input: IntentInput): Promise<IntentRecord>;
62
+ /**
63
+ * Fold the flat file into shards on disk, so the move is visible in a diff rather than implicit.
64
+ *
65
+ * Idempotent, and it does not delete `.reticle/intent.json`: an older build still reads it, and a
66
+ * migration that removes its own source cannot be checked afterwards.
67
+ */
68
+ migrate(): Promise<{
69
+ migrated: number;
70
+ subjects: string[];
71
+ }>;
72
+ }