@reticlehq/server 3.3.0 → 3.5.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 (326) hide show
  1. package/LICENSE-ENTERPRISE +1 -1
  2. package/README.md +3 -3
  3. package/SKILL.md +12 -14
  4. package/dist/chromium-launch-options.d.ts +4 -3
  5. package/dist/chromium-launch-options.js +6 -2
  6. package/dist/command/cli/adhoc-suite.d.ts +22 -0
  7. package/dist/command/cli/adhoc-suite.js +90 -19
  8. package/dist/command/cli/adhoc-verdict.d.ts +35 -0
  9. package/dist/command/cli/adhoc-verdict.js +90 -3
  10. package/dist/command/cli/answers/gate-hook-message.d.ts +6 -0
  11. package/dist/command/cli/answers/gate-hook-message.js +6 -0
  12. package/dist/command/cli/auth/cloud-keystore.d.ts +0 -2
  13. package/dist/command/cli/auth/cloud-keystore.js +0 -3
  14. package/dist/command/cli/auth/cloud-session.d.ts +6 -2
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/auth/device-flow.d.ts +2 -2
  17. package/dist/command/cli/cli-doctor.js +7 -5
  18. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  19. package/dist/command/cli/cli-flow-commands.js +67 -4
  20. package/dist/command/cli/cli-parse.d.ts +8 -1
  21. package/dist/command/cli/cli-parse.js +24 -4
  22. package/dist/command/cli/cli-update-commands.js +8 -1
  23. package/dist/command/cli/cli-verify.d.ts +24 -3
  24. package/dist/command/cli/cli-verify.js +54 -11
  25. package/dist/command/cli/cloud-cli.js +64 -2
  26. package/dist/command/cli/cloud-kit.js +3 -5
  27. package/dist/command/cli/cloud-login.d.ts +1 -1
  28. package/dist/command/cli/cloud-login.js +27 -8
  29. package/dist/command/cli/demo/demo-page.js +6 -3
  30. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  31. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  32. package/dist/command/cli/launch/cli-launch.d.ts +8 -0
  33. package/dist/command/cli/launch/cli-launch.js +21 -9
  34. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  35. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  36. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  37. package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
  38. package/dist/command/cli/ports/resolve/cli-port.js +125 -15
  39. package/dist/command/cli/setup-mcp-cli.js +21 -13
  40. package/dist/command/cli/tutorial.d.ts +2 -2
  41. package/dist/command/cli/tutorial.js +21 -7
  42. package/dist/command/cli.js +79 -11
  43. package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
  44. package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
  45. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  46. package/dist/command/daemon/daemon-resolve.js +1 -1
  47. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  48. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  49. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  50. package/dist/command/dev/stale-issue-guard.js +8 -8
  51. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  52. package/dist/command/setup/bringup/connect-lease.js +38 -0
  53. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  54. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  55. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  56. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  57. package/dist/command/setup/bringup/dev-server-wait.d.ts +2 -0
  58. package/dist/command/setup/bringup/dev-server-wait.js +1 -1
  59. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  60. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  61. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  62. package/dist/command/setup/bringup/static-page-server.js +151 -0
  63. package/dist/command/setup/desktop-shape.d.ts +4 -0
  64. package/dist/command/setup/desktop-shape.js +18 -2
  65. package/dist/command/setup/init/init-port.d.ts +15 -0
  66. package/dist/command/setup/init/init-port.js +44 -0
  67. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  68. package/dist/command/setup/init/init-runtime.js +91 -9
  69. package/dist/command/setup/node-effects.d.ts +27 -14
  70. package/dist/command/setup/node-effects.js +86 -45
  71. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  72. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  73. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  74. package/dist/command/setup/probe/page-probe.js +18 -3
  75. package/dist/command/setup/run-setup.d.ts +28 -1
  76. package/dist/command/setup/run-setup.js +103 -15
  77. package/dist/command/setup/session-pick.d.ts +13 -0
  78. package/dist/command/setup/session-pick.js +20 -0
  79. package/dist/command/setup/setup-command.d.ts +2 -14
  80. package/dist/command/setup/setup-command.js +105 -31
  81. package/dist/command/setup/setup-mcp.d.ts +2 -0
  82. package/dist/command/setup/setup-mcp.js +17 -8
  83. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  84. package/dist/command/setup/terminal/wait-progress.js +17 -0
  85. package/dist/command/version/peer-announce.d.ts +6 -2
  86. package/dist/command/version/peer-announce.js +8 -6
  87. package/dist/command/version/version-nudge.d.ts +17 -3
  88. package/dist/command/version/version-nudge.js +25 -4
  89. package/dist/features/crawl/crawl-empty.js +1 -1
  90. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  91. package/dist/features/crawl/crawl-tools.js +57 -0
  92. package/dist/features/crawl/crawl.js +171 -157
  93. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  94. package/dist/features/exhaust/app-coverage.js +31 -0
  95. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  96. package/dist/features/exhaust/code-coverage.js +77 -0
  97. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  98. package/dist/features/exhaust/exhaust-run.js +82 -0
  99. package/dist/features/exhaust/explorer.d.ts +103 -0
  100. package/dist/features/exhaust/explorer.js +195 -0
  101. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  102. package/dist/features/exhaust/failure-branches.js +92 -0
  103. package/dist/features/exhaust/ledger.d.ts +202 -0
  104. package/dist/features/exhaust/ledger.js +252 -0
  105. package/dist/features/exhaust/session-fold.d.ts +31 -0
  106. package/dist/features/exhaust/session-fold.js +79 -0
  107. package/dist/features/exhaust/session-port.d.ts +36 -0
  108. package/dist/features/exhaust/session-port.js +102 -0
  109. package/dist/features/harness/drive-report.d.ts +0 -9
  110. package/dist/features/harness/drive-report.js +18 -0
  111. package/dist/features/harness/driver.js +4 -7
  112. package/dist/features/harness/fill-values.d.ts +10 -0
  113. package/dist/features/harness/fill-values.js +50 -11
  114. package/dist/features/harness/jev-driver.js +30 -13
  115. package/dist/features/harness/openai-driver.d.ts +12 -0
  116. package/dist/features/harness/openai-driver.js +21 -12
  117. package/dist/features/harness/platform-config.js +5 -6
  118. package/dist/features/license/license.d.ts +2 -2
  119. package/dist/index.d.ts +9 -0
  120. package/dist/index.js +56 -19
  121. package/dist/judgement/capsule/capsule-store.d.ts +4 -4
  122. package/dist/judgement/domain/domain-model.js +3 -1
  123. package/dist/judgement/oracles/self-instrument.js +4 -2
  124. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  125. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  126. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  127. package/dist/judgement/runs/verification-sync.js +1 -1
  128. package/dist/language/flows/change/affected.d.ts +9 -0
  129. package/dist/language/flows/change/affected.js +35 -0
  130. package/dist/language/flows/change/gate.d.ts +20 -0
  131. package/dist/language/flows/change/gate.js +12 -1
  132. package/dist/language/flows/flow-author.d.ts +7 -0
  133. package/dist/language/flows/flow-author.js +19 -0
  134. package/dist/language/flows/flow-expect-grammar.js +1 -1
  135. package/dist/language/flows/flow-journey.d.ts +54 -0
  136. package/dist/language/flows/flow-journey.js +119 -0
  137. package/dist/language/flows/flow-replay-run.d.ts +2 -3
  138. package/dist/language/flows/flow-replay-run.js +13 -47
  139. package/dist/language/flows/flow-replay.d.ts +22 -8
  140. package/dist/language/flows/flow-replay.js +55 -9
  141. package/dist/language/flows/flow-result.d.ts +2 -0
  142. package/dist/language/flows/flow-result.js +25 -0
  143. package/dist/language/flows/flow-tools.js +17 -5
  144. package/dist/language/flows/flows.d.ts +10 -1
  145. package/dist/language/flows/flows.js +90 -14
  146. package/dist/language/flows/recording/replay-halt.js +3 -1
  147. package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
  148. package/dist/language/flows/recording/tape/recordings.js +55 -1
  149. package/dist/language/flows/replay.d.ts +19 -0
  150. package/dist/language/flows/replay.js +42 -6
  151. package/dist/language/flows/verify-change-tools.js +5 -2
  152. package/dist/launch-chromium.d.ts +80 -0
  153. package/dist/launch-chromium.js +218 -0
  154. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  155. package/dist/memory/cloud/cloud-config.js +47 -7
  156. package/dist/memory/cloud/cloud-sync.d.ts +4 -1
  157. package/dist/memory/cloud/cloud-sync.js +6 -9
  158. package/dist/memory/cloud/harness-offer.js +5 -6
  159. package/dist/memory/cloud/harness-switch.js +5 -6
  160. package/dist/memory/cloud/sync-cycle.d.ts +77 -3
  161. package/dist/memory/cloud/sync-cycle.js +325 -55
  162. package/dist/memory/cloud/sync-daemon.js +24 -20
  163. package/dist/memory/cloud/sync-disk.js +16 -2
  164. package/dist/memory/intent/intent-shard.d.ts +10 -32
  165. package/dist/memory/intent/intent-shard.js +0 -10
  166. package/dist/memory/intent/intent-tools.js +8 -3
  167. package/dist/memory/journal/drive-flow.d.ts +18 -13
  168. package/dist/memory/journal/drive-flow.js +45 -37
  169. package/dist/memory/journal/envelope.d.ts +8 -8
  170. package/dist/memory/journal/journal-query.d.ts +14 -0
  171. package/dist/memory/journal/journal-query.js +15 -0
  172. package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
  173. package/dist/memory/journal/session-end.d.ts +9 -1
  174. package/dist/memory/journal/session-end.js +24 -12
  175. package/dist/memory/project/artifact-root-resolver.d.ts +10 -0
  176. package/dist/memory/project/artifact-root-resolver.js +46 -19
  177. package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
  178. package/dist/memory/project/dir/reticle-dir.js +1 -0
  179. package/dist/memory/project/project-tools.js +3 -1
  180. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  181. package/dist/portal/bridge/auth-failure-reason.js +4 -2
  182. package/dist/portal/bridge/bridge.d.ts +12 -3
  183. package/dist/portal/bridge/bridge.js +49 -3
  184. package/dist/portal/input/js-coverage.d.ts +25 -0
  185. package/dist/portal/input/js-coverage.js +30 -0
  186. package/dist/portal/input/real-input.d.ts +11 -0
  187. package/dist/portal/input/real-input.js +29 -4
  188. package/dist/portal/input/viewport-tools.js +16 -6
  189. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  190. package/dist/portal/pool/playwright-launcher.js +20 -13
  191. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  192. package/dist/portal/session/dev-server/dev-command.js +30 -12
  193. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  194. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  195. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  196. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  197. package/dist/portal/session/facts/observed-state.js +34 -0
  198. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  199. package/dist/portal/session/no-session-diagnosis.js +62 -5
  200. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  201. package/dist/portal/session/no-session-next-action.js +48 -9
  202. package/dist/portal/session/no-session-watch.d.ts +18 -0
  203. package/dist/portal/session/no-session-watch.js +88 -2
  204. package/dist/portal/session/session-health.js +3 -2
  205. package/dist/portal/session/session-manager.d.ts +20 -6
  206. package/dist/portal/session/session-manager.js +19 -3
  207. package/dist/portal/session/session-reaper.d.ts +28 -0
  208. package/dist/portal/session/session-reaper.js +49 -1
  209. package/dist/portal/session/session.d.ts +10 -8
  210. package/dist/portal/session/session.js +24 -32
  211. package/dist/surface/http-server.d.ts +3 -2
  212. package/dist/surface/http-server.js +8 -5
  213. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  214. package/dist/surface/mcp/mcp-proxy.js +2 -2
  215. package/dist/surface/mcp/mcp.js +5 -3
  216. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  217. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  218. package/dist/surface/mcp/proxy/proxy-log.js +3 -5
  219. package/dist/surface/mcp/server-instructions.js +9 -7
  220. package/dist/surface/tools/act/after-match.d.ts +17 -0
  221. package/dist/surface/tools/act/after-match.js +30 -0
  222. package/dist/surface/tools/act/durable.d.ts +69 -0
  223. package/dist/surface/tools/act/durable.js +139 -0
  224. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
  225. package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
  226. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  227. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  228. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  229. package/dist/surface/tools/act/proved-controls.js +11 -0
  230. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  231. package/dist/surface/tools/act/sequence-grade.js +32 -1
  232. package/dist/surface/tools/act-capsule.d.ts +7 -0
  233. package/dist/surface/tools/act-capsule.js +46 -1
  234. package/dist/surface/tools/act-sequence-tool.js +25 -8
  235. package/dist/surface/tools/act-tools.js +28 -52
  236. package/dist/surface/tools/assert/assert-grade.js +12 -1
  237. package/dist/surface/tools/assert/assert-verdict.js +2 -2
  238. package/dist/surface/tools/browser-tools.js +14 -2
  239. package/dist/surface/tools/coverage-tools.js +27 -1
  240. package/dist/surface/tools/dynamic-tools.js +29 -2
  241. package/dist/surface/tools/error-recovery.js +5 -6
  242. package/dist/surface/tools/feedback-invite.js +6 -4
  243. package/dist/surface/tools/harness-explore.d.ts +3 -2
  244. package/dist/surface/tools/harness-explore.js +12 -7
  245. package/dist/surface/tools/invoke-tool.js +12 -8
  246. package/dist/surface/tools/lease-tools.js +15 -6
  247. package/dist/surface/tools/live-call-text.js +4 -1
  248. package/dist/surface/tools/observe-tools.js +9 -1
  249. package/dist/surface/tools/tool-kit.d.ts +10 -0
  250. package/dist/surface/tools/tools.js +4 -0
  251. package/dist/telemetry/argument-shape.d.ts +0 -2
  252. package/dist/telemetry/argument-shape.js +1 -5
  253. package/dist/telemetry/error-class.d.ts +2 -3
  254. package/dist/telemetry/error-class.js +2 -3
  255. package/dist/telemetry/harness-drive.d.ts +0 -2
  256. package/dist/telemetry/harness-drive.js +0 -4
  257. package/dist/telemetry/onboarding-firsts.d.ts +2 -2
  258. package/dist/telemetry/onboarding-firsts.js +2 -2
  259. package/dist/wire-journal.d.ts +3 -0
  260. package/dist/wire-journal.js +4 -0
  261. package/dist/workspace-packages.d.ts +0 -2
  262. package/dist/workspace-packages.js +1 -9
  263. package/docs/README.md +4 -2
  264. package/docs/actions.mdx +3 -4
  265. package/docs/agent-cheatsheet.md +4 -4
  266. package/docs/architecture.md +6 -6
  267. package/docs/best-practices.mdx +1 -1
  268. package/docs/capabilities.mdx +32 -1
  269. package/docs/cli/cloud.mdx +20 -8
  270. package/docs/cli/doctor.mdx +4 -0
  271. package/docs/cli/feedback.mdx +2 -2
  272. package/docs/cli/gate.mdx +9 -4
  273. package/docs/cli/update.mdx +10 -0
  274. package/docs/cli.mdx +22 -7
  275. package/docs/desktop.mdx +14 -10
  276. package/docs/environment.mdx +87 -0
  277. package/docs/faq.mdx +18 -16
  278. package/docs/first-drive.md +5 -13
  279. package/docs/flows.md +21 -2
  280. package/docs/frameworks.mdx +29 -10
  281. package/docs/getting-started.md +15 -5
  282. package/docs/http-transport.md +6 -3
  283. package/docs/human-control.md +2 -2
  284. package/docs/index.mdx +16 -6
  285. package/docs/install-agentic.mdx +2 -2
  286. package/docs/install-manual.mdx +1 -1
  287. package/docs/integration-patterns.md +3 -3
  288. package/docs/multi-agent-testing.md +1 -1
  289. package/docs/packages/babel-plugin.mdx +1 -1
  290. package/docs/packages/browser.mdx +2 -2
  291. package/docs/packages/next.mdx +3 -0
  292. package/docs/packages/react.mdx +2 -2
  293. package/docs/packages/tauri.mdx +1 -1
  294. package/docs/packages/vite-plugin.mdx +4 -2
  295. package/docs/packages.mdx +9 -9
  296. package/docs/predicates.mdx +50 -5
  297. package/docs/quickstart.mdx +12 -10
  298. package/docs/recipes.mdx +2 -2
  299. package/docs/state-management.mdx +2 -2
  300. package/docs/system-map.md +1 -1
  301. package/docs/telemetry.md +1 -1
  302. package/docs/token-efficiency.md +3 -3
  303. package/docs/tools/act-and-wait.mdx +1 -0
  304. package/docs/tools/act-sequence.mdx +1 -1
  305. package/docs/tools/act.mdx +1 -1
  306. package/docs/tools/console.mdx +1 -1
  307. package/docs/tools/inspect.mdx +1 -1
  308. package/docs/tools/network.mdx +1 -1
  309. package/docs/tools/query.mdx +1 -1
  310. package/docs/tools/sessions.mdx +1 -1
  311. package/docs/tools/snapshot.mdx +1 -1
  312. package/docs/tools/state.mdx +1 -1
  313. package/docs/tools/wait-for.mdx +1 -1
  314. package/docs/usage.md +7 -7
  315. package/docs/use-cases.mdx +157 -0
  316. package/docs/vs-screenshots.mdx +3 -3
  317. package/docs/what-is-recorded.md +2 -2
  318. package/docs/what-is-reticle.mdx +17 -20
  319. package/docs/why-reticle.mdx +14 -21
  320. package/package.json +10 -9
  321. package/dist/judgement/capsule/minimize.d.ts +0 -12
  322. package/dist/judgement/capsule/minimize.js +0 -22
  323. package/dist/judgement/phenomena/phenomena.d.ts +0 -27
  324. package/dist/judgement/phenomena/phenomena.js +0 -101
  325. package/dist/language/flows/flow-report.d.ts +0 -22
  326. package/dist/language/flows/flow-report.js +0 -106
package/dist/index.js CHANGED
@@ -4,12 +4,12 @@ import { fetchPlatformConfig } from './features/harness/platform-config.js';
4
4
  import { join } from 'node:path';
5
5
  import { existsSync, readFileSync } from 'node:fs';
6
6
  import { homedir } from 'node:os';
7
- import { linkedCloudPort } from './memory/cloud/cloud-config.js';
7
+ import { linkedCloudPort, platformEnvPort } from './memory/cloud/cloud-config.js';
8
8
  import { attachCloudSync } from './memory/cloud/sync-daemon.js';
9
9
  import { wireHooks } from './hooks/hook-commands.js';
10
10
  import { PROJECT_REGISTRY_FILE, parseProjectRegistry, projectCandidates, } from '@reticlehq/core/artifacts';
11
11
  import { discoverProjectConfigs } from './command/cli/config/config-discovery.js';
12
- import { artifactRootResolver } from './memory/project/artifact-root-resolver.js';
12
+ import { artifactRootResolver, projectDirectoryFor, } from './memory/project/artifact-root-resolver.js';
13
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
14
  import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, bridgeWsUrl, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
15
15
  import { originOf } from './portal/session/session-manager.js';
@@ -21,10 +21,12 @@ import { reportAppInstrumented } from './telemetry/app-instrumented.js';
21
21
  import { resolveBridgeSecurityWithAutoToken } from './portal/bridge/bridge-security.js';
22
22
  import { Bridge } from './portal/bridge/bridge.js';
23
23
  import { sdkFixForDirectory } from './command/version/sdk-fix.js';
24
+ import { connectionSkew } from './command/version/version-nudge.js';
24
25
  import { SERVER_VERSION } from './command/version/identity/server-version.js';
25
26
  import { BaselineStore } from './memory/project/baselines.js';
26
27
  import { RecordingStore } from './language/flows/recording/tape/recordings.js';
27
28
  import { initImpact } from './memory/impact/impact-recorder.js';
29
+ import { flowAuthor } from './language/flows/flow-author.js';
28
30
  import { FlowStore } from './language/flows/flows.js';
29
31
  import { buildFlowChips } from './language/flows/flow-scope.js';
30
32
  import { ProjectStore } from './memory/project/project-store.js';
@@ -42,7 +44,7 @@ import { LEASE_ACQUIRE_TOOL } from './surface/tools/lease-tools.js';
42
44
  import { runTool } from './surface/tools/invoke-tool.js';
43
45
  import { SessionReaper, endAllSessions, MCP_DISCONNECT_SUMMARY, } from './portal/session/session-reaper.js';
44
46
  import { wireSessionScope } from './portal/session/no-session-watch.js';
45
- import { buildIdlePredicate } from './command/daemon/lifetime/daemon-usefulness.js';
47
+ import { buildIdlePredicate, noteSetupHold } from './command/daemon/lifetime/daemon-usefulness.js';
46
48
  import { resolveToolSurface } from './surface/tools/tool-surface.js';
47
49
  import { statusPayload } from './status-payload.js';
48
50
  import { CdpRealInputProvider, LaunchedRealInputProvider } from './portal/input/real-input.js';
@@ -54,7 +56,7 @@ import { LeaseReaper } from './portal/pool/lease-reaper.js';
54
56
  import { findProjectConfig, projectIdsAt, readJournalEnabled, readProjectId, } from './command/cli/ports/resolve/cli-port.js';
55
57
  import { hasAnyProjectConnectedBefore } from './memory/recall/prior/connection-memory.js';
56
58
  import { reticleStateHome } from './command/daemon/daemon.js';
57
- import { probeChromium } from './command/cli/doctor/browser/chromium-hint.js';
59
+ import { probeLaunchableChromium } from './launch-chromium.js';
58
60
  import { attachJournal } from './wire-journal.js';
59
61
  import { reportOnboardingStep } from './telemetry/onboarding-funnel.js';
60
62
  import { AMBIENT_RECORDING } from './language/flows/recording/tape/recordings.js';
@@ -80,6 +82,15 @@ export { buildSessionRecommendation } from './portal/session/presence/session-re
80
82
  export { TOOLS } from './surface/tools/tools.js';
81
83
  export { createToolInvoker, UNKNOWN_TOOL_ERROR } from './surface/tools/tool-invoker.js';
82
84
  export { runTool, SESSION_BOUND_TOOLS, SESSION_EXEMPT_TOOLS } from './surface/tools/invoke-tool.js';
85
+ /**
86
+ * The port-presence check the CLI commands (`drive`, `verify`, `status`, `doctor`, `kill`) all share,
87
+ * re-exported so a consumer embedding `start()` in its own process can refuse a taken port the same
88
+ * honest way instead of letting a raw `EADDRINUSE` stack out — the gap `@reticlehq/test`'s
89
+ * `bootSession()` had (reticlehq/reticle#1141): it called `start()` directly with no pre-flight probe.
90
+ */
91
+ export { probePresence, describePresence, PortPresence, } from './command/daemon/binding/port-presence.js';
92
+ export { probeDaemon } from './surface/mcp/proxy/proxy-daemon-probe.js';
93
+ export { fetchStatus } from './command/daemon/binding/daemon-status-probe.js';
83
94
  export { BaselineStore, normalizeLines, diffLines } from './memory/project/baselines.js';
84
95
  export { RecordingStore } from './language/flows/recording/tape/recordings.js';
85
96
  export { FlowStore, recordedStepToFlowStep } from './language/flows/flows.js';
@@ -215,12 +226,12 @@ async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
215
226
  }
216
227
  /** Start the Reticle bridge (browser WS endpoint) and, by default, the MCP stdio server. */
217
228
  /**
218
- * The SDK-upgrade sentence for THIS project's package.json, evaluated at each HELLO so a
219
- * just-edited manifest is what we name. Falls back to the framework-neutral sensor when cwd is
220
- * not an app.
229
+ * The SDK-upgrade sentence for the package.json of the project the page announced, evaluated at
230
+ * each HELLO so a just-edited manifest is what we name. The daemon's cwd only when that project's
231
+ * directory is unknown, and the framework-neutral sensor when that is not an app either (#1135).
221
232
  */
222
- function sdkFixForCwd() {
223
- return sdkFixForDirectory(SERVER_VERSION, process.cwd());
233
+ function sdkFixForProject(projectId) {
234
+ return sdkFixForDirectory(SERVER_VERSION, projectDirectoryFor(projectId) ?? process.cwd());
224
235
  }
225
236
  /**
226
237
  * Resolve a session's artifact root, from everything this machine knows about where projects live.
@@ -263,6 +274,8 @@ function knownProjectRoots() {
263
274
  }
264
275
  return [...roots];
265
276
  }
277
+ /** This project's platform credential (stored, else the env key), as the env the platform readers take. */
278
+ const platformEnvFor = (root) => platformEnvPort(createNodeFileSystem(), root ?? join(process.cwd(), ReticleDir.ROOT), homedir(), process.env);
266
279
  export async function start(options = {}) {
267
280
  const port = options.port ?? RETICLE_DEFAULT_PORT;
268
281
  // Open the user's impact record before anything can connect. Not inside the MCP branch: a daemon
@@ -273,11 +286,11 @@ export async function start(options = {}) {
273
286
  // The daemon owns both sides of this seam, so it is the layer that may join them: the cache
274
287
  // lives in cloud memory, the platform read lives in the harness feature, and neither is allowed
275
288
  // to reach for the other.
276
- config: harnessConfigSource(() => fetchPlatformConfig(process.env)),
289
+ config: harnessConfigSource(async () => fetchPlatformConfig(await platformEnvFor(options.reticleRoot)())),
277
290
  });
278
291
  const uninstallHooks = wireHooks(options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT), readProjectId(process.cwd()));
279
292
  const security = await resolveBridgeSecurityWithAutoToken(options);
280
- const bridge = new Bridge({ port, sdkFix: sdkFixForCwd, ...security });
293
+ const bridge = new Bridge({ port, sdkFix: sdkFixForProject, ...security });
281
294
  // Server-authoritative liveness: a Node-side reaper (immune to browser throttling) ends sessions
282
295
  // whose agent has gone idle, so a forgotten/crashed agent never leaves the HUD "running" forever.
283
296
  const reaper = new SessionReaper(bridge.sessions);
@@ -316,6 +329,7 @@ export async function start(options = {}) {
316
329
  takeAmbientTape: () => recordings.stop(AMBIENT_RECORDING),
317
330
  reportStep: reportOnboardingStep,
318
331
  flows,
332
+ author: flowAuthor,
319
333
  });
320
334
  const project = new ProjectStore(fs, reticleRoot, { now });
321
335
  attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
@@ -352,16 +366,23 @@ export async function start(options = {}) {
352
366
  project,
353
367
  fs,
354
368
  reticleRoot,
369
+ linkedCloud: linkedCloudPort(fs, reticleRoot, homedir(), process.env),
355
370
  artifactRootFor: artifactRootResolver(reticleRoot),
356
371
  now,
357
372
  bridgePort: port,
358
- browserProbe: probeChromium,
373
+ browserProbe: probeLaunchableChromium,
359
374
  // The daemon's OWN project, so a tool can tell "this session is mine" from "this session
360
375
  // belongs to a sibling app under the same daemon". contract_save refuses on the second.
361
376
  ...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
362
377
  };
363
378
  const profile = resolveToolSurface(options.toolProfile);
364
- const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile, hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd())));
379
+ const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile,
380
+ // A session live RIGHT NOW is stronger evidence than the durable memory, which can be empty or
381
+ // stale for a project the plugin wired without writing `.reticle.json` (see connection-memory.ts's
382
+ // KNOWN LIMIT). Without this OR, that project's `initialize` led with the first-install steps
383
+ // while a real session was already connected — see #1138.
384
+ bridge.sessions.count() > 0 ||
385
+ hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd())));
365
386
  // When the agent (the MCP client) disconnects cleanly, end every active session at once so the
366
387
  // HUD doesn't linger. (If the agent instead KILLS this process, the WS dies and the browser
367
388
  // self-ends via SESSION_LIFECYCLE.BRIDGE_LOST_MS — see transport.ts.)
@@ -404,11 +425,16 @@ export async function startDaemon(options = {}) {
404
425
  // The daemon owns both sides of this seam, so it is the layer that may join them: the cache
405
426
  // lives in cloud memory, the platform read lives in the harness feature, and neither is allowed
406
427
  // to reach for the other.
407
- config: harnessConfigSource(() => fetchPlatformConfig(process.env)),
428
+ config: harnessConfigSource(async () => fetchPlatformConfig(await platformEnvFor(options.reticleRoot)())),
408
429
  });
409
430
  const security = await resolveBridgeSecurityWithAutoToken(options);
410
431
  const shared = createSharedServer(security.token === undefined ? {} : { token: security.token });
411
- const bridge = new Bridge({ port, server: shared.httpServer, sdkFix: sdkFixForCwd, ...security });
432
+ const bridge = new Bridge({
433
+ port,
434
+ server: shared.httpServer,
435
+ sdkFix: sdkFixForProject,
436
+ ...security,
437
+ });
412
438
  // The daemon owns listen (below), so the real bind error is reported there; absorb bridge.ready's
413
439
  // mirror rejection so a port collision can't surface as an unhandled promise rejection.
414
440
  void bridge.ready.catch(() => undefined);
@@ -420,7 +446,12 @@ export async function startDaemon(options = {}) {
420
446
  // answers WHY rather than just "sessionCount: 0".
421
447
  // `verifyPort` rides along so a later `serve --http` can tell whether this daemon already honours
422
448
  // the requested `--http-port` instead of silently ignoring the flag (#687).
423
- shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint(), verifyHttp?.port, bridge.sessions.noSessionLead()));
449
+ shared.attachStatus((held) => {
450
+ // `init` holds the daemon it is waiting on, so the idle rule cannot shut it down mid-wait.
451
+ if (held)
452
+ noteSetupHold(Date.now());
453
+ return statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint(), verifyHttp?.port, bridge.sessions.noSessionLead());
454
+ });
424
455
  // Agent-independent presence: the daemon outlives any single agent, so when the LAST agent's MCP
425
456
  // connection drops (it stopped, or is waiting on the human), end every session and push a clear
426
457
  // "go to your terminal" notice to the panel — the human is on the browser and must not lose a typed
@@ -462,6 +493,7 @@ export async function startDaemon(options = {}) {
462
493
  takeAmbientTape: () => recordings.stop(AMBIENT_RECORDING),
463
494
  reportStep: reportOnboardingStep,
464
495
  flows,
496
+ author: flowAuthor,
465
497
  onRunPersisted: () => syncNudge.run?.(),
466
498
  });
467
499
  const project = new ProjectStore(fs, reticleRoot, { now });
@@ -501,7 +533,7 @@ export async function startDaemon(options = {}) {
501
533
  // console and the panel cannot disagree about a setting they both offer. Nothing is awaited and a
502
534
  // failure is not surfaced: the next snapshot re-reads the platform, so a lost write shows up as
503
535
  // the switch springing back, which is the truthful outcome.
504
- bridge.attachHarnessRequest((enabled) => void writeHarnessSwitch(process.env, enabled));
536
+ bridge.attachHarnessRequest((enabled) => void platformEnvFor(reticleRoot)().then((env) => writeHarnessSwitch(env, enabled)));
505
537
  // Scope auto-selection to the active project (from .reticle.json) so a stray tab from another app is
506
538
  // never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
507
539
  // Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
@@ -533,7 +565,7 @@ export async function startDaemon(options = {}) {
533
565
  artifactRootFor: artifactRootResolver(reticleRoot),
534
566
  now,
535
567
  bridgePort: port,
536
- browserProbe: probeChromium,
568
+ browserProbe: probeLaunchableChromium,
537
569
  // A finished verification should not sit behind a one-minute timer — see ToolDeps.onRunPersisted.
538
570
  onRunPersisted: () => cloudSync.nudge(),
539
571
  };
@@ -541,7 +573,12 @@ export async function startDaemon(options = {}) {
541
573
  const effectiveDeps = realInput !== undefined ? { ...deps, realInput } : deps;
542
574
  // Read per attach, not once: a project that gets wired while this daemon is alive should stop
543
575
  // being told to wire itself on the next agent that connects.
544
- shared.attachMcp(() => createMcpServer(effectiveDeps, profile, hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd()))));
576
+ shared.attachMcp((peerSkew) => createMcpServer(peerSkew === undefined
577
+ ? effectiveDeps
578
+ : { ...effectiveDeps, peerSkew: connectionSkew(peerSkew) }, profile,
579
+ // See the sibling call in `start`: a live session outweighs empty/stale durable memory (#1138).
580
+ bridge.sessions.count() > 0 ||
581
+ hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd()))));
545
582
  // `reticle drive <url>` when this daemon already owns the port: it asks HERE instead of trying to
546
583
  // bind a port we are holding, and gets the same pooled context an agent's reticle_lease returns —
547
584
  // through runTool, so it is counted and reported like any other call rather than being a second,
@@ -42,10 +42,10 @@ declare const CapsuleSchema: z.ZodObject<{
42
42
  seen: z.ZodOptional<z.ZodNumber>;
43
43
  lastSeenAt: z.ZodOptional<z.ZodNumber>;
44
44
  }, "strip", z.ZodTypeAny, {
45
- expected: string;
46
- origin: string;
47
45
  version: 1;
48
46
  id: string;
47
+ origin: string;
48
+ expected: string;
49
49
  steps: FlowStep[];
50
50
  createdAt: number;
51
51
  observed: string;
@@ -53,10 +53,10 @@ declare const CapsuleSchema: z.ZodObject<{
53
53
  seen?: number | undefined;
54
54
  lastSeenAt?: number | undefined;
55
55
  }, {
56
- expected: string;
57
- origin: string;
58
56
  version: 1;
59
57
  id: string;
58
+ origin: string;
59
+ expected: string;
60
60
  steps: FlowStep[];
61
61
  createdAt: number;
62
62
  observed: string;
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Pure: no IO, no clock. The tool layer loads flows + contract and calls this.
14
14
  */
15
- import { AnchorKind, PredicateKind, } from '@reticlehq/core';
15
+ import { AnchorKind, PredicateKind, compareSourceClauses, } from '@reticlehq/core';
16
16
  import { classifyFlowAssertions, FlowAssertionGrade } from '../../language/flows/flow-classify.js';
17
17
  import { successLabel } from '../../language/flows/flow-success.js';
18
18
  import { flowRisk, latestRun, rankByRisk, RiskLevel } from './flow-risk.js';
@@ -41,6 +41,8 @@ function walkClauses(predicate) {
41
41
  }
42
42
  if (PredicateKind.NOT === predicate.kind)
43
43
  return walkClauses(predicate.predicate);
44
+ if (PredicateKind.COMPARE === predicate.kind)
45
+ return compareSourceClauses(predicate);
44
46
  return [predicate];
45
47
  }
46
48
  function signalNamesIn(predicate) {
@@ -9,7 +9,7 @@
9
9
  * `missing-signal` while honesty said `no-signal-on-mutation` for the same absence.
10
10
  */
11
11
  import { InstrumentationGapKind } from '@reticlehq/core/artifacts';
12
- /** PascalCase a store name for a `useX.getState` hint (cart → Cart). */
12
+ /** PascalCase a store name for a `useX` hint (cart → Cart). */
13
13
  function pascal(name) {
14
14
  return 0 === name.length ? name : name[0]?.toUpperCase() + name.slice(1);
15
15
  }
@@ -18,7 +18,9 @@ function insertFor(gap) {
18
18
  case InstrumentationGapKind.NO_SIGNAL_ON_MUTATION:
19
19
  return `reticle.signal('${gap.name}');`;
20
20
  case InstrumentationGapKind.NO_STORE_REGISTERED:
21
- return `registerStore('${gap.name}', () => use${pascal(gap.name)}.getState());`;
21
+ // The store itself, so its subscribe is wired too. A `() => useX.getState()` getter is
22
+ // readable but never reports a change, which is the state this gap is about (#1146).
23
+ return `registerStore('${gap.name}', use${pascal(gap.name)});`;
22
24
  case InstrumentationGapKind.MISSING_TESTID:
23
25
  return `data-testid="${gap.name}"`;
24
26
  }
@@ -4,8 +4,8 @@
4
4
  * perspective a flow that drifted no longer behaves, so it must not read as green. The actionable
5
5
  * "why" is lifted from the replay's decision envelope (or its error) into failureReason.
6
6
  */
7
- import { ReplayStatus, RunFlowStatus, type FlowReplayResult, type RunFlowResult } from '@reticlehq/core';
7
+ import { ReplayStatus, RunFlowStatus, type FlowFile, type FlowReplayResult, type RunFlowResult } from '@reticlehq/core';
8
8
  /** OK → PASS; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
9
9
  export declare function runFlowStatusOf(status: ReplayStatus): RunFlowStatus;
10
10
  /** Map one replay (plus its measured duration) into a RunFlowResult for the artifact's flows[]. */
11
- export declare function mapReplayToFlowResult(replay: FlowReplayResult, durationMs: number): RunFlowResult;
11
+ export declare function mapReplayToFlowResult(replay: FlowReplayResult, durationMs: number, flow?: FlowFile): RunFlowResult;
@@ -4,21 +4,52 @@
4
4
  * perspective a flow that drifted no longer behaves, so it must not read as green. The actionable
5
5
  * "why" is lifted from the replay's decision envelope (or its error) into failureReason.
6
6
  */
7
- import { ReplayStatus, RunFlowStatus, } from '@reticlehq/core';
7
+ import { ReplayStatus, RUN_RECORDING_MAX_BYTES, RUN_STEP_RESULTS_MAX, RUN_TEXT_MAX, RunFlowStatus, } from '@reticlehq/core';
8
8
  import { SUCCESS_STEP_TOOL } from '../../../language/flows/flow-success.js';
9
9
  /** OK → PASS; DRIFT/ERROR → FAIL (a healed flow is produced by the heal path, not plain replay). */
10
10
  export function runFlowStatusOf(status) {
11
11
  return status === ReplayStatus.OK ? RunFlowStatus.PASS : RunFlowStatus.FAIL;
12
12
  }
13
+ const clip = (text) => text.length > RUN_TEXT_MAX ? `${text.slice(0, RUN_TEXT_MAX - 1)}…` : text;
14
+ /** One step as synced: what it did, where, and whether it held. */
15
+ function stepResultOf(step) {
16
+ return {
17
+ step: step.step,
18
+ anchor: clip(step.anchor),
19
+ ok: step.ok,
20
+ ...(step.page === undefined ? {} : { page: step.page }),
21
+ ...(step.endPage === undefined ? {} : { endPage: step.endPage }),
22
+ ...(step.consequence === undefined ? {} : { consequence: clip(step.consequence) }),
23
+ ...(step.error === undefined ? {} : { error: clip(step.error) }),
24
+ ...(step.drift === undefined ? {} : { drift: step.drift.reasonKind }),
25
+ };
26
+ }
27
+ /**
28
+ * The flow as recorded, when it is small enough to travel — what lets a teammate replay this run
29
+ * from the cloud. Fill values were already redacted when the flow was saved.
30
+ */
31
+ function recordingOf(flow) {
32
+ const recording = {
33
+ ...(flow.startPath === undefined ? {} : { startPath: flow.startPath }),
34
+ steps: flow.steps,
35
+ };
36
+ return Buffer.byteLength(JSON.stringify(recording)) > RUN_RECORDING_MAX_BYTES
37
+ ? { recordingOmitted: true }
38
+ : { recording };
39
+ }
13
40
  /** Map one replay (plus its measured duration) into a RunFlowResult for the artifact's flows[]. */
14
- export function mapReplayToFlowResult(replay, durationMs) {
15
- const status = runFlowStatusOf(replay.status);
16
- const failureReason = status === RunFlowStatus.FAIL
17
- ? (replay.decision?.whatChanged ??
18
- replay.decision?.summary ??
19
- replay.error?.message ??
20
- 'flow failed')
21
- : undefined;
41
+ export function mapReplayToFlowResult(replay, durationMs, flow) {
42
+ // A replay that could not start returns OK with no steps. Nothing ran, so it is SKIPPED — which
43
+ // every reader of a run (the verdict, the gate) already refuses to count as a pass.
44
+ const status = replay.unverifiable === undefined ? runFlowStatusOf(replay.status) : RunFlowStatus.SKIPPED;
45
+ const failureReason = replay.unverifiable !== undefined
46
+ ? replay.unverifiable.reason
47
+ : status === RunFlowStatus.FAIL
48
+ ? (replay.decision?.whatChanged ??
49
+ replay.decision?.summary ??
50
+ replay.error?.message ??
51
+ 'flow failed')
52
+ : undefined;
22
53
  // A flow with a success oracle gets a synthetic 'success' step appended by replay; surface its label
23
54
  // as the run's `oracle` so the verdict counts this flow as consequence-backed (→ HIGH confidence),
24
55
  // not a bare smoke click. Without this, an oracle-backed pass reads as MEDIUM and undersells itself.
@@ -30,5 +61,9 @@ export function mapReplayToFlowResult(replay, durationMs) {
30
61
  durationMs,
31
62
  ...(oracle !== undefined ? { oracle } : {}),
32
63
  ...(failureReason !== undefined ? { failureReason } : {}),
64
+ ...(0 === replay.steps.length
65
+ ? {}
66
+ : { stepResults: replay.steps.slice(0, RUN_STEP_RESULTS_MAX).map(stepResultOf) }),
67
+ ...(flow === undefined ? {} : recordingOf(flow)),
33
68
  };
34
69
  }
@@ -1,9 +1,11 @@
1
- import { type ProjectId, type FlowReplayResult } from '@reticlehq/core';
1
+ import { type ProjectId, type FlowFile, type FlowReplayResult } from '@reticlehq/core';
2
2
  import type { ToolDeps } from '../../surface/tools/tool-kit.js';
3
3
  /** A replay plus the wall-clock time it took — the shape the verify handler already collects. */
4
4
  export interface TimedReplay {
5
5
  replay: FlowReplayResult;
6
6
  durationMs: number;
7
+ /** The flow file that was replayed, so the synced run can carry its recording. */
8
+ flow?: FlowFile;
7
9
  }
8
10
  /**
9
11
  * Persist the suite's run artifact and best-effort mirror it to the cloud. Never throws into the verify
@@ -23,7 +23,7 @@ import { rootForProjectId } from '../../memory/project/session-root.js';
23
23
  const RETICLE_MCP_AGENT_ID = 'reticle-mcp';
24
24
  /** Assemble the ReticleVerificationRun artifact for a verify suite (pure — no IO). */
25
25
  function assembleRun(deps, timed, projectId) {
26
- const flows = timed.map((t) => mapReplayToFlowResult(t.replay, t.durationMs));
26
+ const flows = timed.map((t) => mapReplayToFlowResult(t.replay, t.durationMs, t.flow));
27
27
  const input = {
28
28
  runId: defaultRunId(),
29
29
  durationMs: flows.reduce((sum, f) => sum + f.durationMs, 0),
@@ -22,3 +22,12 @@ export interface AffectedResult {
22
22
  * a repo-relative change matches an absolute stamp and vice-versa.
23
23
  */
24
24
  export declare function affectedFlows(flows: readonly FlowSources[], changedFiles: readonly string[]): AffectedResult;
25
+ export declare function isInteractiveSource(text: string): boolean;
26
+ /**
27
+ * Changed interactive components that no saved flow's manifest names.
28
+ *
29
+ * `affectedFlows` starts from the flows, so a file no flow ever touched affects nothing — and a gate
30
+ * built only on it passed a brand-new feature that had never been driven. This starts from the
31
+ * change instead. `isInteractive` is injected so the decision stays pure; the CLI reads the file.
32
+ */
33
+ export declare function unflowedFiles(flows: readonly FlowSources[], changedFiles: readonly string[], isInteractive: (file: string) => boolean): string[];
@@ -33,3 +33,38 @@ export function affectedFlows(flows, changedFiles) {
33
33
  }
34
34
  return { affected, unknownProvenance };
35
35
  }
36
+ /** A file a source stamper can put on an element — the only files a flow's manifest can ever name. */
37
+ const COMPONENT_FILE = /\.(?:jsx|tsx|vue|svelte|astro)$/;
38
+ /** Same test-file rule the stampers use, so a test is never demanded a flow. */
39
+ const TEST_FILE = /(?:\.(?:test|spec)\.[^./]+$)|(?:^|\/)__tests__\//;
40
+ /**
41
+ * Something a step could act on: a handler prop (`onClick=`, `@click`, `v-on:`, `on:submit`) or a
42
+ * native control.
43
+ *
44
+ * ponytail: a text heuristic, not a parse. A flow's manifest only names the file of the element a
45
+ * step ACTED on, so a display-only component can never be attributed and demanding a flow for one
46
+ * would block forever. A handler passed in from a parent under another name escapes it; upgrade to
47
+ * recording the source of the element an `expect` matched if that proves common.
48
+ */
49
+ const INTERACTIVE = /\bon[A-Z]\w*=|@[a-z]+[=.]|\bv-on:|\bon:[a-z]+|<(?:button|input|select|textarea|form|a|summary)[\s>/]/;
50
+ export function isInteractiveSource(text) {
51
+ return INTERACTIVE.test(text);
52
+ }
53
+ /**
54
+ * Changed interactive components that no saved flow's manifest names.
55
+ *
56
+ * `affectedFlows` starts from the flows, so a file no flow ever touched affects nothing — and a gate
57
+ * built only on it passed a brand-new feature that had never been driven. This starts from the
58
+ * change instead. `isInteractive` is injected so the decision stays pure; the CLI reads the file.
59
+ */
60
+ export function unflowedFiles(flows, changedFiles, isInteractive) {
61
+ const sources = flows.flatMap((flow) => (flow.sources ?? []).map(normalize));
62
+ return changedFiles.filter((file) => {
63
+ const c = normalize(file);
64
+ if (!COMPONENT_FILE.test(c) || TEST_FILE.test(c))
65
+ return false;
66
+ if (sources.some((s) => c === s || c.endsWith(`/${s}`) || s.endsWith(`/${c}`)))
67
+ return false;
68
+ return isInteractive(file);
69
+ });
70
+ }
@@ -22,6 +22,16 @@ interface GateInput {
22
22
  downgraded?: readonly DowngradedFlow[];
23
23
  /** Flows that covered a changed file but no longer exist — coverage deleted rather than satisfied. */
24
24
  deleted?: readonly string[];
25
+ /** Changed interactive components no saved flow touches — a change nothing has driven. */
26
+ unflowed?: readonly string[];
27
+ /** Coverage levels below the best they ever reached — something new appeared, nothing covered it. */
28
+ coverageRegressed?: readonly {
29
+ level: string;
30
+ was: number;
31
+ now: number;
32
+ }[];
33
+ /** Changed files the browser loaded and never ran a function of. */
34
+ unexecuted?: readonly string[];
25
35
  }
26
36
  interface GateResult {
27
37
  /** True when every affected, non-flaky flow has a passing artifact AND nothing was weakened/deleted. */
@@ -34,6 +44,16 @@ interface GateResult {
34
44
  downgraded: DowngradedFlow[];
35
45
  /** Deleted flows that covered changed files — BLOCKING for the same reason. */
36
46
  deleted: string[];
47
+ /** Changed interactive components no flow covers — BLOCKING: the change was never driven. */
48
+ unflowed: string[];
49
+ /** BLOCKING: the app grew something nothing has covered. */
50
+ coverageRegressed: {
51
+ level: string;
52
+ was: number;
53
+ now: number;
54
+ }[];
55
+ /** BLOCKING: changed code that never ran in any drive. */
56
+ unexecuted: string[];
37
57
  }
38
58
  export declare function gateDecision(input: GateInput): GateResult;
39
59
  export {};
@@ -23,11 +23,22 @@ export function gateDecision(input) {
23
23
  // trivially gameable, which is the exact failure this gate exists to prevent.
24
24
  const downgraded = [...(input.downgraded ?? [])];
25
25
  const deleted = [...(input.deleted ?? [])];
26
+ const unflowed = [...(input.unflowed ?? [])];
27
+ const coverageRegressed = [...(input.coverageRegressed ?? [])];
28
+ const unexecuted = [...(input.unexecuted ?? [])];
26
29
  return {
27
- pass: 0 === uncovered.length && 0 === downgraded.length && 0 === deleted.length,
30
+ pass: 0 === uncovered.length &&
31
+ 0 === downgraded.length &&
32
+ 0 === deleted.length &&
33
+ 0 === unflowed.length &&
34
+ 0 === coverageRegressed.length &&
35
+ 0 === unexecuted.length,
28
36
  uncovered,
29
37
  quarantined,
30
38
  downgraded,
31
39
  deleted,
40
+ unflowed,
41
+ coverageRegressed,
42
+ unexecuted,
32
43
  };
33
44
  }
@@ -0,0 +1,7 @@
1
+ import type { FlowFile } from '@reticlehq/core';
2
+ /**
3
+ * Who is making a flow right now: the agent that announced itself over MCP, and the person signed in
4
+ * to Reticle on this machine. A key handed over in the environment names no person, so none is
5
+ * written — the cloud can still attribute the push by the key that sent it.
6
+ */
7
+ export declare function flowAuthor(env?: NodeJS.ProcessEnv, home?: string): FlowFile['author'];
@@ -0,0 +1,19 @@
1
+ import { homedir } from 'node:os';
2
+ import { mcpClientIdentity } from '../../surface/mcp/peer/client-identity.js';
3
+ import { readAccountState } from '../../memory/cloud/account-state.js';
4
+ /**
5
+ * Who is making a flow right now: the agent that announced itself over MCP, and the person signed in
6
+ * to Reticle on this machine. A key handed over in the environment names no person, so none is
7
+ * written — the cloud can still attribute the push by the key that sent it.
8
+ */
9
+ export function flowAuthor(env = process.env, home = homedir()) {
10
+ const agent = mcpClientIdentity().name;
11
+ const account = readAccountState(home, env);
12
+ const person = account.signedIn ? account.email : undefined;
13
+ if (agent === undefined && person === undefined)
14
+ return undefined;
15
+ return {
16
+ ...(agent === undefined ? {} : { agent }),
17
+ ...(person === undefined ? {} : { person }),
18
+ };
19
+ }
@@ -215,7 +215,7 @@ export function parseFlowFileText(text) {
215
215
  ok: false,
216
216
  code: FlowErrorCode.WRONG_VERSION,
217
217
  detail: `this flow file is version ${String(declared)} and this Reticle reads versions ` +
218
- `${[...READABLE_FLOW_VERSIONS].join(' and ')}. The file is not damaged, the reader is the ` +
218
+ `${[...READABLE_FLOW_VERSIONS].join(', ')}. The file is not damaged, the reader is the ` +
219
219
  'wrong one. ' +
220
220
  'Upgrade or downgrade Reticle rather than editing the flow.',
221
221
  };
@@ -0,0 +1,54 @@
1
+ import { type FlowFile, type FlowStep, type Predicate } from '@reticlehq/core';
2
+ /** A short, stable identity for a journey's shape: its start page and its steps. */
3
+ export declare function journeyFingerprint(flow: Pick<FlowFile, 'startPath' | 'steps'>): string;
4
+ /**
5
+ * Fold two copies of one journey into one file.
6
+ *
7
+ * Gap-filling, never overwriting: whatever the kept flow already says — its name, its intent, a
8
+ * step's declared consequence, a quarantine a person wrote — survives, and the other copy only
9
+ * supplies what is missing. So a merge can add an assertion or an intent and can never remove one.
10
+ */
11
+ export declare function mergeJourney(kept: FlowFile, other: FlowFile): FlowFile;
12
+ /**
13
+ * The name a new journey saves under.
14
+ *
15
+ * Its readable name when that is free or already this journey's; otherwise the same name with the
16
+ * fingerprint's head, so two different journeys that share an intent never overwrite each other.
17
+ */
18
+ export declare function journeyName(base: string, fingerprint: string, taken: ReadonlyMap<string, string>): string;
19
+ /**
20
+ * What must hold before a journey and what it leaves behind, read from where it started and ended.
21
+ *
22
+ * Routes only: the page is the one piece of state every recorded step knows for certain, and a
23
+ * claim written from a guess about the rest would be a precondition nobody met. Written in the
24
+ * exact form `canFollow` compares by value, so A-then-B is answerable from the two files alone.
25
+ */
26
+ export declare function routeClaims(startPath: string | undefined, steps: readonly FlowStep[]): {
27
+ requires?: Predicate[];
28
+ ensures?: Predicate[];
29
+ };
30
+ /**
31
+ * The preconditions only another flow can establish.
32
+ *
33
+ * A route claim on the page the flow starts on is not one of them: replay navigates there itself, so
34
+ * it neither opts the flow out of the reset nor gates the run. That is the claim a saved drive writes
35
+ * for the map (see routeClaims) — reading it as "starts from leftover state" would stop every
36
+ * auto-saved flow from ever being reset.
37
+ */
38
+ export declare function establishedState(flow: FlowFile): readonly Predicate[];
39
+ /**
40
+ * Is the tab where the flow asked to start? Up to a trailing slash, and up to the query the flow
41
+ * did not ask about.
42
+ *
43
+ * `startPath` is the SPECIFICATION, so it decides what counts. A query it recorded is compared:
44
+ * `?tab=wrap` and `?tab=summary` are different pages, and a replay that starts on the wrong one
45
+ * proves nothing about the right one. A query it did NOT record is ignored: the tab carrying
46
+ * `?next=%2F` on a login page, or the identity params Reticle puts on a leased tab, are not the flow
47
+ * being elsewhere, and navigating to strip them costs a session for nothing.
48
+ *
49
+ * The asymmetry is the whole point and the reason `observed` and `expected` are named rather than
50
+ * `a` and `b`. Comparing with the query on both sides always re-navigated a query-bearing
51
+ * `startPath` (#1059, which killed the session mid-flow); comparing with it on neither side reads a
52
+ * tab on `?tab=summary` as already at `?tab=wrap`.
53
+ */
54
+ export declare function samePath(observed: string, expected: string): boolean;