@reticlehq/server 2.12.0 → 2.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (386) hide show
  1. package/SKILL.md +67 -109
  2. package/dist/bridge/auth-failure-reason.d.ts +6 -2
  3. package/dist/bridge/auth-failure-reason.js +15 -8
  4. package/dist/bridge/bridge.d.ts +18 -1
  5. package/dist/bridge/bridge.js +247 -164
  6. package/dist/bridge/pairing-token.d.ts +5 -0
  7. package/dist/bridge/pairing-token.js +28 -0
  8. package/dist/bridge/protocol-skew.d.ts +37 -0
  9. package/dist/bridge/protocol-skew.js +45 -0
  10. package/dist/capsule/capsule-store.d.ts +6 -6
  11. package/dist/capsule/causal-summary.js +7 -2
  12. package/dist/cli/cli-doctor.js +31 -1
  13. package/dist/cli/cli-launch.d.ts +11 -1
  14. package/dist/cli/cli-launch.js +59 -10
  15. package/dist/cli/cli-parse.d.ts +15 -1
  16. package/dist/cli/cli-parse.js +212 -8
  17. package/dist/cli/cli-verify.d.ts +1 -0
  18. package/dist/cli/cli-verify.js +3 -2
  19. package/dist/cli/cloud-cli.js +428 -259
  20. package/dist/cli/cloud-keystore.d.ts +37 -0
  21. package/dist/cli/cloud-keystore.js +97 -0
  22. package/dist/cli/cloud-kit.d.ts +37 -0
  23. package/dist/cli/cloud-kit.js +121 -0
  24. package/dist/cli/cloud-login.d.ts +17 -0
  25. package/dist/cli/cloud-login.js +183 -0
  26. package/dist/cli/cloud-session.d.ts +48 -0
  27. package/dist/cli/cloud-session.js +53 -0
  28. package/dist/cli/config-discovery.js +1 -0
  29. package/dist/cli/daemon-start-options.d.ts +32 -0
  30. package/dist/cli/daemon-start-options.js +38 -0
  31. package/dist/cli/device-flow.d.ts +76 -0
  32. package/dist/cli/device-flow.js +56 -0
  33. package/dist/cli/doctor-rows.d.ts +9 -0
  34. package/dist/cli/doctor-rows.js +9 -0
  35. package/dist/cli/drive-attach.d.ts +13 -0
  36. package/dist/cli/drive-attach.js +18 -0
  37. package/dist/cli/drive-command.js +7 -3
  38. package/dist/cli/mcp-command.js +51 -20
  39. package/dist/cli/project-name.d.ts +30 -0
  40. package/dist/cli/project-name.js +44 -0
  41. package/dist/cli/status-next-action.d.ts +14 -0
  42. package/dist/cli/status-next-action.js +4 -4
  43. package/dist/cli.d.ts +1 -0
  44. package/dist/cli.js +134 -80
  45. package/dist/cloud/cloud-config.d.ts +22 -1
  46. package/dist/cloud/cloud-config.js +64 -5
  47. package/dist/cloud/cloud-sync.d.ts +5 -5
  48. package/dist/cloud/cloud-sync.js +33 -2
  49. package/dist/cloud/sync-cycle.d.ts +110 -0
  50. package/dist/cloud/sync-cycle.js +270 -0
  51. package/dist/cloud/sync-daemon.d.ts +69 -0
  52. package/dist/cloud/sync-daemon.js +260 -0
  53. package/dist/cloud/sync-disk.d.ts +9 -0
  54. package/dist/cloud/sync-disk.js +191 -0
  55. package/dist/cloud/sync-hash.d.ts +2 -0
  56. package/dist/cloud/sync-hash.js +35 -0
  57. package/dist/crawl/crawl-tools.js +5 -3
  58. package/dist/crawl/crawl.d.ts +3 -0
  59. package/dist/crawl/crawl.js +5 -0
  60. package/dist/daemon/daemon-resolve.d.ts +43 -0
  61. package/dist/daemon/daemon-resolve.js +69 -0
  62. package/dist/daemon/dev-servers.d.ts +15 -0
  63. package/dist/daemon/dev-servers.js +68 -0
  64. package/dist/daemon/wake-decision.d.ts +32 -0
  65. package/dist/daemon/wake-decision.js +38 -0
  66. package/dist/domain/domain-tools.js +2 -2
  67. package/dist/events/contradictions.d.ts +18 -0
  68. package/dist/events/contradictions.js +134 -21
  69. package/dist/events/declared.d.ts +7 -4
  70. package/dist/events/declared.js +15 -6
  71. package/dist/events/echo-mismatch.js +64 -14
  72. package/dist/events/event-filters.d.ts +8 -0
  73. package/dist/events/event-filters.js +19 -2
  74. package/dist/events/predicate-eval.js +53 -5
  75. package/dist/events/predicate-precheck.d.ts +8 -0
  76. package/dist/events/predicate-precheck.js +41 -0
  77. package/dist/events/predicate-route.d.ts +14 -0
  78. package/dist/events/predicate-route.js +36 -8
  79. package/dist/events/predicate.d.ts +5 -0
  80. package/dist/events/predicate.js +37 -5
  81. package/dist/events/split-text-miss.d.ts +30 -0
  82. package/dist/events/split-text-miss.js +44 -0
  83. package/dist/flows/flake.d.ts +6 -6
  84. package/dist/flows/flow-memory-consult.d.ts +55 -0
  85. package/dist/flows/flow-memory-consult.js +38 -0
  86. package/dist/flows/flow-replay-run.d.ts +18 -7
  87. package/dist/flows/flow-replay-run.js +239 -22
  88. package/dist/flows/flow-replay.js +11 -2
  89. package/dist/flows/flow-step-runners.d.ts +8 -0
  90. package/dist/flows/flow-step-runners.js +8 -2
  91. package/dist/flows/flow-store-for-session.d.ts +31 -0
  92. package/dist/flows/flow-store-for-session.js +33 -0
  93. package/dist/flows/flow-tools.js +58 -32
  94. package/dist/flows/flows.d.ts +8 -0
  95. package/dist/flows/flows.js +468 -2
  96. package/dist/flows/heal-run.js +5 -2
  97. package/dist/flows/predicate-to-expect.d.ts +17 -12
  98. package/dist/flows/predicate-to-expect.js +30 -12
  99. package/dist/flows/recordings.d.ts +13 -1
  100. package/dist/flows/recordings.js +17 -2
  101. package/dist/flows/replay.d.ts +22 -2
  102. package/dist/flows/replay.js +28 -2
  103. package/dist/flows/verify-change-tools.js +5 -2
  104. package/dist/honesty/blind-spots.d.ts +55 -2
  105. package/dist/honesty/blind-spots.js +63 -3
  106. package/dist/honesty/honesty.d.ts +24 -0
  107. package/dist/honesty/honesty.js +25 -1
  108. package/dist/honesty/instrumentation-gaps.d.ts +9 -0
  109. package/dist/honesty/instrumentation-gaps.js +29 -2
  110. package/dist/honesty/verified.d.ts +2 -1
  111. package/dist/honesty/verified.js +16 -1
  112. package/dist/http-server.js +12 -0
  113. package/dist/impact/impact-recorder.d.ts +31 -8
  114. package/dist/impact/impact-recorder.js +104 -21
  115. package/dist/impact/impact-store.d.ts +15 -1
  116. package/dist/impact/impact-store.js +43 -2
  117. package/dist/index.js +113 -7
  118. package/dist/init/agent-rules.d.ts +13 -6
  119. package/dist/init/agent-rules.js +23 -12
  120. package/dist/init/astro-patch.d.ts +16 -0
  121. package/dist/init/astro-patch.js +28 -0
  122. package/dist/init/capabilities.d.ts +4 -1
  123. package/dist/init/capabilities.js +22 -2
  124. package/dist/init/confirm.d.ts +31 -7
  125. package/dist/init/confirm.js +48 -8
  126. package/dist/init/cra.d.ts +20 -4
  127. package/dist/init/cra.js +57 -17
  128. package/dist/init/csp-check.js +17 -2
  129. package/dist/init/csp-doctor.d.ts +8 -1
  130. package/dist/init/csp-doctor.js +8 -3
  131. package/dist/init/dev-script.d.ts +3 -1
  132. package/dist/init/dev-script.js +8 -5
  133. package/dist/init/format-generated.d.ts +18 -0
  134. package/dist/init/format-generated.js +39 -0
  135. package/dist/init/install-hint.d.ts +9 -0
  136. package/dist/init/install-hint.js +29 -0
  137. package/dist/init/mcp-clients.d.ts +7 -0
  138. package/dist/init/mcp-clients.js +7 -0
  139. package/dist/init/mcp.d.ts +1 -1
  140. package/dist/init/mcp.js +1 -1
  141. package/dist/init/node-io.js +11 -1
  142. package/dist/init/plan-framework.js +81 -45
  143. package/dist/init/plan.d.ts +20 -7
  144. package/dist/init/plan.js +20 -45
  145. package/dist/init/preflight.d.ts +28 -0
  146. package/dist/init/preflight.js +38 -0
  147. package/dist/init/run.d.ts +32 -9
  148. package/dist/init/run.js +141 -119
  149. package/dist/init/slash-command.d.ts +1 -1
  150. package/dist/init/snippets.js +41 -25
  151. package/dist/init/vite-config.d.ts +1 -1
  152. package/dist/init/vite-config.js +39 -25
  153. package/dist/init/workspace-apps.d.ts +17 -10
  154. package/dist/init/workspace-apps.js +110 -0
  155. package/dist/input/network-mock-tools.js +3 -6
  156. package/dist/input/real-input.js +25 -5
  157. package/dist/input/scroll-tools.js +4 -6
  158. package/dist/input/viewport-tools.js +5 -4
  159. package/dist/intent/inline-intent.d.ts +10 -19
  160. package/dist/intent/inline-intent.js +91 -1
  161. package/dist/intent/intent-shard-store.d.ts +72 -0
  162. package/dist/intent/intent-shard-store.js +203 -0
  163. package/dist/intent/intent-shard.d.ts +411 -0
  164. package/dist/intent/intent-shard.js +176 -0
  165. package/dist/intent/intent-store.d.ts +12 -0
  166. package/dist/intent/intent-store.js +21 -0
  167. package/dist/intent/intent-subject.d.ts +45 -0
  168. package/dist/intent/intent-subject.js +177 -0
  169. package/dist/intent/intent-tools.js +65 -2
  170. package/dist/intent/open-intents.d.ts +21 -0
  171. package/dist/intent/open-intents.js +15 -0
  172. package/dist/license/license-env.d.ts +29 -0
  173. package/dist/license/license-env.js +148 -0
  174. package/dist/loopback-agent.d.ts +31 -0
  175. package/dist/loopback-agent.js +57 -0
  176. package/dist/mcp/attach-memory.d.ts +18 -2
  177. package/dist/mcp/attach-memory.js +50 -7
  178. package/dist/mcp/client-identity.d.ts +31 -0
  179. package/dist/mcp/client-identity.js +51 -0
  180. package/dist/mcp/mcp-post-transport.d.ts +26 -0
  181. package/dist/mcp/mcp-post-transport.js +109 -0
  182. package/dist/mcp/mcp-proxy.d.ts +2 -1
  183. package/dist/mcp/mcp-proxy.js +52 -62
  184. package/dist/mcp/mcp.js +7 -1
  185. package/dist/mcp/proxy-backoff.d.ts +11 -0
  186. package/dist/mcp/proxy-backoff.js +13 -0
  187. package/dist/mcp/proxy-daemon-probe.d.ts +1 -0
  188. package/dist/mcp/proxy-daemon-probe.js +7 -1
  189. package/dist/mcp/proxy-handshake.d.ts +23 -0
  190. package/dist/mcp/proxy-handshake.js +31 -0
  191. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  192. package/dist/mcp/tool-catalog-cache.js +12 -0
  193. package/dist/memory/memory-tools.d.ts +2 -0
  194. package/dist/memory/memory-tools.js +82 -0
  195. package/dist/memory/project-memory.d.ts +34 -0
  196. package/dist/memory/project-memory.js +96 -0
  197. package/dist/oracles/propose-consequences.js +12 -3
  198. package/dist/pool/browser-pool.d.ts +63 -4
  199. package/dist/pool/browser-pool.js +113 -4
  200. package/dist/pool/playwright-launcher.js +9 -0
  201. package/dist/project/project-tools.js +3 -3
  202. package/dist/project/reticle-dir.d.ts +15 -2
  203. package/dist/project/reticle-dir.js +21 -4
  204. package/dist/runs/run-store.d.ts +10 -0
  205. package/dist/runs/run-store.js +10 -0
  206. package/dist/runs/verification-sync.d.ts +0 -11
  207. package/dist/runs/verification-sync.js +22 -1
  208. package/dist/runs/verify-server.d.ts +8 -1
  209. package/dist/runs/verify-server.js +14 -2
  210. package/dist/session/command-timeout.d.ts +5 -2
  211. package/dist/session/command-timeout.js +5 -4
  212. package/dist/session/lease-visibility.d.ts +67 -0
  213. package/dist/session/lease-visibility.js +76 -0
  214. package/dist/session/no-session-diagnosis.d.ts +23 -0
  215. package/dist/session/no-session-diagnosis.js +58 -17
  216. package/dist/session/no-session-next-action.d.ts +7 -0
  217. package/dist/session/no-session-next-action.js +42 -3
  218. package/dist/session/no-session-watch.d.ts +10 -6
  219. package/dist/session/no-session-watch.js +51 -19
  220. package/dist/session/pending-commands.d.ts +11 -1
  221. package/dist/session/pending-commands.js +12 -2
  222. package/dist/session/resolve-within.d.ts +34 -0
  223. package/dist/session/resolve-within.js +53 -0
  224. package/dist/session/session-info.d.ts +18 -0
  225. package/dist/session/session-info.js +11 -0
  226. package/dist/session/session-manager.d.ts +14 -0
  227. package/dist/session/session-manager.js +61 -3
  228. package/dist/session/session-mirror.d.ts +16 -0
  229. package/dist/session/session-mirror.js +22 -0
  230. package/dist/session/session-recommendation.d.ts +19 -5
  231. package/dist/session/session-recommendation.js +25 -9
  232. package/dist/session/session-successor.js +36 -1
  233. package/dist/session/session-tools.js +2 -2
  234. package/dist/session/session.d.ts +22 -2
  235. package/dist/session/session.js +87 -7
  236. package/dist/session/wall-clock.d.ts +12 -0
  237. package/dist/session/wall-clock.js +14 -0
  238. package/dist/setup/agent-configs.d.ts +96 -0
  239. package/dist/setup/agent-configs.js +333 -0
  240. package/dist/setup/agent-io.d.ts +9 -0
  241. package/dist/setup/agent-io.js +16 -0
  242. package/dist/setup/agent-writer.d.ts +51 -0
  243. package/dist/setup/agent-writer.js +115 -0
  244. package/dist/setup/approval-migration.d.ts +47 -0
  245. package/dist/setup/approval-migration.js +68 -0
  246. package/dist/setup/auto-approve.d.ts +85 -0
  247. package/dist/setup/auto-approve.js +150 -0
  248. package/dist/setup/bridge-port.d.ts +15 -0
  249. package/dist/setup/bridge-port.js +24 -0
  250. package/dist/setup/desktop-shape.d.ts +57 -0
  251. package/dist/setup/desktop-shape.js +63 -0
  252. package/dist/setup/dev-server-wait.d.ts +68 -0
  253. package/dist/setup/dev-server-wait.js +67 -0
  254. package/dist/setup/drive-agent.d.ts +71 -0
  255. package/dist/setup/drive-agent.js +171 -0
  256. package/dist/setup/drive-plan.d.ts +68 -0
  257. package/dist/setup/drive-plan.js +130 -0
  258. package/dist/setup/ensure-daemon.d.ts +29 -0
  259. package/dist/setup/ensure-daemon.js +58 -0
  260. package/dist/setup/init-runtime.d.ts +47 -0
  261. package/dist/setup/init-runtime.js +160 -0
  262. package/dist/setup/interrupt.d.ts +25 -0
  263. package/dist/setup/interrupt.js +40 -0
  264. package/dist/setup/license-key.d.ts +39 -0
  265. package/dist/setup/license-key.js +72 -0
  266. package/dist/setup/listeners.d.ts +37 -0
  267. package/dist/setup/listeners.js +85 -0
  268. package/dist/setup/node-effects.d.ts +60 -0
  269. package/dist/setup/node-effects.js +210 -0
  270. package/dist/setup/page-probe.d.ts +39 -0
  271. package/dist/setup/page-probe.js +49 -0
  272. package/dist/setup/relaunch.d.ts +36 -0
  273. package/dist/setup/relaunch.js +57 -0
  274. package/dist/setup/remaining-steps.d.ts +19 -0
  275. package/dist/setup/remaining-steps.js +31 -0
  276. package/dist/setup/run-setup.d.ts +99 -0
  277. package/dist/setup/run-setup.js +203 -0
  278. package/dist/setup/session-pick.d.ts +47 -0
  279. package/dist/setup/session-pick.js +51 -0
  280. package/dist/setup/setup-command.d.ts +45 -0
  281. package/dist/setup/setup-command.js +199 -0
  282. package/dist/setup/setup-options.d.ts +74 -0
  283. package/dist/setup/setup-options.js +67 -0
  284. package/dist/setup/transcripts.d.ts +16 -0
  285. package/dist/setup/transcripts.js +43 -0
  286. package/dist/status-payload.d.ts +14 -1
  287. package/dist/status-payload.js +23 -1
  288. package/dist/telemetry/daemon-telemetry.d.ts +8 -1
  289. package/dist/telemetry/daemon-telemetry.js +52 -5
  290. package/dist/telemetry/error-class.js +1 -1
  291. package/dist/telemetry/feedback-context.d.ts +9 -1
  292. package/dist/telemetry/feedback-context.js +60 -29
  293. package/dist/telemetry/feedback-local.d.ts +15 -0
  294. package/dist/telemetry/feedback-local.js +56 -0
  295. package/dist/telemetry/feedback.d.ts +11 -0
  296. package/dist/telemetry/feedback.js +19 -6
  297. package/dist/telemetry/init-telemetry.d.ts +2 -0
  298. package/dist/telemetry/init-telemetry.js +2 -0
  299. package/dist/telemetry/install-source.d.ts +14 -0
  300. package/dist/telemetry/install-source.js +30 -0
  301. package/dist/telemetry/license-activation.d.ts +7 -1
  302. package/dist/telemetry/license-activation.js +9 -5
  303. package/dist/telemetry/mcp-connection.js +7 -0
  304. package/dist/telemetry/project-profile.d.ts +12 -1
  305. package/dist/telemetry/project-profile.js +11 -6
  306. package/dist/telemetry/proxy-telemetry.d.ts +3 -0
  307. package/dist/telemetry/proxy-telemetry.js +21 -0
  308. package/dist/telemetry/session-metrics.d.ts +12 -0
  309. package/dist/telemetry/session-metrics.js +35 -1
  310. package/dist/telemetry/telemetry.js +17 -2
  311. package/dist/telemetry/tool-refused.d.ts +3 -1
  312. package/dist/telemetry/tool-refused.js +34 -2
  313. package/dist/tools/act-capsule.d.ts +10 -0
  314. package/dist/tools/act-capsule.js +8 -2
  315. package/dist/tools/act-danger.d.ts +2 -1
  316. package/dist/tools/act-danger.js +11 -3
  317. package/dist/tools/act-observation.d.ts +41 -0
  318. package/dist/tools/act-observation.js +36 -0
  319. package/dist/tools/act-preflight.d.ts +20 -0
  320. package/dist/tools/act-preflight.js +98 -0
  321. package/dist/tools/act-sequence-retry.d.ts +45 -0
  322. package/dist/tools/act-sequence-retry.js +58 -0
  323. package/dist/tools/act-sequence-tool.d.ts +13 -0
  324. package/dist/tools/act-sequence-tool.js +135 -0
  325. package/dist/tools/act-target.d.ts +14 -0
  326. package/dist/tools/act-target.js +47 -0
  327. package/dist/tools/act-tools.d.ts +21 -1
  328. package/dist/tools/act-tools.js +100 -213
  329. package/dist/tools/assert-verdict.js +22 -16
  330. package/dist/tools/browser-tools.js +6 -2
  331. package/dist/tools/dynamic-tools.js +24 -5
  332. package/dist/tools/error-recovery.d.ts +1 -0
  333. package/dist/tools/error-recovery.js +8 -0
  334. package/dist/tools/invoke-tool.js +59 -9
  335. package/dist/tools/lease-tools.d.ts +30 -4
  336. package/dist/tools/lease-tools.js +169 -5
  337. package/dist/tools/merged-name-redirect.d.ts +13 -0
  338. package/dist/tools/merged-name-redirect.js +22 -0
  339. package/dist/tools/numeric-bounds.d.ts +100 -0
  340. package/dist/tools/numeric-bounds.js +125 -0
  341. package/dist/tools/observe-tools.js +47 -40
  342. package/dist/tools/read-tools.js +28 -7
  343. package/dist/tools/real-input-attempt.d.ts +10 -0
  344. package/dist/tools/real-input-attempt.js +59 -1
  345. package/dist/tools/reconcile-tools.js +4 -4
  346. package/dist/tools/tool-kit.d.ts +12 -0
  347. package/dist/tools/tool-kit.js +6 -1
  348. package/dist/tools/tool-names.d.ts +2 -0
  349. package/dist/tools/tool-names.js +2 -0
  350. package/dist/tools/tools-helpers.d.ts +19 -0
  351. package/dist/tools/tools-helpers.js +46 -0
  352. package/dist/tools/tools.d.ts +2 -0
  353. package/dist/tools/tools.js +56 -8
  354. package/dist/update/reticle-deps.d.ts +9 -0
  355. package/dist/update/reticle-deps.js +27 -0
  356. package/dist/update/sdk-sync.d.ts +1 -2
  357. package/dist/update/sdk-sync.js +1 -21
  358. package/dist/update/update-nudge.d.ts +18 -0
  359. package/dist/update/update-nudge.js +20 -0
  360. package/dist/version/peer-announce.js +4 -2
  361. package/dist/version/sdk-fix.d.ts +47 -0
  362. package/dist/version/sdk-fix.js +127 -0
  363. package/dist/version/version-nudge.d.ts +14 -0
  364. package/dist/version/version-nudge.js +21 -0
  365. package/dist/version/version-skew.d.ts +38 -3
  366. package/dist/version/version-skew.js +83 -7
  367. package/dist/visual/visual-store.d.ts +6 -6
  368. package/dist/visual/visual-store.js +18 -16
  369. package/dist/visual/visual-tools.js +57 -12
  370. package/docs/cli/doctor.mdx +1 -0
  371. package/docs/cli/serve.mdx +4 -2
  372. package/docs/cli.mdx +7 -3
  373. package/docs/desktop.mdx +1 -1
  374. package/docs/docs.json +1 -1
  375. package/docs/getting-started.md +19 -3
  376. package/docs/index.mdx +9 -6
  377. package/docs/install-agentic.mdx +38 -12
  378. package/docs/install-manual.mdx +1 -1
  379. package/docs/packages.mdx +1 -1
  380. package/docs/skill-file.mdx +3 -3
  381. package/docs/telemetry-contract.md +108 -4
  382. package/docs/tools-act-sequence.mdx +16 -0
  383. package/docs/usage.md +1 -1
  384. package/package.json +4 -3
  385. package/dist/init/cursor.d.ts +0 -24
  386. package/dist/init/cursor.js +0 -80
@@ -13,12 +13,15 @@ export interface RecordedStep {
13
13
  interface ActiveRecording {
14
14
  cursor: number;
15
15
  steps: RecordedStep[];
16
+ /** The route the journey began on. See CompiledProgram.startPath. */
17
+ startPath?: string;
16
18
  }
17
19
  /** A finished, replayable program compiled from a recording. */
18
20
  export interface CompiledProgram {
19
21
  name: string;
20
22
  version: number;
21
23
  steps: RecordedStep[];
24
+ startPath?: string;
22
25
  }
23
26
  /**
24
27
  * Tracks in-flight recordings (name -> { buffer cursor at record_start, captured steps })
@@ -26,7 +29,7 @@ export interface CompiledProgram {
26
29
  */
27
30
  export declare class RecordingStore {
28
31
  #private;
29
- start(name: string, cursor: number): void;
32
+ start(name: string, cursor: number, startPath?: string): void;
30
33
  isRecording(name: string): boolean;
31
34
  /**
32
35
  * Number of steps captured so far in the named ACTIVE recording (0 if it
@@ -41,5 +44,14 @@ export declare class RecordingStore {
41
44
  saveCompiled(program: CompiledProgram): void;
42
45
  getCompiled(name: string): CompiledProgram | undefined;
43
46
  active(): string[];
47
+ /**
48
+ * Names of recordings that have been STOPPED and compiled.
49
+ *
50
+ * The mirror of `active()`, and needed for the same defaulting: after `reticle_record stop` a
51
+ * recording is no longer active, so resolving "the obvious one" from `active()` finds nothing.
52
+ * Saving it then demanded the exact name back from the caller, which is a thing to remember for
53
+ * no reason when exactly one recording exists.
54
+ */
55
+ compiled(): string[];
44
56
  }
45
57
  export {};
@@ -5,8 +5,12 @@
5
5
  export class RecordingStore {
6
6
  #active = new Map();
7
7
  #compiled = new Map();
8
- start(name, cursor) {
9
- this.#active.set(name, { cursor, steps: [] });
8
+ start(name, cursor, startPath) {
9
+ this.#active.set(name, {
10
+ cursor,
11
+ steps: [],
12
+ ...(startPath === undefined ? {} : { startPath }),
13
+ });
10
14
  }
11
15
  isRecording(name) {
12
16
  return this.#active.has(name);
@@ -39,4 +43,15 @@ export class RecordingStore {
39
43
  active() {
40
44
  return [...this.#active.keys()];
41
45
  }
46
+ /**
47
+ * Names of recordings that have been STOPPED and compiled.
48
+ *
49
+ * The mirror of `active()`, and needed for the same defaulting: after `reticle_record stop` a
50
+ * recording is no longer active, so resolving "the obvious one" from `active()` finds nothing.
51
+ * Saving it then demanded the exact name back from the caller, which is a thing to remember for
52
+ * no reason when exactly one recording exists.
53
+ */
54
+ compiled() {
55
+ return [...this.#compiled.keys()];
56
+ }
42
57
  }
@@ -13,8 +13,28 @@ export declare function ambiguousTestidNote(value: string): string;
13
13
  * resolution — can't drift between them.
14
14
  */
15
15
  export declare function queryRefs(result: CommandResult): string[];
16
- /** A destructive-action confirmation is one-shot and must never persist into a recording. */
17
- export declare function replayActionArgs(value: unknown, confirmDangerous?: boolean): Record<string, unknown>;
16
+ /**
17
+ * The environment variable that supplies one redacted field.
18
+ *
19
+ * Named after the field so it is guessable from the flow alone: `auth-password` is read from
20
+ * `RETICLE_SECRET_AUTH_PASSWORD`. A scheme requiring a lookup table would mean the flow says a
21
+ * value is missing and cannot say what to set.
22
+ */
23
+ export declare const secretEnvKey: (field: string) => string;
24
+ /**
25
+ * A destructive-action confirmation is one-shot and must never persist into a recording.
26
+ *
27
+ * This is also where a REDACTED fill is supplied back. Stripping credentials out of a git-checked
28
+ * flow is only half a fix: sign-in still has to replay, and a flow that drifts at step two forever
29
+ * because its own password was removed is a flow nobody keeps — along with everything behind the
30
+ * login it guards. The value comes from the environment, the one place a secret can live that is
31
+ * neither the customer's repository nor our database.
32
+ *
33
+ * Left as the PLACEHOLDER when nothing supplies it, rather than blanked. The replay then fails at
34
+ * the login form with `<redacted: supply at replay>` visible in the field, which names its own fix;
35
+ * an empty box fails identically and tells the reader nothing.
36
+ */
37
+ export declare function replayActionArgs(value: unknown, confirmDangerous?: boolean, field?: string): Record<string, unknown>;
18
38
  /**
19
39
  * Capture an act into every in-flight recording, or do nothing when none is running.
20
40
  *
@@ -1,3 +1,4 @@
1
+ import { REDACTED_FILL } from './flows.js';
1
2
  import { DANGEROUS_ACTION_CONFIRM_ARG, ReticleCommand, QueryBy, } from '@reticlehq/core';
2
3
  import { ReticleTool } from '../tools/tool-names.js';
3
4
  import { predicateToExpect, enforcedOnReplay } from './predicate-to-expect.js';
@@ -23,12 +24,37 @@ export function queryRefs(result) {
23
24
  const elements = Array.isArray(payload['elements']) ? payload['elements'] : [];
24
25
  return elements.map((e) => asString(asRecord(e)['ref']) ?? '').filter((r) => r.length > 0);
25
26
  }
26
- /** A destructive-action confirmation is one-shot and must never persist into a recording. */
27
- export function replayActionArgs(value, confirmDangerous = false) {
27
+ /**
28
+ * The environment variable that supplies one redacted field.
29
+ *
30
+ * Named after the field so it is guessable from the flow alone: `auth-password` is read from
31
+ * `RETICLE_SECRET_AUTH_PASSWORD`. A scheme requiring a lookup table would mean the flow says a
32
+ * value is missing and cannot say what to set.
33
+ */
34
+ export const secretEnvKey = (field) => `RETICLE_SECRET_${field.replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase()}`;
35
+ /**
36
+ * A destructive-action confirmation is one-shot and must never persist into a recording.
37
+ *
38
+ * This is also where a REDACTED fill is supplied back. Stripping credentials out of a git-checked
39
+ * flow is only half a fix: sign-in still has to replay, and a flow that drifts at step two forever
40
+ * because its own password was removed is a flow nobody keeps — along with everything behind the
41
+ * login it guards. The value comes from the environment, the one place a secret can live that is
42
+ * neither the customer's repository nor our database.
43
+ *
44
+ * Left as the PLACEHOLDER when nothing supplies it, rather than blanked. The replay then fails at
45
+ * the login form with `<redacted: supply at replay>` visible in the field, which names its own fix;
46
+ * an empty box fails identically and tells the reader nothing.
47
+ */
48
+ export function replayActionArgs(value, confirmDangerous = false, field) {
28
49
  const args = { ...asRecord(value) };
29
50
  delete args[DANGEROUS_ACTION_CONFIRM_ARG];
30
51
  if (confirmDangerous)
31
52
  args[DANGEROUS_ACTION_CONFIRM_ARG] = true;
53
+ if (REDACTED_FILL === args['value'] && field !== undefined) {
54
+ const supplied = process.env[secretEnvKey(field)];
55
+ if (supplied !== undefined && supplied.length > 0)
56
+ args['value'] = supplied;
57
+ }
32
58
  return args;
33
59
  }
34
60
  /** The element's source location from an action result, when the framework stamped one. */
@@ -5,6 +5,7 @@ import { attributedFailures } from './attributed-failure.js';
5
5
  import { Verified } from '@reticlehq/core';
6
6
  import { ReticleTool } from '../tools/tool-names.js';
7
7
  import { asNumber, asRecord, asString } from '../tools/tools-helpers.js';
8
+ import { workerCountSchema } from '../tools/numeric-bounds.js';
8
9
  import { loadNamedFlows, resolveChangedFiles } from '../cli/cli-flow-commands.js';
9
10
  import { affectedSavedFlows } from './flow-sources.js';
10
11
  import { FLOW_TOOLS } from './flow-tools.js';
@@ -25,8 +26,7 @@ export const VERIFY_CHANGE_TOOLS = [
25
26
  .string()
26
27
  .optional()
27
28
  .describe('Git ref to diff against (e.g. "HEAD~1", "main"). Unioned with `files`.'),
28
- parallel: z
29
- .number()
29
+ parallel: workerCountSchema
30
30
  .optional()
31
31
  .describe('Replay this many affected flows at once (needs the browser pool).'),
32
32
  sessionId: z
@@ -200,6 +200,9 @@ async function inspectAfterReplay(deps, args, cursor) {
200
200
  const contradictions = findContradictions(session.eventsSince(cursor), {
201
201
  currentDocumentId: session.currentDocumentId,
202
202
  currentEditEpoch: session.currentEditEpoch,
203
+ appOrigin: session.url,
204
+ // The replay is the action, and the cursor is where it started.
205
+ actionSince: cursor,
203
206
  });
204
207
  const snapshot = await session.command(ReticleCommand.SNAPSHOT, {
205
208
  mode: SnapshotMode.INTERACTIVE,
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export { BlindSpotKind } from '@reticlehq/core';
8
8
  import { BlindSpotKind, type ReticleEvent } from '@reticlehq/core';
9
- interface BlindSpot {
9
+ export interface BlindSpot {
10
10
  kind: BlindSpotKind;
11
11
  count: number;
12
12
  }
@@ -55,6 +55,15 @@ export declare function buildCoverageStatement(spots: readonly BlindSpot[]): Cov
55
55
  * from what the SDK observed during the action, with no extra round-trip.
56
56
  */
57
57
  export declare function blindSpotsFromEvents(events: readonly ReticleEvent[]): BlindSpot[];
58
+ /**
59
+ * Drop Electron-only coverage rows unless this session is actually an Electron renderer.
60
+ *
61
+ * The kinds live in the same vocabulary as "no store registered", so listing whatever is present
62
+ * reported a Vite + React tab as an un-instrumented Electron renderer. The session already knows
63
+ * the runtime (PAGE_HEALTH). Unknown runtime (older SDK) keeps the rows — dropping them would hide
64
+ * a real missing-preload warning.
65
+ */
66
+ export declare function spotsForRuntime(spots: readonly BlindSpot[], runtime: string | undefined): BlindSpot[];
58
67
  /**
59
68
  * Blind spots from the session's remembered LEVEL state rather than from a window of events.
60
69
  *
@@ -63,8 +72,35 @@ export declare function blindSpotsFromEvents(events: readonly ReticleEvent[]): B
63
72
  * reports "full" for a page a third of which is unobservable — and the act tool's own description
64
73
  * tells harnesses to gate on that block. Ask the session what is true now instead of inferring it
65
74
  * from what happened to be said recently.
75
+ *
76
+ * `runtime` gates Electron-only kinds (see `spotsForRuntime`) so a web session cannot inherit them
77
+ * from the coverage vocabulary.
66
78
  */
67
- export declare function blindSpotsFromState(state: Readonly<Record<string, number>>): BlindSpot[];
79
+ export declare function blindSpotsFromState(state: Readonly<Record<string, number>>, runtime?: string): BlindSpot[];
80
+ /**
81
+ * The narrow slice of a predicate the absence-blind-spot check needs.
82
+ *
83
+ * Kept minimal to avoid pulling the full Predicate type (and its transitive deps) into the
84
+ * honesty module. Anything that quacks like this works.
85
+ */
86
+ interface AbsencePredicate {
87
+ kind: string;
88
+ absent?: boolean;
89
+ query?: {
90
+ scope?: unknown;
91
+ };
92
+ }
93
+ /**
94
+ * When an absence assertion targets a page with regions Reticle cannot observe, "the element is
95
+ * absent" means only "it is absent in what I CAN see". Returns a note when that distinction
96
+ * matters, undefined otherwise.
97
+ *
98
+ * Fires on three blind-spot kinds:
99
+ * - CROSS_ORIGIN_IFRAME (only when the predicate is scoped to a frame region)
100
+ * - VIRTUALIZED_UNMOUNTED — a row that was never rendered could hold the element
101
+ * - CLOSED_SHADOW_ROOT — a subtree Reticle cannot see into
102
+ */
103
+ export declare function absenceBlindSpotNote(predicate: AbsencePredicate, spots: readonly BlindSpot[]): string | undefined;
68
104
  /** The impeaching note for a transport gap, or undefined when the window was intact. */
69
105
  export declare function transportGapNote(events: readonly ReticleEvent[]): string | undefined;
70
106
  /**
@@ -76,3 +112,20 @@ export declare function transportGapNote(events: readonly ReticleEvent[]): strin
76
112
  * predicate would eventually disagree with the first about what "unwatched" means.
77
113
  */
78
114
  export declare function isStateUnwatched(spots: readonly BlindSpot[]): boolean;
115
+ /**
116
+ * Does a passing verdict on this predicate REST on the window being complete?
117
+ *
118
+ * Only an absence claim does. A positive assertion that passed found its evidence, and events aged
119
+ * out elsewhere in the buffer do not unmake it. An absence claim concluded "nothing is there" — and
120
+ * the thing the buffer dropped is precisely the disproof, so "absent" degrades to "absent in what I
121
+ * still hold".
122
+ *
123
+ * The distinction is not optional. Scarce-loss is recorded for AGE eviction too, so `lostSince(0)`
124
+ * is true on any session older than the 60s cutoff; impeaching every verdict over a caller-chosen
125
+ * window would make `unknown` the answer to everything, which this repo has already paid for once.
126
+ */
127
+ export declare function restsOnCompleteWindow(predicate: {
128
+ kind: string;
129
+ absent?: boolean;
130
+ predicate?: unknown;
131
+ }): boolean;
@@ -7,7 +7,7 @@
7
7
  // The kind enum lives in core (it crosses the wire in a BLIND_SPOT event); re-exported here so existing
8
8
  // honesty-side imports keep working.
9
9
  export { BlindSpotKind } from '@reticlehq/core';
10
- import { BlindSpotKind, EventType, ReticleEnv, TRANSPORT_LIMITS, } from '@reticlehq/core';
10
+ import { AppRuntime, BlindSpotKind, EventType, PredicateKind, ReticleEnv, TRANSPORT_LIMITS, isDesktopBlindSpot, } from '@reticlehq/core';
11
11
  /**
12
12
  * Whether the observation window saw everything it was asked about.
13
13
  *
@@ -107,6 +107,19 @@ export function blindSpotsFromEvents(events) {
107
107
  }
108
108
  return [...latest].map(([kind, count]) => ({ kind, count }));
109
109
  }
110
+ /**
111
+ * Drop Electron-only coverage rows unless this session is actually an Electron renderer.
112
+ *
113
+ * The kinds live in the same vocabulary as "no store registered", so listing whatever is present
114
+ * reported a Vite + React tab as an un-instrumented Electron renderer. The session already knows
115
+ * the runtime (PAGE_HEALTH). Unknown runtime (older SDK) keeps the rows — dropping them would hide
116
+ * a real missing-preload warning.
117
+ */
118
+ export function spotsForRuntime(spots, runtime) {
119
+ if (runtime === undefined || AppRuntime.ELECTRON === runtime)
120
+ return [...spots];
121
+ return spots.filter((spot) => !isDesktopBlindSpot(spot.kind));
122
+ }
110
123
  /**
111
124
  * Blind spots from the session's remembered LEVEL state rather than from a window of events.
112
125
  *
@@ -115,9 +128,16 @@ export function blindSpotsFromEvents(events) {
115
128
  * reports "full" for a page a third of which is unobservable — and the act tool's own description
116
129
  * tells harnesses to gate on that block. Ask the session what is true now instead of inferring it
117
130
  * from what happened to be said recently.
131
+ *
132
+ * `runtime` gates Electron-only kinds (see `spotsForRuntime`) so a web session cannot inherit them
133
+ * from the coverage vocabulary.
118
134
  */
119
- export function blindSpotsFromState(state) {
120
- return Object.entries(state).map(([kind, count]) => ({ kind: kind, count }));
135
+ export function blindSpotsFromState(state, runtime) {
136
+ const spots = Object.entries(state).map(([kind, count]) => ({
137
+ kind: kind,
138
+ count,
139
+ }));
140
+ return spotsForRuntime(spots, runtime);
121
141
  }
122
142
  /**
123
143
  * Events the BROWSER's own transport queue threw away inside this window, or 0.
@@ -145,6 +165,29 @@ function droppedByTransport(events) {
145
165
  }
146
166
  return dropped;
147
167
  }
168
+ /**
169
+ * When an absence assertion targets a page with regions Reticle cannot observe, "the element is
170
+ * absent" means only "it is absent in what I CAN see". Returns a note when that distinction
171
+ * matters, undefined otherwise.
172
+ *
173
+ * Fires on three blind-spot kinds:
174
+ * - CROSS_ORIGIN_IFRAME (only when the predicate is scoped to a frame region)
175
+ * - VIRTUALIZED_UNMOUNTED — a row that was never rendered could hold the element
176
+ * - CLOSED_SHADOW_ROOT — a subtree Reticle cannot see into
177
+ */
178
+ export function absenceBlindSpotNote(predicate, spots) {
179
+ if ('element' !== predicate.kind || true !== predicate.absent)
180
+ return undefined;
181
+ const relevant = spots.filter((spot) => spot.count > 0 &&
182
+ (spot.kind === BlindSpotKind.CROSS_ORIGIN_IFRAME
183
+ ? undefined !== predicate.query?.scope
184
+ : spot.kind === BlindSpotKind.VIRTUALIZED_UNMOUNTED ||
185
+ spot.kind === BlindSpotKind.CLOSED_SHADOW_ROOT));
186
+ if (0 === relevant.length)
187
+ return undefined;
188
+ const statement = buildCoverageStatement(relevant);
189
+ return `the absence assertion targeted a region Reticle could not observe (${statement.note ?? 'partial coverage'}), so a passing DOM check cannot prove absence`;
190
+ }
148
191
  /** The impeaching note for a transport gap, or undefined when the window was intact. */
149
192
  export function transportGapNote(events) {
150
193
  const dropped = droppedByTransport(events);
@@ -163,3 +206,20 @@ export function transportGapNote(events) {
163
206
  export function isStateUnwatched(spots) {
164
207
  return spots.some((spot) => spot.kind === BlindSpotKind.UNWATCHED_STATE && spot.count > 0);
165
208
  }
209
+ /**
210
+ * Does a passing verdict on this predicate REST on the window being complete?
211
+ *
212
+ * Only an absence claim does. A positive assertion that passed found its evidence, and events aged
213
+ * out elsewhere in the buffer do not unmake it. An absence claim concluded "nothing is there" — and
214
+ * the thing the buffer dropped is precisely the disproof, so "absent" degrades to "absent in what I
215
+ * still hold".
216
+ *
217
+ * The distinction is not optional. Scarce-loss is recorded for AGE eviction too, so `lostSince(0)`
218
+ * is true on any session older than the 60s cutoff; impeaching every verdict over a caller-chosen
219
+ * window would make `unknown` the answer to everything, which this repo has already paid for once.
220
+ */
221
+ export function restsOnCompleteWindow(predicate) {
222
+ if (PredicateKind.NOT === predicate.kind)
223
+ return true;
224
+ return true === predicate.absent;
225
+ }
@@ -22,6 +22,15 @@ interface HonestyInputs {
22
22
  envelopeSamples?: number;
23
23
  coveragePct?: number;
24
24
  coveragePartial?: boolean;
25
+ /**
26
+ * The sentence naming WHAT went unobserved, from `buildCoverageStatement`.
27
+ *
28
+ * Separate from `blindSpots`, which carries only the IMPEACHING notes — the ones that downgrade a
29
+ * verdict. A bounding spot does not impeach anything, so it never entered that list, and the flag
30
+ * that survived (`partial: true`) is unanswerable on its own: the verdict prose tells the reader
31
+ * to see `coverage` for what went unobserved, and there was nothing there to see.
32
+ */
33
+ coverageNote?: string;
25
34
  truncated?: boolean;
26
35
  blindSpots?: readonly string[];
27
36
  /**
@@ -55,6 +64,7 @@ export interface HonestyBlock {
55
64
  coverage: {
56
65
  pct?: number;
57
66
  partial: boolean;
67
+ note?: string;
58
68
  };
59
69
  integrity: {
60
70
  clean: boolean;
@@ -76,4 +86,18 @@ export declare function meetsHonestyBar(block: HonestyBlock, bar?: HonestyBar):
76
86
  ok: boolean;
77
87
  reasons: string[];
78
88
  };
89
+ /**
90
+ * The coverage sentence, kept only on a verdict that points at it.
91
+ *
92
+ * Exactly one branch of `decideVerified` promises it — the YES branch says "coverage was PARTIAL —
93
+ * see `coverage` for what went unobserved". UNKNOWN keeps it too, because "I could not tell" is a
94
+ * statement about what went unseen and the note is the answer. A NO has already named a concrete
95
+ * counter-example, nothing directs the reader to `coverage`, and `partial` plus the spot kinds still
96
+ * travel — so the prose there is cost with no question behind it.
97
+ *
98
+ * Measured: emitting it on every verdict regressed the benchmark's verification efficiency by 4.1%,
99
+ * one note per observation. Accuracy outranks tokens, but this buys no accuracy — the promise is
100
+ * kept wherever it is made.
101
+ */
102
+ export declare function honestyForVerdict(verified: string, honesty: HonestyBlock): HonestyBlock;
79
103
  export {};
@@ -5,6 +5,7 @@
5
5
  * block. Harnesses gate on it (e.g. "grade ≥ net AND integrity clean"). This is pure composition +
6
6
  * assessment — all five components already exist individually; wiring it onto results is a later step.
7
7
  */
8
+ import { Verified } from '@reticlehq/core';
8
9
  /** Assertion grade, strongest first — the tier the verdict actually proved. */
9
10
  export const HonestyGrade = {
10
11
  SIGNAL: 'signal',
@@ -45,7 +46,11 @@ export function buildHonestyBlock(inputs) {
45
46
  ...(samples === undefined
46
47
  ? {}
47
48
  : { envelope: { samples, sufficient: samples >= MIN_ENVELOPE_SAMPLES } }),
48
- coverage: { ...(pct === undefined ? {} : { pct }), partial },
49
+ coverage: {
50
+ ...(pct === undefined ? {} : { pct }),
51
+ partial,
52
+ ...(partial && inputs.coverageNote !== undefined ? { note: inputs.coverageNote } : {}),
53
+ },
49
54
  ...(inputs.settled === undefined ? {} : { settled: inputs.settled }),
50
55
  integrity: {
51
56
  clean: 0 === issues.length,
@@ -69,3 +74,22 @@ export function meetsHonestyBar(block, bar = {}) {
69
74
  }
70
75
  return { ok: 0 === reasons.length, reasons };
71
76
  }
77
+ /**
78
+ * The coverage sentence, kept only on a verdict that points at it.
79
+ *
80
+ * Exactly one branch of `decideVerified` promises it — the YES branch says "coverage was PARTIAL —
81
+ * see `coverage` for what went unobserved". UNKNOWN keeps it too, because "I could not tell" is a
82
+ * statement about what went unseen and the note is the answer. A NO has already named a concrete
83
+ * counter-example, nothing directs the reader to `coverage`, and `partial` plus the spot kinds still
84
+ * travel — so the prose there is cost with no question behind it.
85
+ *
86
+ * Measured: emitting it on every verdict regressed the benchmark's verification efficiency by 4.1%,
87
+ * one note per observation. Accuracy outranks tokens, but this buys no accuracy — the promise is
88
+ * kept wherever it is made.
89
+ */
90
+ export function honestyForVerdict(verified, honesty) {
91
+ if (Verified.NO !== verified || honesty.coverage.note === undefined)
92
+ return honesty;
93
+ const { note: _dropped, ...coverage } = honesty.coverage;
94
+ return { ...honesty, coverage };
95
+ }
@@ -75,5 +75,14 @@ export interface ActionInstrumentationFacts {
75
75
  * finished, and a bare flag makes them read the same.
76
76
  */
77
77
  openIntentCount?: number;
78
+ /**
79
+ * How long the OLDEST open intent has been owed, in ms.
80
+ *
81
+ * A count alone cannot tell "you declared this a minute ago and have not proved it yet" from
82
+ * "this project has owed eighteen things since last week". Only the first is actionable on the
83
+ * result being read, and a number that is always large is a number people learn to skip — which
84
+ * is exactly how an honest gap gets filtered out along with the noise.
85
+ */
86
+ oldestOpenIntentAgeMs?: number;
78
87
  }
79
88
  export declare function gapsForAction(facts: ActionInstrumentationFacts): InstrumentationGap[];
@@ -20,7 +20,19 @@ export function gapsForAction(facts) {
20
20
  //
21
21
  // Reported as the same kind, because it is the same missing thing seen from the other side, and
22
22
  // guarded so the two conditions cannot report it twice.
23
- if (false === facts.hasCapabilities || (facts.stateAsked && facts.stateUnwatched)) {
23
+ //
24
+ // BOTH arms require `stateUnwatched`, which is the store registry. Keying the first on
25
+ // `hasCapabilities` alone made this fire on apps whose store IS registered and readable: an app
26
+ // that calls `registerStore()` without `registerCapabilities()` has no declaration and a perfectly
27
+ // good store, and got a gap saying "no state can be read from it" in the same response that
28
+ // carried `reticle_state { found: true }`, a populated `stateDiffs`, and `statePathsChanged`
29
+ // naming the store (#700). A gap contradicted by its own response tells the agent to redo work it
30
+ // has already done, and teaches it to distrust the whole block.
31
+ //
32
+ // The kind is NO_STORE_REGISTERED and every sentence it carries is about stores, so the store
33
+ // registry is what it must be keyed on. An undeclared-capabilities app with a working store is a
34
+ // different observation, and if it is worth reporting it needs its own kind and its own words.
35
+ if (facts.stateUnwatched && (false === facts.hasCapabilities || facts.stateAsked)) {
24
36
  // Same missing thing, two ways of meeting it — and the sentence has to say which. Describing an
25
37
  // assertion about state to an agent that made no such assertion is a false explanation, and a
26
38
  // gap nobody can act on is worse than no gap: it costs the trip and teaches the wrong lesson.
@@ -55,6 +67,21 @@ export function gapsForAction(facts) {
55
67
  if (true === facts.changeUndeclared) {
56
68
  gaps.push(instrumentationGap(InstrumentationGapKind.UNDECLARED_CHANGE, 'code changed since the last verdict and no intent says what the change was for', 'this verdict can only check the app against itself, so a green here means "nothing visibly broke" rather than "the change did what it was meant to"'));
57
69
  }
70
+ /**
71
+ * Say how old the debt is, when it is old enough to change what the reader should do.
72
+ *
73
+ * The ledger is a PROJECT's, deliberately — "what does this still owe" outlives one session. But an
74
+ * intent declared minutes ago and one abandoned last week produce the same sentence, and the second
75
+ * one is not about the work in front of you. Naming the age separates them without weakening the
76
+ * gap, and without pretending a stale backlog is this verdict's fault.
77
+ */
78
+ const DAY_MS = 86_400_000;
79
+ function describeIntentAge(ageMs) {
80
+ if (ageMs === undefined || ageMs < DAY_MS)
81
+ return '';
82
+ const days = Math.floor(ageMs / DAY_MS);
83
+ return ` (the oldest for ${String(days)} day${1 === days ? '' : 's'} — a backlog this old is probably not what this run is about; retire them or prove them)`;
84
+ }
58
85
  // A green that does not settle what the run OWES.
59
86
  //
60
87
  // The mirror of `changeUndeclared` above, and the more expensive half: that fires when nothing was
@@ -71,7 +98,7 @@ export function gapsForAction(facts) {
71
98
  // to a result whose own `because` says it is not verification.
72
99
  const owed = facts.openIntentCount ?? 0;
73
100
  if (facts.pass && true === facts.proved && 0 < owed) {
74
- gaps.push(instrumentationGap(InstrumentationGapKind.INTENT_UNDISCHARGED, `this verdict passed, and ${String(owed)} declared intent(s) are still unproved`, 'a green settles what it asserted, not what the run set out to do — reporting done here rests on the change looking right rather than on anything having checked it'));
101
+ gaps.push(instrumentationGap(InstrumentationGapKind.INTENT_UNDISCHARGED, `this verdict passed, and ${String(owed)} declared intent(s) are still unproved${describeIntentAge(facts.oldestOpenIntentAgeMs)}`, 'a green settles what it asserted, not what the run set out to do — reporting done here rests on the change looking right rather than on anything having checked it'));
75
102
  }
76
103
  return dedupeGaps(gaps);
77
104
  }
@@ -78,7 +78,8 @@ interface VerifiedInputs {
78
78
  outcomeUnread?: readonly string[];
79
79
  /**
80
80
  * The declared consequence that held does not depend on the response body — an exact string
81
- * rendered, a store path, a signal. See `declaresBodyIndependentChannel`.
81
+ * rendered, a store path, a signal, a route, or an element located by role / name / testid. See
82
+ * `declaresBodyIndependentChannel`.
82
83
  *
83
84
  * The unread-body clause exists for the case where the body is the ONLY channel that could have
84
85
  * contradicted the screen (a 200 with batch/GraphQL errors inside). When a body-independent
@@ -152,9 +152,24 @@ export function decideVerified(inputs) {
152
152
  // with routeChanges 0, because the text was the nav link already on screen — the real navigation
153
153
  // landed 1.8s later. UNKNOWN rather than NO on purpose: the app may well be fine, and reporting a
154
154
  // failure we did not observe would be its own false claim.
155
+ //
156
+ // `no-fault` rather than `unknown`, once the window settled — the same distinction
157
+ // `nothing_declared` already draws, gated the same way.
158
+ //
159
+ // The two words ask for opposite things. `unknown` means LOOK AGAIN WITH BETTER COVERAGE, so an
160
+ // agent reading it reasonably concludes the engine could not see, and spends its remaining turns
161
+ // enabling capture, widening timeouts and re-driving — none of which can help, because the engine
162
+ // saw the whole window and there was nothing wrong with the evidence. The fault is in the
163
+ // ASSERTION: it named something that was true before the action, so it cannot tell success from a
164
+ // no-op. `no-fault` says exactly that, and `because` already carried the remedy that `unknown`
165
+ // was burying.
166
+ //
167
+ // Gated on `true === settled` for the same reason `nothing_declared` is: no-fault claims the whole
168
+ // window was seen, and a call that returned before the app stopped moving has not earned that.
169
+ // Unsettled stays `unknown`, which is then the honest word for it.
155
170
  if (true === inputs.alreadyTrue) {
156
171
  return {
157
- verified: Verified.UNKNOWN,
172
+ verified: true === settled ? Verified.NO_FAULT : Verified.UNKNOWN,
158
173
  verifiedReason: VerifiedReason.ALREADY_TRUE,
159
174
  because: 'the declared consequence was already true before this action, so it proves nothing about it — assert something the action CHANGES (a signal, a request, a route, or store state)',
160
175
  };
@@ -6,6 +6,7 @@ import { noteAgentPeer, PEER_VERSION_PARAM, PEER_CONTRACT_PARAM } from './versio
6
6
  import { log } from './log.js';
7
7
  import { reportMcpConnected } from './telemetry/mcp-connection.js';
8
8
  import { isLoopbackHost, isLocalWebOrigin, isLoopbackPeer, requestToken, tokensMatch, } from './bridge/token-auth.js';
9
+ import { LOOPBACK_IDLE_MS } from './loopback-agent.js';
9
10
  /**
10
11
  * Creates a shared HTTP server that handles both the WebSocket bridge (browser SDK) and the
11
12
  * SSE MCP transport (Claude/agent). Does NOT call listen — caller controls that.
@@ -258,6 +259,17 @@ export function createSharedServer(options = {}) {
258
259
  });
259
260
  });
260
261
  }
262
+ /**
263
+ * The daemon's half of the keep-alive pact with `loopbackAgent`.
264
+ *
265
+ * Node's default is 5 seconds, and the proxy's POST leg is one request per JSON-RPC message. An
266
+ * agent thinks for longer than 5s between tool calls essentially always, so with the default every
267
+ * tool call had to open a fresh socket however the client was configured — which on Windows ends
268
+ * in `ENOBUFS`. This has to stay ABOVE `LOOPBACK_IDLE_MS` so the CLIENT is always the side that
269
+ * retires an idle socket: if the server closed first, the proxy would keep picking up sockets that
270
+ * were already gone. It also has to stay below Node's 60s default `headersTimeout`.
271
+ */
272
+ httpServer.keepAliveTimeout = LOOPBACK_IDLE_MS + 10_000;
261
273
  return {
262
274
  httpServer,
263
275
  attachMcp,
@@ -1,18 +1,30 @@
1
- import { type ImpactCounts, type ImpactSnapshot } from '@reticlehq/core';
1
+ import { type ImpactCounts, type ImpactDefect, type ImpactSnapshot } from '@reticlehq/core';
2
2
  import { ImpactStore, type ImpactFoldMeta } from './impact-store.js';
3
- /** Wire the recorder to a project. Idempotent; the first root wins for the daemon's lifetime. */
3
+ /**
4
+ * Wire the DEFAULT root — the daemon's own. Idempotent; the first one wins.
5
+ *
6
+ * Still idempotent, and still first-wins, because the daemon's own root genuinely does not change
7
+ * while it runs. What changed is that this is no longer the ONLY root: a call that knows its
8
+ * project passes one, and this is the fallback for calls that do not.
9
+ */
4
10
  export declare function initImpact(opts: {
5
11
  reticleRoot: string | undefined;
6
12
  projectName?: string;
7
13
  now?: () => number;
8
14
  }): ImpactStore | undefined;
9
- /** The live store, if one has been initialised. */
10
- export declare function impactStore(): ImpactStore | undefined;
11
- /** Test seam: drop the singleton so a suite can start from a clean record. */
15
+ /** The live store for a root (default when omitted), if one has been initialised. */
16
+ export declare function impactStore(root?: string): ImpactStore | undefined;
17
+ /** Test seam: drop the stores so a suite can start from a clean record. */
12
18
  export declare function resetImpactForTest(): void;
13
- /** Record one delta, if the recorder is live. Never throws: stats must not break a tool call. */
14
- export declare function recordImpact(delta: Partial<ImpactCounts>, meta?: ImpactFoldMeta): void;
15
- export declare function impactSnapshot(): ImpactSnapshot | undefined;
19
+ /**
20
+ * Record one delta against a project's ledger. Never throws: stats must not break a tool call.
21
+ *
22
+ * `root` is the `.reticle` directory this call's evidence belongs to. Omitted means "could not tell
23
+ * which project", which falls back to the daemon's own root — the old behaviour, kept for the paths
24
+ * that genuinely have no session to ask.
25
+ */
26
+ export declare function recordImpact(delta: Partial<ImpactCounts>, meta?: ImpactFoldMeta, root?: string): void;
27
+ export declare function impactSnapshot(root?: string): ImpactSnapshot | undefined;
16
28
  /**
17
29
  * What one tool call did, as counters.
18
30
  *
@@ -21,3 +33,14 @@ export declare function impactSnapshot(): ImpactSnapshot | undefined;
21
33
  * the same rule the product states to agents - everything else moves or reads the app.
22
34
  */
23
35
  export declare function deltaForToolResult(raw: unknown, durationMs: number, refused: boolean): Partial<ImpactCounts>;
36
+ /**
37
+ * What one FAILED verdict was, in a line.
38
+ *
39
+ * Only a verified:"no" produces one. That is the same rule the counters follow and the same rule
40
+ * the product states to agents: an "unknown" is not a defect, it is Reticle admitting it could not
41
+ * tell — recording those here would fill the user's short list with the tool's own blind spots.
42
+ *
43
+ * Everything is read from the result the tool already returned, so this costs nothing extra and
44
+ * cannot disagree with the verdict it describes.
45
+ */
46
+ export declare function defectForToolResult(raw: unknown, at: number): ImpactDefect | undefined;