@reticlehq/server 2.11.0 → 2.12.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 (252) hide show
  1. package/SKILL.md +26 -2
  2. package/dist/bridge/bridge.d.ts +0 -13
  3. package/dist/bridge/bridge.js +1 -1
  4. package/dist/bridge/bridge.test-harness.d.ts +2 -0
  5. package/dist/bridge/bridge.test-harness.js +8 -1
  6. package/dist/capsule/capsule-store.d.ts +2 -1
  7. package/dist/capsule/capsule-store.js +1 -1
  8. package/dist/capsule/causal-summary.d.ts +2 -2
  9. package/dist/chromium-launch-options.d.ts +28 -0
  10. package/dist/chromium-launch-options.js +29 -0
  11. package/dist/cli/cli-doctor.js +8 -0
  12. package/dist/cli/cli-flow-commands.d.ts +3 -1
  13. package/dist/cli/cli-flow-commands.js +34 -4
  14. package/dist/cli/cli-kill.d.ts +2 -1
  15. package/dist/cli/cli-parse.d.ts +3 -1
  16. package/dist/cli/cli-parse.js +29 -7
  17. package/dist/cli/cli-update-commands.js +20 -1
  18. package/dist/cli/cli-verify.d.ts +1 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/cloud-cli.js +12 -6
  21. package/dist/cli/config-discovery.d.ts +2 -1
  22. package/dist/cli/doctor-rows.d.ts +3 -1
  23. package/dist/cli/doctor-rows.js +3 -1
  24. package/dist/cli/gate-exit.d.ts +21 -0
  25. package/dist/cli/gate-exit.js +21 -0
  26. package/dist/cli/gate-hook-message.d.ts +33 -0
  27. package/dist/cli/gate-hook-message.js +46 -0
  28. package/dist/cli/mcp-command.d.ts +25 -0
  29. package/dist/cli/mcp-command.js +143 -0
  30. package/dist/cli/sibling-ports.d.ts +37 -0
  31. package/dist/cli/sibling-ports.js +56 -0
  32. package/dist/cli.js +37 -110
  33. package/dist/cloud/cloud-config.d.ts +2 -1
  34. package/dist/cloud/cloud-sync.d.ts +11 -0
  35. package/dist/cloud/cloud-sync.js +32 -0
  36. package/dist/crawl/crawl.d.ts +9 -13
  37. package/dist/crawl/crawl.js +20 -3
  38. package/dist/daemon/daemon-resolve.d.ts +79 -0
  39. package/dist/daemon/daemon-resolve.js +118 -0
  40. package/dist/daemon/free-port.d.ts +29 -0
  41. package/dist/daemon/free-port.js +55 -0
  42. package/dist/daemon/heartbeat.d.ts +0 -4
  43. package/dist/daemon/heartbeat.js +2 -2
  44. package/dist/dev/stale-issue-guard.d.ts +0 -2
  45. package/dist/dev/stale-issue-guard.js +1 -1
  46. package/dist/events/contradictions.d.ts +41 -0
  47. package/dist/events/contradictions.js +125 -12
  48. package/dist/events/declared.d.ts +14 -0
  49. package/dist/events/declared.js +33 -0
  50. package/dist/events/echo-mismatch.js +14 -0
  51. package/dist/events/observed-in-window.d.ts +12 -0
  52. package/dist/events/observed-in-window.js +17 -0
  53. package/dist/events/predicate-asks.d.ts +13 -0
  54. package/dist/events/predicate-asks.js +22 -0
  55. package/dist/events/predicate-eval.js +64 -22
  56. package/dist/events/predicate-parse.d.ts +0 -6
  57. package/dist/events/predicate-parse.js +43 -3
  58. package/dist/events/predicate-schema.d.ts +44 -0
  59. package/dist/events/predicate-schema.js +126 -23
  60. package/dist/events/predicate.d.ts +15 -0
  61. package/dist/events/predicate.js +41 -2
  62. package/dist/flows/coverage.d.ts +0 -8
  63. package/dist/flows/coverage.js +1 -1
  64. package/dist/flows/decision.d.ts +5 -1
  65. package/dist/flows/decision.js +33 -7
  66. package/dist/flows/flake.d.ts +0 -2
  67. package/dist/flows/flake.js +1 -1
  68. package/dist/flows/flow-intent.d.ts +52 -0
  69. package/dist/flows/flow-intent.js +123 -0
  70. package/dist/flows/flow-replay-run.js +27 -4
  71. package/dist/flows/flow-success.js +15 -3
  72. package/dist/flows/flow-tools.js +32 -4
  73. package/dist/flows/flows.d.ts +6 -1
  74. package/dist/flows/flows.js +34 -22
  75. package/dist/flows/predicate-to-expect.js +6 -3
  76. package/dist/flows/server-verify.js +16 -2
  77. package/dist/flows/verify-change-tools.js +4 -1
  78. package/dist/honesty/blind-spots.d.ts +0 -17
  79. package/dist/honesty/blind-spots.js +1 -1
  80. package/dist/honesty/feature-capture.d.ts +150 -0
  81. package/dist/honesty/feature-capture.js +233 -0
  82. package/dist/honesty/instrumentation-gaps.d.ts +30 -6
  83. package/dist/honesty/instrumentation-gaps.js +61 -5
  84. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  85. package/dist/honesty/tool-hit-rate.js +90 -0
  86. package/dist/honesty/undeclared-change.d.ts +51 -0
  87. package/dist/honesty/undeclared-change.js +56 -0
  88. package/dist/honesty/verified.d.ts +15 -0
  89. package/dist/honesty/verified.js +68 -4
  90. package/dist/http-server.d.ts +0 -1
  91. package/dist/http-server.js +1 -1
  92. package/dist/hunt/hunt-report.d.ts +0 -2
  93. package/dist/hunt/hunt-report.js +1 -1
  94. package/dist/impact/impact-store.d.ts +0 -5
  95. package/dist/impact/impact-store.js +1 -1
  96. package/dist/index.d.ts +4 -1
  97. package/dist/init/agent-rules.d.ts +17 -0
  98. package/dist/init/agent-rules.js +31 -1
  99. package/dist/init/capabilities.d.ts +0 -2
  100. package/dist/init/capabilities.js +1 -1
  101. package/dist/init/closing-hint.d.ts +36 -0
  102. package/dist/init/closing-hint.js +120 -0
  103. package/dist/init/confirm.d.ts +0 -25
  104. package/dist/init/confirm.js +3 -3
  105. package/dist/init/cra.d.ts +14 -1
  106. package/dist/init/cra.js +43 -12
  107. package/dist/init/dev-script.d.ts +48 -0
  108. package/dist/init/dev-script.js +63 -0
  109. package/dist/init/mcp.d.ts +16 -5
  110. package/dist/init/mcp.js +26 -10
  111. package/dist/init/plan-framework.d.ts +0 -5
  112. package/dist/init/plan-framework.js +83 -9
  113. package/dist/init/plan.d.ts +10 -30
  114. package/dist/init/plan.js +30 -15
  115. package/dist/init/refresh-rules.d.ts +39 -0
  116. package/dist/init/refresh-rules.js +51 -0
  117. package/dist/init/run.d.ts +0 -26
  118. package/dist/init/run.js +14 -87
  119. package/dist/init/snippets.js +53 -10
  120. package/dist/input/real-input.d.ts +2 -0
  121. package/dist/input/real-input.js +3 -2
  122. package/dist/input/scroll-find.d.ts +7 -4
  123. package/dist/input/scroll-find.js +63 -25
  124. package/dist/intent/inline-intent.d.ts +46 -0
  125. package/dist/intent/inline-intent.js +103 -0
  126. package/dist/intent/intent-store.d.ts +37 -0
  127. package/dist/intent/intent-store.js +112 -0
  128. package/dist/intent/intent-tools.d.ts +2 -0
  129. package/dist/intent/intent-tools.js +80 -0
  130. package/dist/intent/open-intents.d.ts +21 -0
  131. package/dist/intent/open-intents.js +25 -0
  132. package/dist/journal/envelope.d.ts +26 -26
  133. package/dist/journal/journal-recorder.d.ts +23 -0
  134. package/dist/journal/journal-recorder.js +35 -0
  135. package/dist/mcp/attach-memory.d.ts +67 -0
  136. package/dist/mcp/attach-memory.js +142 -0
  137. package/dist/mcp/mcp-proxy.d.ts +0 -7
  138. package/dist/mcp/mcp-proxy.js +14 -2
  139. package/dist/mcp/mcp.d.ts +2 -0
  140. package/dist/mcp/mcp.js +45 -23
  141. package/dist/mcp/server-instructions.js +45 -3
  142. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  143. package/dist/mcp/tool-catalog-cache.js +10 -0
  144. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  145. package/dist/oracles/flow-instrument-gaps.js +4 -5
  146. package/dist/oracles/self-instrument.d.ts +9 -9
  147. package/dist/oracles/self-instrument.js +19 -13
  148. package/dist/pool/browser-pool.d.ts +12 -1
  149. package/dist/pool/browser-pool.js +31 -1
  150. package/dist/pool/playwright-launcher.js +3 -1
  151. package/dist/project/fs-port.d.ts +3 -0
  152. package/dist/project/fs-port.js +3 -2
  153. package/dist/project/memory-fs.js +6 -1
  154. package/dist/project/project-tools.js +3 -32
  155. package/dist/project/reticle-dir.d.ts +2 -0
  156. package/dist/project/reticle-dir.js +1 -0
  157. package/dist/runs/context-tools.d.ts +2 -0
  158. package/dist/runs/context-tools.js +89 -0
  159. package/dist/runs/run-context.d.ts +40 -0
  160. package/dist/runs/run-context.js +158 -0
  161. package/dist/runs/run-diff.d.ts +2 -1
  162. package/dist/runs/verification-sync.js +2 -2
  163. package/dist/session/dev-server-probe.d.ts +0 -8
  164. package/dist/session/dev-server-probe.js +1 -1
  165. package/dist/session/last-act.d.ts +7 -1
  166. package/dist/session/last-act.js +2 -2
  167. package/dist/session/live-control-tools.js +1 -42
  168. package/dist/session/no-session-diagnosis.d.ts +10 -0
  169. package/dist/session/no-session-diagnosis.js +24 -7
  170. package/dist/session/no-session-next-action.d.ts +5 -0
  171. package/dist/session/no-session-next-action.js +35 -3
  172. package/dist/session/no-session-watch.d.ts +8 -0
  173. package/dist/session/no-session-watch.js +82 -64
  174. package/dist/session/output-budget.js +4 -1
  175. package/dist/session/session-health.d.ts +9 -0
  176. package/dist/session/session-health.js +23 -0
  177. package/dist/session/session-manager.js +50 -1
  178. package/dist/session/session-successor.d.ts +37 -0
  179. package/dist/session/session-successor.js +78 -0
  180. package/dist/session/session.d.ts +31 -1
  181. package/dist/session/session.js +75 -2
  182. package/dist/telemetry/automation-hint.d.ts +2 -1
  183. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  184. package/dist/telemetry/project-profile.d.ts +0 -8
  185. package/dist/telemetry/project-profile.js +1 -1
  186. package/dist/telemetry/run-telemetry.d.ts +0 -2
  187. package/dist/telemetry/run-telemetry.js +1 -1
  188. package/dist/telemetry/verification-of.js +2 -2
  189. package/dist/tools/act-tools.js +126 -21
  190. package/dist/tools/assert-source.d.ts +22 -0
  191. package/dist/tools/assert-source.js +53 -0
  192. package/dist/tools/assert-verdict.d.ts +20 -3
  193. package/dist/tools/assert-verdict.js +36 -4
  194. package/dist/tools/browser-tools.js +0 -20
  195. package/dist/tools/complete-snapshot.d.ts +48 -0
  196. package/dist/tools/complete-snapshot.js +75 -0
  197. package/dist/tools/coverage-tools.d.ts +10 -1
  198. package/dist/tools/coverage-tools.js +123 -69
  199. package/dist/tools/dynamic-tools.js +29 -8
  200. package/dist/tools/error-recovery.d.ts +24 -14
  201. package/dist/tools/error-recovery.js +70 -9
  202. package/dist/tools/feedback-tools.d.ts +1 -1
  203. package/dist/tools/feedback-tools.js +2 -2
  204. package/dist/tools/invoke-tool.js +62 -2
  205. package/dist/tools/lease-hint.d.ts +10 -0
  206. package/dist/tools/lease-hint.js +53 -4
  207. package/dist/tools/lease-tools.d.ts +5 -1
  208. package/dist/tools/lease-tools.js +22 -8
  209. package/dist/tools/navigate-arrival.d.ts +0 -2
  210. package/dist/tools/navigate-arrival.js +1 -1
  211. package/dist/tools/observe-tools.js +63 -26
  212. package/dist/tools/read-tools.js +5 -3
  213. package/dist/tools/real-input-attempt.d.ts +22 -0
  214. package/dist/tools/real-input-attempt.js +153 -1
  215. package/dist/tools/reconcile-tools.js +18 -4
  216. package/dist/tools/resolve-target.d.ts +0 -7
  217. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  218. package/dist/tools/sdk-marker-probe.js +57 -0
  219. package/dist/tools/snapshot-delta.d.ts +1 -1
  220. package/dist/tools/tool-kit.d.ts +12 -0
  221. package/dist/tools/tool-kit.js +16 -0
  222. package/dist/tools/tool-names.d.ts +3 -0
  223. package/dist/tools/tool-names.js +3 -0
  224. package/dist/tools/tool-surface.d.ts +85 -6
  225. package/dist/tools/tool-surface.js +122 -2
  226. package/dist/tools/tools.js +22 -4
  227. package/dist/tools/unadvertised-help.js +4 -4
  228. package/dist/tools/verify-next-baton.d.ts +53 -0
  229. package/dist/tools/verify-next-baton.js +52 -0
  230. package/dist/update/update-nudge.d.ts +7 -10
  231. package/dist/update/update-nudge.js +12 -3
  232. package/dist/update/updater.d.ts +1 -1
  233. package/docs/README.md +2 -1
  234. package/docs/cli/cloud.mdx +1 -1
  235. package/docs/cli/doctor.mdx +1 -0
  236. package/docs/cli/verify.mdx +4 -5
  237. package/docs/desktop.mdx +1 -1
  238. package/docs/docs.json +2 -2
  239. package/docs/gates.md +1 -1
  240. package/docs/install-manual.mdx +1 -1
  241. package/docs/packages/browser.mdx +2 -2
  242. package/docs/packages/server.mdx +1 -1
  243. package/docs/packages.mdx +8 -6
  244. package/docs/platform-integration.md +17 -0
  245. package/docs/predicates.mdx +8 -0
  246. package/docs/reticle-manual.md +106 -0
  247. package/docs/tools-tools-and-run.mdx +2 -4
  248. package/docs/troubleshooting.mdx +9 -0
  249. package/docs/usage.md +3 -2
  250. package/package.json +2 -2
  251. package/dist/session/session-readiness.d.ts +0 -34
  252. package/dist/session/session-readiness.js +0 -36
package/SKILL.md CHANGED
@@ -249,6 +249,30 @@ Work down this list and stop at the first row that fits. Do not hand-drive a flo
249
249
 
250
250
  `reticle_verify {action:"change"}` answers `unknown` when no saved flow covers the files you changed. That is the honest answer and not a failure. Nothing ran, so nothing was proved. It is also the signal to record one. Never read it as a pass.
251
251
 
252
+ ## Two more you have to be told about
253
+
254
+ Same story as `reticle_verify`: extended surface, so they are not in the tool list you were handed, and reached through `reticle_run`.
255
+
256
+ **Context compacted, a turn starting, or a sub-agent taking over?** Ask what this run already established, instead of re-snapshotting to rediscover what you already knew:
257
+
258
+ ```
259
+ reticle_run({ tool: "reticle_context", args: {} })
260
+ ```
261
+
262
+ **About to change something?** Declare what the change is SUPPOSED to make true, in prose, while you still know. A verdict with nothing declared can only be checked against itself:
263
+
264
+ ```
265
+ reticle_run({ tool: "reticle_intent", args: { action: "declare", intents: [{ id: "checkin", statement: "clicking Send check-in makes the badge read 'checked in'" }] } })
266
+ ```
267
+
268
+ Or say it on the verdict itself and skip the round trip. `reticle_act_and_wait` and `reticle_assert` both take an optional `intent`, writing the same ledger:
269
+
270
+ ```
271
+ reticle_act_and_wait({ ref: "e42", action: "click", until: { kind: "signal", name: "checkin:sent" }, intent: "clicking Send check-in makes the badge read 'checked in'" })
272
+ ```
273
+
274
+ The verdict that passes is the one that proves it. Already declared it? Pass the intent's **id** there instead of the prose, and several verdicts can answer to one statement.
275
+
252
276
  ## Record once, replay cheaply
253
277
 
254
278
  The first drive of a journey is expensive. The rest should not be. After you drive something worth keeping:
@@ -270,7 +294,7 @@ Five calls, and the last one is the only one that counts:
270
294
 
271
295
  ```
272
296
  reticle_sessions() // connected? if empty, read `why` — it names the fix
273
- reticle_capabilities({ sessionId }) // the app's whole testable surface, ~1 KB
297
+ reticle_run({ tool: "reticle_capabilities", args: { sessionId } }) // the app's whole testable surface, ~1 KB
274
298
  reticle_snapshot({ sessionId, mode: "interactive" }) // just the controls, with refs
275
299
  reticle_act_and_wait({ sessionId, ref, action: "click", until: { kind: "allOf", predicates: [
276
300
  { kind: "net", urlContains: "/api/...", status: 200 },
@@ -285,7 +309,7 @@ A verdict of `verified: "unknown"` is not a pass. It means Reticle drove the app
285
309
 
286
310
  Then report what you drove, what it produced, and the `file:line` for anything broken.
287
311
 
288
- The surface is deliberately small: `default` 18, `all` 28. Editors budget tools across every MCP server you have connected (Cursor allows 40 in total), so the count is capped rather than allowed to grow. `reticle_tools` loads the argument grammar for the rest on demand, and `reticle_run` invokes any of them by name. Nothing is unreachable; the cold tail just costs one discovery hop.
312
+ The surface is deliberately small: `default` 18, `all` 30. Editors budget tools across every MCP server you have connected (Cursor allows 40 in total), so the count is capped rather than allowed to grow. `reticle_tools` loads the argument grammar for the rest on demand, and `reticle_run` invokes any of them by name. Nothing is unreachable; the cold tail just costs one discovery hop.
289
313
 
290
314
  - Batching, regression suites, reading a verdict: `https://docs.reticle.sh/agent-cheatsheet.md`
291
315
  - Every predicate and action: `https://docs.reticle.sh/predicates.md`, `https://docs.reticle.sh/actions.md`
@@ -26,19 +26,6 @@ export declare const WS_CLOSE_REASON: {
26
26
  */
27
27
  readonly HANDSHAKE_POOL_FULL: string;
28
28
  };
29
- /**
30
- * A dial turned away at the ORIGIN gate, in words the reader can act on.
31
- *
32
- * This is the refusal that used to leave no trace anywhere. It happens before any HELLO, so it
33
- * produced no `noteClosure`, no `session_disconnected`, nothing but an `origin_rejected` log line —
34
- * and the daemon log is the file nobody opens until they have already lost the afternoon. Reported
35
- * from a multi-tenant app resolving its tenant from the `Host` header: on a hosts-file alias every
36
- * dial was refused here, `doctor` reported every check green, and the lease hint blamed the port.
37
- *
38
- * The origin is IN the message because it is the fix: it is either the alias to allow-list, or the
39
- * proof that the page is not being served from where the developer thinks it is.
40
- */
41
- export declare function originRejectedReason(origin: string | undefined): string;
42
29
  interface BridgeOptions {
43
30
  port: number;
44
31
  host?: string;
@@ -65,7 +65,7 @@ export const WS_CLOSE_REASON = {
65
65
  * The origin is IN the message because it is the fix: it is either the alias to allow-list, or the
66
66
  * proof that the page is not being served from where the developer thinks it is.
67
67
  */
68
- export function originRejectedReason(origin) {
68
+ function originRejectedReason(origin) {
69
69
  const named = origin === undefined || 0 === origin.length ? 'a page sending no Origin' : origin;
70
70
  return (`a browser dialled this daemon and was REFUSED at the origin gate: ${named} is not allowed. ` +
71
71
  'Off localhost the SDK needs BOTH `allowNonLocalhost: true` AND a pairing token, and the ' +
@@ -16,6 +16,8 @@ export declare class FakeBrowser {
16
16
  file: string;
17
17
  line: number;
18
18
  } | undefined;
19
+ /** DOM mutations the browser counted inside the acted element; undefined = it counted none. */
20
+ actMutatedWithin: number | undefined;
19
21
  /** when false, ACT reports settled:false + settleReason:'timeout' (throttled-tab path). */
20
22
  actSettled: boolean;
21
23
  /** When false, QUERY by testid returns no match (testid not in current DOM at replay). */
@@ -26,6 +26,8 @@ export class FakeBrowser {
26
26
  actHasTestid = true;
27
27
  /** Provenance the real browser captures alongside the anchor; undefined = app built without it. */
28
28
  actSource = undefined;
29
+ /** DOM mutations the browser counted inside the acted element; undefined = it counted none. */
30
+ actMutatedWithin = undefined;
29
31
  /** when false, ACT reports settled:false + settleReason:'timeout' (throttled-tab path). */
30
32
  actSettled = true;
31
33
  /** When false, QUERY by testid returns no match (testid not in current DOM at replay). */
@@ -101,7 +103,12 @@ export class FakeBrowser {
101
103
  dispatched: true,
102
104
  settled: this.actSettled,
103
105
  settleReason: this.actSettled ? null : 'timeout',
104
- effect: { dispatched: true },
106
+ effect: {
107
+ dispatched: true,
108
+ ...(this.actMutatedWithin === undefined
109
+ ? {}
110
+ : { domMutatedWithin: this.actMutatedWithin }),
111
+ },
105
112
  ...(this.actHasTestid ? { testid: 'pay-btn' } : {}),
106
113
  ...(this.actSource === undefined ? {} : { source: this.actSource }),
107
114
  };
@@ -16,7 +16,7 @@ import type { FileSystemPort } from '../project/fs-port.js';
16
16
  * in the schema literal, and in act-capsule's writer — with the named one used by nobody.
17
17
  */
18
18
  export declare const CAPSULE_VERSION = 1;
19
- export declare const CapsuleSchema: z.ZodObject<{
19
+ declare const CapsuleSchema: z.ZodObject<{
20
20
  version: z.ZodLiteral<1>;
21
21
  id: z.ZodString;
22
22
  /** The flow the failure happened in, when it came from one (a bare assert has no flow). */
@@ -68,3 +68,4 @@ export declare class CapsuleStore {
68
68
  export declare function capsuleId(now: number, label: string): string;
69
69
  /** The minimal reproduction: the steps up to and including the one that failed (prefix trim). */
70
70
  export declare function minimalSteps(steps: readonly FlowStep[], failedIndex: number): FlowStep[];
71
+ export {};
@@ -17,7 +17,7 @@ import { reticleDirPaths } from '../project/reticle-dir.js';
17
17
  * in the schema literal, and in act-capsule's writer — with the named one used by nobody.
18
18
  */
19
19
  export const CAPSULE_VERSION = 1;
20
- export const CapsuleSchema = z.object({
20
+ const CapsuleSchema = z.object({
21
21
  version: z.literal(CAPSULE_VERSION),
22
22
  id: z.string(),
23
23
  /** The flow the failure happened in, when it came from one (a bare assert has no flow). */
@@ -4,7 +4,7 @@ import { type ReticleEvent } from '@reticlehq/core';
4
4
  * app did in the act's window, as counts + a headline, not a raw event dump. Diffs come from the
5
5
  * storage/state events; attribution from. Pure composition over the attributed event window.
6
6
  */
7
- export interface StateDiff {
7
+ interface StateDiff {
8
8
  path: string;
9
9
  from: unknown;
10
10
  to: unknown;
@@ -20,7 +20,7 @@ export interface StateDiff {
20
20
  */
21
21
  atMs: number;
22
22
  }
23
- export interface StorageDiff {
23
+ interface StorageDiff {
24
24
  key: string;
25
25
  from?: unknown;
26
26
  to?: unknown;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The launch options every Reticle-driven Chromium gets, in one place.
3
+ *
4
+ * A pooled browser serves several lease contexts while only one page is ever the visible one, and a
5
+ * driven page can sit in a window the host does not treat as visible. For everyone else Chromium
6
+ * throttles timers, suspends rAF, and deprioritizes the renderer, which is exactly the state a
7
+ * session reports as `throttled: true`: WebSocket- and timer-driven DOM updates land seconds late,
8
+ * asserts time out on content that appears right after the timeout, with no refetch, because the
9
+ * app was fine and the browser was parked. The three switches below are the documented opt-outs,
10
+ * shared by both launch sites so the pooled path (`playwright-launcher`) and the drive path
11
+ * (`real-input`) cannot drift apart.
12
+ */
13
+ /**
14
+ * Chromium switches that keep background pages running at full speed. Named individually rather
15
+ * than as one free string per call site, so a typo is a test failure instead of a silently ignored
16
+ * argument.
17
+ */
18
+ export declare const CHROMIUM_ANTI_THROTTLING_ARGS: readonly ["--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding"];
19
+ /** The options object handed to `chromium.launch` at both launch sites. */
20
+ export interface ChromiumLaunchOptions {
21
+ headless: boolean;
22
+ args: string[];
23
+ }
24
+ /**
25
+ * Build the launch options for a headless or headed Chromium. Pure; returns a fresh mutable array
26
+ * each call so no caller can mutate the constant behind everyone else's back.
27
+ */
28
+ export declare function chromiumLaunchOptions(headless: boolean): ChromiumLaunchOptions;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The launch options every Reticle-driven Chromium gets, in one place.
3
+ *
4
+ * A pooled browser serves several lease contexts while only one page is ever the visible one, and a
5
+ * driven page can sit in a window the host does not treat as visible. For everyone else Chromium
6
+ * throttles timers, suspends rAF, and deprioritizes the renderer, which is exactly the state a
7
+ * session reports as `throttled: true`: WebSocket- and timer-driven DOM updates land seconds late,
8
+ * asserts time out on content that appears right after the timeout, with no refetch, because the
9
+ * app was fine and the browser was parked. The three switches below are the documented opt-outs,
10
+ * shared by both launch sites so the pooled path (`playwright-launcher`) and the drive path
11
+ * (`real-input`) cannot drift apart.
12
+ */
13
+ /**
14
+ * Chromium switches that keep background pages running at full speed. Named individually rather
15
+ * than as one free string per call site, so a typo is a test failure instead of a silently ignored
16
+ * argument.
17
+ */
18
+ export const CHROMIUM_ANTI_THROTTLING_ARGS = [
19
+ '--disable-background-timer-throttling',
20
+ '--disable-backgrounding-occluded-windows',
21
+ '--disable-renderer-backgrounding',
22
+ ];
23
+ /**
24
+ * Build the launch options for a headless or headed Chromium. Pure; returns a fresh mutable array
25
+ * each call so no caller can mutate the constant behind everyone else's back.
26
+ */
27
+ export function chromiumLaunchOptions(headless) {
28
+ return { headless, args: [...CHROMIUM_ANTI_THROTTLING_ARGS] };
29
+ }
@@ -17,6 +17,7 @@ import { diagnoseDesktop, isDesktopProject } from '../init/desktop-doctor.js';
17
17
  import { diagnoseWebCsp } from '../init/csp-doctor.js';
18
18
  import { RETICLE_CONFIG_BASENAME, diagnosePortMismatch, readProjectId, readProjectPort, } from './cli-port.js';
19
19
  import { DoctorRow, doctorRow } from './doctor-rows.js';
20
+ import { findOccupiedSiblings, siblingListenerNote } from './sibling-ports.js';
20
21
  /**
21
22
  * `reticle doctor` — collapse the ~6 independent first-run failure modes into one command. Checks the
22
23
  * Chromium install (the #1 silent failure), whether a daemon is up on the resolved bridge port, and
@@ -91,6 +92,13 @@ export async function handleDoctor(port) {
91
92
  const mismatch = diagnosePortMismatch(port, projectPort);
92
93
  if (mismatch !== undefined)
93
94
  line(doctorRow(DoctorRow.PORT_CHECK, `✗ ${mismatch}`));
95
+ // Remaining half of #261. The SDK dialling a port we are not on is invisible as a refused inbound,
96
+ // but a listener on a well-known Reticle port is visible. Report the observation; do not conclude
97
+ // it is the daemon this app wants — somebody else's daemon looks the same from here.
98
+ const occupiedSiblings = await findOccupiedSiblings(port, probeDaemon);
99
+ const siblingNote = siblingListenerNote(port, occupiedSiblings);
100
+ if (siblingNote !== undefined)
101
+ line(doctorRow(DoctorRow.SIBLING, siblingNote));
94
102
  // The check this checklist was missing: is the APP wired, not just the tools. Everything above can
95
103
  // be green in a project that has never been through `init`, and that combination is precisely the
96
104
  // one `doctor` gets run to explain.
@@ -27,4 +27,6 @@ export declare function handleCapsules(): Promise<void>;
27
27
  * changed files. Flaky flows are quarantined (surfaced, not blocking). The environment-side enforcement
28
28
  * that makes verification unavoidable. Never throws; a fault fails closed (exit 1).
29
29
  */
30
- export declare function handleGate(files: string[], since: string | undefined): Promise<void>;
30
+ export declare function handleGate(files: string[], since: string | undefined,
31
+ /** Hook mode: prose for a human, and silence when there was simply nothing to check. */
32
+ hook?: boolean): Promise<void>;
@@ -5,6 +5,8 @@
5
5
  * opens first. These five functions form one cohesive group: they all read the flow store and report on
6
6
  * verification state, and none of them touch daemon lifecycle, which is what the rest of cli.ts does.
7
7
  */
8
+ import { GateExit } from './gate-exit.js';
9
+ import { gateHookMessage, GATE_SKIP_ENV } from './gate-hook-message.js';
8
10
  import { readProjectId } from './cli-port.js';
9
11
  import { changedFilesSince } from '../flows/git-changed.js';
10
12
  import { join } from 'node:path';
@@ -143,7 +145,9 @@ export async function handleCapsules() {
143
145
  * changed files. Flaky flows are quarantined (surfaced, not blocking). The environment-side enforcement
144
146
  * that makes verification unavoidable. Never throws; a fault fails closed (exit 1).
145
147
  */
146
- export async function handleGate(files, since) {
148
+ export async function handleGate(files, since,
149
+ /** Hook mode: prose for a human, and silence when there was simply nothing to check. */
150
+ hook = false) {
147
151
  try {
148
152
  const fs = createNodeFileSystem();
149
153
  const reticleRoot = join(process.cwd(), ReticleDir.ROOT);
@@ -196,11 +200,37 @@ export async function handleGate(files, since) {
196
200
  ...(result.deleted.length > 0 ? { deletedCoverage: result.deleted } : {}),
197
201
  coverage: flowCoverage,
198
202
  });
199
- if (!pass)
200
- process.exitCode = 1;
203
+ // Two non-zero codes, because two callers want opposite things from the same run. CI wants any
204
+ // problem to fail. A Stop hook wants to block a real regression and NOT block a project that
205
+ // has simply not recorded a flow yet — which is every project on its first day, and measured
206
+ // before this change as an exit 1 that would have blocked every stop. See GateExit.
207
+ if (!pass) {
208
+ const code = result.pass && flowCoverage.outcome !== undefined
209
+ ? GateExit.NOTHING_TO_CHECK
210
+ : GateExit.FAIL;
211
+ process.exitCode = code;
212
+ if (hook) {
213
+ // An honest escape hatch, because the alternative to one is not compliance — it is somebody
214
+ // deleting the hook, and a deleted gate protects nothing. Recorded either way.
215
+ if (process.env[GATE_SKIP_ENV] !== undefined) {
216
+ process.stderr.write(`Reticle: gate skipped via ${GATE_SKIP_ENV}. This change is unverified.\n`);
217
+ process.exitCode = GateExit.PASS;
218
+ return;
219
+ }
220
+ const message = gateHookMessage(code, {
221
+ uncovered: result.uncovered,
222
+ quarantined: result.quarantined,
223
+ // A downgrade is reported per flow with its step indices; the hook names the flow.
224
+ downgraded: result.downgraded.map((d) => d.flow),
225
+ deleted: result.deleted,
226
+ });
227
+ if (message !== undefined)
228
+ process.stderr.write(`${message}\n`);
229
+ }
230
+ }
201
231
  }
202
232
  catch (error) {
203
233
  log('reticle_gate_failed', { error: error instanceof Error ? error.message : String(error) });
204
- process.exitCode = 1;
234
+ process.exitCode = GateExit.FAIL;
205
235
  }
206
236
  }
@@ -25,7 +25,7 @@ export declare const KillAction: {
25
25
  readonly REFUSE: "refuse";
26
26
  };
27
27
  export type KillAction = (typeof KillAction)[keyof typeof KillAction];
28
- export interface KillPlan {
28
+ interface KillPlan {
29
29
  action: KillAction;
30
30
  /** The single pid to signal. Present only for `kill`. */
31
31
  pid?: number;
@@ -56,3 +56,4 @@ export declare function planKill(input: {
56
56
  }): KillPlan;
57
57
  /** `reticle kill` — free the port. Resolves to whether the port is now free. */
58
58
  export declare function runKill(port: number, force: boolean): Promise<boolean>;
59
+ export {};
@@ -13,7 +13,7 @@ export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from '.
13
13
  * telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
14
14
  * the usage block rather than being a footnote somewhere else.
15
15
  */
16
- export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>] (wire Reticle into the project in this directory)\n --app picks WHICH app in a monorepo, when several are found\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N]\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)\n reticle watch [url] (on save, report which saved flows must re-verify)\n reticle drive <url> [--headless] (foreground mode, for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)\n reticle update (install the latest server version and restart)\n reticle rollback (restore the previous server version and restart)\n reticle license (show enterprise license status: active | eval | missing)\n reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't; prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> \"message\"\n (agents: file from anywhere, including a setup that never finished)\n reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]\n (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)\n\nCloud (link this project to Reticle Cloud; runs/flows recorded on the dashboard):\n reticle login --email <e> [--code <c>] [--org <n>] (sign in: mails a code, then exchanges it)\n reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)\n reticle whoami (who am I signed in as, and is this repo attached?)\n reticle project <ls|create <name>> (list or create cloud projects)\n reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]\n reticle push (send local run artifacts to the dashboard)\n reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)\n\n'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay\nand the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
16
+ export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>] (wire Reticle into the project in this directory)\n --app picks WHICH app in a monorepo, when several are found\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N]\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)\n reticle watch [url] (on save, report which saved flows must re-verify)\n reticle drive <url> [--headless] (foreground mode, for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)\n reticle update (install the latest server version and restart)\n reticle rollback (restore the previous server version and restart)\n reticle license (show enterprise license status: active | eval | missing)\n reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't; prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> \"message\"\n (agents: file from anywhere, including a setup that never finished)\n reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]\n (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)\n\nCloud (link this project to Reticle Cloud; runs/flows recorded on the dashboard):\n reticle login --email <e> [--code <c>] [--org <n>] (sign in: mails a code, then exchanges it)\n reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)\n reticle whoami (who am I signed in as, and is this repo attached?)\n reticle project <ls|create <name>> (list or create cloud projects)\n reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]\n reticle push (send local run artifacts to the dashboard)\n reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)\n\n'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay\nand the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
17
17
  /** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
18
18
  export declare const TelemetryAction: {
19
19
  readonly STATUS: "status";
@@ -111,6 +111,7 @@ export type CliResult = {
111
111
  kind: 'verify';
112
112
  url: string;
113
113
  headless: boolean;
114
+ port: number;
114
115
  timeoutMs?: number;
115
116
  storageState?: string;
116
117
  } | {
@@ -126,6 +127,7 @@ export type CliResult = {
126
127
  kind: 'gate';
127
128
  files: string[];
128
129
  since?: string;
130
+ hook?: boolean;
129
131
  } | {
130
132
  kind: 'watch';
131
133
  url?: string;
@@ -29,7 +29,7 @@ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticl
29
29
  reticle status [--port N]
30
30
  reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)
31
31
  reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)
32
- reticle verify <url> [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)
32
+ reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)
33
33
  reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)
34
34
  reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)
35
35
  reticle watch [url] (on save, report which saved flows must re-verify)
@@ -69,6 +69,8 @@ const AFFECTED_COMMAND = 'affected';
69
69
  const HUNT_COMMAND = 'hunt';
70
70
  const CAPSULES_COMMAND = 'capsules';
71
71
  const GATE_COMMAND = 'gate';
72
+ /** Hook mode: prose a human can read, and silence when there was simply nothing to check. */
73
+ const HOOK_FLAG = '--hook';
72
74
  const WATCH_COMMAND = 'watch';
73
75
  const UPDATE_COMMAND = 'update';
74
76
  const ROLLBACK_COMMAND = 'rollback';
@@ -288,14 +290,15 @@ function parseDriveSuffix(args, port, defaultHeadless) {
288
290
  return { kind: 'ok', port, driveUrl, headless };
289
291
  }
290
292
  /**
291
- * Parse `verify <url> [--headed] [--timeout N] [--storage-state <file>]`. The first non-flag token is
292
- * the preview URL.
293
+ * Parse `verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>]`. The first
294
+ * non-flag token is the preview URL. `defaultPort` is already env + `.reticle.json` + 4400.
293
295
  */
294
- function parseVerifySuffix(args) {
296
+ function parseVerifySuffix(args, defaultPort) {
295
297
  let headless = true;
296
298
  let url;
297
299
  let timeoutMs;
298
300
  let storageState;
301
+ let port = defaultPort;
299
302
  let i = 0;
300
303
  while (i < args.length) {
301
304
  const arg = args[i];
@@ -304,6 +307,16 @@ function parseVerifySuffix(args) {
304
307
  if (arg === HEADED_FLAG) {
305
308
  headless = false;
306
309
  }
310
+ else if (arg === PORT_FLAG) {
311
+ i++;
312
+ const n = args[i];
313
+ if (n === undefined)
314
+ return missingValue(PORT_FLAG);
315
+ const parsed = parseInt(n, 10);
316
+ if (isNaN(parsed))
317
+ return notANumber(PORT_FLAG, n);
318
+ port = parsed;
319
+ }
307
320
  else if (arg === TIMEOUT_FLAG) {
308
321
  i++;
309
322
  const n = args[i];
@@ -338,6 +351,7 @@ function parseVerifySuffix(args) {
338
351
  kind: 'ok',
339
352
  url,
340
353
  headless,
354
+ port,
341
355
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
342
356
  ...(storageState !== undefined ? { storageState } : {}),
343
357
  };
@@ -550,13 +564,14 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
550
564
  };
551
565
  }
552
566
  case VERIFY_COMMAND: {
553
- const r = parseVerifySuffix(rest);
567
+ const r = parseVerifySuffix(rest, defaultPort);
554
568
  if ('error' === r.kind)
555
569
  return r;
556
570
  return {
557
571
  kind: 'verify',
558
572
  url: r.url,
559
573
  headless: r.headless,
574
+ port: r.port,
560
575
  ...(r.timeoutMs !== undefined ? { timeoutMs: r.timeoutMs } : {}),
561
576
  ...(r.storageState !== undefined ? { storageState: r.storageState } : {}),
562
577
  };
@@ -575,9 +590,16 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
575
590
  return { kind: 'affected', files: t.files, ...(since === undefined ? {} : { since }) };
576
591
  }
577
592
  case GATE_COMMAND: {
578
- const t = parseTargetArgs(rest);
593
+ // `--hook` is stripped before target parsing, which would reject it as an unknown flag.
594
+ const hook = rest.includes(HOOK_FLAG);
595
+ const t = parseTargetArgs(rest.filter((a) => a !== HOOK_FLAG));
579
596
  const since = t.since ?? implicitSince(t.files);
580
- return { kind: 'gate', files: t.files, ...(since === undefined ? {} : { since }) };
597
+ return {
598
+ kind: 'gate',
599
+ files: t.files,
600
+ ...(since === undefined ? {} : { since }),
601
+ ...(hook ? { hook: true } : {}),
602
+ };
581
603
  }
582
604
  case WATCH_COMMAND: {
583
605
  // `reticle watch [url]` — on file save, report which saved flows must re-verify.
@@ -8,9 +8,10 @@
8
8
  import { checkForUpdate } from '../update/update-checker.js';
9
9
  import { updateTarget } from '../update/update-nudge.js';
10
10
  import { applyUpdate, rollback } from '../update/updater.js';
11
+ import { refreshAgentRules } from '../init/refresh-rules.js';
11
12
  import { SERVER_VERSION } from '../version/server-version.js';
12
13
  import { log } from '../log.js';
13
- import { readFileSync } from 'node:fs';
14
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
14
15
  import { join } from 'node:path';
15
16
  import { reticleDepsOf, sdkSyncCommand } from '../update/sdk-sync.js';
16
17
  import { detectPackageManager } from '../init/detect.js';
@@ -64,6 +65,24 @@ export async function handleUpdate() {
64
65
  return;
65
66
  }
66
67
  log('reticle_update', { ok: true, from: SERVER_VERSION, to: target });
68
+ // Bring this project's agent rules up with the version.
69
+ //
70
+ // The managed block has always been updatable — marker-delimited, idempotent by comparing
71
+ // content — and nothing called it after the first install. `mergeMarkedInstruction` is reachable
72
+ // from `buildPlan` alone, so a project set up on an older release kept that release's
73
+ // instructions forever and every improvement to them reached new projects only.
74
+ //
75
+ // That is backwards: the people who most need better instructions are the ones already
76
+ // installed and not getting value, and this is the moment they are touching the install anyway.
77
+ // Only files that ALREADY carry the block are touched, and only inside the markers.
78
+ const refreshed = refreshAgentRules(process.cwd(), {
79
+ read: (path) => (existsSync(path) ? readFileSync(path, 'utf8') : null),
80
+ write: (path, content) => {
81
+ writeFileSync(path, content, 'utf8');
82
+ },
83
+ });
84
+ if (0 !== refreshed.updated.length)
85
+ log('reticle_rules_refreshed', { files: refreshed.updated, to: target });
67
86
  // The app's SDK FIRST, then the CLI. `reticle update` used to swap only the CLI, so the command
68
87
  // whose job is keeping an install current was itself a way to produce a version-skewed pair —
69
88
  // and the skew message told people to run it to fix an outdated SDK, which it could not do.
@@ -63,6 +63,6 @@ export declare function handleVerify(parsed: {
63
63
  timeoutMs?: number;
64
64
  storageState?: string;
65
65
  /** Bridge port — parseCliArgs already resolves --port / RETICLE_PORT / .reticle.json into this. */
66
- port?: number;
66
+ port: number;
67
67
  }): void;
68
68
  export {};
@@ -16,7 +16,7 @@ import { start } from '../index.js';
16
16
  import { probePresence, PortPresence } from '../daemon/port-presence.js';
17
17
  import { probeDaemon } from '../mcp/mcp-proxy.js';
18
18
  import { fetchStatus } from './cli-launch.js';
19
- import { resolveCloudConfig, syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
19
+ import { cloudFetch, resolveCloudConfig, syncRunToCloud, SyncOutcome, } from '../cloud/cloud-sync.js';
20
20
  import { ReticleRunner } from '../runs/reticle-runner.js';
21
21
  import { createRunnerPort } from '../runs/runner-port.js';
22
22
  import { RunStore } from '../runs/run-store.js';
@@ -94,7 +94,7 @@ async function pushRunToCloud(run, ports) {
94
94
  const config = resolveCloudConfig(process.env);
95
95
  if (null === config)
96
96
  return;
97
- const result = await syncRunToCloud(run, config, (url, init) => fetch(url, init));
97
+ const result = await syncRunToCloud(run, config, cloudFetch);
98
98
  if (result.outcome === SyncOutcome.SYNCED) {
99
99
  ports.out(`↑ run ${run.runId} recorded on the Reticle Cloud dashboard`);
100
100
  }
@@ -16,7 +16,7 @@ import { z } from 'zod';
16
16
  import { createNodeFileSystem } from '../project/fs-port.js';
17
17
  import { RunStore } from '../runs/run-store.js';
18
18
  import { resolveProjectCloud } from '../cloud/cloud-config.js';
19
- import { syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
19
+ import { cloudFetch, syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
20
20
  const DEFAULT_URL = 'http://localhost:8890';
21
21
  const RETICLE_DIR = '.reticle';
22
22
  const SESSION_FILE = 'session.json';
@@ -77,9 +77,15 @@ const readSession = async () => {
77
77
  };
78
78
  const baseUrl = (session) => {
79
79
  const env = process.env['RETICLE_CLOUD_URL'];
80
- return env !== undefined && env.length > 0
81
- ? env.replace(/\/+$/, '')
82
- : (session?.url ?? DEFAULT_URL);
80
+ if (env !== undefined && env.length > 0)
81
+ return env.replace(/\/+$/, '');
82
+ if (null !== session && session.url.length > 0)
83
+ return session.url;
84
+ // Falling back to the local dev default WITHOUT being told to: say so on stderr, so a fresh
85
+ // `reticle login` never silently dials localhost. Humans read the hint; agents parse stdout
86
+ // JSON and ignore stderr, so stdout stays machine-readable either way.
87
+ hint(`no RETICLE_CLOUD_URL set — dialling the local dev default ${DEFAULT_URL} (set it for a real deployment)`);
88
+ return DEFAULT_URL;
83
89
  };
84
90
  /** Bearer for a command: an explicit api key (agent) wins, else the human login token. */
85
91
  const bearer = (session) => {
@@ -101,7 +107,7 @@ const api = async (method, url, token, body) => {
101
107
  };
102
108
  if (body !== undefined)
103
109
  init.body = JSON.stringify(body);
104
- const res = await fetch(url, init);
110
+ const res = await cloudFetch(url, init);
105
111
  const text = await res.text();
106
112
  let json = null;
107
113
  if (text.length > 0) {
@@ -438,7 +444,7 @@ const cmdPush = async () => {
438
444
  const read = await store.read(id);
439
445
  if (!read.ok)
440
446
  continue;
441
- const res = await syncRunToCloud(read.run, cloud.config, (u, init) => fetch(u, init));
447
+ const res = await syncRunToCloud(read.run, cloud.config, cloudFetch);
442
448
  if (res.outcome === SyncOutcome.SYNCED)
443
449
  pushed += 1;
444
450
  else if (res.outcome === SyncOutcome.FAILED)
@@ -19,7 +19,7 @@
19
19
  * silently is how an agent ends up with a confident verdict about an app it never touched. Callers
20
20
  * are handed the list and must name it.
21
21
  */
22
- export interface FoundConfig {
22
+ interface FoundConfig {
23
23
  /** Absolute path to the `.reticle.json`. */
24
24
  path: string;
25
25
  /** The directory it configures — what a reader needs in order to act. */
@@ -33,3 +33,4 @@ export interface ConfigDiscovery {
33
33
  searched: string[];
34
34
  }
35
35
  export declare function discoverProjectConfigs(cwd: string): ConfigDiscovery;
36
+ export {};
@@ -18,7 +18,8 @@
18
18
  * row added and never documented.
19
19
  *
20
20
  * NOT every row prints on every run. `version` prints only on skew, `port check` only on a
21
- * mismatch, `desktop` only on a desktop project, and `sessions` only when a daemon answered. The
21
+ * mismatch, `sibling` only when a well-known Reticle port other than ours has a listener,
22
+ * `desktop` only on a desktop project, and `sessions` only when a daemon answered. The
22
23
  * vocabulary is therefore "what this command can print", not "what one run printed" — a guard built
23
24
  * by scraping a single invocation would silently stop covering the conditional rows, which are
24
25
  * exactly the ones a reader is least likely to have seen before.
@@ -32,6 +33,7 @@ export declare const DoctorRow: {
32
33
  readonly SESSIONS: "sessions";
33
34
  readonly BRIDGE_PORT: "bridge port";
34
35
  readonly PORT_CHECK: "port check";
36
+ readonly SIBLING: "sibling";
35
37
  readonly DAEMON_LOG: "daemon log";
36
38
  readonly TRACING: "tracing";
37
39
  readonly DESKTOP: "desktop";
@@ -18,7 +18,8 @@
18
18
  * row added and never documented.
19
19
  *
20
20
  * NOT every row prints on every run. `version` prints only on skew, `port check` only on a
21
- * mismatch, `desktop` only on a desktop project, and `sessions` only when a daemon answered. The
21
+ * mismatch, `sibling` only when a well-known Reticle port other than ours has a listener,
22
+ * `desktop` only on a desktop project, and `sessions` only when a daemon answered. The
22
23
  * vocabulary is therefore "what this command can print", not "what one run printed" — a guard built
23
24
  * by scraping a single invocation would silently stop covering the conditional rows, which are
24
25
  * exactly the ones a reader is least likely to have seen before.
@@ -32,6 +33,7 @@ export const DoctorRow = {
32
33
  SESSIONS: 'sessions',
33
34
  BRIDGE_PORT: 'bridge port',
34
35
  PORT_CHECK: 'port check',
36
+ SIBLING: 'sibling',
35
37
  DAEMON_LOG: 'daemon log',
36
38
  TRACING: 'tracing',
37
39
  DESKTOP: 'desktop',