@reticlehq/server 3.3.0 → 3.4.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 (299) 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 +4 -0
  15. package/dist/command/cli/auth/cloud-session.js +2 -0
  16. package/dist/command/cli/cli-doctor.js +7 -5
  17. package/dist/command/cli/cli-flow-commands.d.ts +3 -6
  18. package/dist/command/cli/cli-flow-commands.js +67 -4
  19. package/dist/command/cli/cli-parse.d.ts +8 -1
  20. package/dist/command/cli/cli-parse.js +21 -3
  21. package/dist/command/cli/cli-update-commands.js +8 -1
  22. package/dist/command/cli/cli-verify.d.ts +24 -3
  23. package/dist/command/cli/cli-verify.js +54 -11
  24. package/dist/command/cli/cloud-cli.js +5 -2
  25. package/dist/command/cli/cloud-kit.js +3 -5
  26. package/dist/command/cli/cloud-login.js +21 -3
  27. package/dist/command/cli/demo/demo-page.js +6 -3
  28. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +5 -0
  29. package/dist/command/cli/doctor/browser/chromium-hint.js +5 -0
  30. package/dist/command/cli/launch/cli-launch.d.ts +8 -0
  31. package/dist/command/cli/launch/cli-launch.js +21 -9
  32. package/dist/command/cli/launch/sdk-version-match.d.ts +84 -0
  33. package/dist/command/cli/launch/sdk-version-match.js +206 -0
  34. package/dist/command/cli/lifecycle/daemon-lifecycle.js +2 -16
  35. package/dist/command/cli/ports/resolve/cli-port.d.ts +25 -0
  36. package/dist/command/cli/ports/resolve/cli-port.js +125 -15
  37. package/dist/command/cli/setup-mcp-cli.js +13 -13
  38. package/dist/command/cli/tutorial.d.ts +2 -2
  39. package/dist/command/cli/tutorial.js +21 -7
  40. package/dist/command/cli.js +62 -10
  41. package/dist/command/daemon/binding/daemon-status-probe.d.ts +14 -2
  42. package/dist/command/daemon/binding/daemon-status-probe.js +34 -6
  43. package/dist/command/daemon/daemon-resolve.d.ts +9 -0
  44. package/dist/command/daemon/daemon-resolve.js +1 -1
  45. package/dist/command/daemon/lifetime/daemon-usefulness.d.ts +7 -1
  46. package/dist/command/daemon/lifetime/daemon-usefulness.js +30 -7
  47. package/dist/command/dev/stale-issue-guard.d.ts +3 -4
  48. package/dist/command/dev/stale-issue-guard.js +8 -8
  49. package/dist/command/setup/bringup/connect-lease.d.ts +17 -0
  50. package/dist/command/setup/bringup/connect-lease.js +38 -0
  51. package/dist/command/setup/bringup/dev-server-files.d.ts +68 -0
  52. package/dist/command/setup/bringup/dev-server-files.js +131 -0
  53. package/dist/command/setup/bringup/dev-server-log-cap.d.ts +35 -0
  54. package/dist/command/setup/bringup/dev-server-log-cap.js +121 -0
  55. package/dist/command/setup/bringup/ensure-daemon.d.ts +65 -4
  56. package/dist/command/setup/bringup/ensure-daemon.js +135 -16
  57. package/dist/command/setup/bringup/static-page-server.d.ts +35 -0
  58. package/dist/command/setup/bringup/static-page-server.js +151 -0
  59. package/dist/command/setup/desktop-shape.d.ts +4 -0
  60. package/dist/command/setup/desktop-shape.js +18 -2
  61. package/dist/command/setup/init/init-port.d.ts +15 -0
  62. package/dist/command/setup/init/init-port.js +44 -0
  63. package/dist/command/setup/init/init-runtime.d.ts +1 -1
  64. package/dist/command/setup/init/init-runtime.js +39 -2
  65. package/dist/command/setup/node-effects.d.ts +27 -14
  66. package/dist/command/setup/node-effects.js +80 -44
  67. package/dist/command/setup/probe/loopback-probe-urls.d.ts +7 -0
  68. package/dist/command/setup/probe/loopback-probe-urls.js +40 -0
  69. package/dist/command/setup/probe/page-probe.d.ts +13 -2
  70. package/dist/command/setup/probe/page-probe.js +18 -3
  71. package/dist/command/setup/run-setup.d.ts +26 -1
  72. package/dist/command/setup/run-setup.js +96 -13
  73. package/dist/command/setup/session-pick.d.ts +13 -0
  74. package/dist/command/setup/session-pick.js +20 -0
  75. package/dist/command/setup/setup-command.d.ts +2 -14
  76. package/dist/command/setup/setup-command.js +105 -31
  77. package/dist/command/setup/setup-mcp.js +3 -3
  78. package/dist/command/setup/terminal/wait-progress.d.ts +8 -0
  79. package/dist/command/setup/terminal/wait-progress.js +17 -0
  80. package/dist/features/crawl/crawl-empty.js +1 -1
  81. package/dist/features/crawl/crawl-tools.d.ts +6 -0
  82. package/dist/features/crawl/crawl-tools.js +57 -0
  83. package/dist/features/crawl/crawl.js +171 -157
  84. package/dist/features/exhaust/app-coverage.d.ts +33 -0
  85. package/dist/features/exhaust/app-coverage.js +31 -0
  86. package/dist/features/exhaust/code-coverage.d.ts +47 -0
  87. package/dist/features/exhaust/code-coverage.js +77 -0
  88. package/dist/features/exhaust/exhaust-run.d.ts +33 -0
  89. package/dist/features/exhaust/exhaust-run.js +82 -0
  90. package/dist/features/exhaust/explorer.d.ts +103 -0
  91. package/dist/features/exhaust/explorer.js +195 -0
  92. package/dist/features/exhaust/failure-branches.d.ts +33 -0
  93. package/dist/features/exhaust/failure-branches.js +92 -0
  94. package/dist/features/exhaust/ledger.d.ts +202 -0
  95. package/dist/features/exhaust/ledger.js +252 -0
  96. package/dist/features/exhaust/session-fold.d.ts +31 -0
  97. package/dist/features/exhaust/session-fold.js +79 -0
  98. package/dist/features/exhaust/session-port.d.ts +36 -0
  99. package/dist/features/exhaust/session-port.js +102 -0
  100. package/dist/features/harness/drive-report.d.ts +0 -9
  101. package/dist/features/harness/drive-report.js +18 -0
  102. package/dist/features/harness/driver.js +4 -7
  103. package/dist/features/harness/fill-values.d.ts +6 -0
  104. package/dist/features/harness/fill-values.js +3 -0
  105. package/dist/features/harness/jev-driver.js +30 -13
  106. package/dist/features/harness/openai-driver.js +4 -7
  107. package/dist/features/harness/platform-config.js +5 -6
  108. package/dist/index.d.ts +9 -0
  109. package/dist/index.js +40 -11
  110. package/dist/judgement/domain/domain-model.js +3 -1
  111. package/dist/judgement/oracles/self-instrument.js +4 -2
  112. package/dist/judgement/runs/artifact/replay-mapping.d.ts +2 -2
  113. package/dist/judgement/runs/artifact/replay-mapping.js +44 -9
  114. package/dist/judgement/runs/verification-sync.d.ts +3 -1
  115. package/dist/judgement/runs/verification-sync.js +1 -1
  116. package/dist/language/flows/change/affected.d.ts +9 -0
  117. package/dist/language/flows/change/affected.js +35 -0
  118. package/dist/language/flows/change/gate.d.ts +20 -0
  119. package/dist/language/flows/change/gate.js +12 -1
  120. package/dist/language/flows/flow-author.d.ts +7 -0
  121. package/dist/language/flows/flow-author.js +19 -0
  122. package/dist/language/flows/flow-expect-grammar.js +1 -1
  123. package/dist/language/flows/flow-journey.d.ts +54 -0
  124. package/dist/language/flows/flow-journey.js +119 -0
  125. package/dist/language/flows/flow-replay-run.d.ts +2 -3
  126. package/dist/language/flows/flow-replay-run.js +13 -47
  127. package/dist/language/flows/flow-replay.d.ts +22 -8
  128. package/dist/language/flows/flow-replay.js +55 -9
  129. package/dist/language/flows/flow-result.d.ts +2 -0
  130. package/dist/language/flows/flow-result.js +25 -0
  131. package/dist/language/flows/flow-tools.js +17 -5
  132. package/dist/language/flows/flows.d.ts +10 -1
  133. package/dist/language/flows/flows.js +90 -14
  134. package/dist/language/flows/recording/replay-halt.js +3 -1
  135. package/dist/language/flows/recording/tape/recordings.d.ts +30 -1
  136. package/dist/language/flows/recording/tape/recordings.js +55 -1
  137. package/dist/language/flows/replay.d.ts +19 -0
  138. package/dist/language/flows/replay.js +42 -6
  139. package/dist/language/flows/verify-change-tools.js +5 -2
  140. package/dist/launch-chromium.d.ts +80 -0
  141. package/dist/launch-chromium.js +218 -0
  142. package/dist/memory/cloud/cloud-config.d.ts +11 -2
  143. package/dist/memory/cloud/cloud-config.js +47 -7
  144. package/dist/memory/cloud/cloud-sync.d.ts +4 -1
  145. package/dist/memory/cloud/cloud-sync.js +6 -9
  146. package/dist/memory/cloud/harness-offer.js +5 -6
  147. package/dist/memory/cloud/harness-switch.js +5 -6
  148. package/dist/memory/cloud/sync-cycle.d.ts +68 -3
  149. package/dist/memory/cloud/sync-cycle.js +298 -55
  150. package/dist/memory/cloud/sync-daemon.js +24 -20
  151. package/dist/memory/cloud/sync-disk.js +16 -2
  152. package/dist/memory/intent/intent-shard.d.ts +10 -32
  153. package/dist/memory/intent/intent-shard.js +0 -10
  154. package/dist/memory/journal/drive-flow.d.ts +18 -13
  155. package/dist/memory/journal/drive-flow.js +45 -37
  156. package/dist/memory/journal/on-disk/workspace-tiers.js +2 -0
  157. package/dist/memory/journal/session-end.d.ts +9 -1
  158. package/dist/memory/journal/session-end.js +24 -12
  159. package/dist/memory/project/dir/reticle-dir.d.ts +2 -0
  160. package/dist/memory/project/dir/reticle-dir.js +1 -0
  161. package/dist/memory/project/project-tools.js +3 -1
  162. package/dist/portal/bridge/auth-failure-reason.d.ts +1 -1
  163. package/dist/portal/bridge/auth-failure-reason.js +4 -2
  164. package/dist/portal/bridge/bridge.d.ts +8 -0
  165. package/dist/portal/bridge/bridge.js +43 -2
  166. package/dist/portal/input/js-coverage.d.ts +25 -0
  167. package/dist/portal/input/js-coverage.js +30 -0
  168. package/dist/portal/input/real-input.d.ts +11 -0
  169. package/dist/portal/input/real-input.js +29 -4
  170. package/dist/portal/pool/playwright-launcher.d.ts +6 -0
  171. package/dist/portal/pool/playwright-launcher.js +20 -13
  172. package/dist/portal/session/dev-server/dev-command.d.ts +2 -2
  173. package/dist/portal/session/dev-server/dev-command.js +30 -12
  174. package/dist/portal/session/dev-server/dev-server-probe.js +17 -1
  175. package/dist/portal/session/dev-server/route-status-probe.d.ts +24 -0
  176. package/dist/portal/session/dev-server/route-status-probe.js +57 -0
  177. package/dist/portal/session/facts/observed-state.d.ts +11 -0
  178. package/dist/portal/session/facts/observed-state.js +34 -0
  179. package/dist/portal/session/no-session-diagnosis.d.ts +29 -0
  180. package/dist/portal/session/no-session-diagnosis.js +62 -5
  181. package/dist/portal/session/no-session-next-action.d.ts +13 -0
  182. package/dist/portal/session/no-session-next-action.js +48 -9
  183. package/dist/portal/session/no-session-watch.d.ts +18 -0
  184. package/dist/portal/session/no-session-watch.js +88 -2
  185. package/dist/portal/session/session-health.js +3 -2
  186. package/dist/portal/session/session-manager.d.ts +20 -6
  187. package/dist/portal/session/session-manager.js +19 -3
  188. package/dist/portal/session/session-reaper.d.ts +28 -0
  189. package/dist/portal/session/session-reaper.js +49 -1
  190. package/dist/portal/session/session.d.ts +5 -0
  191. package/dist/portal/session/session.js +15 -19
  192. package/dist/surface/http-server.d.ts +2 -1
  193. package/dist/surface/http-server.js +4 -2
  194. package/dist/surface/mcp/mcp-proxy.d.ts +2 -2
  195. package/dist/surface/mcp/mcp-proxy.js +2 -2
  196. package/dist/surface/mcp/mcp.js +4 -2
  197. package/dist/surface/mcp/proxy/proxy-daemon-probe.d.ts +13 -1
  198. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +27 -2
  199. package/dist/surface/mcp/server-instructions.js +9 -7
  200. package/dist/surface/tools/act/after-match.d.ts +17 -0
  201. package/dist/surface/tools/act/after-match.js +30 -0
  202. package/dist/surface/tools/act/durable.d.ts +69 -0
  203. package/dist/surface/tools/act/durable.js +139 -0
  204. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +13 -6
  205. package/dist/surface/tools/act/navigation/navigate-arrival.js +47 -10
  206. package/dist/surface/tools/act/navigation/navigate-result.d.ts +1 -12
  207. package/dist/surface/tools/act/navigation/navigate-result.js +21 -3
  208. package/dist/surface/tools/act/proved-controls.d.ts +9 -0
  209. package/dist/surface/tools/act/proved-controls.js +11 -0
  210. package/dist/surface/tools/act/sequence-grade.d.ts +5 -0
  211. package/dist/surface/tools/act/sequence-grade.js +32 -1
  212. package/dist/surface/tools/act-capsule.d.ts +7 -0
  213. package/dist/surface/tools/act-capsule.js +46 -1
  214. package/dist/surface/tools/act-sequence-tool.js +25 -8
  215. package/dist/surface/tools/act-tools.js +26 -50
  216. package/dist/surface/tools/assert/assert-grade.js +12 -1
  217. package/dist/surface/tools/assert/assert-verdict.js +2 -2
  218. package/dist/surface/tools/browser-tools.js +14 -2
  219. package/dist/surface/tools/coverage-tools.js +27 -1
  220. package/dist/surface/tools/error-recovery.js +4 -4
  221. package/dist/surface/tools/feedback-invite.js +6 -4
  222. package/dist/surface/tools/harness-explore.d.ts +3 -2
  223. package/dist/surface/tools/harness-explore.js +7 -6
  224. package/dist/surface/tools/lease-tools.js +15 -6
  225. package/dist/surface/tools/live-call-text.js +4 -1
  226. package/dist/surface/tools/observe-tools.js +9 -1
  227. package/dist/surface/tools/tools.js +4 -0
  228. package/dist/telemetry/argument-shape.d.ts +0 -2
  229. package/dist/telemetry/argument-shape.js +0 -4
  230. package/dist/telemetry/error-class.d.ts +2 -3
  231. package/dist/telemetry/error-class.js +2 -3
  232. package/dist/telemetry/harness-drive.d.ts +0 -2
  233. package/dist/telemetry/harness-drive.js +0 -4
  234. package/dist/wire-journal.d.ts +3 -0
  235. package/dist/wire-journal.js +4 -0
  236. package/dist/workspace-packages.d.ts +0 -2
  237. package/dist/workspace-packages.js +1 -9
  238. package/docs/README.md +4 -2
  239. package/docs/actions.mdx +3 -3
  240. package/docs/agent-cheatsheet.md +4 -4
  241. package/docs/architecture.md +6 -6
  242. package/docs/best-practices.mdx +1 -1
  243. package/docs/capabilities.mdx +32 -1
  244. package/docs/cli/cloud.mdx +14 -4
  245. package/docs/cli/doctor.mdx +4 -0
  246. package/docs/cli/feedback.mdx +2 -2
  247. package/docs/cli/gate.mdx +9 -4
  248. package/docs/cli/update.mdx +10 -0
  249. package/docs/cli.mdx +9 -0
  250. package/docs/desktop.mdx +14 -10
  251. package/docs/environment.mdx +87 -0
  252. package/docs/faq.mdx +18 -16
  253. package/docs/first-drive.md +5 -13
  254. package/docs/flows.md +21 -2
  255. package/docs/frameworks.mdx +29 -10
  256. package/docs/getting-started.md +3 -3
  257. package/docs/http-transport.md +6 -3
  258. package/docs/human-control.md +2 -2
  259. package/docs/index.mdx +16 -6
  260. package/docs/install-agentic.mdx +1 -1
  261. package/docs/install-manual.mdx +1 -1
  262. package/docs/integration-patterns.md +3 -3
  263. package/docs/multi-agent-testing.md +1 -1
  264. package/docs/packages/babel-plugin.mdx +1 -1
  265. package/docs/packages/browser.mdx +2 -2
  266. package/docs/packages/react.mdx +2 -2
  267. package/docs/packages/tauri.mdx +1 -1
  268. package/docs/packages/vite-plugin.mdx +2 -2
  269. package/docs/packages.mdx +9 -9
  270. package/docs/predicates.mdx +50 -5
  271. package/docs/quickstart.mdx +2 -2
  272. package/docs/recipes.mdx +2 -2
  273. package/docs/state-management.mdx +2 -2
  274. package/docs/system-map.md +1 -1
  275. package/docs/telemetry.md +1 -1
  276. package/docs/token-efficiency.md +3 -3
  277. package/docs/tools/act-and-wait.mdx +1 -0
  278. package/docs/tools/act-sequence.mdx +1 -1
  279. package/docs/tools/console.mdx +1 -1
  280. package/docs/tools/inspect.mdx +1 -1
  281. package/docs/tools/network.mdx +1 -1
  282. package/docs/tools/query.mdx +1 -1
  283. package/docs/tools/sessions.mdx +1 -1
  284. package/docs/tools/snapshot.mdx +1 -1
  285. package/docs/tools/state.mdx +1 -1
  286. package/docs/tools/wait-for.mdx +1 -1
  287. package/docs/usage.md +7 -6
  288. package/docs/use-cases.mdx +157 -0
  289. package/docs/vs-screenshots.mdx +3 -3
  290. package/docs/what-is-recorded.md +2 -2
  291. package/docs/what-is-reticle.mdx +17 -20
  292. package/docs/why-reticle.mdx +14 -21
  293. package/package.json +10 -9
  294. package/dist/judgement/capsule/minimize.d.ts +0 -12
  295. package/dist/judgement/capsule/minimize.js +0 -22
  296. package/dist/judgement/phenomena/phenomena.d.ts +0 -27
  297. package/dist/judgement/phenomena/phenomena.js +0 -101
  298. package/dist/language/flows/flow-report.d.ts +0 -22
  299. package/dist/language/flows/flow-report.js +0 -106
@@ -1,5 +1,5 @@
1
1
  import { REDACTED_FILL, secretEnvKey } from './fields/flow-secret-field.js';
2
- import { FlowPredicateSchema } from '@reticlehq/core';
2
+ import { FlowPredicateSchema, Verified } from '@reticlehq/core';
3
3
  import { DANGEROUS_ACTION_CONFIRM_ARG, ReticleCommand, QueryBy, } from '@reticlehq/core';
4
4
  import { ReticleTool } from '@reticlehq/core';
5
5
  import { formatStepAddress } from 'open-verification';
@@ -71,6 +71,22 @@ function sourceFromResult(res) {
71
71
  out['column'] = source['column'];
72
72
  return out;
73
73
  }
74
+ /**
75
+ * The pathname of a session's live url, or undefined when it has none yet.
76
+ *
77
+ * Pathname rather than the whole url, because that is what `startPath` is compared against and what
78
+ * a hash-routed app makes meaningless in the document location — see routeOfEvent.
79
+ */
80
+ export function pathOf(url) {
81
+ if (url === undefined)
82
+ return undefined;
83
+ try {
84
+ return new URL(url).pathname;
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ }
74
90
  /**
75
91
  * Capture an act into every in-flight recording, or do nothing when none is running.
76
92
  *
@@ -87,20 +103,40 @@ route) {
87
103
  // — "recording is a property of the system rather than a rule an agent has to remember" — was true
88
104
  // about the intent and false about the code, and every drive nobody wrapped by hand was discarded.
89
105
  //
90
- // That is the whole supply of regression tests this product could have had for free: the corpus
91
- // measures 3 of 33 flows mutation-testable and 6 of 112 steps declaring a consequence, and a drive
92
- // that is never recorded cannot contribute to either.
106
+ // That is the whole supply of regression tests this product could have had for free: few saved
107
+ // flows are mutation-testable and few steps declare a consequence, and a drive that is never
108
+ // recorded cannot contribute to either.
93
109
  const step = compileActStep(args, res);
94
110
  // Keep the assertion the agent actually made. `act_and_wait { until }` IS the agent saying what
95
111
  // success means — the large majority of calls carry one — and dropping it produced a flow graded
96
112
  // "assertion-free: it will pass even if the feature is broken", which is the regression-suite
97
- // story failing at its last step. Only kinds FlowExpect can express survive; see
98
- // predicate-to-expect.ts.
113
+ // story failing at its last step. See enforceableExpect below.
99
114
  const expect = enforceableExpect(args['until'] ?? args['predicate']);
100
115
  if (expect !== undefined)
101
116
  step.expect = expect;
117
+ if (route !== undefined)
118
+ step.page = route;
119
+ const intent = asString(args['intent'])?.trim();
120
+ if (intent !== undefined && intent.length > 0)
121
+ step.intent = intent;
102
122
  recordings.capture(step, route);
103
123
  }
124
+ /**
125
+ * Capture an act_and_wait once its verdict is known, keeping `until` as the step's expectation only
126
+ * on a yes. Recorded before the verdict, a step kept an `until` that came back `no` or `no-fault` —
127
+ * a regression test asserting something never once observed to hold because of the action. The
128
+ * action itself is always recorded: it happened, and replay needs it to reach the next step.
129
+ */
130
+ export function captureVerdictedAct(recordings, args, res, route, verified) {
131
+ const { until: _until, predicate: _predicate, ...unproved } = args;
132
+ captureAct(recordings, Verified.YES === verified ? args : unproved, res, route);
133
+ }
134
+ /** Fold a PASSING standalone assertion into the step it proved. See RecordingStore.attachExpect. */
135
+ export function captureAssertion(recordings, raw) {
136
+ const expect = enforceableExpect(raw);
137
+ if (expect !== undefined)
138
+ recordings.attachExpect(expect);
139
+ }
104
140
  /**
105
141
  * A declared predicate, as a recorded expectation — or nothing.
106
142
  *
@@ -172,9 +172,12 @@ export const VERIFY_CHANGE_TOOLS = [
172
172
  const extra = await inspectAfterReplay(deps, args, cursor);
173
173
  if (extra.contradictions.length > 0) {
174
174
  const kinds = extra.contradictions.map((c) => c.kind).join(', ');
175
+ const earned = attributedFailures(affected, unknownProvenance).length > 0;
175
176
  return {
176
- verified: Verified.NO,
177
- because: `every covering flow passed, but channels disagreed during the replay (${kinds}) — a green suite over a failed write is the false green worth looking at${provenanceNote}`,
177
+ verified: earned ? Verified.NO : Verified.UNKNOWN,
178
+ because: earned
179
+ ? `every covering flow passed, but channels disagreed during the replay (${kinds}) — a green suite over a failed write is the false green worth looking at${provenanceNote}`
180
+ : `channels disagreed during the replay (${kinds}), but Reticle cannot tell which sources any of the flows that ran cover (${affected.join(', ')}), so this says nothing about the files you changed. Drive the change directly; the drive is saved as a flow with its sources stamped`,
178
181
  changedFiles,
179
182
  flowsRun: affected,
180
183
  suite,
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The ONE place a Reticle-owned Chromium is launched — the pooled lease browser
3
+ * (`playwright-launcher`) and the drive browser (`real-input`) both come through here.
4
+ *
5
+ * Why it is more than `chromium.launch`: on a fresh machine the `playwright` package is present
6
+ * (npx pulled it in with the server) but its browser build is not, because that is a separate
7
+ * ~150 MiB download nobody told the user about. Every lease, `reticle verify`, `open` and `drive`
8
+ * then refused with "Chromium is not installed for Playwright. Run: npx playwright@… install
9
+ * chromium" — the first run stopped on a human step a machine could do. Most machines already have
10
+ * a Chrome or an Edge, and Playwright drives either through `channel`, so that is tried before any
11
+ * download; only a machine with neither gets the pinned install, run for them and announced.
12
+ *
13
+ * Announcements go to STDERR, never stdout: `reticle mcp` speaks JSON-RPC on stdout and one stray
14
+ * line there corrupts the client's stream.
15
+ */
16
+ import { type ChromiumLaunchOptions } from './chromium-launch-options.js';
17
+ import { type ChromiumProbe } from './command/cli/doctor/browser/chromium-hint.js';
18
+ /** The installed-browser channels Playwright can drive in place of its own build. */
19
+ export declare const ChromiumChannel: {
20
+ readonly CHROME: "chrome";
21
+ readonly MSEDGE: "msedge";
22
+ };
23
+ export type ChromiumChannel = (typeof ChromiumChannel)[keyof typeof ChromiumChannel];
24
+ export declare const CHANNEL_LABEL: Record<ChromiumChannel, string>;
25
+ /** Candidate executable paths for a channel on this platform; empty where it is not installable. */
26
+ export declare function channelExecutableCandidates(channel: ChromiumChannel, platform: NodeJS.Platform, env: NodeJS.ProcessEnv): string[];
27
+ /** The first installed Chrome/Edge on this machine, or undefined. Looks only; installs nothing. */
28
+ export declare function findInstalledChannel(deps: Pick<ChromiumTargetDeps, 'exists' | 'platform' | 'env'>): ChromiumChannel | undefined;
29
+ /** Everything the resolver touches, injected so it runs against a fake machine in tests. */
30
+ export interface ChromiumTargetDeps {
31
+ /** Where the bundled playwright expects its Chromium build. */
32
+ executablePath: () => string;
33
+ exists: (path: string) => boolean;
34
+ platform: NodeJS.Platform;
35
+ env: NodeJS.ProcessEnv;
36
+ /** Run the pinned Chromium install; resolves true when it succeeded. */
37
+ install: () => Promise<boolean>;
38
+ /** A progress line for a human — stderr in production. */
39
+ announce: (line: string) => void;
40
+ }
41
+ /** Which browser to launch: the bundled build (no channel), an installed channel, or none at all. */
42
+ export type ChromiumTarget = {
43
+ found: true;
44
+ channel?: ChromiumChannel;
45
+ } | {
46
+ found: false;
47
+ };
48
+ /**
49
+ * Decide which browser a launch will use, installing the pinned Chromium only as the last resort.
50
+ * Order: bundled build on disk → installed Chrome → installed Edge → install, then bundled.
51
+ */
52
+ export declare function resolveChromiumTarget(deps: ChromiumTargetDeps): Promise<ChromiumTarget>;
53
+ /** The subset of Playwright's `BrowserType` this file needs, so tests can pass a fake. */
54
+ export interface LaunchableChromium<B> {
55
+ executablePath: () => string;
56
+ launch: (options: ChromiumLaunchOptions) => Promise<B>;
57
+ }
58
+ /**
59
+ * A progress writer that says each distinct line once. The Chrome/Edge fallback line was written on
60
+ * every launch, and the pool launches per context, so one daemon repeated the same notice into the
61
+ * terminal of whoever started it.
62
+ */
63
+ export declare function announceOnce(write: (line: string) => void): (line: string) => void;
64
+ /**
65
+ * Launch a Chromium with the shared options, on whichever browser this machine can offer. When none
66
+ * can be found or installed, launch anyway: Playwright's own error is what `chromiumLaunchHint`
67
+ * turns into the pinned install command, so the user still gets the one line that fixes it.
68
+ */
69
+ export declare function launchChromium<B>(chromium: LaunchableChromium<B>, headless: boolean, deps?: ChromiumTargetDeps): Promise<B>;
70
+ /**
71
+ * The lease preflight's probe: "can a launch succeed?", which now includes the Chrome/Edge fallback
72
+ * and the automatic install. Without this the preflight would refuse a lease on a machine the
73
+ * launcher itself could serve. Falls back to the plain probe so a real refusal keeps its evidence.
74
+ */
75
+ export declare function probeLaunchableChromium(): Promise<ChromiumProbe>;
76
+ /**
77
+ * `doctor`'s probe: the plain Chromium probe, plus the installed browser that stands in when the
78
+ * bundled build is missing. Never installs — a diagnostic that downloads 150 MiB is not one.
79
+ */
80
+ export declare function probeChromiumWithFallback(): Promise<ChromiumProbe>;
@@ -0,0 +1,218 @@
1
+ /**
2
+ * The ONE place a Reticle-owned Chromium is launched — the pooled lease browser
3
+ * (`playwright-launcher`) and the drive browser (`real-input`) both come through here.
4
+ *
5
+ * Why it is more than `chromium.launch`: on a fresh machine the `playwright` package is present
6
+ * (npx pulled it in with the server) but its browser build is not, because that is a separate
7
+ * ~150 MiB download nobody told the user about. Every lease, `reticle verify`, `open` and `drive`
8
+ * then refused with "Chromium is not installed for Playwright. Run: npx playwright@… install
9
+ * chromium" — the first run stopped on a human step a machine could do. Most machines already have
10
+ * a Chrome or an Edge, and Playwright drives either through `channel`, so that is tried before any
11
+ * download; only a machine with neither gets the pinned install, run for them and announced.
12
+ *
13
+ * Announcements go to STDERR, never stdout: `reticle mcp` speaks JSON-RPC on stdout and one stray
14
+ * line there corrupts the client's stream.
15
+ */
16
+ import { spawn } from 'node:child_process';
17
+ import { existsSync } from 'node:fs';
18
+ import { createRequire } from 'node:module';
19
+ import { dirname, join } from 'node:path';
20
+ import { chromiumLaunchOptions } from './chromium-launch-options.js';
21
+ import { bundledPlaywrightVersion, chromiumInstallCommand, probeChromium, } from './command/cli/doctor/browser/chromium-hint.js';
22
+ /** The installed-browser channels Playwright can drive in place of its own build. */
23
+ export const ChromiumChannel = {
24
+ CHROME: 'chrome',
25
+ MSEDGE: 'msedge',
26
+ };
27
+ /** Tried in this order: Chrome is the likelier install and the closer match to Playwright's build. */
28
+ const CHANNEL_ORDER = [ChromiumChannel.CHROME, ChromiumChannel.MSEDGE];
29
+ export const CHANNEL_LABEL = {
30
+ [ChromiumChannel.CHROME]: 'Google Chrome',
31
+ [ChromiumChannel.MSEDGE]: 'Microsoft Edge',
32
+ };
33
+ /**
34
+ * Where each channel's executable lives, per platform — the same locations Playwright's own
35
+ * registry checks when it launches a channel, so "we found it" and "Playwright can launch it" agree.
36
+ * Windows is relative to each install root below.
37
+ */
38
+ const CHANNEL_PATHS = {
39
+ [ChromiumChannel.CHROME]: {
40
+ darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
41
+ linux: '/opt/google/chrome/chrome',
42
+ win32: '\\Google\\Chrome\\Application\\chrome.exe',
43
+ },
44
+ [ChromiumChannel.MSEDGE]: {
45
+ darwin: '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
46
+ linux: '/opt/microsoft/msedge/msedge',
47
+ win32: '\\Microsoft\\Edge\\Application\\msedge.exe',
48
+ },
49
+ };
50
+ const WINDOWS_INSTALL_ROOTS = ['LOCALAPPDATA', 'PROGRAMFILES', 'PROGRAMFILES(X86)'];
51
+ /** Candidate executable paths for a channel on this platform; empty where it is not installable. */
52
+ export function channelExecutableCandidates(channel, platform, env) {
53
+ const paths = CHANNEL_PATHS[channel];
54
+ if ('darwin' === platform)
55
+ return [paths.darwin];
56
+ if ('linux' === platform)
57
+ return [paths.linux];
58
+ if ('win32' === platform) {
59
+ return WINDOWS_INSTALL_ROOTS.flatMap((key) => {
60
+ const root = env[key];
61
+ return root === undefined || 0 === root.length ? [] : [`${root}${paths.win32}`];
62
+ });
63
+ }
64
+ return [];
65
+ }
66
+ /** The first installed Chrome/Edge on this machine, or undefined. Looks only; installs nothing. */
67
+ export function findInstalledChannel(deps) {
68
+ return CHANNEL_ORDER.find((channel) => channelExecutableCandidates(channel, deps.platform, deps.env).some((p) => deps.exists(p)));
69
+ }
70
+ const usingChannelLine = (channel) => `[reticle] Playwright's Chromium is not installed; using the installed ${CHANNEL_LABEL[channel]} instead.\n`;
71
+ const installingLine = (command) => `[reticle] No Chromium, Chrome or Edge found; installing Playwright's Chromium (${command}) — a one-time download…\n`;
72
+ const INSTALL_FAILED_LINE = '[reticle] The Chromium install did not complete.\n';
73
+ /**
74
+ * Decide which browser a launch will use, installing the pinned Chromium only as the last resort.
75
+ * Order: bundled build on disk → installed Chrome → installed Edge → install, then bundled.
76
+ */
77
+ export async function resolveChromiumTarget(deps) {
78
+ let bundled;
79
+ try {
80
+ bundled = deps.executablePath();
81
+ }
82
+ catch {
83
+ // A playwright that cannot name its executable is not evidence the browser is missing. Launch
84
+ // as before and let Playwright's own error (translated by chromiumLaunchHint) say what is wrong,
85
+ // rather than download ~150 MiB on a guess.
86
+ return { found: true };
87
+ }
88
+ if (deps.exists(bundled))
89
+ return { found: true };
90
+ const channel = findInstalledChannel(deps);
91
+ if (channel !== undefined) {
92
+ deps.announce(usingChannelLine(channel));
93
+ return { found: true, channel };
94
+ }
95
+ deps.announce(installingLine(chromiumInstallCommand(bundledPlaywrightVersion())));
96
+ const ok = await deps.install();
97
+ if (ok && deps.exists(bundled))
98
+ return { found: true };
99
+ deps.announce(INSTALL_FAILED_LINE);
100
+ return { found: false };
101
+ }
102
+ /**
103
+ * The bundled playwright's own CLI, or undefined when playwright cannot be resolved from here.
104
+ *
105
+ * Run directly rather than through the `npx playwright@<version>` the hint prints. They install the
106
+ * same revision, but `npx` runs in the user's project and inherits everything wrong with it: in the
107
+ * sandbox this was written against, an `overrides` block in the app's package.json made npm refuse
108
+ * with EOVERRIDE before playwright ever ran. The bundled CLI is the same version by construction
109
+ * and needs no registry round trip to find itself.
110
+ */
111
+ function bundledPlaywrightCli() {
112
+ try {
113
+ const pkg = createRequire(import.meta.url).resolve('playwright/package.json');
114
+ return join(dirname(pkg), PLAYWRIGHT_CLI);
115
+ }
116
+ catch {
117
+ return undefined;
118
+ }
119
+ }
120
+ const PLAYWRIGHT_CLI = 'cli.js';
121
+ const INSTALL_ARGS = ['install', 'chromium'];
122
+ /**
123
+ * Run the pinned install with its progress on stderr. Memoized per process: the pool can race
124
+ * several launches on the first acquire, and two concurrent installs into one browsers root would
125
+ * fight over the same directory. A failed install clears the memo so a later launch can retry.
126
+ */
127
+ let installing;
128
+ function installPinnedChromium() {
129
+ if (installing === undefined) {
130
+ const cli = bundledPlaywrightCli();
131
+ installing = new Promise((resolve) => {
132
+ if (cli === undefined) {
133
+ resolve(false);
134
+ return;
135
+ }
136
+ // stdout goes to fd 2 for the JSON-RPC reason in this file's header.
137
+ const child = spawn(process.execPath, [cli, ...INSTALL_ARGS], { stdio: ['ignore', 2, 2] });
138
+ child.on('error', () => resolve(false));
139
+ child.on('exit', (code) => resolve(0 === code));
140
+ }).then((ok) => {
141
+ if (!ok)
142
+ installing = undefined;
143
+ return ok;
144
+ });
145
+ }
146
+ return installing;
147
+ }
148
+ /**
149
+ * A progress writer that says each distinct line once. The Chrome/Edge fallback line was written on
150
+ * every launch, and the pool launches per context, so one daemon repeated the same notice into the
151
+ * terminal of whoever started it.
152
+ */
153
+ export function announceOnce(write) {
154
+ const said = new Set();
155
+ return (line) => {
156
+ if (said.has(line))
157
+ return;
158
+ said.add(line);
159
+ write(line);
160
+ };
161
+ }
162
+ /** Once per PROCESS, so it is shared by every launch rather than made per call. */
163
+ // ponytail: an install retried after a failure is not re-announced; the install itself still runs.
164
+ const announceToStderr = announceOnce((line) => {
165
+ process.stderr.write(line);
166
+ });
167
+ function defaultDeps(chromium) {
168
+ return {
169
+ executablePath: () => chromium.executablePath(),
170
+ exists: existsSync,
171
+ platform: process.platform,
172
+ env: process.env,
173
+ install: installPinnedChromium,
174
+ announce: announceToStderr,
175
+ };
176
+ }
177
+ /**
178
+ * Launch a Chromium with the shared options, on whichever browser this machine can offer. When none
179
+ * can be found or installed, launch anyway: Playwright's own error is what `chromiumLaunchHint`
180
+ * turns into the pinned install command, so the user still gets the one line that fixes it.
181
+ */
182
+ export async function launchChromium(chromium, headless, deps = defaultDeps(chromium)) {
183
+ const target = await resolveChromiumTarget(deps);
184
+ const channel = target.found ? target.channel : undefined;
185
+ return chromium.launch(chromiumLaunchOptions(headless, channel));
186
+ }
187
+ /**
188
+ * The lease preflight's probe: "can a launch succeed?", which now includes the Chrome/Edge fallback
189
+ * and the automatic install. Without this the preflight would refuse a lease on a machine the
190
+ * launcher itself could serve. Falls back to the plain probe so a real refusal keeps its evidence.
191
+ */
192
+ export async function probeLaunchableChromium() {
193
+ try {
194
+ const { chromium } = await import('playwright');
195
+ const target = await resolveChromiumTarget(defaultDeps(chromium));
196
+ if (target.found)
197
+ return { exists: true };
198
+ }
199
+ catch {
200
+ // No playwright at all — probeChromium below says so, with where it looked.
201
+ }
202
+ return probeChromium();
203
+ }
204
+ /**
205
+ * `doctor`'s probe: the plain Chromium probe, plus the installed browser that stands in when the
206
+ * bundled build is missing. Never installs — a diagnostic that downloads 150 MiB is not one.
207
+ */
208
+ export async function probeChromiumWithFallback() {
209
+ const probe = await probeChromium();
210
+ if (probe.exists)
211
+ return probe;
212
+ const channel = findInstalledChannel({
213
+ exists: existsSync,
214
+ platform: process.platform,
215
+ env: process.env,
216
+ });
217
+ return channel === undefined ? probe : { ...probe, fallback: CHANNEL_LABEL[channel] };
218
+ }
@@ -40,6 +40,8 @@ export interface ProjectCloud {
40
40
  verify: VerifyMode;
41
41
  /** The cloud project id this project is linked to (for display/logging); null when env-fallback. */
42
42
  projectId: string | null;
43
+ /** Why `config` is null, in words a person can act on. Absent when attached. */
44
+ reason?: string;
43
45
  }
44
46
  /** Look up the API key for a cloud project id in the user credentials map. */
45
47
  /**
@@ -63,8 +65,8 @@ export interface ProjectCloud {
63
65
  export declare const credentialSlot: (url: string, projectId: string, orgId?: string) => string;
64
66
  /**
65
67
  * Resolve the cloud picture for a project rooted at `reticleRoot`. Reads the project's link file + the
66
- * user credential store; if the project isn't linked (no cloud.json), falls back to the global env creds
67
- * so the single-project / CI flow is unchanged. `homeDir` is injected (testable; `os.homedir()` at call).
68
+ * user credential store, else the env key; if the project isn't linked (no cloud.json), the env creds
69
+ * are the whole story (the single-project / CI flow). `homeDir` is injected (testable; `os.homedir()` at call).
68
70
  */
69
71
  export declare function resolveProjectCloud(fs: FileSystemPort, reticleRoot: string, homeDir: string, env: NodeJS.ProcessEnv): Promise<ProjectCloud>;
70
72
  /**
@@ -76,4 +78,11 @@ export declare function resolveProjectCloud(fs: FileSystemPort, reticleRoot: str
76
78
  * needs no exported key — see `withLinkedCredential`.
77
79
  */
78
80
  export declare const linkedCloudPort: (fs: FileSystemPort, reticleRoot: string, homeDir: string, env: NodeJS.ProcessEnv) => () => Promise<CloudConfig | null>;
81
+ /**
82
+ * The environment with this project's resolved credential written into it, for the readers that
83
+ * take an env record (the panel's model config and harness switch). Handed `process.env` alone,
84
+ * they never saw a key `reticle link` had stored. Resolved per call, so a link made mid-session
85
+ * takes effect without a restart.
86
+ */
87
+ export declare const platformEnvPort: (fs: FileSystemPort, reticleRoot: string, homeDir: string, env: NodeJS.ProcessEnv) => () => Promise<NodeJS.ProcessEnv>;
79
88
  export {};
@@ -10,7 +10,7 @@
10
10
  * user-level credential. "Cloud attached" = a valid link file AND a key for its project id (or env creds).
11
11
  */
12
12
  import { join } from 'node:path';
13
- import { ReticleDir } from '@reticlehq/core';
13
+ import { DEFAULT_PLATFORM_URL, ReticleDir, ReticleEnv } from '@reticlehq/core';
14
14
  import { resolveCloudConfig } from './cloud-sync.js';
15
15
  /** `<reticleRoot>/cloud.json` — the project's cloud binding + sync policy (non-secret). */
16
16
  /** Re-exported so the existing importers keep working; the string itself lives in core. */
@@ -121,18 +121,22 @@ function credentialFor(raw, projectId, url, orgId) {
121
121
  const normalizeCloudUrl = (url) => url.replace(/\/+$/, '');
122
122
  /**
123
123
  * Resolve the cloud picture for a project rooted at `reticleRoot`. Reads the project's link file + the
124
- * user credential store; if the project isn't linked (no cloud.json), falls back to the global env creds
125
- * so the single-project / CI flow is unchanged. `homeDir` is injected (testable; `os.homedir()` at call).
124
+ * user credential store, else the env key; if the project isn't linked (no cloud.json), the env creds
125
+ * are the whole story (the single-project / CI flow). `homeDir` is injected (testable; `os.homedir()` at call).
126
126
  */
127
127
  export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
128
128
  const link = parseLink(await readJson(fs, join(reticleRoot, CLOUD_LINK_FILE)));
129
129
  if (null === link) {
130
130
  // No per-project link → the env vars are the whole story (legacy single-project / CI behaviour).
131
+ const config = resolveCloudConfig(env);
131
132
  return {
132
- config: resolveCloudConfig(env),
133
+ config,
133
134
  policy: DEFAULT_SYNC_POLICY,
134
135
  verify: VerifyMode.LOCAL,
135
136
  projectId: null,
137
+ ...(null === config
138
+ ? { reason: 'cloud not attached here: run `reticle link`, or set RETICLE_API_KEY' }
139
+ : {}),
136
140
  };
137
141
  }
138
142
  const policy = {
@@ -141,9 +145,33 @@ export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
141
145
  flows: link.sync.flows ?? DEFAULT_SYNC_POLICY.flows,
142
146
  capsules: link.sync.capsules ?? DEFAULT_SYNC_POLICY.capsules,
143
147
  };
144
- const key = credentialFor(await readJson(fs, join(homeDir, ReticleDir.ROOT, CREDENTIALS_FILE)), link.projectId, link.url, link.orgId);
145
- const config = key !== null ? { url: link.url.replace(/\/+$/, ''), apiKey: key } : null;
146
- return { config, policy, verify: link.verify, projectId: link.projectId };
148
+ const stored = credentialFor(await readJson(fs, join(homeDir, ReticleDir.ROOT, CREDENTIALS_FILE)), link.projectId, link.url, link.orgId);
149
+ /*
150
+ * No stored key, so the environment's — which is the whole of CI: `cloud.json` is committed, a
151
+ * CI clone is therefore linked, and it has no keystore. Only when the environment's key is FOR
152
+ * this link's host (its URL, else the hosted default): a key exported for one cloud is not a
153
+ * credential for a repo linked to another, the same rule `credentialFor` applies to stored keys.
154
+ */
155
+ const fromEnv = resolveCloudConfig(env);
156
+ const key = stored ??
157
+ (fromEnv !== null && fromEnv.url === normalizeCloudUrl(link.url) ? fromEnv.apiKey : null);
158
+ const linkedUrl = normalizeCloudUrl(link.url);
159
+ const config = key !== null ? { url: linkedUrl, apiKey: key } : null;
160
+ const reason = config !== null
161
+ ? undefined
162
+ : fromEnv !== null
163
+ ? `this repo is linked to ${linkedUrl}, and RETICLE_API_KEY is set for ${fromEnv.url}: ` +
164
+ `set RETICLE_CLOUD_URL=${linkedUrl} to use that key here, or run \`reticle login\` for it`
165
+ : `this repo is linked to ${linkedUrl}, and this machine has no key for it: run ` +
166
+ '`reticle login`, or set RETICLE_API_KEY' +
167
+ (DEFAULT_PLATFORM_URL === linkedUrl ? '' : ` with RETICLE_CLOUD_URL=${linkedUrl}`);
168
+ return {
169
+ config,
170
+ policy,
171
+ verify: link.verify,
172
+ projectId: link.projectId,
173
+ ...(reason === undefined ? {} : { reason }),
174
+ };
147
175
  }
148
176
  /**
149
177
  * The linked credential, as a port the tool surface can be handed.
@@ -154,3 +182,15 @@ export async function resolveProjectCloud(fs, reticleRoot, homeDir, env) {
154
182
  * needs no exported key — see `withLinkedCredential`.
155
183
  */
156
184
  export const linkedCloudPort = (fs, reticleRoot, homeDir, env) => async () => (await resolveProjectCloud(fs, reticleRoot, homeDir, env)).config;
185
+ /**
186
+ * The environment with this project's resolved credential written into it, for the readers that
187
+ * take an env record (the panel's model config and harness switch). Handed `process.env` alone,
188
+ * they never saw a key `reticle link` had stored. Resolved per call, so a link made mid-session
189
+ * takes effect without a restart.
190
+ */
191
+ export const platformEnvPort = (fs, reticleRoot, homeDir, env) => async () => {
192
+ const config = (await resolveProjectCloud(fs, reticleRoot, homeDir, env)).config;
193
+ return null === config
194
+ ? env
195
+ : { ...env, [ReticleEnv.API_KEY]: config.apiKey, [ReticleEnv.CLOUD_URL]: config.url };
196
+ };
@@ -24,7 +24,10 @@ export interface CloudConfig {
24
24
  url: string;
25
25
  apiKey: string;
26
26
  }
27
- /** Resolve cloud credentials from the environment, or null when not logged in (sync disabled). */
27
+ /**
28
+ * Resolve cloud credentials from the environment, or null without a key (sync disabled). The key
29
+ * alone is enough: the URL defaults to the hosted service, which is what the platform tells CI.
30
+ */
28
31
  export declare function resolveCloudConfig(env: NodeJS.ProcessEnv): CloudConfig | null;
29
32
  export declare const CLOUD_VERIFY_TIMEOUT_MS = 120000;
30
33
  /**
@@ -9,7 +9,7 @@
9
9
  * cloud copy is an enhancement, so a push error is logged and swallowed.
10
10
  */
11
11
  import { z } from 'zod';
12
- import { VERIFY_PROGRESS_MAX_EVENTS, apiKeyFrom, } from '@reticlehq/core';
12
+ import { VERIFY_PROGRESS_MAX_EVENTS, platformCredentialFrom, } from '@reticlehq/core';
13
13
  /**
14
14
  * Env var names — the presence of BOTH is what "logged in" means for cloud sync.
15
15
  *
@@ -27,15 +27,12 @@ const CLOUD_PROJECT_RUNS_PATH = '/v1/project/runs';
27
27
  const CLOUD_PROJECT_REGRESSION_PATH = '/v1/project/regression';
28
28
  const CLOUD_VERIFICATIONS_PATH = '/v1/verifications';
29
29
  const CLOUD_PROGRESS_PATH = '/v1/connect/progress';
30
- /** Resolve cloud credentials from the environment, or null when not logged in (sync disabled). */
30
+ /**
31
+ * Resolve cloud credentials from the environment, or null without a key (sync disabled). The key
32
+ * alone is enough: the URL defaults to the hosted service, which is what the platform tells CI.
33
+ */
31
34
  export function resolveCloudConfig(env) {
32
- const url = env[CloudEnv.URL];
33
- const apiKey = apiKeyFrom(env);
34
- if (typeof url !== 'string' || 0 === url.length)
35
- return null;
36
- if (apiKey === undefined)
37
- return null;
38
- return { url: url.replace(/\/+$/, ''), apiKey };
35
+ return platformCredentialFrom(env) ?? null;
39
36
  }
40
37
  /**
41
38
  * Budget for a cloud call. Node's `fetch` has NO default timeout, so a connection that opens and then
@@ -15,7 +15,7 @@
15
15
  * the background is the whole design: readers get what we last heard, and a stale answer is only
16
16
  * ever stale in the safe direction — the card the person is looking at is the one they can act on.
17
17
  */
18
- import { ReticleEnv, apiKeyFrom } from '@reticlehq/core';
18
+ import { platformCredentialFrom } from '@reticlehq/core';
19
19
  const OFFER_PATH = '/v1/harness/offer';
20
20
  /** Same budget as the model-config read: a drive must never wait on an advert. */
21
21
  const TIMEOUT_MS = 2_000;
@@ -65,12 +65,11 @@ function parse(body, claimUrl) {
65
65
  }
66
66
  /** Ask the platform where this workspace stands. `undefined` means "no answer", never "not claimed". */
67
67
  export async function fetchHarnessOffer(env, claimUrl, doFetch = (url, init) => fetch(url, init), timeoutMs = TIMEOUT_MS) {
68
- const key = apiKeyFrom(env);
69
- const host = env[ReticleEnv.CLOUD_URL];
70
- if (key === undefined || 0 === key.length)
71
- return undefined;
72
- if (host === undefined || 0 === host.length)
68
+ const cloud = platformCredentialFrom(env);
69
+ if (cloud === undefined)
73
70
  return undefined;
71
+ const key = cloud.apiKey;
72
+ const host = cloud.url;
74
73
  const controller = new AbortController();
75
74
  const timer = setTimeout(() => {
76
75
  controller.abort();
@@ -17,18 +17,17 @@
17
17
  * that quietly did not save; the next snapshot re-reads the platform, so a lost write shows up as
18
18
  * the switch springing back, which is a truthful thing for it to do.
19
19
  */
20
- import { ReticleEnv, apiKeyFrom } from '@reticlehq/core';
20
+ import { platformCredentialFrom } from '@reticlehq/core';
21
21
  const CONFIG_PATH = '/v1/model/config';
22
22
  /** Same budget as the read beside it: a settings write must never hold anything up. */
23
23
  const TIMEOUT_MS = 2_000;
24
24
  /** `true` when the platform accepted it. Callers use this for logging, never to block a person. */
25
25
  export async function writeHarnessSwitch(env, enabled, doFetch = (url, init) => fetch(url, init), timeoutMs = TIMEOUT_MS) {
26
- const key = apiKeyFrom(env);
27
- const host = env[ReticleEnv.CLOUD_URL];
28
- if (key === undefined || 0 === key.length)
29
- return false;
30
- if (host === undefined || 0 === host.length)
26
+ const cloud = platformCredentialFrom(env);
27
+ if (cloud === undefined)
31
28
  return false;
29
+ const key = cloud.apiKey;
30
+ const host = cloud.url;
32
31
  const controller = new AbortController();
33
32
  const timer = setTimeout(() => {
34
33
  controller.abort();