@reticlehq/server 2.13.1 → 2.14.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 (449) hide show
  1. package/SKILL.md +8 -18
  2. package/bin/reticle.js +3 -0
  3. package/dist/bridge/auth-failure-reason.js +12 -1
  4. package/dist/bridge/bridge-security.d.ts +8 -0
  5. package/dist/bridge/bridge-security.js +6 -1
  6. package/dist/bridge/bridge.d.ts +3 -0
  7. package/dist/bridge/bridge.js +23 -1
  8. package/dist/bridge/bridge.test-harness.d.ts +10 -1
  9. package/dist/bridge/bridge.test-harness.js +7 -1
  10. package/dist/bridge/pairing-token.d.ts +17 -0
  11. package/dist/bridge/pairing-token.js +26 -0
  12. package/dist/bridge/token-auth.d.ts +7 -0
  13. package/dist/bridge/token-auth.js +7 -1
  14. package/dist/capsule/capsule-store.d.ts +4 -4
  15. package/dist/capsule/capsule.d.ts +1 -1
  16. package/dist/capsule/capsule.js +3 -3
  17. package/dist/capsule/causal-summary.d.ts +21 -1
  18. package/dist/capsule/causal-summary.js +8 -6
  19. package/dist/capsule/divergence.d.ts +1 -1
  20. package/dist/capsule/divergence.js +26 -10
  21. package/dist/chromium-launch-options.d.ts +2 -1
  22. package/dist/cli/adhoc-verdict.d.ts +58 -0
  23. package/dist/cli/adhoc-verdict.js +115 -0
  24. package/dist/cli/chromium-hint.d.ts +18 -4
  25. package/dist/cli/chromium-hint.js +54 -6
  26. package/dist/cli/cli-doctor.js +1 -2
  27. package/dist/cli/cli-flow-commands.d.ts +16 -2
  28. package/dist/cli/cli-flow-commands.js +28 -9
  29. package/dist/cli/cli-parse-feedback.js +1 -1
  30. package/dist/cli/cli-parse.d.ts +2 -1
  31. package/dist/cli/cli-parse.js +38 -0
  32. package/dist/cli/cli-port.d.ts +5 -13
  33. package/dist/cli/cli-port.js +5 -38
  34. package/dist/cli/cli-update-commands.js +4 -4
  35. package/dist/cli/cli-verify.d.ts +24 -7
  36. package/dist/cli/cli-verify.js +87 -13
  37. package/dist/cli/cloud-keystore.d.ts +2 -1
  38. package/dist/cli/cloud-login.d.ts +2 -1
  39. package/dist/cli/cloud-session.d.ts +2 -1
  40. package/dist/cli/cloud-session.js +1 -1
  41. package/dist/cli/daemon-start-options.d.ts +2 -1
  42. package/dist/cli/device-flow.d.ts +2 -2
  43. package/dist/cli/doctor-daemon-line.d.ts +2 -1
  44. package/dist/cli/doctor-project-line.d.ts +2 -1
  45. package/dist/cli/doctor-rows.d.ts +2 -1
  46. package/dist/cli/drive-attach.d.ts +4 -3
  47. package/dist/cli/gate-hook-message.d.ts +2 -1
  48. package/dist/cli/status-next-action.js +10 -11
  49. package/dist/cli.d.ts +1 -0
  50. package/dist/cli.js +21 -9
  51. package/dist/cloud/account-state.d.ts +20 -0
  52. package/dist/cloud/account-state.js +65 -0
  53. package/dist/cloud/cloud-sync.d.ts +46 -3
  54. package/dist/cloud/cloud-sync.js +116 -0
  55. package/dist/cloud/sync-cycle.d.ts +5 -4
  56. package/dist/cloud/sync-cycle.js +1 -1
  57. package/dist/cloud/sync-daemon.d.ts +3 -19
  58. package/dist/cloud/sync-daemon.js +2 -2
  59. package/dist/crawl/crawl-tools.js +1 -1
  60. package/dist/crawl/crawl.d.ts +5 -5
  61. package/dist/crawl/crawl.js +138 -4
  62. package/dist/daemon/daemon-resilience.d.ts +3 -2
  63. package/dist/daemon/daemon-resilience.js +43 -4
  64. package/dist/daemon/daemon-resolve.d.ts +0 -9
  65. package/dist/daemon/daemon-resolve.js +2 -2
  66. package/dist/daemon/dev-servers.d.ts +1 -1
  67. package/dist/daemon/dev-servers.js +1 -1
  68. package/dist/daemon/loopback-alias.d.ts +2 -1
  69. package/dist/daemon/startup-failure.d.ts +11 -0
  70. package/dist/daemon/startup-failure.js +46 -0
  71. package/dist/events/contradictions.d.ts +10 -16
  72. package/dist/events/contradictions.js +149 -17
  73. package/dist/events/declared.d.ts +13 -1
  74. package/dist/events/declared.js +59 -3
  75. package/dist/events/echo-mismatch.js +75 -6
  76. package/dist/events/lineage.d.ts +52 -0
  77. package/dist/events/lineage.js +122 -0
  78. package/dist/events/net-evidence.d.ts +21 -0
  79. package/dist/events/net-evidence.js +81 -0
  80. package/dist/events/predicate-console.d.ts +13 -0
  81. package/dist/events/predicate-console.js +138 -0
  82. package/dist/events/predicate-element.d.ts +13 -0
  83. package/dist/events/predicate-element.js +214 -0
  84. package/dist/events/predicate-eval.d.ts +15 -3
  85. package/dist/events/predicate-eval.js +128 -110
  86. package/dist/events/predicate-parse.js +34 -1
  87. package/dist/events/predicate-request-body.d.ts +50 -0
  88. package/dist/events/predicate-request-body.js +120 -0
  89. package/dist/events/predicate-route.d.ts +21 -1
  90. package/dist/events/predicate-route.js +149 -21
  91. package/dist/events/predicate-schema.d.ts +8 -1
  92. package/dist/events/predicate-schema.js +34 -0
  93. package/dist/events/predicate.d.ts +2 -0
  94. package/dist/events/predicate.js +52 -159
  95. package/dist/events/split-text-miss.d.ts +6 -4
  96. package/dist/events/split-text-miss.js +12 -5
  97. package/dist/flows/affected-tools.js +22 -9
  98. package/dist/flows/annotate.d.ts +2 -1
  99. package/dist/flows/annotate.js +19 -1
  100. package/dist/flows/coverage.d.ts +2 -2
  101. package/dist/flows/flake.d.ts +3 -3
  102. package/dist/flows/flow-expect-grammar.d.ts +33 -0
  103. package/dist/flows/flow-expect-grammar.js +155 -0
  104. package/dist/flows/flow-replay-run.d.ts +1 -1
  105. package/dist/flows/flow-replay-run.js +28 -15
  106. package/dist/flows/flow-replay.d.ts +10 -0
  107. package/dist/flows/flow-replay.js +57 -22
  108. package/dist/flows/flow-secret-field.d.ts +14 -0
  109. package/dist/flows/flow-secret-field.js +22 -0
  110. package/dist/flows/flow-step-runners.d.ts +1 -0
  111. package/dist/flows/flow-step-runners.js +91 -34
  112. package/dist/flows/flow-success.js +22 -0
  113. package/dist/flows/flow-tools.js +27 -8
  114. package/dist/flows/flow-upload-resolve.d.ts +23 -0
  115. package/dist/flows/flow-upload-resolve.js +46 -0
  116. package/dist/flows/flows.d.ts +15 -0
  117. package/dist/flows/flows.js +17 -14
  118. package/dist/flows/git-changed.d.ts +23 -4
  119. package/dist/flows/git-changed.js +32 -6
  120. package/dist/flows/heal-run.js +2 -2
  121. package/dist/flows/predicate-to-expect.d.ts +3 -2
  122. package/dist/flows/predicate-to-expect.js +11 -5
  123. package/dist/flows/recording-backtrack.d.ts +24 -0
  124. package/dist/flows/recording-backtrack.js +73 -0
  125. package/dist/flows/recordings.d.ts +6 -0
  126. package/dist/flows/replay-halt.d.ts +26 -0
  127. package/dist/flows/replay-halt.js +30 -0
  128. package/dist/flows/replay.d.ts +0 -8
  129. package/dist/flows/replay.js +15 -3
  130. package/dist/flows/suite-flakes.d.ts +2 -1
  131. package/dist/flows/verify-change-tools.js +16 -4
  132. package/dist/honesty/already-true.d.ts +14 -0
  133. package/dist/honesty/already-true.js +47 -4
  134. package/dist/honesty/blind-spots.d.ts +3 -1
  135. package/dist/honesty/blind-spots.js +24 -1
  136. package/dist/honesty/body-capture-remedy.d.ts +42 -0
  137. package/dist/honesty/body-capture-remedy.js +95 -0
  138. package/dist/honesty/feature-capture.d.ts +3 -2
  139. package/dist/honesty/feature-capture.js +1 -1
  140. package/dist/honesty/gap-ledger.d.ts +1 -1
  141. package/dist/honesty/gap-ledger.js +1 -1
  142. package/dist/honesty/instrumentation-gaps.d.ts +8 -1
  143. package/dist/honesty/instrumentation-gaps.js +12 -2
  144. package/dist/honesty/observability.d.ts +4 -3
  145. package/dist/honesty/tool-hit-rate.d.ts +3 -2
  146. package/dist/honesty/uncaptured-bodies.d.ts +3 -1
  147. package/dist/honesty/uncaptured-bodies.js +6 -2
  148. package/dist/honesty/unsettled.d.ts +9 -0
  149. package/dist/honesty/unsettled.js +45 -0
  150. package/dist/honesty/verified.d.ts +32 -1
  151. package/dist/honesty/verified.js +37 -10
  152. package/dist/impact/impact-store.d.ts +5 -14
  153. package/dist/impact/impact-store.js +49 -6
  154. package/dist/index.js +1 -1
  155. package/dist/input/network-detail.d.ts +26 -1
  156. package/dist/input/network-detail.js +44 -12
  157. package/dist/input/network-mock-tools.js +24 -17
  158. package/dist/input/real-input.js +1 -1
  159. package/dist/input/viewport-tools.js +25 -13
  160. package/dist/intent/inline-intent.d.ts +0 -10
  161. package/dist/intent/inline-intent.js +1 -1
  162. package/dist/intent/intent-shard-store.d.ts +3 -2
  163. package/dist/intent/intent-shard-store.js +1 -1
  164. package/dist/intent/intent-shard.d.ts +14 -14
  165. package/dist/intent/intent-shard.js +4 -4
  166. package/dist/intent/intent-store.d.ts +1 -1
  167. package/dist/intent/intent-store.js +1 -1
  168. package/dist/intent/intent-subject.d.ts +2 -1
  169. package/dist/intent/open-intents.d.ts +3 -2
  170. package/dist/license/license.d.ts +4 -4
  171. package/dist/license/license.js +1 -1
  172. package/dist/mcp/attach-memory.d.ts +2 -1
  173. package/dist/mcp/client-identity.d.ts +2 -1
  174. package/dist/mcp/mcp-outage.d.ts +1 -1
  175. package/dist/mcp/mcp-outage.js +1 -1
  176. package/dist/mcp/mcp-proxy.d.ts +3 -1
  177. package/dist/mcp/mcp-proxy.js +32 -7
  178. package/dist/mcp/mcp.js +3 -1
  179. package/dist/mcp/proxy-handshake.d.ts +2 -1
  180. package/dist/mcp/proxy-idle-exit.d.ts +45 -0
  181. package/dist/mcp/proxy-idle-exit.js +94 -0
  182. package/dist/mcp/server-instructions.d.ts +2 -1
  183. package/dist/memory/project-memory.d.ts +2 -3
  184. package/dist/memory/project-memory.js +1 -1
  185. package/dist/oracles/propose-consequences.js +5 -5
  186. package/dist/oracles/self-instrument.d.ts +2 -2
  187. package/dist/oracles/self-instrument.js +1 -1
  188. package/dist/platform.d.ts +3 -5
  189. package/dist/platform.js +3 -5
  190. package/dist/pool/browser-pool.d.ts +92 -0
  191. package/dist/pool/browser-pool.js +119 -5
  192. package/dist/pool/playwright-launcher.js +29 -2
  193. package/dist/pool/storage-seed.d.ts +23 -0
  194. package/dist/pool/storage-seed.js +198 -0
  195. package/dist/project/artifact-root.d.ts +2 -1
  196. package/dist/project/memory-fs.d.ts +2 -1
  197. package/dist/project/reticle-dir.js +1 -1
  198. package/dist/project/session-root.d.ts +8 -0
  199. package/dist/project/session-root.js +9 -2
  200. package/dist/runs/reticle-runner.d.ts +14 -1
  201. package/dist/runs/reticle-runner.js +39 -2
  202. package/dist/session/attachment-history.d.ts +2 -1
  203. package/dist/session/fake-session.d.ts +37 -0
  204. package/dist/session/fake-session.js +81 -0
  205. package/dist/session/lease-visibility.d.ts +2 -1
  206. package/dist/session/live-control-tools.js +6 -0
  207. package/dist/session/no-session-diagnosis.d.ts +17 -1
  208. package/dist/session/no-session-diagnosis.js +32 -6
  209. package/dist/session/no-session-next-action.d.ts +14 -0
  210. package/dist/session/no-session-next-action.js +18 -0
  211. package/dist/session/no-session-watch.js +46 -3
  212. package/dist/session/registered-projects.d.ts +54 -0
  213. package/dist/session/registered-projects.js +73 -0
  214. package/dist/session/review-store.d.ts +2 -1
  215. package/dist/session/session-health.d.ts +62 -1
  216. package/dist/session/session-health.js +82 -2
  217. package/dist/session/session-manager.d.ts +11 -1
  218. package/dist/session/session-manager.js +18 -3
  219. package/dist/session/session-replaced.d.ts +24 -0
  220. package/dist/session/session-replaced.js +29 -0
  221. package/dist/session/session-successor.js +20 -4
  222. package/dist/session/session.d.ts +13 -5
  223. package/dist/session/session.js +31 -23
  224. package/dist/setup/agent-configs.d.ts +3 -2
  225. package/dist/setup/agent-writer.d.ts +3 -2
  226. package/dist/setup/approval-migration.d.ts +2 -1
  227. package/dist/setup/approval-migration.js +1 -1
  228. package/dist/setup/auto-approve.d.ts +11 -19
  229. package/dist/setup/auto-approve.js +15 -12
  230. package/dist/{init → setup}/confirm.js +1 -1
  231. package/dist/setup/desktop-shape.d.ts +3 -2
  232. package/dist/setup/drive-agent.d.ts +2 -3
  233. package/dist/setup/drive-agent.js +1 -1
  234. package/dist/setup/drive-plan.d.ts +3 -2
  235. package/dist/setup/ensure-daemon.d.ts +2 -1
  236. package/dist/setup/existing-app.d.ts +12 -0
  237. package/dist/setup/existing-app.js +13 -0
  238. package/dist/setup/init-host.d.ts +11 -0
  239. package/dist/setup/init-host.js +26 -0
  240. package/dist/setup/init-runtime.d.ts +4 -10
  241. package/dist/setup/init-runtime.js +23 -16
  242. package/dist/setup/license-key.d.ts +2 -1
  243. package/dist/setup/listeners.d.ts +2 -1
  244. package/dist/setup/loopback-probe-urls.d.ts +18 -0
  245. package/dist/setup/loopback-probe-urls.js +52 -0
  246. package/dist/setup/node-effects.d.ts +3 -0
  247. package/dist/setup/node-effects.js +26 -11
  248. package/dist/setup/page-probe.d.ts +5 -0
  249. package/dist/setup/relaunch.d.ts +2 -1
  250. package/dist/setup/run-setup.d.ts +5 -1
  251. package/dist/setup/run-setup.js +141 -50
  252. package/dist/setup/setup-command.d.ts +3 -2
  253. package/dist/setup/setup-command.js +8 -0
  254. package/dist/setup/setup-options.d.ts +5 -3
  255. package/dist/setup/setup-options.js +3 -2
  256. package/dist/setup/wait-progress.d.ts +28 -0
  257. package/dist/setup/wait-progress.js +43 -0
  258. package/dist/telemetry/app-instrumented.d.ts +2 -1
  259. package/dist/telemetry/app-instrumented.js +1 -1
  260. package/dist/telemetry/automation-hint.d.ts +1 -1
  261. package/dist/telemetry/automation-hint.js +1 -1
  262. package/dist/telemetry/cli-telemetry.js +1 -1
  263. package/dist/telemetry/connect-failure.d.ts +1 -1
  264. package/dist/telemetry/connect-failure.js +1 -1
  265. package/dist/telemetry/crash-cause.d.ts +3 -2
  266. package/dist/telemetry/crash-cause.js +1 -1
  267. package/dist/telemetry/daemon-telemetry.d.ts +3 -2
  268. package/dist/telemetry/daemon-telemetry.js +1 -1
  269. package/dist/telemetry/error-class.d.ts +3 -2
  270. package/dist/telemetry/error-class.js +1 -1
  271. package/dist/telemetry/feedback-cli.js +1 -1
  272. package/dist/telemetry/feedback-context.d.ts +3 -2
  273. package/dist/telemetry/feedback-context.js +5 -4
  274. package/dist/telemetry/feedback-local.d.ts +3 -2
  275. package/dist/telemetry/feedback.d.ts +4 -3
  276. package/dist/telemetry/feedback.js +1 -1
  277. package/dist/telemetry/git-facts.d.ts +2 -2
  278. package/dist/telemetry/git-facts.js +1 -1
  279. package/dist/telemetry/identify.d.ts +1 -1
  280. package/dist/telemetry/identify.js +1 -1
  281. package/dist/telemetry/init-telemetry.d.ts +2 -14
  282. package/dist/telemetry/init-telemetry.js +4 -13
  283. package/dist/telemetry/install-source.d.ts +7 -13
  284. package/dist/telemetry/install-source.js +7 -29
  285. package/dist/telemetry/license-activation.d.ts +3 -2
  286. package/dist/telemetry/license-activation.js +1 -1
  287. package/dist/telemetry/machine-snapshot.d.ts +1 -1
  288. package/dist/telemetry/mcp-connection.js +1 -1
  289. package/dist/telemetry/project-profile.d.ts +0 -14
  290. package/dist/telemetry/project-profile.js +1 -1
  291. package/dist/telemetry/proxy-telemetry.js +1 -1
  292. package/dist/telemetry/session-metrics.d.ts +1 -1
  293. package/dist/telemetry/session-metrics.js +1 -1
  294. package/dist/telemetry/telemetry.d.ts +1 -17
  295. package/dist/telemetry/telemetry.js +4 -4
  296. package/dist/telemetry/tool-refused.d.ts +1 -1
  297. package/dist/telemetry/tool-refused.js +1 -1
  298. package/dist/tools/act-observation.d.ts +3 -2
  299. package/dist/tools/act-preflight.js +8 -3
  300. package/dist/tools/act-target.d.ts +4 -0
  301. package/dist/tools/act-target.js +14 -5
  302. package/dist/tools/act-tools.d.ts +0 -2
  303. package/dist/tools/act-tools.js +45 -15
  304. package/dist/tools/assert-source.d.ts +30 -0
  305. package/dist/tools/assert-source.js +21 -0
  306. package/dist/tools/assert-verdict.d.ts +1 -1
  307. package/dist/tools/assert-verdict.js +14 -4
  308. package/dist/tools/browser-tools.js +28 -8
  309. package/dist/tools/complete-snapshot.d.ts +3 -2
  310. package/dist/tools/dynamic-tools.js +8 -0
  311. package/dist/tools/error-recovery.d.ts +10 -0
  312. package/dist/tools/error-recovery.js +23 -0
  313. package/dist/tools/feedback-tools.js +1 -1
  314. package/dist/tools/invoke-tool.js +2 -0
  315. package/dist/tools/lease-tools.d.ts +48 -20
  316. package/dist/tools/lease-tools.js +311 -76
  317. package/dist/tools/lineage-tools.d.ts +14 -0
  318. package/dist/tools/lineage-tools.js +64 -0
  319. package/dist/tools/merge-tools.js +1 -3
  320. package/dist/tools/navigate-arrival.d.ts +45 -1
  321. package/dist/tools/navigate-arrival.js +39 -6
  322. package/dist/tools/navigate-result.d.ts +8 -1
  323. package/dist/tools/navigate-result.js +14 -4
  324. package/dist/tools/numeric-bounds.d.ts +0 -11
  325. package/dist/tools/numeric-bounds.js +4 -4
  326. package/dist/tools/observe-tools.js +22 -26
  327. package/dist/tools/observed-nothing.d.ts +9 -0
  328. package/dist/tools/observed-nothing.js +2 -1
  329. package/dist/tools/read-tools.js +12 -17
  330. package/dist/tools/real-input-attempt.d.ts +14 -6
  331. package/dist/tools/real-input-attempt.js +75 -18
  332. package/dist/tools/reconcile-tools.js +2 -1
  333. package/dist/tools/reload-result.d.ts +6 -1
  334. package/dist/tools/reload-result.js +7 -2
  335. package/dist/tools/resolve-target.d.ts +7 -1
  336. package/dist/tools/resolve-target.js +72 -9
  337. package/dist/tools/settle-in-flight.d.ts +1 -1
  338. package/dist/tools/settle-in-flight.js +9 -2
  339. package/dist/tools/snapshot-delta.d.ts +24 -1
  340. package/dist/tools/snapshot-delta.js +66 -7
  341. package/dist/tools/tool-names.d.ts +1 -0
  342. package/dist/tools/tool-names.js +1 -0
  343. package/dist/tools/tool-surface.js +20 -2
  344. package/dist/tools/tools.js +120 -11
  345. package/dist/tools/verify-next-baton.d.ts +2 -1
  346. package/dist/update/sdk-sync.d.ts +1 -1
  347. package/dist/update/sdk-sync.js +1 -1
  348. package/dist/update/updater.js +1 -1
  349. package/dist/version/sdk-fix.d.ts +2 -1
  350. package/dist/version/version-skew.d.ts +0 -8
  351. package/dist/version/version-skew.js +1 -1
  352. package/docs/README.md +8 -6
  353. package/docs/actions.mdx +1 -1
  354. package/docs/agent-cheatsheet.md +4 -2
  355. package/docs/architecture.md +1 -1
  356. package/docs/cli/mcp.mdx +3 -1
  357. package/docs/cli/watch.mdx +1 -1
  358. package/docs/cli.mdx +1 -1
  359. package/docs/desktop.mdx +1 -1
  360. package/docs/docs.json +8 -3
  361. package/docs/faq.mdx +9 -0
  362. package/docs/first-drive.md +136 -0
  363. package/docs/frameworks.mdx +29 -7
  364. package/docs/gate-plan.md +2 -2
  365. package/docs/gates.md +19 -11
  366. package/docs/getting-started.md +7 -1
  367. package/docs/http-transport.md +164 -0
  368. package/docs/index.mdx +5 -3
  369. package/docs/install-manual.mdx +1 -1
  370. package/docs/packages/tauri.mdx +1 -1
  371. package/docs/packages.mdx +15 -3
  372. package/docs/platform-integration.md +1 -1
  373. package/docs/predicates.mdx +2 -0
  374. package/docs/system-map.md +2 -2
  375. package/docs/telemetry-contract.md +4 -4
  376. package/docs/telemetry-events.mdx +1 -1
  377. package/docs/telemetry.md +3 -3
  378. package/docs/tools-navigate.mdx +14 -6
  379. package/docs/usage.md +4 -4
  380. package/package.json +7 -4
  381. package/dist/init/agent-rules.d.ts +0 -116
  382. package/dist/init/agent-rules.js +0 -279
  383. package/dist/init/app-choice.d.ts +0 -20
  384. package/dist/init/app-choice.js +0 -25
  385. package/dist/init/astro-host.d.ts +0 -21
  386. package/dist/init/astro-host.js +0 -27
  387. package/dist/init/astro-patch.d.ts +0 -33
  388. package/dist/init/astro-patch.js +0 -300
  389. package/dist/init/capabilities.d.ts +0 -40
  390. package/dist/init/capabilities.js +0 -150
  391. package/dist/init/closing-hint.d.ts +0 -36
  392. package/dist/init/closing-hint.js +0 -120
  393. package/dist/init/connect-steps.d.ts +0 -2
  394. package/dist/init/connect-steps.js +0 -50
  395. package/dist/init/cra.d.ts +0 -79
  396. package/dist/init/cra.js +0 -178
  397. package/dist/init/csp-check.d.ts +0 -38
  398. package/dist/init/csp-check.js +0 -107
  399. package/dist/init/csp-doctor.d.ts +0 -32
  400. package/dist/init/csp-doctor.js +0 -54
  401. package/dist/init/desktop-doctor.d.ts +0 -41
  402. package/dist/init/desktop-doctor.js +0 -198
  403. package/dist/init/detect.d.ts +0 -96
  404. package/dist/init/detect.js +0 -209
  405. package/dist/init/dev-script.d.ts +0 -50
  406. package/dist/init/dev-script.js +0 -66
  407. package/dist/init/format-generated.d.ts +0 -18
  408. package/dist/init/format-generated.js +0 -39
  409. package/dist/init/install-hint.d.ts +0 -9
  410. package/dist/init/install-hint.js +0 -29
  411. package/dist/init/mcp-clients.d.ts +0 -89
  412. package/dist/init/mcp-clients.js +0 -299
  413. package/dist/init/mcp-registered.d.ts +0 -12
  414. package/dist/init/mcp-registered.js +0 -14
  415. package/dist/init/mcp.d.ts +0 -89
  416. package/dist/init/mcp.js +0 -129
  417. package/dist/init/next-patch.d.ts +0 -39
  418. package/dist/init/next-patch.js +0 -196
  419. package/dist/init/node-io.d.ts +0 -7
  420. package/dist/init/node-io.js +0 -117
  421. package/dist/init/non-js-project.d.ts +0 -27
  422. package/dist/init/non-js-project.js +0 -84
  423. package/dist/init/patch-kind.d.ts +0 -20
  424. package/dist/init/patch-kind.js +0 -10
  425. package/dist/init/plan-framework.d.ts +0 -64
  426. package/dist/init/plan-framework.js +0 -565
  427. package/dist/init/plan.d.ts +0 -242
  428. package/dist/init/plan.js +0 -703
  429. package/dist/init/preflight.d.ts +0 -28
  430. package/dist/init/preflight.js +0 -38
  431. package/dist/init/project-id.d.ts +0 -12
  432. package/dist/init/project-id.js +0 -25
  433. package/dist/init/refresh-rules.d.ts +0 -39
  434. package/dist/init/refresh-rules.js +0 -51
  435. package/dist/init/run.d.ts +0 -117
  436. package/dist/init/run.js +0 -806
  437. package/dist/init/slash-command.d.ts +0 -27
  438. package/dist/init/slash-command.js +0 -132
  439. package/dist/init/snippets.d.ts +0 -134
  440. package/dist/init/snippets.js +0 -572
  441. package/dist/init/vite-config.d.ts +0 -18
  442. package/dist/init/vite-config.js +0 -114
  443. package/dist/init/windows-quote.d.ts +0 -38
  444. package/dist/init/windows-quote.js +0 -70
  445. package/dist/init/workspace-apps.d.ts +0 -30
  446. package/dist/init/workspace-apps.js +0 -178
  447. package/dist/project/remember-project.d.ts +0 -24
  448. package/dist/project/remember-project.js +0 -40
  449. /package/dist/{init → setup}/confirm.d.ts +0 -0
@@ -0,0 +1,14 @@
1
+ import { type FlowAnchor } from '@reticlehq/core';
2
+ /**
3
+ * What the anchor CALLS the field it points at.
4
+ *
5
+ * Every anchor kind names its target differently and all of them can name a password: a testid
6
+ * (auth-password), an accessible name (role=textbox, name="Password"), a signal. Checking only the
7
+ * testid variant would redact the app that uses test ids and quietly leak the one that does not —
8
+ * and an app without test ids is exactly the app whose flows were recorded by role.
9
+ *
10
+ * Save and replay MUST share this. Redaction keys off the field name; substitution looks up
11
+ * `RETICLE_SECRET_<FIELD>` from the same string. Two copies would redact `Password` and then fail
12
+ * to find the env var because the runner passed a role or a label instead.
13
+ */
14
+ export declare function anchorFieldName(anchor: FlowAnchor): string | undefined;
@@ -0,0 +1,22 @@
1
+ import { AnchorKind } from '@reticlehq/core';
2
+ /**
3
+ * What the anchor CALLS the field it points at.
4
+ *
5
+ * Every anchor kind names its target differently and all of them can name a password: a testid
6
+ * (auth-password), an accessible name (role=textbox, name="Password"), a signal. Checking only the
7
+ * testid variant would redact the app that uses test ids and quietly leak the one that does not —
8
+ * and an app without test ids is exactly the app whose flows were recorded by role.
9
+ *
10
+ * Save and replay MUST share this. Redaction keys off the field name; substitution looks up
11
+ * `RETICLE_SECRET_<FIELD>` from the same string. Two copies would redact `Password` and then fail
12
+ * to find the env var because the runner passed a role or a label instead.
13
+ */
14
+ export function anchorFieldName(anchor) {
15
+ if (AnchorKind.TESTID === anchor.kind)
16
+ return anchor.value;
17
+ if (AnchorKind.ROLE === anchor.kind)
18
+ return anchor.name;
19
+ if (AnchorKind.SIGNAL === anchor.kind)
20
+ return anchor.name;
21
+ return undefined;
22
+ }
@@ -11,6 +11,7 @@ import type { FlowReplaySession, Sleep } from './flow-replay.js';
11
11
  export declare function runRoleStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
12
12
  kind: typeof AnchorKind.ROLE;
13
13
  }>, confirmDangerous: boolean, sleep: Sleep): Promise<FlowStepResult>;
14
+ export declare function replayDestructiveActionHint(rawError: string): string;
14
15
  /** Run one component-anchored step: re-resolve via QUERY by:'component', ACT on the live ref, else drift. */
15
16
  export declare function runComponentStep(session: FlowReplaySession, step: FlowStep, index: number, anchor: Extract<FlowAnchor, {
16
17
  kind: typeof AnchorKind.COMPONENT;
@@ -9,6 +9,9 @@
9
9
  import { AnchorKind, DEGRADED_ANCHOR_ROLE, DriftReason, QueryBy, ReticleCommand, } from '@reticlehq/core';
10
10
  import { ReticleTool } from '../tools/tool-names.js';
11
11
  import { replayActionArgs } from './replay.js';
12
+ import { isStaleRefError } from '../tools/act-sequence-retry.js';
13
+ import { waitForReaction } from '../tools/react-grace.js';
14
+ import { anchorFieldName } from './flows.js';
12
15
  import { anchorLabel, componentLabel, componentQueryArgs, resolveQuery, testidDrift, } from './flow-replay.js';
13
16
  import { nearestRoleName } from './role-anchor-nearest.js';
14
17
  import { roleDriftReason } from './role-drift-reason.js';
@@ -81,30 +84,98 @@ export async function runRoleStep(session, step, index, anchor, confirmDangerous
81
84
  },
82
85
  };
83
86
  }
84
- return await actOnResolvedRef(session, step, index, label, ref, confirmDangerous);
87
+ return await actOnResolvedRef(session, step, index, label, ref, confirmDangerous, anchorFieldName(anchor),
88
+ // The retry's whole point: run the SAME locator against the DOM as it is now.
89
+ async () => (await resolveQuery(session, roleQueryArgs(anchor), sleep)).refs[0]);
90
+ }
91
+ /**
92
+ * `assertActionAllowed`'s refusal — "potentially destructive action blocked; retry with
93
+ * args.confirmDangerous=true" — is written for a LIVE call, where retrying with that arg works. It
94
+ * does not during replay: `replayActionArgs` deliberately strips any `confirmDangerous` a step's own
95
+ * args carry and restores it only from the ONE call-level boolean this run was given, because a
96
+ * destructive confirmation has to be a decision made for THIS run, never a value saved into the
97
+ * recording (#94). A caller who hand-edits the flow file's step args to add it gets it silently
98
+ * deleted, replays again, and reads the identical refusal — which reads as though nothing they tried
99
+ * worked, when what happened is they tried the one thing that cannot work here.
100
+ *
101
+ * Appends the path that does, rather than replacing the message: an agent that has already learned
102
+ * to recognise the raw refusal still finds it, plus the fix for this specific caller.
103
+ */
104
+ const DESTRUCTIVE_ACTION_PATTERN = /potentially destructive (?:\w+ )*(?:action|tool) blocked/i;
105
+ export function replayDestructiveActionHint(rawError) {
106
+ if (!DESTRUCTIVE_ACTION_PATTERN.test(rawError))
107
+ return rawError;
108
+ return (`${rawError} — this is a flow REPLAY: a step's own args.confirmDangerous is stripped before ` +
109
+ 'dispatch and never persists, so editing the flow file does nothing. Pass it at the call level ' +
110
+ `instead: ${ReticleTool.FLOW_REPLAY} { confirmDangerous: true } acknowledges every flagged step ` +
111
+ // `reticle_verify{action:"flows"}` (the batch suite runner) has no such argument today — naming
112
+ // it here would repeat the exact defect this message exists to fix, so the honest statement is
113
+ // that a flow with a flagged step currently has to be replayed on its own to pass it.
114
+ 'for this run. The batch reticle_verify{action:"flows"} has no equivalent option yet, so a flow ' +
115
+ 'with a flagged step has to be replayed on its own to acknowledge it.');
85
116
  }
86
117
  /**
87
118
  * Dispatch one already-resolved step. Shared so every anchor kind runs the action the same way —
88
119
  * including the action window, whose open/close must not depend on which anchor found the element.
89
120
  */
90
- async function actOnResolvedRef(session, step, index, label, ref, confirmDangerous) {
91
- session.beginAction?.(ReticleTool.FLOW_REPLAY, { ref, action: step.action ?? '' });
92
- let act;
93
- try {
94
- act = await session.command(ReticleCommand.ACT, {
95
- ref,
96
- action: step.action ?? '',
97
- args: replayActionArgs(step.args, confirmDangerous),
121
+ async function actOnResolvedRef(session, step, index, label, ref, confirmDangerous,
122
+ /**
123
+ * The field name a redacted fill is supplied under, from `anchorFieldName` — the SAME function
124
+ * redaction uses to decide what to hide. Without it a role-anchored secret is redacted at save
125
+ * and looked up at replay under no name at all, so the flow types the placeholder into the form.
126
+ */
127
+ field,
128
+ /**
129
+ * Resolve the anchor again. Supplied by the caller because only it knows how this step's anchor is
130
+ * found, and the retry below is worthless without it: re-dispatching the SAME dead ref fails
131
+ * identically. What changed between the two attempts is the DOM, so the locator has to be run
132
+ * against it again.
133
+ */
134
+ reresolve) {
135
+ const dispatch = async (at) => {
136
+ session.beginAction?.(ReticleTool.FLOW_REPLAY, { ref: at, action: step.action ?? '' });
137
+ try {
138
+ const r = await session.command(ReticleCommand.ACT, {
139
+ ref: at,
140
+ action: step.action ?? '',
141
+ args: replayActionArgs(step.args, confirmDangerous, field),
142
+ });
143
+ return { ok: r.ok, error: r.error };
144
+ }
145
+ finally {
146
+ session.finishAction?.();
147
+ }
148
+ };
149
+ let act = await dispatch(ref);
150
+ /*
151
+ * One retry, and only for staleness.
152
+ *
153
+ * A saved flow of `fill` then `click` failed with "ref 'eNNNN' no longer resolves to an element",
154
+ * a different ref each attempt: the fill re-rendered the page between the click step resolving its
155
+ * element and dispatching at it. `flow_heal` answered `unhealable` and was right — the locator was
156
+ * correct, only the timing was wrong — and the flow format has no way to express a wait, so there
157
+ * was nothing the user could do.
158
+ *
159
+ * `act_sequence` already solved this race for the same reason; the predicate and the grace period
160
+ * come from there rather than being written twice. A step that failed for any OTHER reason is
161
+ * never retried: a replay that quietly repeats actions turns one click into two, which is worse
162
+ * than the failure it papers over.
163
+ */
164
+ if (!act.ok && isStaleRefError(act.error) && reresolve !== undefined) {
165
+ await waitForReaction(session, 0, STALE_REF_GRACE_MS, {
166
+ sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
98
167
  });
99
- }
100
- finally {
101
- session.finishAction?.();
168
+ const fresh = await reresolve();
169
+ if (fresh !== undefined)
170
+ act = await dispatch(fresh);
102
171
  }
103
172
  const result = { step: index, tool: step.tool, anchor: label, ok: act.ok };
104
173
  if (!act.ok)
105
- result.error = act.error ?? 'command failed';
174
+ result.error = replayDestructiveActionHint(act.error ?? 'command failed');
106
175
  return result;
107
176
  }
177
+ /** How long to let the app finish re-rendering before the one retry. Matches the sequence path. */
178
+ const STALE_REF_GRACE_MS = 400;
108
179
  /** Run one component-anchored step: re-resolve via QUERY by:'component', ACT on the live ref, else drift. */
109
180
  export async function runComponentStep(session, step, index, anchor, confirmDangerous, sleep) {
110
181
  const label = componentLabel(anchor);
@@ -124,25 +195,9 @@ export async function runComponentStep(session, step, index, anchor, confirmDang
124
195
  };
125
196
  }
126
197
  const ref = refs[0] ?? '';
127
- // Attribute the step's effects to the step. Without this window they arrive with no actionId and are
128
- // learned as ambient churn on the very regions the flow exercises.
129
- session.beginAction?.(ReticleTool.FLOW_REPLAY, { ref, action: step.action ?? '' });
130
- let act;
131
- try {
132
- act = await session.command(ReticleCommand.ACT, {
133
- ref,
134
- action: step.action ?? '',
135
- args: replayActionArgs(step.args, confirmDangerous),
136
- });
137
- }
138
- finally {
139
- // Close on every exit so a throwing step cannot leak the window onto the next step's events.
140
- session.finishAction?.();
141
- }
142
- const result = { step: index, tool: step.tool, anchor: label, ok: act.ok };
143
- if (!act.ok)
144
- result.error = act.error ?? 'command failed';
145
- return result;
198
+ // Shared with the role path so both get the same action window AND the same stale-ref retry —
199
+ // this used to dispatch inline, which is how one anchor kind could gain a fix the other lacked.
200
+ return await actOnResolvedRef(session, step, index, label, ref, confirmDangerous, anchorFieldName(anchor), async () => (await resolveQuery(session, componentQueryArgs(anchor), sleep)).refs[0]);
146
201
  }
147
202
  /**
148
203
  * DEGRADED_ANCHOR_ROLE is a MARKER ("no anchor could be determined"), never a locator.
@@ -235,7 +290,9 @@ export async function runSequenceStep(session, step, index, subs, confirmDangero
235
290
  live.push({
236
291
  ref,
237
292
  action: sub.action ?? '',
238
- args: replayActionArgs(sub.args, confirmDangerous),
293
+ // Each sub-step carries its OWN anchor, so each gets its own field name. A sequence that ends
294
+ // in a login is the shape this was reported on, and the sub-step is where the fill lives.
295
+ args: replayActionArgs(sub.args, confirmDangerous, anchorFieldName(sub.anchor)),
239
296
  });
240
297
  }
241
298
  session.beginAction?.(ReticleTool.FLOW_REPLAY, { steps: live.length });
@@ -253,6 +310,6 @@ export async function runSequenceStep(session, step, index, subs, confirmDangero
253
310
  ok: act.ok,
254
311
  };
255
312
  if (!act.ok)
256
- result.error = act.error ?? 'command failed';
313
+ result.error = replayDestructiveActionHint(act.error ?? 'command failed');
257
314
  return result;
258
315
  }
@@ -32,6 +32,8 @@ export function successLabel(success) {
32
32
  }
33
33
  if (success.state !== undefined)
34
34
  return `state:${success.state.path}`;
35
+ if (success.text !== undefined)
36
+ return `text:${success.text.contains}`;
35
37
  return success.element?.testid ?? success.element?.name ?? success.element?.role ?? 'success';
36
38
  }
37
39
  /** Compile a success FlowExpect into a predicate. undefined → nothing assertable (vacuously met). */
@@ -122,6 +124,26 @@ export function successToPredicate(success, dynamic) {
122
124
  parts.push({ kind: PredicateKind.ELEMENT, query });
123
125
  }
124
126
  }
127
+ const text = success.text;
128
+ if (text !== undefined) {
129
+ const part = {
130
+ kind: PredicateKind.TEXT,
131
+ contains: text.contains,
132
+ };
133
+ if (text.scope !== undefined)
134
+ part.scope = text.scope;
135
+ if (text.visible !== undefined)
136
+ part.visible = text.visible;
137
+ if (true === text.absent) {
138
+ part.absent = true;
139
+ // Same post-settle reasoning as `console.absent` and `state.hold`: a wait-until-true waiter
140
+ // reads "not there yet" on the first poll and passes BEFORE the text it is meant to see
141
+ // disappear has even been rendered. Gate on `settled` so the read happens after the page
142
+ // quiets, by which point a text that was going to appear has.
143
+ parts.push({ kind: PredicateKind.SETTLED });
144
+ }
145
+ parts.push(part);
146
+ }
125
147
  const [first] = parts;
126
148
  if (0 === parts.length)
127
149
  return undefined;
@@ -14,6 +14,7 @@ import { homedir } from 'node:os';
14
14
  import { resolveProjectCloud } from '../cloud/cloud-config.js';
15
15
  import { buildSuiteVerdict } from './decision.js';
16
16
  import { classifyFlowAssertions } from './flow-classify.js';
17
+ import { recordingBacktrackWarning } from './recording-backtrack.js';
17
18
  import { isValidFlowName, flowPath } from '../project/reticle-dir.js';
18
19
  import {} from './flows.js';
19
20
  import { flowsForSession } from './flow-store-for-session.js';
@@ -84,7 +85,7 @@ const INTENT_GAP_FIELD = z
84
85
  export const FLOW_TOOLS = [
85
86
  {
86
87
  name: ReticleTool.FLOW_SAVE,
87
- description: 'Persist the last/active recording (by name) as a git-checked, anchor-resolved flow at .reticle/flows/<name>.json. Each step is bound to a SEMANTIC anchor (testid/role/signal), never a volatile ref; steps without a resolvable testid are kept with degraded:true (a "add a data-testid here" marker) rather than dropped. Returns { name, stepCount, degraded, empty, assertions } — `assertions.grade` is asserted | presence-only | assertion-free: a flow that only acts (or only checks element presence) will pass even if the feature breaks, so when grade is not "asserted" follow assertions.warning and add a consequence assertion via reticle_annotate (assert-signal / assert-net / success-state).',
88
+ description: 'Persist the last/active recording (by name) as a git-checked, anchor-resolved flow at .reticle/flows/<name>.json. Each step is bound to a SEMANTIC anchor (testid/role/signal), never a volatile ref; steps without a resolvable testid are kept with degraded:true (a "add a data-testid here" marker) rather than dropped. Returns { name, stepCount, degraded, empty, assertions, warning? } — `assertions.grade` is asserted | presence-only | assertion-free: a flow that only acts (or only checks element presence) will pass even if the feature breaks, so when grade is not "asserted" follow assertions.warning and add a consequence assertion via reticle_annotate (assert-signal / assert-net / success-state). `warning` is present when the recording left a page and returned to it: replay has no navigation steps, so the next click will look for a control on a page the tab is no longer on.',
88
89
  inputSchema: {
89
90
  flow: z
90
91
  .string()
@@ -128,6 +129,10 @@ export const FLOW_TOOLS = [
128
129
  })
129
130
  .optional(),
130
131
  intentGap: INTENT_GAP_FIELD,
132
+ warning: z
133
+ .string()
134
+ .optional()
135
+ .describe('Present when the recording left a page and returned to it. Replay has no navigation steps, so the next click will look for a control on a page the tab is no longer on.'),
131
136
  error: z.string().optional(),
132
137
  code: z.string().optional(),
133
138
  },
@@ -189,7 +194,7 @@ export const FLOW_TOOLS = [
189
194
  const toSave = saveAs === undefined ? program : { ...program, name: saveAs };
190
195
  return flows.save(toSave, annotations, projectId).then(async (res) => {
191
196
  if (!res.ok)
192
- return { error: flowErrorMessage(res.code), code: res.code };
197
+ return { error: flowErrorMessage(res.code, res.detail), code: res.code };
193
198
  deps.annotations.clear(name);
194
199
  // Grade the saved flow's assertions so the agent learns immediately if it just saved a flow
195
200
  // that asserts nothing observable (passes even when the feature is broken).
@@ -199,13 +204,19 @@ export const FLOW_TOOLS = [
199
204
  // back from a save that succeeded, so the guard is a type narrowing rather than a doubt.
200
205
  const saved = res.value.name;
201
206
  const path = isValidFlowName(saved) ? flowPath(root, saved, projectId) : undefined;
207
+ const backtrack = recordingBacktrackWarning(program.routes ?? []);
202
208
  return loaded.ok
203
209
  ? {
204
210
  ...res.value,
205
211
  ...(path === undefined ? {} : { path }),
206
212
  assertions: classifyFlowAssertions(loaded.value),
213
+ ...(backtrack === undefined ? {} : { warning: backtrack }),
207
214
  }
208
- : { ...res.value, ...(path === undefined ? {} : { path }) };
215
+ : {
216
+ ...res.value,
217
+ ...(path === undefined ? {} : { path }),
218
+ ...(backtrack === undefined ? {} : { warning: backtrack }),
219
+ };
209
220
  });
210
221
  },
211
222
  },
@@ -246,7 +257,7 @@ export const FLOW_TOOLS = [
246
257
  },
247
258
  {
248
259
  name: ReticleTool.FLOW_LOAD,
249
- description: 'Read + validate a saved flow by flowName from .reticle/flows/<flowName>.json. Returns the FlowFile (version, flowName, createdAt, anchored steps) or a structured { error, code }.',
260
+ description: 'Read + validate a saved flow by flowName from .reticle/flows/<flowName>.json. Returns the FlowFile (version, flowName, createdAt, anchored steps) or a structured { error, code }. Step `expect` (and flow `success`) accepts the same allOf grammar as reticle_act_and_wait; a parse failure names the step and key rather than calling the file malformed.',
250
261
  inputSchema: {
251
262
  flow: z
252
263
  .string()
@@ -275,7 +286,7 @@ export const FLOW_TOOLS = [
275
286
  .load(asString(aliasParam(args, 'flowName', ['flow'])['flowName']) ?? '', projectId)
276
287
  .then((res) => {
277
288
  if (!res.ok)
278
- return { error: flowErrorMessage(res.code), code: res.code };
289
+ return { error: flowErrorMessage(res.code, res.detail), code: res.code };
279
290
  const { name, ...rest } = res.value;
280
291
  return { flowName: name, ...rest };
281
292
  });
@@ -310,7 +321,7 @@ export const FLOW_TOOLS = [
310
321
  .remove(asString(aliasParam(args, 'flowName', ['flow'])['flowName']) ?? '', projectId)
311
322
  .then((res) => {
312
323
  if (!res.ok)
313
- return { error: flowErrorMessage(res.code), code: res.code };
324
+ return { error: flowErrorMessage(res.code, res.detail), code: res.code };
314
325
  return { deleted: true };
315
326
  });
316
327
  },
@@ -324,7 +335,10 @@ export const FLOW_TOOLS = [
324
335
  'Returns { name, status: ok|drift|error, steps:[...] }; missing/malformed files and action ' +
325
336
  'failures are status:error with a structured code (distinct from contract-changed drift). ' +
326
337
  'An ok replay of a flow that asserts no consequence also carries unverifiable:{reason} — it ' +
327
- 'would read ok even if the feature were broken, so do NOT treat that ok as proof.',
338
+ 'would read ok even if the feature were broken, so do NOT treat that ok as proof. ' +
339
+ 'A run that stops early carries halted:{atStep, notAttempted}: `steps` is SHORTER than the ' +
340
+ 'flow because replay stopped, not because a step was skipped — do not read a missing step as ' +
341
+ 'an action that failed to dispatch.',
328
342
  inputSchema: {
329
343
  flow: z
330
344
  .string()
@@ -354,6 +368,11 @@ export const FLOW_TOOLS = [
354
368
  .optional()
355
369
  .describe('Push-default deviation report over the replay segments: ranked deviations vs the learned envelope + a nominal count, or a fall-back note below 3 runs.'),
356
370
  error: z.object({ code: z.string(), message: z.string() }).optional(),
371
+ // Same rule as `unverifiable` below, and the same failure if it is left out.
372
+ halted: z
373
+ .object({ atStep: z.number(), notAttempted: z.number() })
374
+ .optional()
375
+ .describe('Set when the replay STOPPED at a failing step: `steps` is shorter than the flow, and this says where it stopped and how many steps were never attempted. Absent means every step ran.'),
357
376
  // Declared here or a validating profile strips it -- the same way `name` was stripped once.
358
377
  // A field the handler sets and the schema omits arrives as nothing, silently.
359
378
  unverifiable: z
@@ -566,7 +585,7 @@ export const FLOW_TOOLS = [
566
585
  // it — and the pair that disagreed were the two ways to save the same thing.
567
586
  const res = await flowsForSession(deps, session.projectId).flows.saveFlow(flow, session.projectId);
568
587
  if (!res.ok)
569
- return { error: flowErrorMessage(res.code), code: res.code };
588
+ return { error: flowErrorMessage(res.code, res.detail), code: res.code };
570
589
  // If logged in to Reticle, mirror the saved flow to the team's regression suite. Best-effort
571
590
  // and non-blocking: the flow is already on disk, so a sync failure never fails the save.
572
591
  void syncSavedFlowToCloud(deps, flow, session.projectId);
@@ -0,0 +1,23 @@
1
+ import { type FlowFile } from '@reticlehq/core';
2
+ import type { ToolDeps } from '../tools/tools.js';
3
+ /**
4
+ * Turn a recorded upload's `path` into the bytes the browser can actually take, once, before replay
5
+ * runs a single step.
6
+ *
7
+ * The recorder and the replayer disagreed about what an upload step IS, and the disagreement was
8
+ * total: `reticle_record` writes `{"action":"upload","args":{"path":"test-fixtures/pipe.step"}}` —
9
+ * the only form the live `reticle_act` accepts, and the form that works interactively — while replay
10
+ * dispatched it straight at the browser, which refused with "upload does not read path, so it would
11
+ * be dropped". A recorded upload could therefore never replay as recorded. The capability was there
12
+ * the whole time: an agent who hand-patched the flow JSON to `{name, content, type}` got a real
13
+ * upload, a real `POST /api/v0/files -> 200`, and the app went on to perceive the file.
14
+ *
15
+ * Resolved HERE rather than at each of the four dispatch sites inside replay, and rather than at
16
+ * save time: the flow file stays small and legible, the fixture stays on disk where it can be
17
+ * reviewed and updated, and a flow that names a file nobody checked in fails saying so instead of
18
+ * carrying a stale copy of it.
19
+ *
20
+ * `rewriteUploadArgs` is the same function the live path uses, so the two cannot drift again — which
21
+ * is the whole reason this defect existed.
22
+ */
23
+ export declare function resolveFlowUploads(deps: Pick<ToolDeps, 'fs' | 'reticleRoot'>, flow: FlowFile): Promise<FlowFile>;
@@ -0,0 +1,46 @@
1
+ import { ActionType } from '@reticlehq/core';
2
+ import { rewriteUploadArgs } from '../tools/real-input-attempt.js';
3
+ /**
4
+ * Turn a recorded upload's `path` into the bytes the browser can actually take, once, before replay
5
+ * runs a single step.
6
+ *
7
+ * The recorder and the replayer disagreed about what an upload step IS, and the disagreement was
8
+ * total: `reticle_record` writes `{"action":"upload","args":{"path":"test-fixtures/pipe.step"}}` —
9
+ * the only form the live `reticle_act` accepts, and the form that works interactively — while replay
10
+ * dispatched it straight at the browser, which refused with "upload does not read path, so it would
11
+ * be dropped". A recorded upload could therefore never replay as recorded. The capability was there
12
+ * the whole time: an agent who hand-patched the flow JSON to `{name, content, type}` got a real
13
+ * upload, a real `POST /api/v0/files -> 200`, and the app went on to perceive the file.
14
+ *
15
+ * Resolved HERE rather than at each of the four dispatch sites inside replay, and rather than at
16
+ * save time: the flow file stays small and legible, the fixture stays on disk where it can be
17
+ * reviewed and updated, and a flow that names a file nobody checked in fails saying so instead of
18
+ * carrying a stale copy of it.
19
+ *
20
+ * `rewriteUploadArgs` is the same function the live path uses, so the two cannot drift again — which
21
+ * is the whole reason this defect existed.
22
+ */
23
+ export async function resolveFlowUploads(deps, flow) {
24
+ const resolveStep = async (step) => {
25
+ const nested = step.steps;
26
+ const steps = nested === undefined ? undefined : await Promise.all(nested.map(resolveStep));
27
+ const isUpload = step.action === ActionType.UPLOAD && step.args?.['path'] !== undefined;
28
+ const args = isUpload
29
+ ? await rewriteUploadArgs(deps, ActionType.UPLOAD, step.args ?? {})
30
+ : step.args;
31
+ return {
32
+ ...step,
33
+ ...(args === undefined ? {} : { args }),
34
+ ...(steps === undefined ? {} : { steps }),
35
+ };
36
+ };
37
+ // Untouched flows are returned as-is so the common journey allocates nothing and the on-disk file
38
+ // and the replayed one stay the same object to compare.
39
+ if (!hasUpload(flow.steps))
40
+ return flow;
41
+ return { ...flow, steps: await Promise.all(flow.steps.map(resolveStep)) };
42
+ }
43
+ function hasUpload(steps) {
44
+ return steps.some((s) => (s.action === ActionType.UPLOAD && s.args?.['path'] !== undefined) ||
45
+ (s.steps !== undefined && hasUpload(s.steps)));
46
+ }
@@ -13,6 +13,7 @@ export type FlowResult<T> = {
13
13
  } | {
14
14
  ok: false;
15
15
  code: FlowErrorCode;
16
+ detail?: string;
16
17
  };
17
18
  /** Pick the on-disk anchor for a normalized step: testid > component(auto) > degraded role. */
18
19
  export declare function anchorForStep(args: Record<string, unknown>): {
@@ -27,6 +28,20 @@ export declare function anchorForStep(args: Record<string, unknown>): {
27
28
  * reader what to do: the value belongs in the environment, not in a file they are about to commit.
28
29
  */
29
30
  export declare const REDACTED_FILL = "<redacted: supply at replay>";
31
+ /**
32
+ * What the anchor CALLS the field it points at.
33
+ *
34
+ * Every anchor kind names its target differently and all of them can name a password: a testid
35
+ * (auth-password), an accessible name (role=textbox, name="Password"), a signal. Checking only the
36
+ * testid variant would redact the app that uses test ids and quietly leak the one that does not —
37
+ * and an app without test ids is exactly the app whose flows were recorded by role.
38
+ *
39
+ * Exported because REPLAY has to agree with redaction about what the field is called. It did not:
40
+ * the replay paths took a `field` argument that only the testid caller passed, so a role-anchored
41
+ * secret was redacted at save under one name and looked up at replay under none — the flow typed
42
+ * the placeholder into the form. One function decides it for both sides, or the two drift again.
43
+ */
44
+ export declare function anchorFieldName(anchor: FlowAnchor): string | undefined;
30
45
  /**
31
46
  * Pure: map one normalized RecordedStep → FlowStep with a semantic anchor (+ degraded marker).
32
47
  * A ref-only (stable:false) step is recorded with a best-effort anchor and degraded:true —
@@ -6,6 +6,7 @@ import { applyHealChanges } from './heal.js';
6
6
  import { flowIntentGap, linkFlowIntent } from './flow-intent.js';
7
7
  import { IntentStore } from '../intent/intent-store.js';
8
8
  import { flowDir, flowPath, reticleDirPaths, isValidFlowName } from '../project/reticle-dir.js';
9
+ import { describeFlowZodFailure, parseFlowFileText } from './flow-expect-grammar.js';
9
10
  /**
10
11
  * A projectId only scopes storage when it's a safe single path segment (it's stamped from the
11
12
  * session's HELLO, but the store defends the disk boundary itself). An unsafe/absent value collapses
@@ -540,8 +541,13 @@ function redactSecretFill(anchor, args) {
540
541
  * (auth-password), an accessible name (role=textbox, name="Password"), a signal. Checking only the
541
542
  * testid variant would redact the app that uses test ids and quietly leak the one that does not —
542
543
  * and an app without test ids is exactly the app whose flows were recorded by role.
544
+ *
545
+ * Exported because REPLAY has to agree with redaction about what the field is called. It did not:
546
+ * the replay paths took a `field` argument that only the testid caller passed, so a role-anchored
547
+ * secret was redacted at save under one name and looked up at replay under none — the flow typed
548
+ * the placeholder into the form. One function decides it for both sides, or the two drift again.
543
549
  */
544
- function anchorFieldName(anchor) {
550
+ export function anchorFieldName(anchor) {
545
551
  if (AnchorKind.TESTID === anchor.kind)
546
552
  return anchor.value;
547
553
  if (AnchorKind.ROLE === anchor.kind)
@@ -700,8 +706,15 @@ export class FlowStore {
700
706
  // per-project subdir. Both come from the same `pid`, so on-disk location and content always agree.
701
707
  const stamped = pid === undefined ? flow : { ...flow, projectId: pid };
702
708
  const parsed = FlowFileSchema.safeParse(stamped);
703
- if (!parsed.success)
704
- return { ok: false, code: FlowErrorCode.PARSE_FAILED };
709
+ if (!parsed.success) {
710
+ // Named, not bare: the load path already says which step and key it choked on, and a save
711
+ // that refuses in silence sends the caller to the file to guess at what a load would tell it.
712
+ return {
713
+ ok: false,
714
+ code: FlowErrorCode.PARSE_FAILED,
715
+ detail: describeFlowZodFailure(parsed.error),
716
+ };
717
+ }
705
718
  const valid = await this.#linkIntent(parsed.data);
706
719
  await this.#fs.mkdir(flowDir(this.#root, pid));
707
720
  await this.#fs.writeFile(flowPath(this.#root, asFlowName(valid.name), pid), this.#serialize(valid));
@@ -837,17 +850,7 @@ export class FlowStore {
837
850
  code: this.#fs.isNotFound(error) ? FlowErrorCode.NOT_FOUND : FlowErrorCode.PARSE_FAILED,
838
851
  };
839
852
  }
840
- let parsed;
841
- try {
842
- parsed = JSON.parse(text);
843
- }
844
- catch {
845
- return { ok: false, code: FlowErrorCode.PARSE_FAILED };
846
- }
847
- const result = FlowFileSchema.safeParse(parsed);
848
- if (!result.success)
849
- return { ok: false, code: FlowErrorCode.PARSE_FAILED };
850
- return { ok: true, value: result.data };
853
+ return parseFlowFileText(text);
851
854
  }
852
855
  /**
853
856
  * Delete a flow's file so a renamed/obsolete flow stops lingering in the replay list. Resolves the
@@ -1,8 +1,27 @@
1
1
  /** Parse `git diff --name-only` output into a clean file list. Pure; exported for testing. */
2
2
  export declare function parseGitFiles(output: string): string[];
3
3
  /**
4
- * Files changed since a git ref (`git diff --name-only <ref>`), so `reticle gate --since main` works from
5
- * the real diff instead of hand-listed files. Returns [] on any git failure (not a repo, bad ref) rather
6
- * than throwing — the caller degrades to "no changes" and the gate simply passes, never crashes CI.
4
+ * The outcome of asking git what changed — files, and whether the question could be answered.
5
+ *
6
+ * `files: []` used to mean both "the tree is clean" and "git could not tell us", and those are
7
+ * opposite answers to the only question `reticle_affected` and `reticle_verify_change` exist to ask.
8
+ * A caller that cannot distinguish them reports "nothing to re-verify" over a mistyped ref, a
9
+ * shallow clone with no history, or a daemon outside the repository — and the agent ships.
7
10
  */
8
- export declare function changedFilesSince(ref: string, cwd: string): Promise<string[]>;
11
+ export interface ChangedFiles {
12
+ files: string[];
13
+ /** True when git could not answer. `files` is then empty because nothing was learned. */
14
+ failed: boolean;
15
+ /** Why, when it failed — for the caller to surface, never to swallow. */
16
+ reason?: string;
17
+ }
18
+ /**
19
+ * Files changed since a git ref (`git diff --name-only <ref>`), so `reticle gate --since main` works
20
+ * from the real diff instead of hand-listed files.
21
+ *
22
+ * Never throws, and never pretends. The CLI gate may still degrade to "no changes" and pass rather
23
+ * than crash CI — that reasoning was right and is unchanged — but it now has to opt into doing so by
24
+ * ignoring `failed`, instead of being handed a clean-looking empty list. The MCP tools read the flag
25
+ * and say they could not tell.
26
+ */
27
+ export declare function changedFilesSince(ref: string, cwd: string): Promise<ChangedFiles>;
@@ -9,16 +9,42 @@ export function parseGitFiles(output) {
9
9
  .filter((line) => line.length > 0);
10
10
  }
11
11
  /**
12
- * Files changed since a git ref (`git diff --name-only <ref>`), so `reticle gate --since main` works from
13
- * the real diff instead of hand-listed files. Returns [] on any git failure (not a repo, bad ref) rather
14
- * than throwing — the caller degrades to "no changes" and the gate simply passes, never crashes CI.
12
+ * A ref beginning with `-` is read by git as an OPTION, not a ref.
13
+ *
14
+ * `execFile` runs no shell, so this is argument injection rather than command injection — but
15
+ * `--output=<path>` in the ref position is enough to make git write somewhere it was not asked to.
16
+ * Refused rather than escaped: `--end-of-options` is not supported by every git version we might
17
+ * meet, and a ref that starts with a dash is not a thing a caller legitimately wants.
18
+ */
19
+ function refIsOptionLike(ref) {
20
+ return ref.startsWith('-');
21
+ }
22
+ /**
23
+ * Files changed since a git ref (`git diff --name-only <ref>`), so `reticle gate --since main` works
24
+ * from the real diff instead of hand-listed files.
25
+ *
26
+ * Never throws, and never pretends. The CLI gate may still degrade to "no changes" and pass rather
27
+ * than crash CI — that reasoning was right and is unchanged — but it now has to opt into doing so by
28
+ * ignoring `failed`, instead of being handed a clean-looking empty list. The MCP tools read the flag
29
+ * and say they could not tell.
15
30
  */
16
31
  export async function changedFilesSince(ref, cwd) {
32
+ if (refIsOptionLike(ref)) {
33
+ return {
34
+ files: [],
35
+ failed: true,
36
+ reason: `refusing ref "${ref}": a leading dash makes git read it as an option, not a ref`,
37
+ };
38
+ }
17
39
  try {
18
40
  const { stdout } = await run('git', ['diff', '--name-only', ref], { cwd });
19
- return parseGitFiles(stdout);
41
+ return { files: parseGitFiles(stdout), failed: false };
20
42
  }
21
- catch {
22
- return [];
43
+ catch (error) {
44
+ return {
45
+ files: [],
46
+ failed: true,
47
+ reason: error instanceof Error ? error.message : String(error),
48
+ };
23
49
  }
24
50
  }
@@ -36,8 +36,8 @@ export async function healFlow(deps, args) {
36
36
  applied: false,
37
37
  proposals: [],
38
38
  changed: [],
39
- message: flowErrorMessage(loaded.code),
40
- error: { code: loaded.code, message: flowErrorMessage(loaded.code) },
39
+ message: flowErrorMessage(loaded.code, loaded.detail),
40
+ error: { code: loaded.code, message: flowErrorMessage(loaded.code, loaded.detail) },
41
41
  };
42
42
  }
43
43
  const session = deps.sessions.resolve(asString(args['sessionId']));
@@ -37,7 +37,8 @@ export declare function predicateToExpect(predicate: Predicate): FlowExpect | un
37
37
  * `reticle_verify` then correctly called it `unverifiable` — the record → save → verify path
38
38
  * completing all the way to a run that could never be a pass. Found by driving it.
39
39
  *
40
- * The list below is exactly what `successToPredicate` reads, plus the testid the step runner
41
- * asserts directly against the DOM. Anything it cannot compile is still dropped.
40
+ * The list below is exactly what `successToPredicate` reads, including an element located by
41
+ * role or name. A testid is also asserted directly against the DOM by the step runner. Anything
42
+ * it cannot compile is still dropped.
42
43
  */
43
44
  export declare function enforcedOnReplay(expect: FlowExpect | undefined): FlowExpect | undefined;