@reticlehq/server 2.12.0 → 2.13.1

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 +68 -0
  253. package/dist/setup/dev-server-wait.js +67 -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 +203 -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
@@ -1,6 +1,9 @@
1
- import { EventType, FLOW_SIGNAL_TIMEOUT_MS, FlowErrorCode, RecordedFlowSchema, ReplayStatus, RunKind, RunStatus, } from '@reticlehq/core';
2
- import { asString } from '../tools/tools-helpers.js';
1
+ import { EventType, FLOW_SIGNAL_TIMEOUT_MS, FlowErrorCode, RecordedFlowSchema, ReplayStatus, ReticleCommand, RunKind, RunStatus, } from '@reticlehq/core';
2
+ import { asRecord, asString } from '../tools/tools-helpers.js';
3
+ import { carryReticleIdentity } from '../tools/lease-tools.js';
3
4
  import { replayFlow } from './flow-replay.js';
5
+ import { anchorQueryArgs } from './flow-step-runners.js';
6
+ import { queryRefs } from './replay.js';
4
7
  import { assertSuccess, dynamicTestids, successLabel, SUCCESS_STEP_TOOL } from './flow-success.js';
5
8
  import { buildDecision, unverifiableReason } from './decision.js';
6
9
  import { classifyFlowAssertions } from './flow-classify.js';
@@ -16,7 +19,9 @@ import { EnvelopeStore } from '../journal/envelope-store.js';
16
19
  import { homedir } from 'node:os';
17
20
  import { cloudFetch, syncRunRecordToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
18
21
  import { resolveProjectCloud } from '../cloud/cloud-config.js';
22
+ import { consultSubjectFor, selectConsulted } from './flow-memory-consult.js';
19
23
  import { log } from '../log.js';
24
+ import { flowsForSession } from './flow-store-for-session.js';
20
25
  export function latestRecordedFlow(events) {
21
26
  for (let i = events.length - 1; i >= 0; i--) {
22
27
  const event = events[i];
@@ -54,10 +59,12 @@ function replayToRunStatus(status) {
54
59
  }
55
60
  /**
56
61
  * Append a flow-replay outcome to .reticle/project.json (never throws into replay) and, when logged in,
57
- * best-effort mirror it to Reticle Cloud so the team's server-side regression history stays current. The
62
+ * best-effort mirror it to Reticle so the team's server-side regression history stays current. The
58
63
  * cloud push is fire-and-forget: not logged in → skipped, a network failure is logged and swallowed.
59
64
  */
60
- async function recordReplayRun(deps, name, status, driftSteps, durationMs, projectId) {
65
+ async function recordReplayRun(deps, name, status, driftSteps, durationMs, projectId,
66
+ /** The APP's `.reticle`, resolved from the session — never the daemon's own. */
67
+ recordRoot) {
61
68
  const runStatus = replayToRunStatus(status);
62
69
  await deps.project.recordRun({
63
70
  kind: RunKind.FLOW_REPLAY,
@@ -67,7 +74,10 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs, proje
67
74
  durationMs,
68
75
  });
69
76
  // Per-project cloud: push memory outcomes only when cloud is attached AND memory sync is enabled.
70
- const cloud = await resolveProjectCloud(deps.fs, deps.reticleRoot, homedir(), process.env);
77
+ // Rooted at the APP, not the daemon — see consultProjectMemory. This call had the same defect and
78
+ // it was worse here: a replay outcome that should have reached the dashboard silently did not,
79
+ // because the daemon's own directory has no link file.
80
+ const cloud = await resolveProjectCloud(deps.fs, recordRoot, homedir(), process.env);
71
81
  if (null === cloud.config || !cloud.policy.memory)
72
82
  return; // not attached / memory disabled → local only
73
83
  const result = await syncRunRecordToCloud({ kind: RunKind.FLOW_REPLAY, name, status: runStatus, at: deps.now(), durationMs }, projectId, cloud.config, cloudFetch);
@@ -75,27 +85,212 @@ async function recordReplayRun(deps, name, status, driftSteps, durationMs, proje
75
85
  log('cloud-run-record-sync-failed', { flow: name, status: result.status, error: result.error });
76
86
  }
77
87
  }
88
+ /**
89
+ * The route the tab is on now: the last observed route.change, falling back to the pathname of the
90
+ * session's own URL. The fallback matters — a tab that hard-loaded its page emits no route event
91
+ * (the route observer only sees pushState/replaceState/popstate), but the HELLO url still names
92
+ * where it sits, and without it a wrong-page replay reported plain drift with no hint at all.
93
+ */
94
+ function currentPathOf(session) {
95
+ const routes = session.eventsSince(0).filter((e) => e.type === EventType.ROUTE_CHANGE);
96
+ const data = routes.at(-1)?.data ?? {};
97
+ // pathname + hash, because `startPath` is compared against this and must stay NAVIGABLE. Reading
98
+ // the pathname alone made both sides `/` on a hash router — always "same path", so the hint never
99
+ // fired however far the tab had drifted, on the router desktop renderers use by default.
100
+ const observed = asString(data['pathname']) ?? asString(data['to']);
101
+ if (observed !== undefined)
102
+ return `${observed}${asString(data['hash']) ?? ''}`;
103
+ if (session.url === undefined)
104
+ return undefined;
105
+ try {
106
+ const parsed = new URL(session.url);
107
+ return `${parsed.pathname}${parsed.hash}`;
108
+ }
109
+ catch {
110
+ return undefined;
111
+ }
112
+ }
113
+ /** Pathname equality up to a trailing slash — a router normalising one must not read as "elsewhere". */
114
+ function samePath(a, b) {
115
+ return a.replace(/\/$/, '') === b.replace(/\/$/, '');
116
+ }
117
+ /**
118
+ * Ask the project what it already knows about this flow.
119
+ *
120
+ * Best-effort in the strongest sense: an unlinked project, a disabled memory policy, an offline
121
+ * laptop and a server that answers nonsense all reach the same place — the verdict returns without
122
+ * a memory block. A verification that FAILED because the knowledge lookup failed would be a worse
123
+ * product than one that never looked, and this is the path every replay takes.
124
+ *
125
+ * The read is also what makes the coverage map's fetch counts mean anything: they were zero across
126
+ * the entire corpus, not because memory is useless but because consulting it was a separate act
127
+ * nobody performed. Now the platform performs it.
128
+ */
129
+ async function consultProjectMemory(deps, flow, root) {
130
+ const subject = consultSubjectFor(flow);
131
+ if (subject === undefined)
132
+ return undefined;
133
+ try {
134
+ // The APP's root, not the daemon's. `deps.reticleRoot` is wherever the daemon was launched,
135
+ // which for a user-scoped MCP registration is almost never the project being verified — so the
136
+ // link file it reads is the wrong one, or absent, and every project silently reads as
137
+ // "not attached". Same class of bug as the flow store resolving per daemon instead of per
138
+ // session, and it is invisible: the feature simply never appears.
139
+ const cloud = await resolveProjectCloud(deps.fs, root, homedir(), process.env);
140
+ if (null === cloud.config || !cloud.policy.memory)
141
+ return undefined;
142
+ const url = `${cloud.config.url}/v1/memory?subject=${encodeURIComponent(subject)}`;
143
+ const res = await cloudFetch(url, {
144
+ method: 'GET',
145
+ headers: { authorization: `Bearer ${cloud.config.apiKey}` },
146
+ });
147
+ if (200 !== res.status)
148
+ return undefined;
149
+ // `cloudFetch` hands back a real `Response`, so the body is a METHOD. Reading `res.json` as a
150
+ // property yields the function itself, `.entries` on it is undefined, and the whole feature
151
+ // fails silently to "the project knows nothing" — which is indistinguishable from the honest
152
+ // empty case and is why this took a live drive to notice at all.
153
+ const body = (await res.json());
154
+ const entries = body?.entries;
155
+ if (!Array.isArray(entries))
156
+ return undefined;
157
+ const picked = selectConsulted(entries);
158
+ return 0 === picked.length ? undefined : picked;
159
+ }
160
+ catch {
161
+ // See the note above: never the reason a verdict fails to return.
162
+ return undefined;
163
+ }
164
+ }
78
165
  /**
79
166
  * When a flow records the page its journey started on (`startPath`) and the tab is currently on a
80
167
  * different route, step 1 drifts for a reason that has nothing to do with the app regressing — the
81
168
  * anchor simply isn't on this page yet. Detect that so the decision says "navigate there first"
82
169
  * instead of a mystifying "a step no longer matches". Returns undefined when the routes agree or the
83
- * current route is unobservable (no route event) — never a false alarm. Replay itself does NOT
84
- * navigate: a full-page load mid-replay tears down the session socket; the agent navigates between
85
- * tool calls (reticle_navigate) where the session is re-resolved fresh.
170
+ * current route is unobservable — never a false alarm. Replay normally never gets here on a
171
+ * wrong page (arriveAtStartPath navigates before step 1); this is the fallback for when that
172
+ * navigation was refused or the SDK never reconnected in the window.
86
173
  */
87
174
  export function startPathMismatchHint(flow, session) {
88
175
  const startPath = flow.startPath;
89
176
  if (startPath === undefined || 0 === startPath.length)
90
177
  return undefined;
91
- const routes = session.eventsSince(0).filter((e) => e.type === EventType.ROUTE_CHANGE);
92
- const last = routes.at(-1);
93
- const data = last?.data ?? {};
94
- const current = asString(data['pathname']) ?? asString(data['to']);
95
- if (current === undefined || current === startPath)
178
+ const current = currentPathOf(session);
179
+ if (current === undefined || samePath(current, startPath))
96
180
  return undefined;
97
181
  return `this flow's journey starts on ${startPath} but the tab is on ${current} — navigate there (reticle_navigate { url: "${startPath}" }), then replay`;
98
182
  }
183
+ /** How long replay waits for the SDK to reconnect on the start page before falling back to the hint. */
184
+ const START_PATH_ARRIVAL_TIMEOUT_MS = 5_000;
185
+ const START_PATH_POLL_MS = 100;
186
+ const REAL_ARRIVAL_CLOCK = {
187
+ now: () => Date.now(),
188
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
189
+ };
190
+ /**
191
+ * The session `oldId` denotes right now — itself, or the tombstone successor `resolve` rebinds to
192
+ * after a full-document navigation — but only once it actually sits on `target`. Undefined while
193
+ * the tab is still travelling (or in the gap between teardown and the successor's HELLO, when
194
+ * `resolve` throws). Keyed to the navigated tab's own identity on purpose: matching "any session at
195
+ * the target page" would happily hand replay an unrelated tab that was already sitting there.
196
+ */
197
+ function arrivedSuccessor(sessions, oldId, target) {
198
+ try {
199
+ const candidate = sessions.resolve(oldId);
200
+ const path = currentPathOf(candidate);
201
+ return path !== undefined && samePath(path, target) ? candidate : undefined;
202
+ }
203
+ catch {
204
+ return undefined;
205
+ }
206
+ }
207
+ /**
208
+ * Replay's half of the FlowFile contract: navigate to the flow's `startPath` before step 1.
209
+ *
210
+ * A full-page load tears down the session socket, so the navigation must happen here — before any
211
+ * step runs — and the replay continues on the session the SDK reconnects as (found via the same
212
+ * tombstone rebind that lets `resolve(oldId)` answer after any navigation). Best-effort by design:
213
+ * when the flow carries no startPath, the tab is already there, the current route is unobservable,
214
+ * the navigation is refused, or the SDK never reconnects in the window, it returns undefined and
215
+ * replay proceeds on the connected session as before — with startPathMismatchHint turning any
216
+ * resulting drift into an actionable next move rather than a mystifying one.
217
+ */
218
+ /**
219
+ * Can step 1 start from where the tab already is?
220
+ *
221
+ * `startPathMismatchHint` states the premise the navigation below exists to serve: on the wrong
222
+ * route "the anchor simply isn't on this page yet". That is the condition worth a page load — and
223
+ * it was never the condition checked. `arriveAtStartPath` fired on a route mismatch ALONE, so a
224
+ * flow whose first anchor lives somewhere persistent (a sidebar, a header, a nav rail — present on
225
+ * every route) was navigated for a problem it did not have.
226
+ *
227
+ * A navigation is a full page load, which tears the session down and brings the app back in its
228
+ * cold state. For the ordinary case of a flow recorded after signing in, that cold state is the
229
+ * LOGIN SCREEN: step 1 then reports its anchor missing and names the component that holds it, which
230
+ * is a correct sentence about a file that is completely fine. Found by the benchmark, on two flows
231
+ * with identical steps where the first replayed clean and the second — run after it, with the tab
232
+ * drifted off `/` — did not.
233
+ *
234
+ * So the question is asked directly. A best-effort step that can leave the caller WORSE off than
235
+ * skipping it is not best-effort; checking first is what makes the description true.
236
+ *
237
+ * Unresolvable either way (no anchor we can query, a failed query) reads as "cannot tell", and the
238
+ * navigation goes ahead — the pre-existing behaviour, and the safe direction for a check whose whole
239
+ * job is to avoid making things worse.
240
+ */
241
+ async function firstStepResolvesHere(session, flow) {
242
+ const first = flow.steps?.[0];
243
+ if (first === undefined)
244
+ return false;
245
+ const args = anchorQueryArgs(first.anchor);
246
+ if (null === args)
247
+ return false;
248
+ try {
249
+ return 0 < queryRefs(await session.command(ReticleCommand.QUERY, args)).length;
250
+ }
251
+ catch {
252
+ return false;
253
+ }
254
+ }
255
+ export async function arriveAtStartPath(sessions, session, flow, timeoutMs = START_PATH_ARRIVAL_TIMEOUT_MS, clock = REAL_ARRIVAL_CLOCK) {
256
+ const target = flow.startPath;
257
+ if (target === undefined || 0 === target.length)
258
+ return undefined;
259
+ const current = currentPathOf(session);
260
+ if (current === undefined || samePath(current, target))
261
+ return undefined;
262
+ // The route differs — but that only matters if it stops the flow starting. See above.
263
+ if (await firstStepResolvesHere(session, flow))
264
+ return undefined;
265
+ let destination;
266
+ try {
267
+ // startPath is a pathname (a host belongs to the machine, not the journey) — resolve it
268
+ // against the tab's own URL to get something the browser can be sent to.
269
+ destination = new URL(target, session.url).toString();
270
+ }
271
+ catch {
272
+ return undefined; // no usable base URL — nowhere to navigate from
273
+ }
274
+ // A leased tab is addressed by URL params, so navigating without them would strand the lease.
275
+ const url = carryReticleIdentity(session.url, destination);
276
+ try {
277
+ const outcome = await session.command(ReticleCommand.NAVIGATE, { url });
278
+ if (!outcome.ok || true !== asRecord(outcome.result)['ok'])
279
+ return undefined;
280
+ }
281
+ catch {
282
+ return undefined;
283
+ }
284
+ const deadline = clock.now() + timeoutMs;
285
+ for (;;) {
286
+ const arrived = arrivedSuccessor(sessions, session.id, target);
287
+ if (arrived !== undefined)
288
+ return arrived;
289
+ if (clock.now() >= deadline)
290
+ return undefined;
291
+ await clock.sleep(START_PATH_POLL_MS);
292
+ }
293
+ }
99
294
  /**
100
295
  * Replay one named flow end to end: load → re-resolve+run each step → assert the success oracle →
101
296
  * status + decision. Shared by reticle_flow_replay (single flow) and reticle_flow_verify (whole suite) so
@@ -114,9 +309,11 @@ export async function replayNamedFlow(deps, args) {
114
309
  catch {
115
310
  projectId = undefined;
116
311
  }
117
- const loaded = await deps.flows.load(name, projectId);
312
+ // The app's store, not the daemon's: this load answering `flow_not_found` for a flow plainly on
313
+ // disk is what made replay unusable from a daemon started outside the project.
314
+ const loaded = await flowsForSession(deps, projectId).flows.load(name, projectId);
118
315
  if (!loaded.ok) {
119
- await recordReplayRun(deps, name, ReplayStatus.ERROR, 0, deps.now() - startedAt, projectId);
316
+ await recordReplayRun(deps, name, ReplayStatus.ERROR, 0, deps.now() - startedAt, projectId, sessionRoot(deps, asString(args['sessionId'])));
120
317
  return {
121
318
  name,
122
319
  status: ReplayStatus.ERROR,
@@ -127,13 +324,19 @@ export async function replayNamedFlow(deps, args) {
127
324
  // What this flow is FOR, from the shared ledger — so a failure can report the business outcome
128
325
  // that stopped being true before the step that stopped being green. Undefined when nothing
129
326
  // declared one, which the decision then says plainly rather than inventing a goal from step names.
130
- const intents = new IntentStore(deps.fs, sessionRoot(deps, asString(args['sessionId'])), {
131
- now: deps.now,
132
- });
327
+ /*
328
+ * The APP's `.reticle`, resolved once. Everything below that reaches the project's own files —
329
+ * the intent ledger, the cloud link, the memory consultation — takes THIS, not `deps.reticleRoot`,
330
+ * which is wherever the daemon happened to be launched.
331
+ */
332
+ const replayRoot = sessionRoot(deps, asString(args['sessionId']));
333
+ const intents = new IntentStore(deps.fs, replayRoot, { now: deps.now });
133
334
  const intentSaid = await flowIntentStatement(intents, loaded.value);
134
- const session = deps.sessions.resolve(asString(args['sessionId']));
135
- // Captured before replay: if the tab isn't on the flow's start page, a step-1 drift is a wrong-page
136
- // symptom, not a regression — surface that on the decision instead of a bare "a step no longer matches".
335
+ const connected = deps.sessions.resolve(asString(args['sessionId']));
336
+ // The FlowFile contract: replay navigates to the flow's startPath before step 1, and the steps run
337
+ // on the session the SDK reconnects as. When arrival can't be confirmed, replay proceeds on the
338
+ // connected session — and the hint below turns the wrong-page drift into an actionable next move.
339
+ const session = (await arriveAtStartPath(deps.sessions, connected, loaded.value)) ?? connected;
137
340
  const startPathHint = startPathMismatchHint(loaded.value, session);
138
341
  // Floor the success oracle at the start of THIS replay so a stale signal from a prior run
139
342
  // in the same session can't fake a pass.
@@ -157,7 +360,7 @@ export async function replayNamedFlow(deps, args) {
157
360
  const driftSteps = steps.filter((s) => s.drift !== undefined).length;
158
361
  const allOk = steps.every((s) => s.ok);
159
362
  const status = driftSteps > 0 ? ReplayStatus.DRIFT : allOk ? ReplayStatus.OK : ReplayStatus.ERROR;
160
- await recordReplayRun(deps, name, status, driftSteps, deps.now() - startedAt, projectId);
363
+ await recordReplayRun(deps, name, status, driftSteps, deps.now() - startedAt, projectId, replayRoot);
161
364
  // Anti-reward-hacking baseline: record what this flow asserted ONLY when it passed clean. A
162
365
  // failing run must never become the baseline a later weakening is measured against.
163
366
  if (status === ReplayStatus.OK) {
@@ -182,6 +385,16 @@ export async function replayNamedFlow(deps, args) {
182
385
  }
183
386
  // Push-default: the deviation report over this drive's segments, learned across runs. Best-effort.
184
387
  const deviation = await computeReplayDeviation(deps, session, replayFloor);
388
+ /*
389
+ * What the team already knows about this flow, fetched on the agent's behalf.
390
+ *
391
+ * Attached to the FAILING path as well as the clean one, deliberately: a drift is exactly the
392
+ * moment somebody needs to know what this feature is supposed to do and who established it. A
393
+ * knowledge base you only see when everything is already fine is decoration.
394
+ */
395
+ // No projectId argument: the API key is already bound to one project server-side, so passing a
396
+ // second opinion about which project this is would only create a way for the two to disagree.
397
+ const knows = await consultProjectMemory(deps, loaded.value, replayRoot);
185
398
  const failed = steps.find((step) => !step.ok && step.drift === undefined);
186
399
  if (failed !== undefined) {
187
400
  const errored = {
@@ -194,9 +407,13 @@ export async function replayNamedFlow(deps, args) {
194
407
  applyStartPathHint(errored, startPathHint);
195
408
  if (deviation !== undefined)
196
409
  errored.deviation = deviation;
410
+ if (knows !== undefined)
411
+ errored.knows = knows;
197
412
  return errored;
198
413
  }
199
414
  const result = { name, status, steps };
415
+ if (knows !== undefined)
416
+ result.knows = knows;
200
417
  // A green that cannot go red is not a pass. `reticle_flow_verify` already refuses to count these,
201
418
  // via this same function -- a single-flow caller saw a bare `ok` and had no way to learn the flow
202
419
  // asserts nothing. Derived from the same helper on purpose: two copies of this judgement would
@@ -1,4 +1,5 @@
1
1
  import { span } from '../trace.js';
2
+ import { routePathOf } from '../events/predicate-route.js';
2
3
  import { AnchorKind, DriftReason, EventType, ReticleCommand, QueryBy, PredicateKind, } from '@reticlehq/core';
3
4
  import { asRecord, asString } from '../tools/tools-helpers.js';
4
5
  import { replayActionArgs, ambiguousTestidNote, queryRefs } from './replay.js';
@@ -198,8 +199,14 @@ function currentRoute(session) {
198
199
  if (last === undefined)
199
200
  return undefined;
200
201
  const data = last.data ?? {};
202
+ // The ROUTER's path. This field answers "which page did this step run on", and the document
203
+ // pathname is `/` on every page of a hash-routed app — so a whole desktop replay reported `/` for
204
+ // every step. Sixth place the same reading was wrong; see routePathOf.
201
205
  const pathname = asString(data['pathname']) ?? asString(data['to']);
202
- return pathname !== undefined && pathname.length > 0 ? pathname : undefined;
206
+ if (pathname === undefined || 0 === pathname.length)
207
+ return undefined;
208
+ const routed = routePathOf(pathname, asString(data['hash']) ?? '');
209
+ return routed.length > 0 ? routed : undefined;
203
210
  }
204
211
  /** Pathname only (drop origin + query) so a net URL stays terse in the journey. */
205
212
  function trimUrl(url) {
@@ -294,7 +301,9 @@ async function runTestidStep(session, step, index, value, dynamic, confirmDanger
294
301
  act = await session.command(ReticleCommand.ACT, {
295
302
  ref,
296
303
  action: step.action ?? '',
297
- args: replayActionArgs(step.args, confirmDangerous),
304
+ // `value` is the anchor's own name — the field this step types into — so a redacted fill can
305
+ // be supplied from RETICLE_SECRET_<FIELD> without the flow carrying the secret.
306
+ args: replayActionArgs(step.args, confirmDangerous, value),
298
307
  });
299
308
  }
300
309
  finally {
@@ -30,6 +30,14 @@ export declare function runComponentStep(session: FlowReplaySession, step: FlowS
30
30
  */
31
31
  export declare function isDegradedAnchor(anchor: FlowAnchor): boolean;
32
32
  export declare function degradedStepResult(step: FlowStep, index: number, label: string): FlowStepResult;
33
+ /**
34
+ * QUERY args for an element anchor — null when the anchor addresses no element.
35
+ *
36
+ * Exported for `arriveAtStartPath`, which has to ask "can step 1 resolve where we already are?"
37
+ * before deciding to navigate. Sharing this mapping rather than repeating it keeps that question and
38
+ * the step runner's own resolution asking the same thing of the same anchor.
39
+ */
40
+ export declare function anchorQueryArgs(anchor: FlowAnchor): Record<string, unknown> | null;
33
41
  /**
34
42
  * Run an act_sequence step: resolve every sub-step's OWN anchor, then dispatch the whole thing as one
35
43
  * ACT_SEQUENCE — the same shape `replayProgram` already uses, so a recorded sequence and a saved one
@@ -181,8 +181,14 @@ export function degradedStepResult(step, index, label) {
181
181
  },
182
182
  };
183
183
  }
184
- /** QUERY args for an element anchor — null when the anchor addresses no element. */
185
- function anchorQueryArgs(anchor) {
184
+ /**
185
+ * QUERY args for an element anchor — null when the anchor addresses no element.
186
+ *
187
+ * Exported for `arriveAtStartPath`, which has to ask "can step 1 resolve where we already are?"
188
+ * before deciding to navigate. Sharing this mapping rather than repeating it keeps that question and
189
+ * the step runner's own resolution asking the same thing of the same anchor.
190
+ */
191
+ export function anchorQueryArgs(anchor) {
186
192
  if (anchor.kind === AnchorKind.TESTID)
187
193
  return { by: QueryBy.TESTID, value: anchor.value };
188
194
  if (anchor.kind === AnchorKind.COMPONENT)
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The flow store a SESSION's flows belong in.
3
+ *
4
+ * `deps.flows` is bound to the daemon's own root at construction, which is right only when the
5
+ * daemon happens to have been started in the project it is driving. A user-scoped MCP registration
6
+ * is the common case and the editor starts it wherever it likes, so "where the daemon was launched"
7
+ * and "which app is being verified" are usually different directories.
8
+ *
9
+ * This used to live inside flow-tools and was applied at exactly ONE of ten call sites. The result
10
+ * is the half-applied state `sessionRoot` warns about in its own comment, reached in practice:
11
+ *
12
+ * - the HUD listed the daemon repo's flows while driving somebody else's app — Electron and Tauri
13
+ * flows shown over a React dashboard, none of which existed in the checkout being verified
14
+ * - `flow_replay` answered `flow_not_found` for a flow sitting on disk, because it looked in the
15
+ * daemon's root while `reticle verify` (which roots itself at cwd) replayed the same flow fine
16
+ * - and the two save paths disagreed with each other, so where a flow landed depended on which
17
+ * tool wrote it
18
+ *
19
+ * Reported as "running the daemon from here but taking the directory from there", which is exactly
20
+ * what it was.
21
+ *
22
+ * Rebinding is cheap — a FlowStore is a filesystem port, a root and a clock — and the common case
23
+ * allocates nothing: when no resolver is wired, or it names the root already in use, `deps.flows` is
24
+ * returned unchanged.
25
+ */
26
+ import { FlowStore } from './flows.js';
27
+ import type { ToolDeps } from '../tools/tool-kit.js';
28
+ export declare function flowsForSession(deps: ToolDeps, projectId: string | undefined): {
29
+ flows: FlowStore;
30
+ root: string;
31
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The flow store a SESSION's flows belong in.
3
+ *
4
+ * `deps.flows` is bound to the daemon's own root at construction, which is right only when the
5
+ * daemon happens to have been started in the project it is driving. A user-scoped MCP registration
6
+ * is the common case and the editor starts it wherever it likes, so "where the daemon was launched"
7
+ * and "which app is being verified" are usually different directories.
8
+ *
9
+ * This used to live inside flow-tools and was applied at exactly ONE of ten call sites. The result
10
+ * is the half-applied state `sessionRoot` warns about in its own comment, reached in practice:
11
+ *
12
+ * - the HUD listed the daemon repo's flows while driving somebody else's app — Electron and Tauri
13
+ * flows shown over a React dashboard, none of which existed in the checkout being verified
14
+ * - `flow_replay` answered `flow_not_found` for a flow sitting on disk, because it looked in the
15
+ * daemon's root while `reticle verify` (which roots itself at cwd) replayed the same flow fine
16
+ * - and the two save paths disagreed with each other, so where a flow landed depended on which
17
+ * tool wrote it
18
+ *
19
+ * Reported as "running the daemon from here but taking the directory from there", which is exactly
20
+ * what it was.
21
+ *
22
+ * Rebinding is cheap — a FlowStore is a filesystem port, a root and a clock — and the common case
23
+ * allocates nothing: when no resolver is wired, or it names the root already in use, `deps.flows` is
24
+ * returned unchanged.
25
+ */
26
+ import { FlowStore } from './flows.js';
27
+ export function flowsForSession(deps, projectId) {
28
+ const resolved = deps.artifactRootFor?.(projectId);
29
+ if (resolved === undefined || resolved.root === deps.reticleRoot) {
30
+ return { flows: deps.flows, root: deps.reticleRoot };
31
+ }
32
+ return { flows: new FlowStore(deps.fs, resolved.root, { now: deps.now }), root: resolved.root };
33
+ }