@reticlehq/server 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (386) hide show
  1. package/SKILL.md +67 -109
  2. package/dist/bridge/auth-failure-reason.d.ts +6 -2
  3. package/dist/bridge/auth-failure-reason.js +15 -8
  4. package/dist/bridge/bridge.d.ts +18 -1
  5. package/dist/bridge/bridge.js +247 -164
  6. package/dist/bridge/pairing-token.d.ts +5 -0
  7. package/dist/bridge/pairing-token.js +28 -0
  8. package/dist/bridge/protocol-skew.d.ts +37 -0
  9. package/dist/bridge/protocol-skew.js +45 -0
  10. package/dist/capsule/capsule-store.d.ts +6 -6
  11. package/dist/capsule/causal-summary.js +7 -2
  12. package/dist/cli/cli-doctor.js +31 -1
  13. package/dist/cli/cli-launch.d.ts +11 -1
  14. package/dist/cli/cli-launch.js +59 -10
  15. package/dist/cli/cli-parse.d.ts +15 -1
  16. package/dist/cli/cli-parse.js +212 -8
  17. package/dist/cli/cli-verify.d.ts +1 -0
  18. package/dist/cli/cli-verify.js +3 -2
  19. package/dist/cli/cloud-cli.js +428 -259
  20. package/dist/cli/cloud-keystore.d.ts +37 -0
  21. package/dist/cli/cloud-keystore.js +97 -0
  22. package/dist/cli/cloud-kit.d.ts +37 -0
  23. package/dist/cli/cloud-kit.js +121 -0
  24. package/dist/cli/cloud-login.d.ts +17 -0
  25. package/dist/cli/cloud-login.js +183 -0
  26. package/dist/cli/cloud-session.d.ts +48 -0
  27. package/dist/cli/cloud-session.js +53 -0
  28. package/dist/cli/config-discovery.js +1 -0
  29. package/dist/cli/daemon-start-options.d.ts +32 -0
  30. package/dist/cli/daemon-start-options.js +38 -0
  31. package/dist/cli/device-flow.d.ts +76 -0
  32. package/dist/cli/device-flow.js +56 -0
  33. package/dist/cli/doctor-rows.d.ts +9 -0
  34. package/dist/cli/doctor-rows.js +9 -0
  35. package/dist/cli/drive-attach.d.ts +13 -0
  36. package/dist/cli/drive-attach.js +18 -0
  37. package/dist/cli/drive-command.js +7 -3
  38. package/dist/cli/mcp-command.js +51 -20
  39. package/dist/cli/project-name.d.ts +30 -0
  40. package/dist/cli/project-name.js +44 -0
  41. package/dist/cli/status-next-action.d.ts +14 -0
  42. package/dist/cli/status-next-action.js +4 -4
  43. package/dist/cli.d.ts +1 -0
  44. package/dist/cli.js +134 -80
  45. package/dist/cloud/cloud-config.d.ts +22 -1
  46. package/dist/cloud/cloud-config.js +64 -5
  47. package/dist/cloud/cloud-sync.d.ts +5 -5
  48. package/dist/cloud/cloud-sync.js +33 -2
  49. package/dist/cloud/sync-cycle.d.ts +110 -0
  50. package/dist/cloud/sync-cycle.js +270 -0
  51. package/dist/cloud/sync-daemon.d.ts +69 -0
  52. package/dist/cloud/sync-daemon.js +260 -0
  53. package/dist/cloud/sync-disk.d.ts +9 -0
  54. package/dist/cloud/sync-disk.js +191 -0
  55. package/dist/cloud/sync-hash.d.ts +2 -0
  56. package/dist/cloud/sync-hash.js +35 -0
  57. package/dist/crawl/crawl-tools.js +5 -3
  58. package/dist/crawl/crawl.d.ts +3 -0
  59. package/dist/crawl/crawl.js +5 -0
  60. package/dist/daemon/daemon-resolve.d.ts +43 -0
  61. package/dist/daemon/daemon-resolve.js +69 -0
  62. package/dist/daemon/dev-servers.d.ts +15 -0
  63. package/dist/daemon/dev-servers.js +68 -0
  64. package/dist/daemon/wake-decision.d.ts +32 -0
  65. package/dist/daemon/wake-decision.js +38 -0
  66. package/dist/domain/domain-tools.js +2 -2
  67. package/dist/events/contradictions.d.ts +18 -0
  68. package/dist/events/contradictions.js +134 -21
  69. package/dist/events/declared.d.ts +7 -4
  70. package/dist/events/declared.js +15 -6
  71. package/dist/events/echo-mismatch.js +64 -14
  72. package/dist/events/event-filters.d.ts +8 -0
  73. package/dist/events/event-filters.js +19 -2
  74. package/dist/events/predicate-eval.js +53 -5
  75. package/dist/events/predicate-precheck.d.ts +8 -0
  76. package/dist/events/predicate-precheck.js +41 -0
  77. package/dist/events/predicate-route.d.ts +14 -0
  78. package/dist/events/predicate-route.js +36 -8
  79. package/dist/events/predicate.d.ts +5 -0
  80. package/dist/events/predicate.js +37 -5
  81. package/dist/events/split-text-miss.d.ts +30 -0
  82. package/dist/events/split-text-miss.js +44 -0
  83. package/dist/flows/flake.d.ts +6 -6
  84. package/dist/flows/flow-memory-consult.d.ts +55 -0
  85. package/dist/flows/flow-memory-consult.js +38 -0
  86. package/dist/flows/flow-replay-run.d.ts +18 -7
  87. package/dist/flows/flow-replay-run.js +239 -22
  88. package/dist/flows/flow-replay.js +11 -2
  89. package/dist/flows/flow-step-runners.d.ts +8 -0
  90. package/dist/flows/flow-step-runners.js +8 -2
  91. package/dist/flows/flow-store-for-session.d.ts +31 -0
  92. package/dist/flows/flow-store-for-session.js +33 -0
  93. package/dist/flows/flow-tools.js +58 -32
  94. package/dist/flows/flows.d.ts +8 -0
  95. package/dist/flows/flows.js +468 -2
  96. package/dist/flows/heal-run.js +5 -2
  97. package/dist/flows/predicate-to-expect.d.ts +17 -12
  98. package/dist/flows/predicate-to-expect.js +30 -12
  99. package/dist/flows/recordings.d.ts +13 -1
  100. package/dist/flows/recordings.js +17 -2
  101. package/dist/flows/replay.d.ts +22 -2
  102. package/dist/flows/replay.js +28 -2
  103. package/dist/flows/verify-change-tools.js +5 -2
  104. package/dist/honesty/blind-spots.d.ts +55 -2
  105. package/dist/honesty/blind-spots.js +63 -3
  106. package/dist/honesty/honesty.d.ts +24 -0
  107. package/dist/honesty/honesty.js +25 -1
  108. package/dist/honesty/instrumentation-gaps.d.ts +9 -0
  109. package/dist/honesty/instrumentation-gaps.js +29 -2
  110. package/dist/honesty/verified.d.ts +2 -1
  111. package/dist/honesty/verified.js +16 -1
  112. package/dist/http-server.js +12 -0
  113. package/dist/impact/impact-recorder.d.ts +31 -8
  114. package/dist/impact/impact-recorder.js +104 -21
  115. package/dist/impact/impact-store.d.ts +15 -1
  116. package/dist/impact/impact-store.js +43 -2
  117. package/dist/index.js +113 -7
  118. package/dist/init/agent-rules.d.ts +13 -6
  119. package/dist/init/agent-rules.js +23 -12
  120. package/dist/init/astro-patch.d.ts +16 -0
  121. package/dist/init/astro-patch.js +28 -0
  122. package/dist/init/capabilities.d.ts +4 -1
  123. package/dist/init/capabilities.js +22 -2
  124. package/dist/init/confirm.d.ts +31 -7
  125. package/dist/init/confirm.js +48 -8
  126. package/dist/init/cra.d.ts +20 -4
  127. package/dist/init/cra.js +57 -17
  128. package/dist/init/csp-check.js +17 -2
  129. package/dist/init/csp-doctor.d.ts +8 -1
  130. package/dist/init/csp-doctor.js +8 -3
  131. package/dist/init/dev-script.d.ts +3 -1
  132. package/dist/init/dev-script.js +8 -5
  133. package/dist/init/format-generated.d.ts +18 -0
  134. package/dist/init/format-generated.js +39 -0
  135. package/dist/init/install-hint.d.ts +9 -0
  136. package/dist/init/install-hint.js +29 -0
  137. package/dist/init/mcp-clients.d.ts +7 -0
  138. package/dist/init/mcp-clients.js +7 -0
  139. package/dist/init/mcp.d.ts +1 -1
  140. package/dist/init/mcp.js +1 -1
  141. package/dist/init/node-io.js +11 -1
  142. package/dist/init/plan-framework.js +81 -45
  143. package/dist/init/plan.d.ts +20 -7
  144. package/dist/init/plan.js +20 -45
  145. package/dist/init/preflight.d.ts +28 -0
  146. package/dist/init/preflight.js +38 -0
  147. package/dist/init/run.d.ts +32 -9
  148. package/dist/init/run.js +141 -119
  149. package/dist/init/slash-command.d.ts +1 -1
  150. package/dist/init/snippets.js +41 -25
  151. package/dist/init/vite-config.d.ts +1 -1
  152. package/dist/init/vite-config.js +39 -25
  153. package/dist/init/workspace-apps.d.ts +17 -10
  154. package/dist/init/workspace-apps.js +110 -0
  155. package/dist/input/network-mock-tools.js +3 -6
  156. package/dist/input/real-input.js +25 -5
  157. package/dist/input/scroll-tools.js +4 -6
  158. package/dist/input/viewport-tools.js +5 -4
  159. package/dist/intent/inline-intent.d.ts +10 -19
  160. package/dist/intent/inline-intent.js +91 -1
  161. package/dist/intent/intent-shard-store.d.ts +72 -0
  162. package/dist/intent/intent-shard-store.js +203 -0
  163. package/dist/intent/intent-shard.d.ts +411 -0
  164. package/dist/intent/intent-shard.js +176 -0
  165. package/dist/intent/intent-store.d.ts +12 -0
  166. package/dist/intent/intent-store.js +21 -0
  167. package/dist/intent/intent-subject.d.ts +45 -0
  168. package/dist/intent/intent-subject.js +177 -0
  169. package/dist/intent/intent-tools.js +65 -2
  170. package/dist/intent/open-intents.d.ts +21 -0
  171. package/dist/intent/open-intents.js +15 -0
  172. package/dist/license/license-env.d.ts +29 -0
  173. package/dist/license/license-env.js +148 -0
  174. package/dist/loopback-agent.d.ts +31 -0
  175. package/dist/loopback-agent.js +57 -0
  176. package/dist/mcp/attach-memory.d.ts +18 -2
  177. package/dist/mcp/attach-memory.js +50 -7
  178. package/dist/mcp/client-identity.d.ts +31 -0
  179. package/dist/mcp/client-identity.js +51 -0
  180. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  181. package/dist/mcp/mcp-post-transport.js +109 -0
  182. package/dist/mcp/mcp-proxy.d.ts +2 -1
  183. package/dist/mcp/mcp-proxy.js +52 -62
  184. package/dist/mcp/mcp.js +7 -1
  185. package/dist/mcp/proxy-backoff.d.ts +11 -0
  186. package/dist/mcp/proxy-backoff.js +13 -0
  187. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  188. package/dist/mcp/proxy-daemon-probe.js +7 -1
  189. package/dist/mcp/proxy-handshake.d.ts +23 -0
  190. package/dist/mcp/proxy-handshake.js +31 -0
  191. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  192. package/dist/mcp/tool-catalog-cache.js +12 -0
  193. package/dist/memory/memory-tools.d.ts +2 -0
  194. package/dist/memory/memory-tools.js +82 -0
  195. package/dist/memory/project-memory.d.ts +34 -0
  196. package/dist/memory/project-memory.js +96 -0
  197. package/dist/oracles/propose-consequences.js +12 -3
  198. package/dist/pool/browser-pool.d.ts +63 -4
  199. package/dist/pool/browser-pool.js +113 -4
  200. package/dist/pool/playwright-launcher.js +9 -0
  201. package/dist/project/project-tools.js +3 -3
  202. package/dist/project/reticle-dir.d.ts +15 -2
  203. package/dist/project/reticle-dir.js +21 -4
  204. package/dist/runs/run-store.d.ts +10 -0
  205. package/dist/runs/run-store.js +10 -0
  206. package/dist/runs/verification-sync.d.ts +0 -11
  207. package/dist/runs/verification-sync.js +22 -1
  208. package/dist/runs/verify-server.d.ts +8 -1
  209. package/dist/runs/verify-server.js +14 -2
  210. package/dist/session/command-timeout.d.ts +5 -2
  211. package/dist/session/command-timeout.js +5 -4
  212. package/dist/session/lease-visibility.d.ts +67 -0
  213. package/dist/session/lease-visibility.js +76 -0
  214. package/dist/session/no-session-diagnosis.d.ts +23 -0
  215. package/dist/session/no-session-diagnosis.js +58 -17
  216. package/dist/session/no-session-next-action.d.ts +7 -0
  217. package/dist/session/no-session-next-action.js +42 -3
  218. package/dist/session/no-session-watch.d.ts +10 -6
  219. package/dist/session/no-session-watch.js +51 -19
  220. package/dist/session/pending-commands.d.ts +11 -1
  221. package/dist/session/pending-commands.js +12 -2
  222. package/dist/session/resolve-within.d.ts +34 -0
  223. package/dist/session/resolve-within.js +53 -0
  224. package/dist/session/session-info.d.ts +18 -0
  225. package/dist/session/session-info.js +11 -0
  226. package/dist/session/session-manager.d.ts +14 -0
  227. package/dist/session/session-manager.js +61 -3
  228. package/dist/session/session-mirror.d.ts +16 -0
  229. package/dist/session/session-mirror.js +22 -0
  230. package/dist/session/session-recommendation.d.ts +19 -5
  231. package/dist/session/session-recommendation.js +25 -9
  232. package/dist/session/session-successor.js +36 -1
  233. package/dist/session/session-tools.js +2 -2
  234. package/dist/session/session.d.ts +22 -2
  235. package/dist/session/session.js +87 -7
  236. package/dist/session/wall-clock.d.ts +12 -0
  237. package/dist/session/wall-clock.js +14 -0
  238. package/dist/setup/agent-configs.d.ts +96 -0
  239. package/dist/setup/agent-configs.js +333 -0
  240. package/dist/setup/agent-io.d.ts +9 -0
  241. package/dist/setup/agent-io.js +16 -0
  242. package/dist/setup/agent-writer.d.ts +51 -0
  243. package/dist/setup/agent-writer.js +115 -0
  244. package/dist/setup/approval-migration.d.ts +47 -0
  245. package/dist/setup/approval-migration.js +68 -0
  246. package/dist/setup/auto-approve.d.ts +85 -0
  247. package/dist/setup/auto-approve.js +150 -0
  248. package/dist/setup/bridge-port.d.ts +15 -0
  249. package/dist/setup/bridge-port.js +24 -0
  250. package/dist/setup/desktop-shape.d.ts +57 -0
  251. package/dist/setup/desktop-shape.js +63 -0
  252. package/dist/setup/dev-server-wait.d.ts +60 -0
  253. package/dist/setup/dev-server-wait.js +65 -0
  254. package/dist/setup/drive-agent.d.ts +71 -0
  255. package/dist/setup/drive-agent.js +171 -0
  256. package/dist/setup/drive-plan.d.ts +68 -0
  257. package/dist/setup/drive-plan.js +130 -0
  258. package/dist/setup/ensure-daemon.d.ts +29 -0
  259. package/dist/setup/ensure-daemon.js +58 -0
  260. package/dist/setup/init-runtime.d.ts +47 -0
  261. package/dist/setup/init-runtime.js +160 -0
  262. package/dist/setup/interrupt.d.ts +25 -0
  263. package/dist/setup/interrupt.js +40 -0
  264. package/dist/setup/license-key.d.ts +39 -0
  265. package/dist/setup/license-key.js +72 -0
  266. package/dist/setup/listeners.d.ts +37 -0
  267. package/dist/setup/listeners.js +85 -0
  268. package/dist/setup/node-effects.d.ts +60 -0
  269. package/dist/setup/node-effects.js +210 -0
  270. package/dist/setup/page-probe.d.ts +39 -0
  271. package/dist/setup/page-probe.js +49 -0
  272. package/dist/setup/relaunch.d.ts +36 -0
  273. package/dist/setup/relaunch.js +57 -0
  274. package/dist/setup/remaining-steps.d.ts +19 -0
  275. package/dist/setup/remaining-steps.js +31 -0
  276. package/dist/setup/run-setup.d.ts +99 -0
  277. package/dist/setup/run-setup.js +187 -0
  278. package/dist/setup/session-pick.d.ts +47 -0
  279. package/dist/setup/session-pick.js +51 -0
  280. package/dist/setup/setup-command.d.ts +45 -0
  281. package/dist/setup/setup-command.js +199 -0
  282. package/dist/setup/setup-options.d.ts +74 -0
  283. package/dist/setup/setup-options.js +67 -0
  284. package/dist/setup/transcripts.d.ts +16 -0
  285. package/dist/setup/transcripts.js +43 -0
  286. package/dist/status-payload.d.ts +14 -1
  287. package/dist/status-payload.js +23 -1
  288. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  289. package/dist/telemetry/daemon-telemetry.js +52 -5
  290. package/dist/telemetry/error-class.js +1 -1
  291. package/dist/telemetry/feedback-context.d.ts +9 -1
  292. package/dist/telemetry/feedback-context.js +60 -29
  293. package/dist/telemetry/feedback-local.d.ts +15 -0
  294. package/dist/telemetry/feedback-local.js +56 -0
  295. package/dist/telemetry/feedback.d.ts +11 -0
  296. package/dist/telemetry/feedback.js +19 -6
  297. package/dist/telemetry/init-telemetry.d.ts +2 -0
  298. package/dist/telemetry/init-telemetry.js +2 -0
  299. package/dist/telemetry/install-source.d.ts +14 -0
  300. package/dist/telemetry/install-source.js +30 -0
  301. package/dist/telemetry/license-activation.d.ts +7 -1
  302. package/dist/telemetry/license-activation.js +9 -5
  303. package/dist/telemetry/mcp-connection.js +7 -0
  304. package/dist/telemetry/project-profile.d.ts +12 -1
  305. package/dist/telemetry/project-profile.js +11 -6
  306. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  307. package/dist/telemetry/proxy-telemetry.js +21 -0
  308. package/dist/telemetry/session-metrics.d.ts +12 -0
  309. package/dist/telemetry/session-metrics.js +35 -1
  310. package/dist/telemetry/telemetry.js +17 -2
  311. package/dist/telemetry/tool-refused.d.ts +3 -1
  312. package/dist/telemetry/tool-refused.js +34 -2
  313. package/dist/tools/act-capsule.d.ts +10 -0
  314. package/dist/tools/act-capsule.js +8 -2
  315. package/dist/tools/act-danger.d.ts +2 -1
  316. package/dist/tools/act-danger.js +11 -3
  317. package/dist/tools/act-observation.d.ts +41 -0
  318. package/dist/tools/act-observation.js +36 -0
  319. package/dist/tools/act-preflight.d.ts +20 -0
  320. package/dist/tools/act-preflight.js +98 -0
  321. package/dist/tools/act-sequence-retry.d.ts +45 -0
  322. package/dist/tools/act-sequence-retry.js +58 -0
  323. package/dist/tools/act-sequence-tool.d.ts +13 -0
  324. package/dist/tools/act-sequence-tool.js +135 -0
  325. package/dist/tools/act-target.d.ts +14 -0
  326. package/dist/tools/act-target.js +47 -0
  327. package/dist/tools/act-tools.d.ts +21 -1
  328. package/dist/tools/act-tools.js +100 -213
  329. package/dist/tools/assert-verdict.js +22 -16
  330. package/dist/tools/browser-tools.js +6 -2
  331. package/dist/tools/dynamic-tools.js +24 -5
  332. package/dist/tools/error-recovery.d.ts +1 -0
  333. package/dist/tools/error-recovery.js +8 -0
  334. package/dist/tools/invoke-tool.js +59 -9
  335. package/dist/tools/lease-tools.d.ts +30 -4
  336. package/dist/tools/lease-tools.js +169 -5
  337. package/dist/tools/merged-name-redirect.d.ts +13 -0
  338. package/dist/tools/merged-name-redirect.js +22 -0
  339. package/dist/tools/numeric-bounds.d.ts +100 -0
  340. package/dist/tools/numeric-bounds.js +125 -0
  341. package/dist/tools/observe-tools.js +47 -40
  342. package/dist/tools/read-tools.js +28 -7
  343. package/dist/tools/real-input-attempt.d.ts +10 -0
  344. package/dist/tools/real-input-attempt.js +59 -1
  345. package/dist/tools/reconcile-tools.js +4 -4
  346. package/dist/tools/tool-kit.d.ts +12 -0
  347. package/dist/tools/tool-kit.js +6 -1
  348. package/dist/tools/tool-names.d.ts +2 -0
  349. package/dist/tools/tool-names.js +2 -0
  350. package/dist/tools/tools-helpers.d.ts +19 -0
  351. package/dist/tools/tools-helpers.js +46 -0
  352. package/dist/tools/tools.d.ts +2 -0
  353. package/dist/tools/tools.js +56 -8
  354. package/dist/update/reticle-deps.d.ts +9 -0
  355. package/dist/update/reticle-deps.js +27 -0
  356. package/dist/update/sdk-sync.d.ts +1 -2
  357. package/dist/update/sdk-sync.js +1 -21
  358. package/dist/update/update-nudge.d.ts +18 -0
  359. package/dist/update/update-nudge.js +20 -0
  360. package/dist/version/peer-announce.js +4 -2
  361. package/dist/version/sdk-fix.d.ts +47 -0
  362. package/dist/version/sdk-fix.js +127 -0
  363. package/dist/version/version-nudge.d.ts +14 -0
  364. package/dist/version/version-nudge.js +21 -0
  365. package/dist/version/version-skew.d.ts +38 -3
  366. package/dist/version/version-skew.js +83 -7
  367. package/dist/visual/visual-store.d.ts +6 -6
  368. package/dist/visual/visual-store.js +18 -16
  369. package/dist/visual/visual-tools.js +57 -12
  370. package/docs/cli/doctor.mdx +1 -0
  371. package/docs/cli/serve.mdx +4 -2
  372. package/docs/cli.mdx +7 -3
  373. package/docs/desktop.mdx +1 -1
  374. package/docs/docs.json +1 -1
  375. package/docs/getting-started.md +19 -3
  376. package/docs/index.mdx +9 -6
  377. package/docs/install-agentic.mdx +38 -12
  378. package/docs/install-manual.mdx +1 -1
  379. package/docs/packages.mdx +1 -1
  380. package/docs/skill-file.mdx +3 -3
  381. package/docs/telemetry-contract.md +108 -4
  382. package/docs/tools-act-sequence.mdx +16 -0
  383. package/docs/usage.md +1 -1
  384. package/package.json +4 -3
  385. package/dist/init/cursor.d.ts +0 -24
  386. package/dist/init/cursor.js +0 -80
@@ -0,0 +1,63 @@
1
+ /**
2
+ * How a desktop app differs from a web app, for the phases that run after the files are written.
3
+ *
4
+ * `init` already understands Electron and Tauri when it writes files: desktop-doctor.ts checks the
5
+ * Tauri CSP for a blocked bridge or IPC, the Electron preload, the capture helper. The runtime
6
+ * phases did not, and they made three web-shaped assumptions that are wrong here — one of them
7
+ * actively harmful.
8
+ *
9
+ * 1. **Do not open a browser.** The app's own window IS the client. Opening a tab creates a SECOND
10
+ * session that is not the app, and the session picker would then have to choose between them —
11
+ * which is exactly the stale-tab false green, arranged deliberately.
12
+ * 2. **Do not wait for an HTTP port.** Tauri serves `tauri://localhost` (`http://tauri.localhost`
13
+ * on Windows) from inside the webview; there is nothing for `fetch` to probe. Electron under
14
+ * electron-vite does serve a renderer over HTTP, but the window is what matters, not the port.
15
+ * 3. **Wait for the app to launch instead.** `tauri dev` builds Rust before a window appears, which
16
+ * is minutes on a cold cache, so the budget has to be the app's, not a web app's.
17
+ */
18
+ /** What kind of thing setup is about to run. */
19
+ export const AppShape = {
20
+ WEB: 'web',
21
+ ELECTRON: 'electron',
22
+ TAURI: 'tauri',
23
+ };
24
+ export const isDesktop = (shape) => AppShape.WEB !== shape;
25
+ /**
26
+ * Which shell this is.
27
+ *
28
+ * Tauri wins when both are present: a project with a Tauri config is a Tauri app whatever else it
29
+ * has installed, and electron sometimes appears in a monorepo's root devDependencies.
30
+ */
31
+ export function readShape(evidence) {
32
+ if (evidence.hasTauriConf)
33
+ return AppShape.TAURI;
34
+ if (evidence.hasElectronDep)
35
+ return AppShape.ELECTRON;
36
+ return AppShape.WEB;
37
+ }
38
+ /** A Tauri webview's own origin, which no HTTP probe from outside can reach. */
39
+ const TAURI_ORIGINS = ['tauri://localhost', 'http://tauri.localhost'];
40
+ export function isDesktopOrigin(url) {
41
+ return TAURI_ORIGINS.some((origin) => url.startsWith(origin));
42
+ }
43
+ const WEB_CONNECT_MS = 120_000;
44
+ /** A cold `tauri dev` compiles the Rust side before a window exists. */
45
+ const DESKTOP_CONNECT_MS = 10 * 60_000;
46
+ export function policyFor(shape) {
47
+ if (AppShape.WEB === shape) {
48
+ return {
49
+ openBrowser: true,
50
+ requireHttpReady: true,
51
+ connectBudgetMs: WEB_CONNECT_MS,
52
+ note: undefined,
53
+ };
54
+ }
55
+ return {
56
+ openBrowser: false,
57
+ requireHttpReady: false,
58
+ connectBudgetMs: DESKTOP_CONNECT_MS,
59
+ note: AppShape.TAURI === shape
60
+ ? 'Tauri app: not opening a browser, because its own window is the client. A cold `tauri dev` builds the Rust side first, so the window can take minutes to appear.'
61
+ : 'Electron app: not opening a browser, because its own window is the client. Waiting for the app to launch and dial in.',
62
+ };
63
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Deciding when a dev server we started is up, hung, or gone.
3
+ *
4
+ * Nothing else in the package owns a spawned dev server, so this is the one genuinely new decision
5
+ * setup contributes. All three cases below were failures in the field:
6
+ *
7
+ * - a url in the output is an ANNOUNCEMENT, not readiness. Next prints `- Local: …` before it can
8
+ * serve, so a tab opened on that line lands on a 404 and the whole connect budget is then spent
9
+ * blaming the SDK for a server that had not finished starting.
10
+ * - a launcher that EXITS is not necessarily a dead server. `astro dev` forks the real one, prints
11
+ * the url and returns, which was read as death and failed Astro every time.
12
+ * - a fixed budget is the wrong instrument for "is this hung?". The heaviest real monorepo builds
13
+ * two shared packages before its app serves and was measured taking twenty minutes, while a
14
+ * genuinely wedged server writes nothing at all and gets exactly the same budget.
15
+ */
16
+ /** Everything the decision needs, gathered by the caller. */
17
+ export interface WaitFacts {
18
+ /** The dev server's own output so far. */
19
+ readonly output: string;
20
+ /** Whether the process we spawned has exited. */
21
+ readonly launcherExited: boolean;
22
+ /** Whether the url we are watching answers. */
23
+ readonly serving: boolean;
24
+ /** Milliseconds since the dev server last wrote anything. */
25
+ readonly quietForMs: number;
26
+ /** Milliseconds since the wait began. */
27
+ readonly elapsedMs: number;
28
+ }
29
+ export declare const WaitVerdict: {
30
+ /** It is up. Carry on. */
31
+ readonly READY: "ready";
32
+ /** Still building or still starting; keep waiting. */
33
+ readonly WAITING: "waiting";
34
+ /** The launcher exited and nothing it started is answering. */
35
+ readonly DEAD: "dead";
36
+ /** It has gone silent without ever serving. */
37
+ readonly HUNG: "hung";
38
+ };
39
+ export type WaitVerdict = (typeof WaitVerdict)[keyof typeof WaitVerdict];
40
+ /** Silence for this long, with nothing served, means hung rather than busy. */
41
+ export declare const QUIET_MEANS_HUNG_MS = 45000;
42
+ /** Even a talkative dev server has had enough rope by here. */
43
+ export declare const WAIT_CEILING_MS: number;
44
+ /**
45
+ * Whether to keep waiting.
46
+ *
47
+ * `serving` outranks everything: a launcher that exited while the port answers has DAEMONIZED, and
48
+ * that is a live server whatever its parent did.
49
+ */
50
+ export declare function judgeWait(facts: WaitFacts): WaitVerdict;
51
+ export declare function announcedUrl(output: string): string | undefined;
52
+ /**
53
+ * The url to watch: whatever the tool announced, else one composed from a port we OBSERVED our own
54
+ * process group binding.
55
+ *
56
+ * The second is not a guess. `react-scripts` prints no parseable url outside a tty, so a CRA app
57
+ * compiled successfully, served happily, and setup timed out insisting no url appeared — the
58
+ * evidence there is the process we spawned, not a convention.
59
+ */
60
+ export declare function urlToWatch(output: string, observedPorts: readonly number[]): string | undefined;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Deciding when a dev server we started is up, hung, or gone.
3
+ *
4
+ * Nothing else in the package owns a spawned dev server, so this is the one genuinely new decision
5
+ * setup contributes. All three cases below were failures in the field:
6
+ *
7
+ * - a url in the output is an ANNOUNCEMENT, not readiness. Next prints `- Local: …` before it can
8
+ * serve, so a tab opened on that line lands on a 404 and the whole connect budget is then spent
9
+ * blaming the SDK for a server that had not finished starting.
10
+ * - a launcher that EXITS is not necessarily a dead server. `astro dev` forks the real one, prints
11
+ * the url and returns, which was read as death and failed Astro every time.
12
+ * - a fixed budget is the wrong instrument for "is this hung?". The heaviest real monorepo builds
13
+ * two shared packages before its app serves and was measured taking twenty minutes, while a
14
+ * genuinely wedged server writes nothing at all and gets exactly the same budget.
15
+ */
16
+ export const WaitVerdict = {
17
+ /** It is up. Carry on. */
18
+ READY: 'ready',
19
+ /** Still building or still starting; keep waiting. */
20
+ WAITING: 'waiting',
21
+ /** The launcher exited and nothing it started is answering. */
22
+ DEAD: 'dead',
23
+ /** It has gone silent without ever serving. */
24
+ HUNG: 'hung',
25
+ };
26
+ /** Silence for this long, with nothing served, means hung rather than busy. */
27
+ export const QUIET_MEANS_HUNG_MS = 45_000;
28
+ /** Even a talkative dev server has had enough rope by here. */
29
+ export const WAIT_CEILING_MS = 25 * 60_000;
30
+ /**
31
+ * Whether to keep waiting.
32
+ *
33
+ * `serving` outranks everything: a launcher that exited while the port answers has DAEMONIZED, and
34
+ * that is a live server whatever its parent did.
35
+ */
36
+ export function judgeWait(facts) {
37
+ if (facts.serving)
38
+ return WaitVerdict.READY;
39
+ if (facts.launcherExited)
40
+ return WaitVerdict.DEAD;
41
+ if (facts.elapsedMs >= WAIT_CEILING_MS)
42
+ return WaitVerdict.HUNG;
43
+ // Output IS progress. Silence is what distinguishes a build from a wedge.
44
+ return facts.quietForMs >= QUIET_MEANS_HUNG_MS ? WaitVerdict.HUNG : WaitVerdict.WAITING;
45
+ }
46
+ /** A url the dev server announced, if it has announced one. Never composed, only read. */
47
+ const URL_IN_OUTPUT = /https?:\/\/(?:localhost|127\.0\.0\.1|\[::1\]):\d+/;
48
+ export function announcedUrl(output) {
49
+ return URL_IN_OUTPUT.exec(output)?.[0];
50
+ }
51
+ /**
52
+ * The url to watch: whatever the tool announced, else one composed from a port we OBSERVED our own
53
+ * process group binding.
54
+ *
55
+ * The second is not a guess. `react-scripts` prints no parseable url outside a tty, so a CRA app
56
+ * compiled successfully, served happily, and setup timed out insisting no url appeared — the
57
+ * evidence there is the process we spawned, not a convention.
58
+ */
59
+ export function urlToWatch(output, observedPorts) {
60
+ const announced = announcedUrl(output);
61
+ if (undefined !== announced)
62
+ return announced;
63
+ const port = observedPorts[0];
64
+ return undefined === port ? undefined : `http://localhost:${port}`;
65
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Asking a child agent to drive the app, and reading what it says back.
3
+ *
4
+ * The drive needs a model, and the model does not need to be the caller. A client reads its MCP
5
+ * server list once at startup and cannot reload it, so an agent that has just run `init` does not
6
+ * yet have the reticle_* tools — which is why the hand-driven procedure asks for a restart, and why
7
+ * three runs in five ended there having shown the user nothing. A child process started after init
8
+ * reads the list init just wrote. No restart, no lost context, no waiting on a human.
9
+ *
10
+ * Two details here are not stylistic, and both cost a real run to learn.
11
+ */
12
+ import { type DriverSpec } from './drive-plan.js';
13
+ /** Ten minutes. Long, because the drive is a model working, and bounded, because it can wedge. */
14
+ export declare const DRIVE_TIMEOUT_MS: number;
15
+ export interface DriveRequest {
16
+ readonly url: string;
17
+ readonly sessionId: string;
18
+ /** The journey to drive, in the caller's own words. Absent means the child picks one. */
19
+ readonly flow?: string | undefined;
20
+ /** The capabilities file, when the session says it was never finished. */
21
+ readonly unfinishedCapabilitiesFile?: string | undefined;
22
+ /** The tab is hidden or throttled, so the child should not blame the app for a frozen page. */
23
+ readonly tabThrottled: boolean;
24
+ readonly budgetUsd: number;
25
+ readonly model?: string | undefined;
26
+ }
27
+ export interface DriveReport {
28
+ /** The agent's own account, kept verbatim and never treated as a pass on its own. */
29
+ readonly text: string;
30
+ readonly grade?: string | undefined;
31
+ readonly turns?: number | undefined;
32
+ readonly costUsd?: number | undefined;
33
+ /** Set when the run was cut short, naming where it had got to. */
34
+ readonly incomplete?: string | undefined;
35
+ }
36
+ /**
37
+ * Findings are the point, not the verdict.
38
+ *
39
+ * A green verdict demonstrates the mechanism. What people remember is Reticle telling them
40
+ * something they did not know — measured on a real app, a drive's own assertion passed and Reticle
41
+ * overruled it on a 504 fired inside the action window, which the drive then reported rather than
42
+ * explaining away. That only happens if the drive is asked for it: asked for a verdict alone, it
43
+ * returns a verdict alone, and everything else the page did goes unmentioned.
44
+ */
45
+ /**
46
+ * The task LEADS.
47
+ *
48
+ * Measured: with the situation first and a capabilities dump in the middle, a model answered "I
49
+ * don't see an actual task or request from you yet" and drove nothing — one turn, no verdict, and
50
+ * setup reported success. Context goes after the ask, never before it.
51
+ */
52
+ export declare function buildDrivePrompt(req: DriveRequest): string;
53
+ /**
54
+ * NDJSON, one event per line.
55
+ *
56
+ * `--output-format json` emits nothing until the run completes, so a drive killed at the timeout
57
+ * left no trace of what it had been doing — measured four times on one app, every one reporting
58
+ * "the drive produced no output". Streaming turns that into evidence, because spawnSync hands back
59
+ * the stdout it collected before killing the child.
60
+ */
61
+ export declare function readDriveOutput(out: string): DriveReport;
62
+ /**
63
+ * The prompt goes on STDIN.
64
+ *
65
+ * `--allowedTools` is variadic: with the prompt passed positionally after it, the flag swallowed
66
+ * the prompt as another tool name and the run exited in two seconds having done nothing, with the
67
+ * error on stderr — which the caller was discarding. No flag ordering is safe across four CLIs.
68
+ */
69
+ export declare function driveWith(driver: DriverSpec, req: DriveRequest, cwd: string): DriveReport;
70
+ /** Everything that is not Claude Code reports prose, so the report IS its output. */
71
+ export declare function readPlainOutput(out: string): DriveReport;
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Asking a child agent to drive the app, and reading what it says back.
3
+ *
4
+ * The drive needs a model, and the model does not need to be the caller. A client reads its MCP
5
+ * server list once at startup and cannot reload it, so an agent that has just run `init` does not
6
+ * yet have the reticle_* tools — which is why the hand-driven procedure asks for a restart, and why
7
+ * three runs in five ended there having shown the user nothing. A child process started after init
8
+ * reads the list init just wrote. No restart, no lost context, no waiting on a human.
9
+ *
10
+ * Two details here are not stylistic, and both cost a real run to learn.
11
+ */
12
+ import { spawnSync } from 'node:child_process';
13
+ import { ASSERTED, readAssertionsGrade } from './drive-plan.js';
14
+ /** Ten minutes. Long, because the drive is a model working, and bounded, because it can wedge. */
15
+ export const DRIVE_TIMEOUT_MS = 10 * 60_000;
16
+ /** Only the Reticle surface, unless the capabilities file has to be finished first. */
17
+ const RETICLE_TOOLS = 'mcp__reticle';
18
+ const CAPABILITY_TOOLS = 'mcp__reticle,Read,Edit,Write';
19
+ /**
20
+ * Findings are the point, not the verdict.
21
+ *
22
+ * A green verdict demonstrates the mechanism. What people remember is Reticle telling them
23
+ * something they did not know — measured on a real app, a drive's own assertion passed and Reticle
24
+ * overruled it on a 504 fired inside the action window, which the drive then reported rather than
25
+ * explaining away. That only happens if the drive is asked for it: asked for a verdict alone, it
26
+ * returns a verdict alone, and everything else the page did goes unmentioned.
27
+ */
28
+ /**
29
+ * The task LEADS.
30
+ *
31
+ * Measured: with the situation first and a capabilities dump in the middle, a model answered "I
32
+ * don't see an actual task or request from you yet" and drove nothing — one turn, no verdict, and
33
+ * setup reported success. Context goes after the ask, never before it.
34
+ */
35
+ export function buildDrivePrompt(req) {
36
+ const task = undefined === req.flow ? 'drive one user flow' : `drive THIS flow — ${req.flow}`;
37
+ const capabilities = undefined === req.unfinishedCapabilitiesFile
38
+ ? ''
39
+ : `\n\nFIRST: this session reports hasCapabilities:false, so ${req.unfinishedCapabilitiesFile} was scaffolded and never finished. Register whatever the app's state is readable through, and list the testids your flow touches. EDIT ONLY THAT FILE. This is somebody's repository, not a scratch copy: if the app is broken in a way that blocks the drive (a build error, a missing asset, an auth wall), say so and stop. That is a finding worth having and it is theirs to fix.`;
40
+ const throttled = req.tabThrottled
41
+ ? '\n\nNOTE: this tab is hidden or throttled, so timers and rAF are clamped. If an action seems to land on a page that never advances, that is why — say so rather than reporting `unknown` as if the app were at fault.'
42
+ : '';
43
+ return (`TASK: ${task} in this running app and produce a verdict. Do it now; do not ask questions, there is nobody to answer.\n\n` +
44
+ `The app is at ${req.url} and Reticle session ${req.sessionId} is connected to it.${capabilities}${throttled}\n\n` +
45
+ 'In as few calls as you can: ONE reticle_snapshot({mode:"interactive"}) for the whole flow, ' +
46
+ 'reticle_act_sequence for every fill and intermediate click in one call, then ONE ' +
47
+ 'reticle_act_and_wait({ref,action,until}) — that is the call that produces the verdict, and ' +
48
+ '`until` names the expected consequence BEFORE the action fires. Then reticle_state() once. ' +
49
+ 'Wrap it in reticle_record start/stop and reticle_flow_save.\n\n' +
50
+ `Then CHECK the grade reticle_flow_save returns. Anything other than \`${ASSERTED}\` means the ` +
51
+ 'flow only ACTS: it will pass even when the feature is broken, and this setup replays that flow ' +
52
+ 'on every later run, so an unasserted flow becomes a permanent green. If it is weaker, record ' +
53
+ 'again with an `until` that names a consequence the action CHANGES.\n\n' +
54
+ 'Then, whatever the verdict says: call reticle_observe ONCE and report anything the app did that ' +
55
+ 'its owner would want to know. A console error, a failed or slow request, an unhandled ' +
56
+ 'rejection, a state that did not update, a request fired twice. Give the `file:line` wherever ' +
57
+ 'you have it. A flow that passed with a 500 behind it is not a clean app, and this is the part ' +
58
+ 'they cannot see for themselves — it is the most useful thing you will produce, so do not skip ' +
59
+ 'it because the verdict was green.\n\n' +
60
+ 'Report in this order: FINDINGS first (or "nothing else worth reporting" if the page really was ' +
61
+ 'clean), then the flow name, the verdict, and assertions.grade. A verdict of "unknown" or ' +
62
+ '"no-fault" is NOT a pass — say so plainly rather than weakening the check until it passes.');
63
+ }
64
+ /**
65
+ * NDJSON, one event per line.
66
+ *
67
+ * `--output-format json` emits nothing until the run completes, so a drive killed at the timeout
68
+ * left no trace of what it had been doing — measured four times on one app, every one reporting
69
+ * "the drive produced no output". Streaming turns that into evidence, because spawnSync hands back
70
+ * the stdout it collected before killing the child.
71
+ */
72
+ export function readDriveOutput(out) {
73
+ const events = [];
74
+ for (const line of String(out).split('\n')) {
75
+ if ('' === line.trim())
76
+ continue;
77
+ try {
78
+ const parsed = JSON.parse(line);
79
+ if ('object' === typeof parsed && null !== parsed)
80
+ events.push(parsed);
81
+ }
82
+ catch {
83
+ /* a truncated final line is what a killed run looks like */
84
+ }
85
+ }
86
+ const finished = [...events].reverse().find((e) => undefined !== e['total_cost_usd']);
87
+ if (undefined !== finished) {
88
+ const text = 'string' === typeof finished['result'] ? finished['result'] : '';
89
+ return {
90
+ text,
91
+ ...(undefined === readAssertionsGrade(text) ? {} : { grade: readAssertionsGrade(text) }),
92
+ ...('number' === typeof finished['num_turns'] ? { turns: finished['num_turns'] } : {}),
93
+ ...('number' === typeof finished['total_cost_usd']
94
+ ? { costUsd: finished['total_cost_usd'] }
95
+ : {}),
96
+ };
97
+ }
98
+ return { text: '', incomplete: describeUnfinished(events) };
99
+ }
100
+ /** Where a killed run had got to, from the last thing it did. */
101
+ function describeUnfinished(events) {
102
+ for (const event of [...events].reverse()) {
103
+ if ('assistant' !== event['type'])
104
+ continue;
105
+ const message = event['message'];
106
+ const content = 'object' === typeof message && null !== message
107
+ ? message.content
108
+ : undefined;
109
+ if (!Array.isArray(content))
110
+ continue;
111
+ for (const part of content) {
112
+ if ('object' !== typeof part || null === part)
113
+ continue;
114
+ const p = part;
115
+ if ('tool_use' === p.type && 'string' === typeof p.name)
116
+ return `it was calling \`${p.name}\``;
117
+ if ('text' === p.type && 'string' === typeof p.text)
118
+ return `the last thing it said was: ${p.text.slice(0, 300)}`;
119
+ }
120
+ }
121
+ return 'it produced no events at all';
122
+ }
123
+ /**
124
+ * The prompt goes on STDIN.
125
+ *
126
+ * `--allowedTools` is variadic: with the prompt passed positionally after it, the flag swallowed
127
+ * the prompt as another tool name and the run exited in two seconds having done nothing, with the
128
+ * error on stderr — which the caller was discarding. No flag ordering is safe across four CLIs.
129
+ */
130
+ export function driveWith(driver, req, cwd) {
131
+ const tools = undefined === req.unfinishedCapabilitiesFile ? RETICLE_TOOLS : CAPABILITY_TOOLS;
132
+ const prompt = buildDrivePrompt(req);
133
+ const args = driver.argv({
134
+ tools,
135
+ budgetUsd: req.budgetUsd,
136
+ model: req.model,
137
+ prompt,
138
+ });
139
+ const child = spawnSync(driver.bin, args, {
140
+ cwd,
141
+ encoding: 'utf8',
142
+ timeout: DRIVE_TIMEOUT_MS,
143
+ maxBuffer: 64 * 1024 * 1024,
144
+ // Only for the drivers that read it. Passing stdin to one that takes the prompt as an argument
145
+ // is harmless, but passing it as an argument to one that reads stdin is not: `--allowedTools`
146
+ // is variadic and swallowed a positional prompt, and the run exited in two seconds having done
147
+ // nothing at all.
148
+ ...('stdin' === driver.promptVia ? { input: prompt } : {}),
149
+ });
150
+ const stdout = child.stdout ?? '';
151
+ const report = driver.streamsNdjson ? readDriveOutput(stdout) : readPlainOutput(stdout);
152
+ if ('' !== report.text || undefined !== report.incomplete)
153
+ return report;
154
+ const stderr = String(child.stderr ?? '')
155
+ .trim()
156
+ .split('\n')
157
+ .slice(-3)
158
+ .join(' ');
159
+ return {
160
+ text: '',
161
+ incomplete: '' === stderr ? 'it produced no output at all' : stderr.slice(0, 300),
162
+ };
163
+ }
164
+ /** Everything that is not Claude Code reports prose, so the report IS its output. */
165
+ export function readPlainOutput(out) {
166
+ const text = String(out).trim();
167
+ if ('' === text)
168
+ return { text: '', incomplete: 'it produced no output at all' };
169
+ const grade = readAssertionsGrade(text);
170
+ return { text, ...(undefined === grade ? {} : { grade }) };
171
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Who drives the app, and whether what they left behind is worth keeping.
3
+ *
4
+ * The drive is the only part of setup that needs a model, and the only part that can succeed
5
+ * expensively and leave something worthless. Both of those are decisions, so both live here where
6
+ * they can be tested without spending anything.
7
+ */
8
+ /** What a driver needs to build its own invocation. */
9
+ export interface DriveInvocation {
10
+ /** The tools the drive may use, comma-separated, in Claude Code's spelling. */
11
+ readonly tools: string;
12
+ readonly budgetUsd: number;
13
+ readonly model: string | undefined;
14
+ /** Passed as an argument only by drivers whose promptVia is 'arg'. */
15
+ readonly prompt: string;
16
+ }
17
+ /**
18
+ * An agent CLI that can be asked to drive, in preference order.
19
+ *
20
+ * Each carries its OWN argv, because they share nothing. A table that lists four drivers and builds
21
+ * one CLI's flags for all of them does not support four drivers: it supports one and fails the
22
+ * others with an unknown-flag error, which is what this looked like before.
23
+ */
24
+ export interface DriverSpec {
25
+ readonly id: string;
26
+ readonly bin: string;
27
+ /** How its prompt is delivered. Gemini and Codex take it as an argument; the rest read stdin. */
28
+ readonly promptVia: 'stdin' | 'arg';
29
+ /** Its own flags, in its own spelling. */
30
+ readonly argv: (invocation: DriveInvocation) => string[];
31
+ /** NDJSON, for the one CLI that streams it. Everything else reports prose. */
32
+ readonly streamsNdjson: boolean;
33
+ /** True where the invocation is documented but has not been watched driving a real app here. */
34
+ readonly unverified?: boolean;
35
+ }
36
+ export declare const DRIVERS: readonly DriverSpec[];
37
+ /**
38
+ * The first driver that is present AND runs.
39
+ *
40
+ * The second half is the one that has bitten: a CLI can be on PATH and broken — a half-installed
41
+ * codex whose vendor binary is missing exits non-zero on every invocation — and driving with one of
42
+ * those produces an empty session that looks exactly like success.
43
+ */
44
+ export declare function chooseDriver(drivers: readonly DriverSpec[], probe: (bin: string) => {
45
+ readonly present: boolean;
46
+ readonly runs: boolean;
47
+ }): DriverSpec | null;
48
+ /** The grade `reticle_flow_save` reported, read out of the drive's own prose. */
49
+ export declare function readAssertionsGrade(text: string | undefined): string | undefined;
50
+ /** The only grade that makes a saved flow worth replaying. */
51
+ export declare const ASSERTED = "asserted";
52
+ export interface EscalationInput {
53
+ readonly escalationEnabled: boolean;
54
+ /** Set only when a faster model was chosen, since escalation means retrying without it. */
55
+ readonly fasterModel: string | undefined;
56
+ readonly flowSaved: boolean;
57
+ readonly grade: string | undefined;
58
+ }
59
+ /**
60
+ * Whether to re-record with the stronger model.
61
+ *
62
+ * Measured: a faster model reaches the same `verified: "yes"` about three times quicker and leaves
63
+ * an `assertion-free` or `presence-only` flow in three runs out of four. Such a flow only ACTS, so
64
+ * it passes even when the feature is broken — and setup replays saved flows on every later run,
65
+ * which turns one weak recording into a permanent green. Presenting that as a trade for the user to
66
+ * choose is worse than spending a second drive on it.
67
+ */
68
+ export declare function shouldEscalate(input: EscalationInput): boolean;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Who drives the app, and whether what they left behind is worth keeping.
3
+ *
4
+ * The drive is the only part of setup that needs a model, and the only part that can succeed
5
+ * expensively and leave something worthless. Both of those are decisions, so both live here where
6
+ * they can be tested without spending anything.
7
+ */
8
+ export const DRIVERS = [
9
+ {
10
+ id: 'claude',
11
+ bin: 'claude',
12
+ promptVia: 'stdin',
13
+ streamsNdjson: true,
14
+ argv: ({ tools, budgetUsd, model }) => [
15
+ '-p',
16
+ '--permission-mode',
17
+ 'acceptEdits',
18
+ '--allowedTools',
19
+ tools,
20
+ ...(undefined === model ? [] : ['--model', model]),
21
+ '--max-budget-usd',
22
+ String(budgetUsd),
23
+ // stream-json, not json: `json` emits nothing until the run completes, so a drive killed at
24
+ // the timeout left no trace of where it had got to.
25
+ '--output-format',
26
+ 'stream-json',
27
+ '--verbose',
28
+ ],
29
+ },
30
+ {
31
+ id: 'codex',
32
+ bin: 'codex',
33
+ // `codex exec` is its headless form and takes the prompt positionally.
34
+ promptVia: 'arg',
35
+ streamsNdjson: false,
36
+ unverified: true,
37
+ argv: ({ prompt, model }) => [
38
+ 'exec',
39
+ ...(undefined === model ? [] : ['--model', model]),
40
+ prompt,
41
+ ],
42
+ },
43
+ {
44
+ id: 'opencode',
45
+ bin: 'opencode',
46
+ // `opencode run [message..]`, verified against its own --help on this machine.
47
+ promptVia: 'arg',
48
+ streamsNdjson: false,
49
+ unverified: true,
50
+ argv: ({ prompt, model }) => [
51
+ 'run',
52
+ ...(undefined === model ? [] : ['--model', model]),
53
+ prompt,
54
+ ],
55
+ },
56
+ {
57
+ id: 'cursor-agent',
58
+ bin: 'cursor-agent',
59
+ promptVia: 'arg',
60
+ streamsNdjson: false,
61
+ unverified: true,
62
+ argv: ({ prompt, model }) => [
63
+ '-p',
64
+ ...(undefined === model ? [] : ['--model', model]),
65
+ '--output-format',
66
+ 'text',
67
+ prompt,
68
+ ],
69
+ },
70
+ {
71
+ id: 'gemini',
72
+ bin: 'gemini',
73
+ // Gemini takes its prompt as the value of -p, and gates MCP servers by name.
74
+ promptVia: 'arg',
75
+ streamsNdjson: false,
76
+ unverified: true,
77
+ argv: ({ prompt, model }) => [
78
+ '-p',
79
+ prompt,
80
+ ...(undefined === model ? [] : ['--model', model]),
81
+ '--allowed-mcp-server-names',
82
+ 'reticle',
83
+ '--output-format',
84
+ 'text',
85
+ ],
86
+ },
87
+ ];
88
+ /**
89
+ * The first driver that is present AND runs.
90
+ *
91
+ * The second half is the one that has bitten: a CLI can be on PATH and broken — a half-installed
92
+ * codex whose vendor binary is missing exits non-zero on every invocation — and driving with one of
93
+ * those produces an empty session that looks exactly like success.
94
+ */
95
+ export function chooseDriver(drivers, probe) {
96
+ for (const d of drivers) {
97
+ const { present, runs } = probe(d.bin);
98
+ if (present && runs)
99
+ return d;
100
+ }
101
+ return null;
102
+ }
103
+ /** The grade `reticle_flow_save` reported, read out of the drive's own prose. */
104
+ export function readAssertionsGrade(text) {
105
+ const source = text ?? '';
106
+ return (/assertions?\.?grade\W+`?([a-z-]+)/i.exec(source)?.[1] ??
107
+ /grade\W+`?([a-z-]+)/i.exec(source)?.[1]);
108
+ }
109
+ /** The only grade that makes a saved flow worth replaying. */
110
+ export const ASSERTED = 'asserted';
111
+ /**
112
+ * Whether to re-record with the stronger model.
113
+ *
114
+ * Measured: a faster model reaches the same `verified: "yes"` about three times quicker and leaves
115
+ * an `assertion-free` or `presence-only` flow in three runs out of four. Such a flow only ACTS, so
116
+ * it passes even when the feature is broken — and setup replays saved flows on every later run,
117
+ * which turns one weak recording into a permanent green. Presenting that as a trade for the user to
118
+ * choose is worse than spending a second drive on it.
119
+ */
120
+ export function shouldEscalate(input) {
121
+ if (!input.escalationEnabled)
122
+ return false;
123
+ if (undefined === input.fasterModel)
124
+ return false;
125
+ if (!input.flowSaved)
126
+ return false;
127
+ if (undefined === input.grade)
128
+ return false;
129
+ return ASSERTED !== input.grade;
130
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Making sure something is listening before the app is asked to dial it.
3
+ *
4
+ * The setup phases wait for the page to appear as a session, which cannot happen unless a daemon
5
+ * holds the bridge port. Nothing in `init` started one. On a developer machine that is invisible,
6
+ * because an editor running `reticle mcp` has had a daemon up for hours; on a machine installing
7
+ * Reticle for the first time, which is every machine this command exists for, there is nothing to
8
+ * dial and setup ends with "the SDK is in the page and never dialled the bridge" — the SDK being
9
+ * blamed for the absence of the thing it was dialling.
10
+ *
11
+ * Idempotent by construction: an alive daemon for this project is adopted, and one belonging to
12
+ * somebody else is refused rather than stolen, which is the same rule `reticle mcp` follows.
13
+ */
14
+ export interface EnsureDaemonDeps {
15
+ readonly usable: (port: number) => Promise<boolean>;
16
+ readonly spawn: (port: number) => boolean;
17
+ readonly waitReady: (port: number) => Promise<unknown>;
18
+ readonly scriptPath: string | undefined;
19
+ }
20
+ export declare const EnsureDaemon: {
21
+ /** Something usable was already there. */
22
+ readonly ADOPTED: "adopted";
23
+ readonly STARTED: "started";
24
+ /** Could not be started, and the caller has to say so rather than blame the page. */
25
+ readonly UNAVAILABLE: "unavailable";
26
+ };
27
+ export type EnsureDaemon = (typeof EnsureDaemon)[keyof typeof EnsureDaemon];
28
+ export declare function ensureDaemon(port: number, deps: EnsureDaemonDeps): Promise<EnsureDaemon>;
29
+ export declare function nodeEnsureDaemonDeps(): EnsureDaemonDeps;