@reticlehq/server 2.7.0 → 2.9.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 (357) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +130 -778
  3. package/dist/bridge/bridge.d.ts +23 -0
  4. package/dist/bridge/bridge.js +80 -2
  5. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  6. package/dist/bridge/bridge.test-harness.js +6 -4
  7. package/dist/capsule/causal-summary.d.ts +36 -1
  8. package/dist/capsule/causal-summary.js +33 -7
  9. package/dist/capsule/divergence.js +7 -1
  10. package/dist/cli/chromium-hint.d.ts +33 -5
  11. package/dist/cli/chromium-hint.js +82 -12
  12. package/dist/cli/cli-doctor.js +65 -34
  13. package/dist/cli/cli-flow-commands.js +8 -8
  14. package/dist/cli/cli-kill.d.ts +58 -0
  15. package/dist/cli/cli-kill.js +162 -0
  16. package/dist/cli/cli-parse.d.ts +16 -1
  17. package/dist/cli/cli-parse.js +32 -9
  18. package/dist/cli/cli-port.d.ts +32 -0
  19. package/dist/cli/cli-port.js +82 -35
  20. package/dist/cli/cli-verify.d.ts +21 -0
  21. package/dist/cli/cli-verify.js +47 -3
  22. package/dist/cli/config-discovery.d.ts +35 -0
  23. package/dist/cli/config-discovery.js +163 -0
  24. package/dist/cli/doctor-daemon-line.js +4 -1
  25. package/dist/cli/doctor-project-line.d.ts +37 -0
  26. package/dist/cli/doctor-project-line.js +35 -0
  27. package/dist/cli/doctor-rows.d.ts +51 -0
  28. package/dist/cli/doctor-rows.js +54 -0
  29. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  30. package/dist/cli/doctor-sessions-line.js +69 -0
  31. package/dist/cli/drive-attach.d.ts +99 -0
  32. package/dist/cli/drive-attach.js +203 -0
  33. package/dist/cli/drive-command.d.ts +13 -0
  34. package/dist/cli/drive-command.js +103 -0
  35. package/dist/cli/open-note.d.ts +20 -0
  36. package/dist/cli/open-note.js +37 -0
  37. package/dist/cli/port-holder.d.ts +10 -0
  38. package/dist/cli/port-holder.js +26 -2
  39. package/dist/cli/status-next-action.d.ts +37 -0
  40. package/dist/cli/status-next-action.js +61 -0
  41. package/dist/cli.js +119 -39
  42. package/dist/crawl/crawl-tools.js +12 -2
  43. package/dist/crawl/crawl.d.ts +13 -0
  44. package/dist/crawl/crawl.js +50 -2
  45. package/dist/events/contradictions.d.ts +46 -0
  46. package/dist/events/contradictions.js +35 -22
  47. package/dist/events/declared.d.ts +45 -0
  48. package/dist/events/declared.js +73 -0
  49. package/dist/events/echo-mismatch.d.ts +8 -2
  50. package/dist/events/echo-mismatch.js +33 -5
  51. package/dist/events/event-filters.d.ts +1 -1
  52. package/dist/events/event-filters.js +7 -4
  53. package/dist/events/predicate-eval.d.ts +17 -72
  54. package/dist/events/predicate-eval.js +117 -236
  55. package/dist/events/predicate-parse.js +31 -3
  56. package/dist/events/predicate-route.d.ts +32 -0
  57. package/dist/events/predicate-route.js +102 -0
  58. package/dist/events/predicate-schema.d.ts +99 -0
  59. package/dist/events/predicate-schema.js +309 -0
  60. package/dist/events/predicate.d.ts +8 -0
  61. package/dist/events/predicate.js +296 -22
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-classify.js +1 -1
  69. package/dist/flows/flow-replay-run.js +8 -1
  70. package/dist/flows/flow-tools.js +21 -5
  71. package/dist/flows/heal-run.js +1 -1
  72. package/dist/flows/replay.js +3 -0
  73. package/dist/honesty/accepted-write.d.ts +3 -0
  74. package/dist/honesty/accepted-write.js +8 -3
  75. package/dist/honesty/already-true.js +15 -0
  76. package/dist/honesty/blind-spots.js +10 -2
  77. package/dist/honesty/honesty.d.ts +11 -0
  78. package/dist/honesty/honesty.js +1 -0
  79. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  80. package/dist/honesty/uncaptured-bodies.js +9 -2
  81. package/dist/honesty/unread-outcome.d.ts +8 -0
  82. package/dist/honesty/unread-outcome.js +36 -9
  83. package/dist/honesty/unsettled.d.ts +49 -0
  84. package/dist/honesty/unsettled.js +82 -0
  85. package/dist/honesty/verified.d.ts +36 -3
  86. package/dist/honesty/verified.js +54 -11
  87. package/dist/http-server.d.ts +19 -0
  88. package/dist/http-server.js +202 -4
  89. package/dist/impact/impact-recorder.d.ts +23 -0
  90. package/dist/impact/impact-recorder.js +76 -0
  91. package/dist/impact/impact-store.d.ts +47 -0
  92. package/dist/impact/impact-store.js +171 -0
  93. package/dist/index.d.ts +7 -0
  94. package/dist/index.js +47 -10
  95. package/dist/init/agent-rules.d.ts +36 -0
  96. package/dist/init/agent-rules.js +132 -22
  97. package/dist/init/astro-patch.d.ts +2 -1
  98. package/dist/init/astro-patch.js +38 -6
  99. package/dist/init/capabilities.d.ts +22 -0
  100. package/dist/init/capabilities.js +76 -0
  101. package/dist/init/confirm.d.ts +114 -0
  102. package/dist/init/confirm.js +165 -0
  103. package/dist/init/connect-steps.d.ts +2 -0
  104. package/dist/init/connect-steps.js +50 -0
  105. package/dist/init/cra.js +3 -2
  106. package/dist/init/csp-check.d.ts +38 -0
  107. package/dist/init/csp-check.js +92 -0
  108. package/dist/init/csp-doctor.d.ts +25 -0
  109. package/dist/init/csp-doctor.js +49 -0
  110. package/dist/init/cursor.js +6 -9
  111. package/dist/init/detect.d.ts +8 -0
  112. package/dist/init/detect.js +33 -3
  113. package/dist/init/mcp-clients.d.ts +1 -0
  114. package/dist/init/mcp-clients.js +51 -7
  115. package/dist/init/mcp.d.ts +15 -0
  116. package/dist/init/mcp.js +19 -0
  117. package/dist/init/non-js-project.js +29 -5
  118. package/dist/init/plan-framework.d.ts +10 -0
  119. package/dist/init/plan-framework.js +78 -16
  120. package/dist/init/plan.d.ts +47 -6
  121. package/dist/init/plan.js +228 -69
  122. package/dist/init/run.d.ts +24 -2
  123. package/dist/init/run.js +157 -45
  124. package/dist/init/slash-command.d.ts +1 -1
  125. package/dist/init/slash-command.js +54 -6
  126. package/dist/init/snippets.d.ts +45 -5
  127. package/dist/init/snippets.js +194 -42
  128. package/dist/init/vite-config.js +53 -5
  129. package/dist/input/network-mock-tools.d.ts +19 -0
  130. package/dist/input/network-mock-tools.js +37 -5
  131. package/dist/input/scroll-find.d.ts +6 -0
  132. package/dist/input/scroll-find.js +17 -2
  133. package/dist/input/scroll-tools.js +4 -0
  134. package/dist/journal/attach-journal.d.ts +9 -1
  135. package/dist/journal/attach-journal.js +13 -2
  136. package/dist/journal/journal-query.d.ts +13 -0
  137. package/dist/journal/journal-query.js +28 -3
  138. package/dist/journal/workspace-gitignore.d.ts +10 -0
  139. package/dist/journal/workspace-gitignore.js +59 -0
  140. package/dist/license/license.js +1 -1
  141. package/dist/mcp/mcp-proxy.d.ts +35 -30
  142. package/dist/mcp/mcp-proxy.js +306 -104
  143. package/dist/mcp/mcp.d.ts +10 -1
  144. package/dist/mcp/mcp.js +70 -34
  145. package/dist/mcp/proxy-handshake.d.ts +82 -1
  146. package/dist/mcp/proxy-handshake.js +109 -2
  147. package/dist/mcp/proxy-lifecycle.d.ts +11 -2
  148. package/dist/mcp/proxy-lifecycle.js +12 -3
  149. package/dist/mcp/proxy-log.d.ts +63 -0
  150. package/dist/mcp/proxy-log.js +132 -0
  151. package/dist/mcp/server-instructions.d.ts +17 -0
  152. package/dist/mcp/server-instructions.js +56 -0
  153. package/dist/mcp/shared-params.d.ts +27 -0
  154. package/dist/mcp/shared-params.js +36 -0
  155. package/dist/pool/browser-pool.d.ts +22 -2
  156. package/dist/pool/browser-pool.js +44 -4
  157. package/dist/project/learned-routes.d.ts +24 -0
  158. package/dist/project/learned-routes.js +63 -0
  159. package/dist/project/project-store.d.ts +6 -0
  160. package/dist/project/project-store.js +25 -1
  161. package/dist/project/reticle-dir.d.ts +2 -0
  162. package/dist/project/reticle-dir.js +1 -0
  163. package/dist/session/connection-memory.d.ts +59 -0
  164. package/dist/session/connection-memory.js +108 -0
  165. package/dist/session/dev-command.d.ts +26 -0
  166. package/dist/session/dev-command.js +89 -0
  167. package/dist/session/dev-server-probe.d.ts +68 -0
  168. package/dist/session/dev-server-probe.js +102 -7
  169. package/dist/session/last-act.d.ts +26 -0
  170. package/dist/session/last-act.js +35 -1
  171. package/dist/session/live-control-tools.js +42 -5
  172. package/dist/session/no-session-diagnosis.d.ts +67 -2
  173. package/dist/session/no-session-diagnosis.js +236 -27
  174. package/dist/session/no-session-next-action.d.ts +46 -0
  175. package/dist/session/no-session-next-action.js +85 -0
  176. package/dist/session/no-session-watch.d.ts +40 -1
  177. package/dist/session/no-session-watch.js +174 -8
  178. package/dist/session/pending-commands.d.ts +12 -0
  179. package/dist/session/pending-commands.js +13 -1
  180. package/dist/session/ref-provenance.d.ts +54 -0
  181. package/dist/session/ref-provenance.js +61 -0
  182. package/dist/session/session-lease.js +1 -1
  183. package/dist/session/session-manager.d.ts +4 -0
  184. package/dist/session/session-manager.js +35 -5
  185. package/dist/session/session.d.ts +10 -1
  186. package/dist/session/session.js +146 -5
  187. package/dist/telemetry/app-instrumented.d.ts +8 -0
  188. package/dist/telemetry/app-instrumented.js +10 -0
  189. package/dist/telemetry/automation-hint.d.ts +17 -0
  190. package/dist/telemetry/automation-hint.js +71 -0
  191. package/dist/telemetry/bug-found.d.ts +15 -17
  192. package/dist/telemetry/bug-found.js +52 -21
  193. package/dist/telemetry/cli-telemetry.js +7 -1
  194. package/dist/telemetry/daemon-telemetry.js +24 -0
  195. package/dist/telemetry/feedback.js +1 -1
  196. package/dist/telemetry/init-telemetry.js +9 -1
  197. package/dist/telemetry/install-source.d.ts +56 -0
  198. package/dist/telemetry/install-source.js +74 -0
  199. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  200. package/dist/telemetry/instrumentation-stall.js +90 -0
  201. package/dist/telemetry/run-telemetry.js +4 -1
  202. package/dist/telemetry/session-metrics.d.ts +8 -0
  203. package/dist/telemetry/session-metrics.js +15 -0
  204. package/dist/telemetry/telemetry.d.ts +7 -1
  205. package/dist/telemetry/telemetry.js +26 -2
  206. package/dist/telemetry/tool-refused.d.ts +19 -0
  207. package/dist/telemetry/tool-refused.js +55 -0
  208. package/dist/telemetry/verification-of.js +46 -7
  209. package/dist/temp-dir.d.ts +27 -0
  210. package/dist/temp-dir.js +46 -0
  211. package/dist/tools/act-tools.js +209 -28
  212. package/dist/tools/assert-grade.d.ts +1 -1
  213. package/dist/tools/assert-grade.js +27 -5
  214. package/dist/tools/assert-verdict.js +49 -7
  215. package/dist/tools/browser-tools.js +10 -0
  216. package/dist/tools/contract-tools.js +4 -0
  217. package/dist/tools/dynamic-tools.js +16 -7
  218. package/dist/tools/error-recovery.d.ts +10 -0
  219. package/dist/tools/error-recovery.js +73 -9
  220. package/dist/tools/feedback-invite.js +1 -1
  221. package/dist/tools/invoke-tool.js +159 -26
  222. package/dist/tools/lease-hint.d.ts +30 -8
  223. package/dist/tools/lease-hint.js +65 -14
  224. package/dist/tools/lease-tools.d.ts +40 -1
  225. package/dist/tools/lease-tools.js +181 -83
  226. package/dist/tools/navigate-result.js +1 -1
  227. package/dist/tools/observe-tools.js +10 -4
  228. package/dist/tools/observed-nothing.js +1 -1
  229. package/dist/tools/react-grace.d.ts +52 -0
  230. package/dist/tools/react-grace.js +90 -0
  231. package/dist/tools/read-tools.js +67 -5
  232. package/dist/tools/resolve-target.d.ts +33 -0
  233. package/dist/tools/resolve-target.js +53 -0
  234. package/dist/tools/settle-in-flight.d.ts +94 -0
  235. package/dist/tools/settle-in-flight.js +146 -0
  236. package/dist/tools/snapshot-delta.d.ts +10 -0
  237. package/dist/tools/snapshot-delta.js +194 -9
  238. package/dist/tools/tool-kit.d.ts +8 -0
  239. package/dist/tools/tool-surface.d.ts +60 -0
  240. package/dist/tools/tool-surface.js +86 -1
  241. package/dist/tools/tools.js +61 -8
  242. package/dist/tools/unadvertised-help.js +9 -6
  243. package/docs/README.md +55 -11
  244. package/docs/actions.mdx +120 -0
  245. package/docs/agent-cheatsheet.md +95 -29
  246. package/docs/architecture.md +52 -35
  247. package/docs/benchmarks.md +127 -75
  248. package/docs/best-practices.mdx +216 -0
  249. package/docs/capabilities.mdx +534 -0
  250. package/docs/cli/affected.mdx +53 -0
  251. package/docs/cli/capsules.mdx +70 -0
  252. package/docs/cli/cloud.mdx +153 -0
  253. package/docs/cli/doctor.mdx +109 -0
  254. package/docs/cli/drive.mdx +74 -0
  255. package/docs/cli/feedback.mdx +67 -0
  256. package/docs/cli/gate.mdx +64 -0
  257. package/docs/cli/help.mdx +94 -0
  258. package/docs/cli/hunt.mdx +73 -0
  259. package/docs/cli/identify.mdx +76 -0
  260. package/docs/cli/init.mdx +121 -0
  261. package/docs/cli/kill.mdx +91 -0
  262. package/docs/cli/license.mdx +48 -0
  263. package/docs/cli/mcp.mdx +61 -0
  264. package/docs/cli/open.mdx +74 -0
  265. package/docs/cli/restart.mdx +60 -0
  266. package/docs/cli/rollback.mdx +35 -0
  267. package/docs/cli/serve.mdx +69 -0
  268. package/docs/cli/status.mdx +88 -0
  269. package/docs/cli/stop.mdx +64 -0
  270. package/docs/cli/telemetry.mdx +57 -0
  271. package/docs/cli/update.mdx +52 -0
  272. package/docs/cli/verify.mdx +76 -0
  273. package/docs/cli/version.mdx +39 -0
  274. package/docs/cli/watch.mdx +62 -0
  275. package/docs/cli.mdx +241 -0
  276. package/docs/debugging.md +16 -10
  277. package/docs/deploy-checks.md +45 -12
  278. package/docs/desktop.mdx +384 -0
  279. package/docs/docs.json +364 -0
  280. package/docs/enterprise.md +22 -16
  281. package/docs/faq.mdx +102 -0
  282. package/docs/fixtures.md +13 -7
  283. package/docs/flows.md +29 -23
  284. package/docs/for-agents.md +71 -0
  285. package/docs/frameworks.mdx +238 -0
  286. package/docs/gate-plan.md +48 -42
  287. package/docs/gates.md +28 -22
  288. package/docs/getting-started.md +129 -77
  289. package/docs/human-control.md +24 -18
  290. package/docs/index.mdx +143 -0
  291. package/docs/install-agentic.mdx +224 -0
  292. package/docs/install-manual.mdx +196 -0
  293. package/docs/instrumentation.mdx +238 -0
  294. package/docs/integration-patterns.md +41 -35
  295. package/docs/local-registry.md +19 -13
  296. package/docs/multi-agent-testing.md +19 -13
  297. package/docs/packages/babel-plugin.mdx +79 -0
  298. package/docs/packages/browser.mdx +111 -0
  299. package/docs/packages/core.mdx +116 -0
  300. package/docs/packages/electron.mdx +85 -0
  301. package/docs/packages/eslint-plugin.mdx +86 -0
  302. package/docs/packages/next.mdx +89 -0
  303. package/docs/packages/react.mdx +91 -0
  304. package/docs/packages/server.mdx +98 -0
  305. package/docs/packages/tauri.mdx +63 -0
  306. package/docs/packages/test.mdx +119 -0
  307. package/docs/packages/vite-plugin.mdx +94 -0
  308. package/docs/packages.mdx +218 -0
  309. package/docs/platform-integration.md +35 -26
  310. package/docs/predicates.mdx +248 -0
  311. package/docs/quickstart.mdx +257 -0
  312. package/docs/recipes.mdx +282 -0
  313. package/docs/skill-file.mdx +107 -0
  314. package/docs/state-management.mdx +199 -0
  315. package/docs/style.css +14 -0
  316. package/docs/system-map.md +50 -33
  317. package/docs/telemetry-contract.md +106 -52
  318. package/docs/telemetry-events.mdx +475 -0
  319. package/docs/telemetry.md +63 -55
  320. package/docs/testing.md +44 -6
  321. package/docs/token-efficiency.md +37 -27
  322. package/docs/tools-act-and-wait.mdx +173 -0
  323. package/docs/tools-act-sequence.mdx +137 -0
  324. package/docs/tools-act.mdx +95 -0
  325. package/docs/tools-assert.mdx +129 -0
  326. package/docs/tools-console.mdx +63 -0
  327. package/docs/tools-inspect.mdx +88 -0
  328. package/docs/tools-navigate.mdx +99 -0
  329. package/docs/tools-network.mdx +119 -0
  330. package/docs/tools-observe.mdx +145 -0
  331. package/docs/tools-overview.mdx +160 -0
  332. package/docs/tools-query.mdx +126 -0
  333. package/docs/tools-session-and-feedback.mdx +112 -0
  334. package/docs/tools-sessions.mdx +94 -0
  335. package/docs/tools-snapshot.mdx +129 -0
  336. package/docs/tools-state.mdx +104 -0
  337. package/docs/tools-tools-and-run.mdx +156 -0
  338. package/docs/tools-wait-for.mdx +85 -0
  339. package/docs/troubleshooting.mdx +263 -0
  340. package/docs/usage.md +209 -165
  341. package/docs/vs-chrome-devtools-mcp.mdx +123 -0
  342. package/docs/vs-playwright-mcp.mdx +125 -0
  343. package/docs/vs-screenshots.mdx +137 -0
  344. package/docs/what-is-reticle.mdx +63 -0
  345. package/docs/why-reticle.mdx +109 -0
  346. package/package.json +5 -5
  347. package/docs/desktop-apps.md +0 -256
  348. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  349. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  350. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  351. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  352. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  353. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  354. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  355. package/docs/matrix/MATRIX.md +0 -26
  356. package/docs/matrix/README.md +0 -59
  357. package/docs/mint.json +0 -61
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The sentence `status` owes the user, because `init` promised it.
3
+ *
4
+ * `init` closes with "run `npx @reticlehq/server status` — it confirms the app connected, or says
5
+ * exactly why it has not". On a real install, following that instruction printed `running: false,
6
+ * presence: "free"` and nothing else: no reason, no next step, no mention of the app. This is the last
7
+ * checkpoint in the setup path, so a user who arrives here and learns nothing has nowhere left to go.
8
+ *
9
+ * Deliberately thin. The differential already exists in `nextActionFor`, which ranks these causes from
10
+ * what the daemon knows and was rewritten this week to stop leading with a port mismatch; a second
11
+ * differential here could disagree with the first, and two confident answers pointing different ways
12
+ * is worse than one. This only supplies the facts the CLI has and renders the answer.
13
+ */
14
+ import { nextActionFor } from '../session/no-session-next-action.js';
15
+ /**
16
+ * What to do next, or `undefined` when a session is connected and there is nothing to fix.
17
+ *
18
+ * The success case has to stay silent: advice printed beside a working session reads as though
19
+ * something is still wrong, which is its own kind of lie.
20
+ */
21
+ export function statusNextAction(facts) {
22
+ if (facts.sessionCount > 0)
23
+ return undefined;
24
+ // Ahead of everything else, because it dominates: an app that was never wired cannot connect, and
25
+ // no advice about dev servers or ports applies until it is. `previouslyConnected` overrides it —
26
+ // an app CAN be wired by the Vite or Babel plugin with no `.reticle.json` at all, so a project
27
+ // that has connected here before is wired whatever this file says, and sending it back to `init`
28
+ // would be the same wrong answer in the other direction.
29
+ if (!facts.initialized && !facts.previouslyConnected) {
30
+ return ('no app has ever connected for this project, and there is no Reticle config here — so the ' +
31
+ 'tools are registered and the app itself is not instrumented. Those are two different halves ' +
32
+ 'of the install. Run `npx @reticlehq/server init` in the app directory, then start the dev ' +
33
+ 'server and load the page.');
34
+ }
35
+ if (!facts.running) {
36
+ // `running: false` reads as "Reticle is broken", and it usually means the opposite: the daemon is
37
+ // started by an agent, on demand, and idles out when nobody is driving. A user who has not
38
+ // attached an agent yet is exactly on track, and saying so is the whole job of this branch.
39
+ const wiring = facts.previouslyConnected
40
+ ? 'This project has connected before, so the wiring is correct.'
41
+ : '';
42
+ return (`no daemon is running on this port, which is normal — an agent starts it when it first calls a ` +
43
+ `Reticle tool, and it exits again when idle. ${wiring} Ask your agent to verify something, then ` +
44
+ `run status again.`).replace(/\s+/g, ' ');
45
+ }
46
+ // The daemon is up and no page has connected. `nextActionFor` owns the ranking; the CLI cannot see
47
+ // listening ports or a dev command from here, so it passes what it has and lets that function say
48
+ // what is missing rather than guessing at it.
49
+ const next = nextActionFor({
50
+ // The durable bit IS this side's `everConnected`. In-process the flag means "this daemon has served
51
+ // a session", which a daemon seconds old cannot know; from the CLI the honest equivalent is "an app
52
+ // for this project has connected on this port", which is the fact that decides whether the wiring
53
+ // is in question at all.
54
+ everConnected: facts.previouslyConnected,
55
+ initialized: facts.initialized,
56
+ listening: [],
57
+ dev: undefined,
58
+ previouslyConnected: facts.previouslyConnected,
59
+ });
60
+ return next.reason;
61
+ }
package/dist/cli.js CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { pathToFileURL } from 'node:url';
3
+ import { openFailureNote } from './cli/open-note.js';
3
4
  import { realpathSync } from 'node:fs';
4
5
  import { join } from 'node:path';
5
6
  import { stateDirProblem } from './daemon/state-dir.js';
7
+ import { statusNextAction } from './cli/status-next-action.js';
8
+ import { hasConnectedBefore } from './session/connection-memory.js';
6
9
  import { reticleStateHome } from './daemon/daemon.js';
7
10
  import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
8
11
  import { RETICLE_DEFAULT_PORT, ReticleDir, ReticleEnv } from '@reticlehq/core';
@@ -11,7 +14,7 @@ import { createNodeFileSystem } from './project/fs-port.js';
11
14
  import { affectedSavedFlows } from './flows/flow-sources.js';
12
15
  import { availableUpdate } from './update/update-nudge.js';
13
16
  import { handleUpdate, handleRollback } from './cli/cli-update-commands.js';
14
- import { start, startDaemon } from './index.js';
17
+ import { startDaemon } from './index.js';
15
18
  import { isCloudCommand, runCloudCommand } from './cli/cloud-cli.js';
16
19
  import { SERVER_VERSION } from './version/server-version.js';
17
20
  import { log } from './log.js';
@@ -22,10 +25,13 @@ import { installDaemonResilience, installProxyResilience, recordExitReason, Daem
22
25
  import { IdleShutdown, resolveIdleShutdownMs, resolveIdleCheckMs } from './daemon/idle-shutdown.js';
23
26
  import { DaemonHeartbeat, resolveHeartbeatMs } from './daemon/heartbeat.js';
24
27
  import { everServedToolCall } from './daemon/daemon-usefulness.js';
25
- import { fetchStatus, summarizeStatus, warnOnDaemonSkew, decideOpen, openInBrowser, } from './cli/cli-launch.js';
28
+ import { fetchStatus, summarizeStatus, warnOnDaemonSkew, decideOpen, openInBrowser, openCommand, } from './cli/cli-launch.js';
29
+ import { handleDrive } from './cli/drive-command.js';
26
30
  import { handleVerify } from './cli/cli-verify.js';
31
+ import { runKill } from './cli/cli-kill.js';
27
32
  import { summarizeHunt } from './hunt/hunt-report.js';
28
33
  import { runInit } from './init/run.js';
34
+ import { confirmInstall, nodeConfirmDeps } from './init/confirm.js';
29
35
  import { handleDoctor } from './cli/cli-doctor.js';
30
36
  import { buildNodeIo } from './init/node-io.js';
31
37
  import { describeLicense } from './license/license.js';
@@ -38,6 +44,7 @@ import { reportCliRun } from './telemetry/cli-telemetry.js';
38
44
  export { parseCliArgs, CLI_USAGE };
39
45
  function handleInit(parsed) {
40
46
  const cwd = process.cwd();
47
+ const io = buildNodeIo(cwd);
41
48
  const result = runInit({
42
49
  cwd,
43
50
  port: parsed.port,
@@ -45,9 +52,14 @@ function handleInit(parsed) {
45
52
  dryRun: parsed.dryRun,
46
53
  install: parsed.install,
47
54
  ...(parsed.app === undefined ? {} : { app: parsed.app }),
48
- }, buildNodeIo(cwd));
49
- if (!result.ok)
50
- process.exit(1);
55
+ // The outcome is reported by confirmInstall instead, once it knows whether an app connected —
56
+ // `init` writing files was never the same thing as `init` working (#269).
57
+ deferOutcome: true,
58
+ }, io);
59
+ void confirmInstall(result, io, nodeConfirmDeps(parsed.port ?? RETICLE_DEFAULT_PORT)).then(() => {
60
+ if (!result.ok)
61
+ process.exit(1);
62
+ });
51
63
  }
52
64
  function handleServe(parsed) {
53
65
  void serveWithHonestExit(parsed);
@@ -190,17 +202,56 @@ function handleStop(port, quiet) {
190
202
  }
191
203
  }, 100);
192
204
  }
205
+ /**
206
+ * `reticle restart` — free the port, then put a daemon back on it and prove it bound.
207
+ *
208
+ * The two halves are reported separately on purpose. A restart that killed the old daemon and then
209
+ * could not start a new one leaves the user in a different place from one that could not kill
210
+ * anything at all, and a single `ok: false` cannot tell them which happened.
211
+ */
212
+ async function handleRestart(port, force) {
213
+ const freed = await runKill(port, force);
214
+ if (!freed) {
215
+ log('reticle_restart_aborted', {
216
+ port,
217
+ reason: 'the port was not freed, so nothing was started — the old holder is still there',
218
+ });
219
+ process.exit(1);
220
+ return;
221
+ }
222
+ // Reuses `serve`'s path, which waits for the daemon to ANSWER before claiming it started and exits
223
+ // non-zero when it does not. A restart that reports success for a daemon that never bound is the
224
+ // same lie `serve` already stopped telling.
225
+ await serveWithHonestExit({ port, headless: true, http: false });
226
+ }
227
+ /** `{ nextAction }` when there is one, `{}` when a session is connected — so the success case is silent. */
228
+ function withNextAction(facts) {
229
+ const next = statusNextAction(facts);
230
+ return next === undefined ? {} : { nextAction: next };
231
+ }
193
232
  function handleStatus(port) {
194
233
  const pid = readPid(port);
234
+ // Durable, so it survives the daemon idling out — which is the state `status` is most often run in.
235
+ const projectId = readProjectId(process.cwd());
236
+ const previouslyConnected = hasConnectedBefore(reticleStateHome(), port, projectId);
237
+ // Whether `init` has run HERE. Registering the MCP server does not wire the app, and more than one
238
+ // path does the first without the second — so this is the commonest reason `status` has nothing to
239
+ // report, and it was not among the facts this command could state.
240
+ const initialized = projectId !== undefined;
195
241
  if (null === pid || !isAlive(pid)) {
196
242
  // `running: false` on its own has been reported about a port that was demonstrably occupied,
197
243
  // because the pid file is not the port. Ask the port before answering.
198
244
  void probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then((presence) => {
245
+ const running = presenceIsUsable(presence);
199
246
  log('reticle_status', {
200
247
  port,
201
- running: presenceIsUsable(presence),
248
+ running,
202
249
  presence,
203
250
  ...(presence === PortPresence.FOREIGN ? { reason: describePresence(presence, port) } : {}),
251
+ // `init` promises this command says why the app has not connected. Without it the answer was
252
+ // `running: false` and nothing else, which reads as "Reticle is broken" for what is usually
253
+ // just a daemon that has not been asked to do anything yet.
254
+ ...withNextAction({ running, sessionCount: 0, previouslyConnected, initialized }),
204
255
  });
205
256
  });
206
257
  return;
@@ -213,10 +264,23 @@ function handleStatus(port) {
213
264
  const update = availableUpdate();
214
265
  const nudge = update === undefined ? {} : { updateAvailable: update };
215
266
  if (payload === undefined) {
216
- log('reticle_status', { port, running: true, pid, ...nudge });
267
+ log('reticle_status', {
268
+ port,
269
+ running: true,
270
+ pid,
271
+ ...nudge,
272
+ ...withNextAction({ running: true, sessionCount: 0, previouslyConnected, initialized }),
273
+ });
217
274
  return;
218
275
  }
219
- log('reticle_status', { port, running: true, pid, ...summarizeStatus(payload), ...nudge });
276
+ const summary = summarizeStatus(payload);
277
+ // Only when the daemon did NOT already explain itself. It has the whole diagnosis in-process and
278
+ // puts it on the wire as `why`; printing a second, thinner opinion beside it risks two confident
279
+ // answers pointing different ways, which is worse than one.
280
+ const next = summary.why === undefined
281
+ ? withNextAction({ running: true, ...summary, previouslyConnected, initialized })
282
+ : {};
283
+ log('reticle_status', { port, running: true, pid, ...summary, ...next, ...nudge });
220
284
  });
221
285
  }
222
286
  /** Print the running package version (resolved once in server-version.ts). */
@@ -288,13 +352,20 @@ async function handleHunt(dir) {
288
352
  }
289
353
  /** Ensure a daemon is reachable on `port` (probe the real port; spawn + wait only if nothing's there). */
290
354
  function ensureDaemon(port) {
291
- return probeDaemon(port).then(async (listening) => {
355
+ return probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus }).then(async (presence) => {
292
356
  // Attaching to whatever already owns the port is the whole point of a daemon — but it means an
293
357
  // upgrade does NOT take effect until that daemon dies, and nothing used to say so. Say it here,
294
358
  // where both versions are in hand, and keep attaching: killing another agent's daemon on a
295
359
  // version bump is worse than a loud warning.
296
- if (listening)
360
+ if (presenceIsUsable(presence))
297
361
  return warnOnDaemonSkew(port);
362
+ // Not free, not a daemon: a stranger holds the port. Spawning here is guaranteed to fail —
363
+ // the child cannot bind — and `waitForDaemon` would then report READY anyway, because its
364
+ // probe is the same bare TCP connect and the stranger accepts. That is the "serve reports
365
+ // success for a daemon that never bound" shape. Refuse instead, with the sentence `doctor`
366
+ // says.
367
+ if (PortPresence.FREE !== presence)
368
+ throw new Error(describePresence(presence, port));
298
369
  const scriptPath = process.argv[1];
299
370
  if (scriptPath === undefined)
300
371
  throw new Error('cannot locate the reticle daemon script');
@@ -364,8 +435,12 @@ function handleOpen(requestedPort, url) {
364
435
  log('reticle_open', {
365
436
  port,
366
437
  error: `could not launch a browser: ${launchError}`,
367
- recovery: 'Open the url yourself, or run `reticle doctor` — it checks the pieces this command ' +
368
- 'depends on.',
438
+ // Points at the OS handler, not at `reticle doctor`. Doctor's browser check is about
439
+ // Reticle's own Chromium, which this command never touches — the same misdirect the
440
+ // no-session note below spells out at length, left standing here for one more release.
441
+ recovery: 'Nothing was opened. This command asks the OS to open a url in your default browser ' +
442
+ `(\`${openCommand(decision.url, process.platform).cmd}\` on this platform) and that ` +
443
+ 'failed, so the fix is on the OS side: open the url yourself, or set a default browser.',
369
444
  });
370
445
  process.exit(1);
371
446
  return;
@@ -376,6 +451,7 @@ function handleOpen(requestedPort, url) {
376
451
  const connected = await waitForNewSession(port, sessions.length);
377
452
  log('reticle_open', {
378
453
  port,
454
+ ...(port === requestedPort ? {} : { requestedPort }),
379
455
  opened: decision.url,
380
456
  connected,
381
457
  ...(connected
@@ -388,14 +464,7 @@ function handleOpen(requestedPort, url) {
388
464
  // about a browser THIS command never uses, so a missing Chromium reads as the
389
465
  // explanation for a session that is missing for an unrelated reason. Both misdirects
390
466
  // were reported from the field, each costing several calls of app-side wiring hunt.
391
- note: `the URL was handed to the system default browser (this command does not use ` +
392
- `Reticle's own Chromium, so a chromium warning from \`reticle doctor\` is unrelated ` +
393
- `to this). No Reticle session appeared. By far the likeliest cause is that the app ` +
394
- `carries no Reticle SDK, or dials a port other than ${String(port)} — run ` +
395
- `\`reticle init\` in the app's directory and restart its dev server. If the app IS ` +
396
- `wired, give the page a moment and check the browser console: the SDK announces its ` +
397
- `own connect failures there, including the one it refuses to make from a ` +
398
- `non-localhost host without allowNonLocalhost.`,
467
+ note: openFailureNote(port, requestedPort),
399
468
  }),
400
469
  });
401
470
  })
@@ -444,6 +513,12 @@ function handleDaemonInner(parsed) {
444
513
  // which is after everything below has run. Without it the exit line says `code: 0` and a
445
514
  // reader cannot tell a tidy stop from the bridge disappearing — see #123.
446
515
  recordExitReason(reason);
516
+ // Say so on the wire before anything closes. The proxy on the other end sees a clean stream
517
+ // end whether we retired on schedule or died under it, and it has no other way to tell —
518
+ // which is how a designed shutdown came to dominate the metric that means "the agent lost
519
+ // its tools". Told, the proxy classifies its own drop honestly. First, because the send has
520
+ // to reach a socket that is still open.
521
+ server.announceShutdown?.();
447
522
  // Awaited before the close/exit chain: `process.exit(0)` kills an in-flight POST, and this is
448
523
  // the one event carrying the whole session. A failed send resolves anyway (emit swallows its
449
524
  // own errors), so this can delay the exit by at most the send timeout, never prevent it.
@@ -538,8 +613,15 @@ function handleMcp(opts) {
538
613
  * disappeared mid-session with nothing said. Respawning here makes the reconnect self-healing.
539
614
  */
540
615
  const ensure = async () => {
541
- if (await probeDaemon(port))
616
+ // The same question every other surface asks. It used to be a bare TCP connect, so a stranger on
617
+ // the bridge port answered "a daemon is here" — the proxy connected, the stream ended, and each
618
+ // client request woke into the identical non-answer. Rejecting here is what puts the proxy
619
+ // dormant with a reason, instead of pretending the wake succeeded.
620
+ const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
621
+ if (presenceIsUsable(presence))
542
622
  return;
623
+ if (PortPresence.FREE !== presence)
624
+ throw new Error(describePresence(presence, port));
543
625
  const scriptPath = process.argv[1];
544
626
  if (scriptPath === undefined) {
545
627
  log('reticle_mcp_no_script', {});
@@ -560,8 +642,13 @@ function handleMcp(opts) {
560
642
  daemonArgs.push(HTTP_TOKEN_FLAG, httpToken);
561
643
  }
562
644
  spawnDaemon(process.execPath, scriptPath, daemonArgs, port);
563
- log('reticle_mcp_daemon_started', { port, ...(driveUrl !== undefined ? { driveUrl } : {}) });
645
+ // Announce the daemon only once the PORT ACCEPTS. This line used to be written the instant the
646
+ // child was spawned, which on a Windows first bootstrap meant `reticle_mcp_daemon_started`
647
+ // followed about ten seconds later by `reticle_mcp_daemon_unavailable` and a first
648
+ // `reticle_sessions` that expired. A readiness signal that precedes readiness is worse than
649
+ // none: a client that believes it stops waiting for the thing that has not happened.
564
650
  await waitForDaemon(port);
651
+ log('reticle_mcp_daemon_started', { port, ...(driveUrl !== undefined ? { driveUrl } : {}) });
565
652
  };
566
653
  // Start the proxy WHATEVER happened to the daemon.
567
654
  //
@@ -601,22 +688,6 @@ function handleMcp(opts) {
601
688
  });
602
689
  });
603
690
  }
604
- function handleLegacyDrive(parsed) {
605
- const options = {
606
- port: parsed.port,
607
- driveUrl: parsed.driveUrl,
608
- headless: parsed.headless,
609
- };
610
- start(options)
611
- .then(() => {
612
- log('reticle_started', { port: parsed.port });
613
- })
614
- .catch((error) => {
615
- const message = error instanceof Error ? error.message : String(error);
616
- log('reticle_start_failed', { error: message });
617
- process.exit(1);
618
- });
619
- }
620
691
  function main() {
621
692
  // Before anything reads process.env — notably the telemetry gate and the bridge's security
622
693
  // options — fold in a project-local `.env`. Values already in the environment always win.
@@ -669,6 +740,15 @@ function main() {
669
740
  case 'stop':
670
741
  handleStop(parsed.port, parsed.quiet);
671
742
  break;
743
+ case 'kill':
744
+ void runKill(parsed.port, parsed.force).then((freed) => {
745
+ if (!freed)
746
+ process.exit(1);
747
+ });
748
+ break;
749
+ case 'restart':
750
+ void handleRestart(parsed.port, parsed.force);
751
+ break;
672
752
  case 'status':
673
753
  handleStatus(parsed.port);
674
754
  break;
@@ -697,7 +777,7 @@ function main() {
697
777
  handleOpen(parsed.port, parsed.url);
698
778
  break;
699
779
  case 'drive':
700
- handleLegacyDrive(parsed);
780
+ handleDrive(parsed);
701
781
  break;
702
782
  case 'verify':
703
783
  handleVerify(parsed);
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { ReticleTool } from '../tools/tool-names.js';
3
3
  import { asNumber, asString } from '../tools/tools-helpers.js';
4
4
  import { crawl } from './crawl.js';
5
+ import { routeFromUrl, routesFromEvents } from '../project/learned-routes.js';
5
6
  const nodeSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6
7
  /**
7
8
  * The autonomous "smart monkey" tool. Builds on reticle_explore (which only LISTS) by
@@ -58,8 +59,10 @@ export const CRAWL_TOOLS = [
58
59
  .describe('Present only when the page exceeded one snapshot, so controls past the cap were never listed. When present, a zero-anomaly result does NOT mean the app is clean.'),
59
60
  truncated: z.boolean(),
60
61
  },
61
- handler: (deps, args) => {
62
+ handler: async (deps, args) => {
62
63
  const session = deps.sessions.resolve(asString(args['sessionId']));
64
+ const since = session.elapsed();
65
+ const initialRoute = routeFromUrl(session.url);
63
66
  const maxSteps = asNumber(args['maxSteps']);
64
67
  const settleMs = asNumber(args['settleMs']);
65
68
  const scope = asString(args['scope']);
@@ -69,7 +72,14 @@ export const CRAWL_TOOLS = [
69
72
  ...(scope !== undefined ? { scope } : {}),
70
73
  ...(true === args['confirmDangerous'] ? { confirmDangerous: true } : {}),
71
74
  };
72
- return crawl(session, opts, nodeSleep);
75
+ const report = await crawl(session, opts, nodeSleep);
76
+ const routes = [
77
+ ...(initialRoute === undefined ? [] : [initialRoute]),
78
+ ...routesFromEvents(session.eventsSince(since)),
79
+ ];
80
+ if (routes.length > 0)
81
+ await deps.project.recordRoutes(routes);
82
+ return report;
73
83
  },
74
84
  },
75
85
  ];
@@ -39,6 +39,19 @@ export interface CrawlAnomaly {
39
39
  }
40
40
  /** Said out loud when the page was larger than one snapshot, so a clean sweep cannot mean "all clear". */
41
41
  export declare const CAPPED_SNAPSHOT_NOTE = "the page exceeded one snapshot, so controls past the cap were never seen \u2014 interactiveFound is a floor, not a total; re-run with a narrower `scope` to cover the rest";
42
+ /**
43
+ * Said out loud when the crawl's OWN clicks put controls on the page that it then never visited.
44
+ *
45
+ * The enumeration happens once, before the first click, so a control that only exists after a login,
46
+ * a modal or a tab switch is invisible to this run. Measured on the bench app: a crawl of the login
47
+ * screen visited three controls, its click on "Sign in" produced seven more, and it returned
48
+ * `truncated: false` with an empty anomaly list and nine of twelve steps unused. Read as a sweep,
49
+ * that is a clean bill of health for an app the crawl never entered.
50
+ *
51
+ * The same words as the capped-snapshot case on purpose — it is the same idea (interactiveFound is a
52
+ * floor) reached by a different route, and a second vocabulary for it would just be more to learn.
53
+ */
54
+ export declare function revealedControlsNote(count: number): string;
42
55
  export interface CrawlReport {
43
56
  interactiveFound: number;
44
57
  stepsRun: number;
@@ -6,6 +6,23 @@ import { ReticleTool } from '../tools/tool-names.js';
6
6
  import { findContradictions } from '../events/contradictions.js';
7
7
  /** Said out loud when the page was larger than one snapshot, so a clean sweep cannot mean "all clear". */
8
8
  export const CAPPED_SNAPSHOT_NOTE = 'the page exceeded one snapshot, so controls past the cap were never seen — interactiveFound is a floor, not a total; re-run with a narrower `scope` to cover the rest';
9
+ /**
10
+ * Said out loud when the crawl's OWN clicks put controls on the page that it then never visited.
11
+ *
12
+ * The enumeration happens once, before the first click, so a control that only exists after a login,
13
+ * a modal or a tab switch is invisible to this run. Measured on the bench app: a crawl of the login
14
+ * screen visited three controls, its click on "Sign in" produced seven more, and it returned
15
+ * `truncated: false` with an empty anomaly list and nine of twelve steps unused. Read as a sweep,
16
+ * that is a clean bill of health for an app the crawl never entered.
17
+ *
18
+ * The same words as the capped-snapshot case on purpose — it is the same idea (interactiveFound is a
19
+ * floor) reached by a different route, and a second vocabulary for it would just be more to learn.
20
+ */
21
+ export function revealedControlsNote(count) {
22
+ return (`the page changed as you clicked: ${String(count)} control(s) that did not exist when this crawl ` +
23
+ 'started were never visited — interactiveFound is a floor, not a total; re-run the crawl from ' +
24
+ 'the state you are now in to cover them');
25
+ }
9
26
  /** Any buffer event that proves the app reacted to a click (vs a dead/no-op control). */
10
27
  function isActivity(e) {
11
28
  return (e.type === EventType.NET_REQUEST ||
@@ -50,6 +67,25 @@ export function legitimatelyInert(desc) {
50
67
  return true;
51
68
  return /\b(textbox|searchbox|combobox|spinbutton)\b/.test(desc);
52
69
  }
70
+ /**
71
+ * How many interactive controls exist now that did not exist when the crawl enumerated.
72
+ *
73
+ * Keyed on ref, which is the element's identity: a control that re-rendered keeps its ref and is not
74
+ * new, and one that vanished simply does not appear. Returns 0 rather than throwing if the snapshot
75
+ * fails — this is a disclosure, and failing the whole crawl to report on coverage would be worse
76
+ * than the silence it is fixing.
77
+ */
78
+ async function countRevealed(session, opts, before) {
79
+ const snap = await session.command(ReticleCommand.SNAPSHOT, {
80
+ mode: 'interactive',
81
+ ...(opts.scope !== undefined ? { scope: opts.scope } : {}),
82
+ });
83
+ if (!snap.ok)
84
+ return 0;
85
+ const tree = (snap.result ?? {}).tree ?? '';
86
+ const known = new Set(before.map((i) => (i.ref !== '' ? i.ref : i.desc)));
87
+ return parseInteractive(tree).filter((i) => !known.has(i.ref !== '' ? i.ref : i.desc)).length;
88
+ }
53
89
  export async function crawl(session, opts, sleep) {
54
90
  const maxSteps = opts.maxSteps ?? CRAWL_DEFAULTS.MAX_STEPS;
55
91
  const settleMs = opts.settleMs ?? CRAWL_DEFAULTS.SETTLE_MS;
@@ -168,6 +204,14 @@ export async function crawl(session, opts, sleep) {
168
204
  });
169
205
  }
170
206
  }
207
+ // One more look, AFTER the clicks. The enumeration above happened before any of them, so anything
208
+ // the crawl itself unlocked — a dashboard behind a login, a modal's contents, a tab's panel — was
209
+ // never in `items` and never had a chance to be visited. Counted by ref against what was
210
+ // enumerated, so a control that merely moved or re-rendered under the same ref is not miscounted
211
+ // as new, and a surface that SHRANK (a modal closed) reports nothing, because nothing went
212
+ // unvisited. Skipped when the snapshot was already capped: that case has its own note and a second
213
+ // one would only muddy it.
214
+ const revealed = coverageCapped ? 0 : await countRevealed(session, opts, items);
171
215
  return {
172
216
  interactiveFound: items.length,
173
217
  stepsRun,
@@ -177,8 +221,12 @@ export async function crawl(session, opts, sleep) {
177
221
  // True when coverage was bounded for EITHER reason: the step budget ran out, or the page was
178
222
  // bigger than one snapshot. Conflating "I stopped early" with "I saw everything" is what turns a
179
223
  // partial sweep into "all controls healthy".
180
- truncated: items.length > stepsRun || coverageCapped,
181
- ...(coverageCapped ? { coverageNote: CAPPED_SNAPSHOT_NOTE } : {}),
224
+ truncated: items.length > stepsRun || coverageCapped || revealed > 0,
225
+ ...(coverageCapped
226
+ ? { coverageNote: CAPPED_SNAPSHOT_NOTE }
227
+ : revealed > 0
228
+ ? { coverageNote: revealedControlsNote(revealed) }
229
+ : {}),
182
230
  // A bare `stepsRun: 0` made "the page had no controls" and "it had 34 and none were clicked" the
183
231
  // same answer. See crawl-empty.
184
232
  ...(() => {
@@ -1,4 +1,5 @@
1
1
  import { ContradictionKind, type ReticleEvent } from '@reticlehq/core';
2
+ import { type DeclaredNetFailure } from './declared.js';
2
3
  /**
3
4
  * The contradiction hunter.
4
5
  *
@@ -35,6 +36,20 @@ export interface ContradictionOptions {
35
36
  * contain. See `findUnitMismatches` for the measured case this exists for.
36
37
  */
37
38
  prior?: readonly ReticleEvent[] | undefined;
39
+ /**
40
+ * Event time at which the action that opened this window was dispatched — the attribution floor.
41
+ *
42
+ * `duplicate-request` claims "one user action was performed", and that claim is only sound over ONE
43
+ * action's window. `reticle_observe` takes a caller-supplied window that can be arbitrarily wide, so
44
+ * two legitimate separate saves to the same endpoint read as a double submit — and the finding then
45
+ * sat on every verdict for that tab. Undefined means nothing attributed the window to an action, and
46
+ * the rule stays silent rather than accusing an app of something nobody can show it did.
47
+ *
48
+ * ponytail: a NET_REQUEST is stamped when it COMPLETED, so a write dispatched before the action and
49
+ * landing after it counts as inside. Keying on the matching NET_PENDING would fix that; it has not
50
+ * been worth the second index.
51
+ */
52
+ actionSince?: number | undefined;
38
53
  /**
39
54
  * DOM mutations observed INSIDE the target's own subtree, as the act tool measured them.
40
55
  *
@@ -49,5 +64,36 @@ export interface ContradictionOptions {
49
64
  * test, which is weaker but never wrong in the direction of a false accusation.
50
65
  */
51
66
  mutatedWithin?: number | undefined;
67
+ /**
68
+ * Requests the CALLER declared would fail, read off the oracle it wrote before acting.
69
+ *
70
+ * `ui-advanced-request-failed` cannot tell "the app swallowed the error and carried on" from "the
71
+ * app rendered the error, which is the behaviour under test" — both are a moved DOM beside a
72
+ * failed request. `failureAcknowledged` recovers the first from the app's own state, and an app
73
+ * that renders its error straight into the DOM without touching a store defeats it.
74
+ *
75
+ * The declaration is the missing evidence, and it costs no new API: an agent verifying an error
76
+ * path already writes `{ net, POST, /api/login, status: 500 }` into the predicate. Measured in the
77
+ * field: every branch of a login error path (500, 401, 503) passed every declared clause and every
78
+ * run still returned `verified: "no" / contradicted`, so error handling — empty states, offline
79
+ * banners, 4xx/5xx messaging, lockouts — was the code least able to reach a green verdict and the
80
+ * most worth verifying.
81
+ *
82
+ * Scoped as narrowly as it can be: it suppresses ONLY the heuristic rule that cannot see the
83
+ * difference. A success signal fired over the declared failure still contradicts, and a server
84
+ * fault blamed on the user is still misattributed — see the negative controls in
85
+ * `contradictions.declared.test.ts`.
86
+ */
87
+ expectedFailures?: readonly DeclaredNetFailure[] | undefined;
88
+ /**
89
+ * The caller declared an on-screen consequence and it HELD in this window.
90
+ *
91
+ * `route-rendered-nothing` infers a blank destination from the absence of DOM events, and a
92
+ * verdict that names it beside an element match — heading found, with its source file and line —
93
+ * is a clause its own evidence disproves. Positive evidence outranks the absence it is inferred
94
+ * from. Undefined/false leaves the rule exactly as it was, which is the case that catches a route
95
+ * with no view.
96
+ */
97
+ renderProved?: boolean | undefined;
52
98
  }
53
99
  export declare function findContradictions(allEvents: readonly ReticleEvent[], options?: ContradictionOptions): Contradiction[];