@reticlehq/server 2.10.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 (290) hide show
  1. package/SKILL.md +73 -11
  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 +41 -0
  45. package/dist/dev/stale-issue-guard.js +75 -0
  46. package/dist/domain/domain-tools.js +3 -2
  47. package/dist/events/contradictions.d.ts +41 -0
  48. package/dist/events/contradictions.js +125 -12
  49. package/dist/events/declared.d.ts +14 -0
  50. package/dist/events/declared.js +33 -0
  51. package/dist/events/echo-mismatch.js +14 -0
  52. package/dist/events/observed-in-window.d.ts +12 -0
  53. package/dist/events/observed-in-window.js +17 -0
  54. package/dist/events/predicate-asks.d.ts +30 -0
  55. package/dist/events/predicate-asks.js +48 -0
  56. package/dist/events/predicate-eval.js +254 -25
  57. package/dist/events/predicate-parse.d.ts +0 -6
  58. package/dist/events/predicate-parse.js +51 -5
  59. package/dist/events/predicate-schema.d.ts +62 -0
  60. package/dist/events/predicate-schema.js +162 -4
  61. package/dist/events/predicate.d.ts +15 -0
  62. package/dist/events/predicate.js +41 -2
  63. package/dist/flows/coverage.d.ts +0 -8
  64. package/dist/flows/coverage.js +1 -1
  65. package/dist/flows/decision.d.ts +5 -1
  66. package/dist/flows/decision.js +33 -7
  67. package/dist/flows/flake.d.ts +0 -2
  68. package/dist/flows/flake.js +1 -1
  69. package/dist/flows/flow-intent.d.ts +52 -0
  70. package/dist/flows/flow-intent.js +123 -0
  71. package/dist/flows/flow-replay-run.js +27 -4
  72. package/dist/flows/flow-success.js +15 -3
  73. package/dist/flows/flow-tools.js +65 -8
  74. package/dist/flows/flows.d.ts +6 -1
  75. package/dist/flows/flows.js +34 -22
  76. package/dist/flows/predicate-to-expect.js +14 -3
  77. package/dist/flows/server-verify.js +16 -2
  78. package/dist/flows/verify-change-tools.js +9 -2
  79. package/dist/honesty/blind-spots.d.ts +8 -16
  80. package/dist/honesty/blind-spots.js +18 -5
  81. package/dist/honesty/feature-capture.d.ts +150 -0
  82. package/dist/honesty/feature-capture.js +233 -0
  83. package/dist/honesty/gap-ledger.d.ts +52 -0
  84. package/dist/honesty/gap-ledger.js +63 -0
  85. package/dist/honesty/instrumentation-gaps.d.ts +79 -0
  86. package/dist/honesty/instrumentation-gaps.js +77 -0
  87. package/dist/honesty/observability.d.ts +37 -0
  88. package/dist/honesty/observability.js +40 -0
  89. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  90. package/dist/honesty/tool-hit-rate.js +90 -0
  91. package/dist/honesty/undeclared-change.d.ts +51 -0
  92. package/dist/honesty/undeclared-change.js +56 -0
  93. package/dist/honesty/verified.d.ts +15 -0
  94. package/dist/honesty/verified.js +68 -4
  95. package/dist/http-server.d.ts +0 -1
  96. package/dist/http-server.js +1 -1
  97. package/dist/hunt/hunt-report.d.ts +0 -2
  98. package/dist/hunt/hunt-report.js +1 -1
  99. package/dist/impact/impact-store.d.ts +0 -5
  100. package/dist/impact/impact-store.js +1 -1
  101. package/dist/index.d.ts +4 -2
  102. package/dist/index.js +41 -0
  103. package/dist/init/agent-rules.d.ts +17 -0
  104. package/dist/init/agent-rules.js +31 -1
  105. package/dist/init/capabilities.d.ts +0 -2
  106. package/dist/init/capabilities.js +1 -1
  107. package/dist/init/closing-hint.d.ts +36 -0
  108. package/dist/init/closing-hint.js +120 -0
  109. package/dist/init/confirm.d.ts +0 -25
  110. package/dist/init/confirm.js +3 -3
  111. package/dist/init/cra.d.ts +14 -1
  112. package/dist/init/cra.js +43 -12
  113. package/dist/init/dev-script.d.ts +48 -0
  114. package/dist/init/dev-script.js +63 -0
  115. package/dist/init/mcp.d.ts +16 -5
  116. package/dist/init/mcp.js +26 -10
  117. package/dist/init/node-io.js +3 -0
  118. package/dist/init/plan-framework.d.ts +0 -5
  119. package/dist/init/plan-framework.js +83 -9
  120. package/dist/init/plan.d.ts +10 -30
  121. package/dist/init/plan.js +30 -15
  122. package/dist/init/refresh-rules.d.ts +39 -0
  123. package/dist/init/refresh-rules.js +51 -0
  124. package/dist/init/run.d.ts +2 -10
  125. package/dist/init/run.js +26 -68
  126. package/dist/init/slash-command.d.ts +1 -1
  127. package/dist/init/slash-command.js +33 -10
  128. package/dist/init/snippets.js +53 -10
  129. package/dist/input/real-input.d.ts +2 -0
  130. package/dist/input/real-input.js +3 -2
  131. package/dist/input/scroll-find.d.ts +7 -4
  132. package/dist/input/scroll-find.js +63 -25
  133. package/dist/intent/inline-intent.d.ts +46 -0
  134. package/dist/intent/inline-intent.js +103 -0
  135. package/dist/intent/intent-store.d.ts +37 -0
  136. package/dist/intent/intent-store.js +112 -0
  137. package/dist/intent/intent-tools.d.ts +2 -0
  138. package/dist/intent/intent-tools.js +80 -0
  139. package/dist/intent/open-intents.d.ts +21 -0
  140. package/dist/intent/open-intents.js +25 -0
  141. package/dist/journal/envelope.d.ts +2 -2
  142. package/dist/journal/journal-recorder.d.ts +23 -0
  143. package/dist/journal/journal-recorder.js +35 -0
  144. package/dist/license/license.d.ts +8 -2
  145. package/dist/license/license.js +10 -15
  146. package/dist/mcp/attach-memory.d.ts +67 -0
  147. package/dist/mcp/attach-memory.js +142 -0
  148. package/dist/mcp/mcp-proxy.d.ts +0 -7
  149. package/dist/mcp/mcp-proxy.js +14 -2
  150. package/dist/mcp/mcp.d.ts +2 -0
  151. package/dist/mcp/mcp.js +45 -23
  152. package/dist/mcp/server-instructions.js +45 -3
  153. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  154. package/dist/mcp/tool-catalog-cache.js +10 -0
  155. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  156. package/dist/oracles/flow-instrument-gaps.js +4 -5
  157. package/dist/oracles/self-instrument.d.ts +9 -9
  158. package/dist/oracles/self-instrument.js +19 -13
  159. package/dist/pool/browser-pool.d.ts +12 -1
  160. package/dist/pool/browser-pool.js +31 -1
  161. package/dist/pool/playwright-launcher.js +3 -1
  162. package/dist/project/artifact-root.d.ts +83 -0
  163. package/dist/project/artifact-root.js +112 -0
  164. package/dist/project/fs-port.d.ts +3 -0
  165. package/dist/project/fs-port.js +3 -2
  166. package/dist/project/memory-fs.d.ts +9 -0
  167. package/dist/project/memory-fs.js +93 -0
  168. package/dist/project/project-store.d.ts +17 -0
  169. package/dist/project/project-store.js +33 -0
  170. package/dist/project/project-tools.js +3 -32
  171. package/dist/project/remember-project.d.ts +24 -0
  172. package/dist/project/remember-project.js +40 -0
  173. package/dist/project/reticle-dir.d.ts +2 -0
  174. package/dist/project/reticle-dir.js +1 -0
  175. package/dist/project/session-root.d.ts +15 -0
  176. package/dist/project/session-root.js +31 -0
  177. package/dist/runs/context-tools.d.ts +2 -0
  178. package/dist/runs/context-tools.js +89 -0
  179. package/dist/runs/run-context.d.ts +40 -0
  180. package/dist/runs/run-context.js +158 -0
  181. package/dist/runs/run-diff.d.ts +2 -1
  182. package/dist/runs/run-tools.js +2 -1
  183. package/dist/runs/verification-sync.js +2 -2
  184. package/dist/session/dev-server-probe.d.ts +0 -8
  185. package/dist/session/dev-server-probe.js +1 -1
  186. package/dist/session/last-act.d.ts +7 -1
  187. package/dist/session/last-act.js +2 -2
  188. package/dist/session/lease-availability.d.ts +40 -0
  189. package/dist/session/lease-availability.js +43 -0
  190. package/dist/session/live-control-tools.js +1 -42
  191. package/dist/session/no-session-diagnosis.d.ts +20 -0
  192. package/dist/session/no-session-diagnosis.js +41 -12
  193. package/dist/session/no-session-next-action.d.ts +5 -0
  194. package/dist/session/no-session-next-action.js +35 -3
  195. package/dist/session/no-session-watch.d.ts +8 -0
  196. package/dist/session/no-session-watch.js +83 -65
  197. package/dist/session/output-budget.js +4 -1
  198. package/dist/session/session-health.d.ts +9 -0
  199. package/dist/session/session-health.js +23 -0
  200. package/dist/session/session-manager.js +50 -1
  201. package/dist/session/session-successor.d.ts +37 -0
  202. package/dist/session/session-successor.js +78 -0
  203. package/dist/session/session.d.ts +40 -1
  204. package/dist/session/session.js +84 -2
  205. package/dist/session/stall-clock.d.ts +18 -0
  206. package/dist/session/stall-clock.js +48 -0
  207. package/dist/telemetry/automation-hint.d.ts +2 -1
  208. package/dist/telemetry/daemon-telemetry.js +12 -23
  209. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  210. package/dist/telemetry/project-profile.d.ts +0 -8
  211. package/dist/telemetry/project-profile.js +1 -1
  212. package/dist/telemetry/run-telemetry.d.ts +0 -2
  213. package/dist/telemetry/run-telemetry.js +1 -1
  214. package/dist/telemetry/session-metrics.d.ts +1 -1
  215. package/dist/telemetry/session-metrics.js +1 -1
  216. package/dist/telemetry/telemetry.d.ts +12 -3
  217. package/dist/telemetry/telemetry.js +25 -5
  218. package/dist/telemetry/verification-of.js +2 -2
  219. package/dist/tools/act-tools.js +159 -24
  220. package/dist/tools/assert-source.d.ts +22 -0
  221. package/dist/tools/assert-source.js +53 -0
  222. package/dist/tools/assert-verdict.d.ts +21 -2
  223. package/dist/tools/assert-verdict.js +59 -4
  224. package/dist/tools/browser-tools.js +0 -20
  225. package/dist/tools/complete-snapshot.d.ts +48 -0
  226. package/dist/tools/complete-snapshot.js +75 -0
  227. package/dist/tools/contract-tools.js +5 -3
  228. package/dist/tools/coverage-tools.d.ts +10 -1
  229. package/dist/tools/coverage-tools.js +124 -37
  230. package/dist/tools/dynamic-tools.js +29 -8
  231. package/dist/tools/error-recovery.d.ts +24 -14
  232. package/dist/tools/error-recovery.js +70 -9
  233. package/dist/tools/feedback-tools.d.ts +1 -1
  234. package/dist/tools/feedback-tools.js +2 -2
  235. package/dist/tools/invoke-tool.js +62 -2
  236. package/dist/tools/lease-hint.d.ts +10 -0
  237. package/dist/tools/lease-hint.js +53 -4
  238. package/dist/tools/lease-tools.d.ts +5 -1
  239. package/dist/tools/lease-tools.js +22 -8
  240. package/dist/tools/navigate-arrival.d.ts +0 -2
  241. package/dist/tools/navigate-arrival.js +1 -1
  242. package/dist/tools/observe-tools.js +70 -27
  243. package/dist/tools/read-tools.js +5 -3
  244. package/dist/tools/real-input-attempt.d.ts +22 -0
  245. package/dist/tools/real-input-attempt.js +153 -1
  246. package/dist/tools/reconcile-tools.js +18 -4
  247. package/dist/tools/resolve-target.d.ts +0 -7
  248. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  249. package/dist/tools/sdk-marker-probe.js +57 -0
  250. package/dist/tools/snapshot-delta.d.ts +1 -1
  251. package/dist/tools/tool-kit.d.ts +28 -1
  252. package/dist/tools/tool-kit.js +16 -0
  253. package/dist/tools/tool-names.d.ts +3 -0
  254. package/dist/tools/tool-names.js +3 -0
  255. package/dist/tools/tool-surface.d.ts +85 -6
  256. package/dist/tools/tool-surface.js +122 -2
  257. package/dist/tools/tools.js +22 -4
  258. package/dist/tools/unadvertised-help.js +4 -4
  259. package/dist/tools/verify-next-baton.d.ts +53 -0
  260. package/dist/tools/verify-next-baton.js +52 -0
  261. package/dist/update/update-nudge.d.ts +7 -10
  262. package/dist/update/update-nudge.js +12 -3
  263. package/dist/update/updater.d.ts +1 -1
  264. package/docs/README.md +3 -1
  265. package/docs/cli/cloud.mdx +1 -1
  266. package/docs/cli/doctor.mdx +1 -0
  267. package/docs/cli/verify.mdx +4 -5
  268. package/docs/desktop.mdx +1 -1
  269. package/docs/docs.json +3 -3
  270. package/docs/enterprise.md +3 -1
  271. package/docs/gates.md +1 -1
  272. package/docs/install-manual.mdx +1 -1
  273. package/docs/license-activation.mdx +145 -0
  274. package/docs/packages/browser.mdx +2 -2
  275. package/docs/packages/server.mdx +1 -1
  276. package/docs/packages.mdx +8 -6
  277. package/docs/platform-integration.md +17 -0
  278. package/docs/predicates.mdx +8 -0
  279. package/docs/reticle-manual.md +106 -0
  280. package/docs/telemetry-contract.md +1 -12
  281. package/docs/telemetry-events.mdx +1 -34
  282. package/docs/telemetry.md +0 -1
  283. package/docs/tools-tools-and-run.mdx +2 -4
  284. package/docs/troubleshooting.mdx +9 -0
  285. package/docs/usage.md +3 -2
  286. package/package.json +2 -2
  287. package/dist/session/session-readiness.d.ts +0 -34
  288. package/dist/session/session-readiness.js +0 -36
  289. package/dist/telemetry/instrumentation-stall.d.ts +0 -57
  290. package/dist/telemetry/instrumentation-stall.js +0 -103
@@ -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',
@@ -0,0 +1,21 @@
1
+ /**
2
+ * What `reticle gate` means by its exit code.
3
+ *
4
+ * Split out of the handler because two different readers depend on it and they want opposite
5
+ * things. CI wants any problem to be a failure. A Stop hook — the layer that makes verification
6
+ * unskippable — wants to block a real regression and NOT block a project that has simply not
7
+ * recorded a flow yet, which is every project on its first day.
8
+ *
9
+ * `NOTHING_TO_CHECK` stays non-zero deliberately. The gate exits non-zero over an empty suite
10
+ * because "nothing was checked" and "everything passed" are indistinguishable from the outside,
11
+ * and that reasoning does not stop being true just because a second caller appeared.
12
+ */
13
+ export declare const GateExit: {
14
+ /** Affected flows were checked and hold. */
15
+ readonly PASS: 0;
16
+ /** Something is actually wrong: an affected flow with no passing artifact, a downgraded
17
+ * assertion, deleted coverage. A caller should stop here. */
18
+ readonly FAIL: 1;
19
+ /** The gate ran and had nothing to judge — no flows recorded. Non-zero, but not a regression. */
20
+ readonly NOTHING_TO_CHECK: 2;
21
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * What `reticle gate` means by its exit code.
3
+ *
4
+ * Split out of the handler because two different readers depend on it and they want opposite
5
+ * things. CI wants any problem to be a failure. A Stop hook — the layer that makes verification
6
+ * unskippable — wants to block a real regression and NOT block a project that has simply not
7
+ * recorded a flow yet, which is every project on its first day.
8
+ *
9
+ * `NOTHING_TO_CHECK` stays non-zero deliberately. The gate exits non-zero over an empty suite
10
+ * because "nothing was checked" and "everything passed" are indistinguishable from the outside,
11
+ * and that reasoning does not stop being true just because a second caller appeared.
12
+ */
13
+ export const GateExit = {
14
+ /** Affected flows were checked and hold. */
15
+ PASS: 0,
16
+ /** Something is actually wrong: an affected flow with no passing artifact, a downgraded
17
+ * assertion, deleted coverage. A caller should stop here. */
18
+ FAIL: 1,
19
+ /** The gate ran and had nothing to judge — no flows recorded. Non-zero, but not a regression. */
20
+ NOTHING_TO_CHECK: 2,
21
+ };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What a Stop hook shows the human when the gate blocks.
3
+ *
4
+ * A hook runs at the moment an agent says it is finished, and whatever it prints is the last thing
5
+ * anybody reads. The gate's normal output is one long JSON line built for CI to parse; pasted into
6
+ * a terminal at the end of a session it is a wall of names with no instruction attached.
7
+ *
8
+ * So the hook path gets prose. It has to answer three things in the order a person asks them: what
9
+ * is not verified, why that matters right now, and what to do about it — including how to proceed
10
+ * anyway, because a gate with no honest escape hatch gets disabled entirely and then protects
11
+ * nothing.
12
+ */
13
+ /** Named so a reader of the hook output knows the flag is real and not a suggestion. */
14
+ export declare const GATE_SKIP_ENV = "RETICLE_SKIP_GATE";
15
+ export interface GateHookInput {
16
+ /** Flows the changed files touch that have no passing artifact. */
17
+ uncovered: readonly string[];
18
+ /** Flows held back as flaky — reported, never counted as a pass. */
19
+ quarantined: readonly string[];
20
+ /** Assertions that dropped from a real consequence to a fakeable one. */
21
+ downgraded: readonly string[];
22
+ /** Flows whose coverage was deleted while their files changed. */
23
+ deleted: readonly string[];
24
+ }
25
+ /**
26
+ * The message, or undefined when there is nothing to say.
27
+ *
28
+ * `NOTHING_TO_CHECK` returns undefined on purpose: a project that has not recorded a flow yet is
29
+ * every project on its first day, and blocking there would make the first experience of Reticle an
30
+ * agent that cannot stop. The gate still reports it — this decides only whether a HUMAN is
31
+ * interrupted.
32
+ */
33
+ export declare function gateHookMessage(exit: number, input: GateHookInput): string | undefined;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * What a Stop hook shows the human when the gate blocks.
3
+ *
4
+ * A hook runs at the moment an agent says it is finished, and whatever it prints is the last thing
5
+ * anybody reads. The gate's normal output is one long JSON line built for CI to parse; pasted into
6
+ * a terminal at the end of a session it is a wall of names with no instruction attached.
7
+ *
8
+ * So the hook path gets prose. It has to answer three things in the order a person asks them: what
9
+ * is not verified, why that matters right now, and what to do about it — including how to proceed
10
+ * anyway, because a gate with no honest escape hatch gets disabled entirely and then protects
11
+ * nothing.
12
+ */
13
+ import { GateExit } from './gate-exit.js';
14
+ /** Named so a reader of the hook output knows the flag is real and not a suggestion. */
15
+ export const GATE_SKIP_ENV = 'RETICLE_SKIP_GATE';
16
+ const MAX_NAMED = 5;
17
+ /** A short list, then a count — a hook that prints forty names is a hook people turn off. */
18
+ function name(list) {
19
+ const shown = list.slice(0, MAX_NAMED).join(', ');
20
+ return list.length > MAX_NAMED ? `${shown} (+${String(list.length - MAX_NAMED)} more)` : shown;
21
+ }
22
+ /**
23
+ * The message, or undefined when there is nothing to say.
24
+ *
25
+ * `NOTHING_TO_CHECK` returns undefined on purpose: a project that has not recorded a flow yet is
26
+ * every project on its first day, and blocking there would make the first experience of Reticle an
27
+ * agent that cannot stop. The gate still reports it — this decides only whether a HUMAN is
28
+ * interrupted.
29
+ */
30
+ export function gateHookMessage(exit, input) {
31
+ if (GateExit.FAIL !== exit)
32
+ return undefined;
33
+ const lines = ['Reticle: this change is not verified.'];
34
+ if (input.uncovered.length > 0)
35
+ lines.push(` no passing run covers: ${name(input.uncovered)}`);
36
+ if (input.downgraded.length > 0)
37
+ lines.push(` assertions weakened to something fakeable: ${name(input.downgraded)} — a check that cannot fail is not a check`);
38
+ if (input.deleted.length > 0)
39
+ lines.push(` coverage deleted while its files changed: ${name(input.deleted)}`);
40
+ if (input.quarantined.length > 0)
41
+ lines.push(` held back as flaky, so proving nothing: ${name(input.quarantined)}`);
42
+ lines.push('');
43
+ lines.push(' Replay them: npx @reticlehq/server verify <your app url>');
44
+ lines.push(` Or proceed anyway: ${GATE_SKIP_ENV}=1 — it is recorded, not silent.`);
45
+ return lines.join('\n');
46
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `reticle mcp` — the process an editor launches, and the one whose failure a user experiences as
3
+ * "the MCP server disconnected".
4
+ *
5
+ * Split out of `cli.ts` when that file passed the 1000-line backstop. It earns its own module rather
6
+ * than being an arbitrary slice: everything here is about ONE decision — which daemon this agent
7
+ * talks to for the rest of its life — and the port resolution at the top is the fix for agents on
8
+ * different projects silently sharing one daemon, one identity and one blast radius.
9
+ */
10
+ /**
11
+ * MCP proxy mode: ensures the daemon is running, then bridges Claude Code's
12
+ * stdin/stdout to the daemon's SSE endpoint. This is the recommended way to
13
+ * configure Reticle in.mcp.json — users never need to manage the daemon manually.
14
+ *
15
+ * Pass --drive <url> to have the daemon launch its own Playwright browser at that
16
+ * URL. The agent then has full autonomous control without relying on the user's browser.
17
+ */
18
+ export declare function handleMcp(opts: {
19
+ port: number;
20
+ driveUrl?: string;
21
+ headless: boolean;
22
+ http: boolean;
23
+ httpPort?: number;
24
+ httpToken?: string;
25
+ }): Promise<void>;
@@ -0,0 +1,143 @@
1
+ /**
2
+ * `reticle mcp` — the process an editor launches, and the one whose failure a user experiences as
3
+ * "the MCP server disconnected".
4
+ *
5
+ * Split out of `cli.ts` when that file passed the 1000-line backstop. It earns its own module rather
6
+ * than being an arbitrary slice: everything here is about ONE decision — which daemon this agent
7
+ * talks to for the rest of its life — and the port resolution at the top is the fix for agents on
8
+ * different projects silently sharing one daemon, one identity and one blast radius.
9
+ */
10
+ import { log } from '../log.js';
11
+ import { isAlive, spawnDaemon, reticleStateHome } from '../daemon/daemon.js';
12
+ import { PortPresence, probePresence, presenceIsUsable, describePresence, } from '../daemon/port-presence.js';
13
+ import { waitForDaemon, startMcpProxy, probeDaemon, proxyLog, setProxyLogPort, } from '../mcp/mcp-proxy.js';
14
+ import { installProxyResilience } from '../daemon/daemon-resilience.js';
15
+ import { readProjectId } from './cli-port.js';
16
+ import { DAEMON_INNER_COMMAND, PORT_FLAG, DRIVE_FLAG, HEADED_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, } from './cli-parse.js';
17
+ import { resolveMcpPort } from '../daemon/daemon-resolve.js';
18
+ import { pickDaemonPortToBind } from '../daemon/free-port.js';
19
+ import { fetchStatus } from './cli-launch.js';
20
+ /**
21
+ * MCP proxy mode: ensures the daemon is running, then bridges Claude Code's
22
+ * stdin/stdout to the daemon's SSE endpoint. This is the recommended way to
23
+ * configure Reticle in.mcp.json — users never need to manage the daemon manually.
24
+ *
25
+ * Pass --drive <url> to have the daemon launch its own Playwright browser at that
26
+ * URL. The agent then has full autonomous control without relying on the user's browser.
27
+ */
28
+ export async function handleMcp(opts) {
29
+ const { driveUrl, headless, http, httpPort, httpToken } = opts;
30
+ // Decided before anything else, because every line below is about ONE port and the wrong one means
31
+ // serving another project's daemon. `resolveMcpPort` prefers our own daemon wherever it moved to,
32
+ // takes the requested port when it is free or unclaimed, and relocates to an OS-assigned port
33
+ // rather than adopting a daemon that belongs to somebody else.
34
+ //
35
+ // Synchronous-looking on purpose: a probe is one loopback connect, and doing it here keeps the
36
+ // proxy's transport built around a single fixed number. Making the proxy chase a moving port would
37
+ // mean reworking the reconnect path, which is the one piece whose failure reads to a user as
38
+ // "the MCP server disappeared mid-session".
39
+ const port = await resolveMcpPort(opts.port, readProjectId(process.cwd()), reticleStateHome(), {
40
+ alive: isAlive,
41
+ daemonPresent: async (p) => presenceIsUsable(await probePresence(p, { tcpOpen: probeDaemon, status: fetchStatus })),
42
+ pickPort: (p) => pickDaemonPortToBind(p),
43
+ });
44
+ if (port !== opts.port) {
45
+ log('reticle_mcp_port_relocated', {
46
+ requested: opts.port,
47
+ port,
48
+ reason: 'the requested port serves a different project',
49
+ });
50
+ }
51
+ // The proxy IS the MCP server the editor launched. Nothing respawns it, so an uncaught throw here
52
+ // is what a user experiences as "the MCP server disconnected — open /mcp and reconnect". Log it,
53
+ // report it, keep serving: the reconnect and dormant paths already know how to rebuild the only
54
+ // state this process has. See installProxyResilience for why its rule inverts the daemon's.
55
+ // The proxy's crash handlers must write to the proxy's LOG FILE, not just stderr. The editor
56
+ // swallows stderr, so wiring these to `log` meant a crash was handled and then unrecorded — the
57
+ // failure a user reports as "it disconnected" left nothing behind to read.
58
+ setProxyLogPort(port);
59
+ installProxyResilience(process, proxyLog);
60
+ /**
61
+ * Make sure a daemon is on the port, spawning one if not.
62
+ *
63
+ * Called on first start AND on every reconnect. The proxy used to only retry the socket, so a
64
+ * daemon that exited — crashed, `reticle stop`, or self-shut-down as idle — meant the retries hit
65
+ * a dead port until the budget ran out and the agent's MCP server exited with it. Reticle simply
66
+ * disappeared mid-session with nothing said. Respawning here makes the reconnect self-healing.
67
+ */
68
+ const ensure = async () => {
69
+ // The same question every other surface asks. It used to be a bare TCP connect, so a stranger on
70
+ // the bridge port answered "a daemon is here" — the proxy connected, the stream ended, and each
71
+ // client request woke into the identical non-answer. Rejecting here is what puts the proxy
72
+ // dormant with a reason, instead of pretending the wake succeeded.
73
+ const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
74
+ if (presenceIsUsable(presence))
75
+ return;
76
+ if (PortPresence.FREE !== presence)
77
+ throw new Error(describePresence(presence, port));
78
+ const scriptPath = process.argv[1];
79
+ if (scriptPath === undefined) {
80
+ log('reticle_mcp_no_script', {});
81
+ process.exit(1);
82
+ }
83
+ const daemonArgs = [DAEMON_INNER_COMMAND, PORT_FLAG, String(port)];
84
+ if (driveUrl !== undefined) {
85
+ daemonArgs.push(DRIVE_FLAG, driveUrl);
86
+ if (!headless)
87
+ daemonArgs.push(HEADED_FLAG);
88
+ }
89
+ // Forward the HTTP-verify flags too (previously silently dropped for `reticle mcp`).
90
+ if (http) {
91
+ daemonArgs.push(HTTP_FLAG);
92
+ if (httpPort !== undefined)
93
+ daemonArgs.push(HTTP_PORT_FLAG, String(httpPort));
94
+ if (httpToken !== undefined)
95
+ daemonArgs.push(HTTP_TOKEN_FLAG, httpToken);
96
+ }
97
+ spawnDaemon(process.execPath, scriptPath, daemonArgs, port);
98
+ // Announce the daemon only once the PORT ACCEPTS. This line used to be written the instant the
99
+ // child was spawned, which on a Windows first bootstrap meant `reticle_mcp_daemon_started`
100
+ // followed about ten seconds later by `reticle_mcp_daemon_unavailable` and a first
101
+ // `reticle_sessions` that expired. A readiness signal that precedes readiness is worse than
102
+ // none: a client that believes it stops waiting for the thing that has not happened.
103
+ await waitForDaemon(port);
104
+ log('reticle_mcp_daemon_started', { port, ...(driveUrl !== undefined ? { driveUrl } : {}) });
105
+ };
106
+ // Start the proxy WHATEVER happened to the daemon.
107
+ //
108
+ // This used to exit(1) when `ensure` failed, which is the third way an MCP server disappears on a
109
+ // user: something else is holding the bridge port — a foreign daemon from another project, a
110
+ // half-dead process, a port a colleague's tool grabbed — the spawned daemon cannot bind, and the
111
+ // editor shows a server that failed to start. Nothing about that is unrecoverable: the proxy
112
+ // answers `initialize` itself, serves the cached catalog, and its wake path retries a daemon on
113
+ // every client request. Present-and-complaining beats absent, because absent needs a human.
114
+ // `void`: the chain handles its own failure and the process must not wait on it — the proxy is
115
+ // started from `finally` either way.
116
+ void ensure()
117
+ .catch((err) => {
118
+ log('reticle_mcp_daemon_unavailable', {
119
+ error: err instanceof Error ? err.message : String(err),
120
+ note: 'serving anyway — the next tool call will try to start a daemon again',
121
+ });
122
+ })
123
+ .finally(() => {
124
+ // Also `void`: the proxy runs for the life of the process and is never awaited by anyone.
125
+ //
126
+ // The `.catch` is load-bearing despite that. `startMcpProxy` rejects when its FIRST connect
127
+ // fails — which is precisely the case this block exists to tolerate, a daemon that is not
128
+ // there yet — and with nothing attached that reject became an unhandledRejection. Reported
129
+ // from a win32 user as a crash reading `connect ECONNREFUSED` with an EMPTY frame list,
130
+ // because the stack of a refused socket is entirely node internals and the privacy filter
131
+ // keeps only Reticle frames. So the one path we most want diagnosable arrived as an anonymous
132
+ // crash. The proxy itself is unaffected — it has already installed its stdin reader and goes
133
+ // on serving from cache, waking a daemon on the next request — which is exactly why this
134
+ // must be logged as the expected condition it is rather than reported as a defect.
135
+ void startMcpProxy(port, ensure).catch((err) => {
136
+ log('reticle_mcp_proxy_first_connect_failed', {
137
+ port,
138
+ error: err instanceof Error ? err.message : String(err),
139
+ note: 'serving from cache; the next client request will try to start a daemon',
140
+ });
141
+ });
142
+ });
143
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Well-known Reticle ports other than the one this daemon bound.
3
+ *
4
+ * The remaining half of #261: when the SDK dials 4460 and we listen on 4400, we cannot see the
5
+ * refused inbound — but we CAN notice that a well-known Reticle port other than ours has a
6
+ * listener. That is an observation, not a diagnosis. A neighbour's daemon, another project, or an
7
+ * unrelated process on 4460 all look identical from here, so the sentence that reports it must not
8
+ * conclude that it is the daemon this app wants. Maintainer constraint on the issue: report the
9
+ * observation without the conclusion.
10
+ */
11
+ /**
12
+ * The port the bench fixture's SDK dials.
13
+ *
14
+ * Deliberate, and must not be "fixed" onto the default: `bench/harness/ports.mjs` documents a past
15
+ * incident caused by having more than one source of truth for it. It exists so a default daemon on
16
+ * 4400 and a bench run never collide. Doctor and the no-session diagnosis probe it as a sibling
17
+ * of 4400 for that reason — it is the port the original silent no-connect was on.
18
+ */
19
+ export declare const RETICLE_BENCH_PORT = 4460;
20
+ /** Ports a Reticle daemon is known to occupy. Probe every one except the port we ourselves bound. */
21
+ export declare const WELL_KNOWN_RETICLE_PORTS: readonly number[];
22
+ /** Well-known Reticle ports other than `boundPort`. Empty when we are the only well-known occupant. */
23
+ export declare function siblingPorts(boundPort: number, known?: readonly number[]): readonly number[];
24
+ /**
25
+ * One sentence for a listener on a sibling Reticle port. `undefined` when there is nothing to say.
26
+ *
27
+ * Wording is load-bearing: it names the observation and refuses the conclusion. A test pins that it
28
+ * does not claim the listener is the daemon this app wants, or that the SDK is dialling it.
29
+ */
30
+ export declare function siblingListenerNote(boundPort: number, occupiedSiblings: readonly number[]): string | undefined;
31
+ /**
32
+ * Which well-known siblings currently accept a TCP connection.
33
+ *
34
+ * Pure in the rule, impure at the edge: `isListening` is injected so this can be tested without a
35
+ * socket, and so a lying probe is a separate failure from a lying sentence.
36
+ */
37
+ export declare function findOccupiedSiblings(boundPort: number, isListening: (port: number) => Promise<boolean>): Promise<readonly number[]>;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Well-known Reticle ports other than the one this daemon bound.
3
+ *
4
+ * The remaining half of #261: when the SDK dials 4460 and we listen on 4400, we cannot see the
5
+ * refused inbound — but we CAN notice that a well-known Reticle port other than ours has a
6
+ * listener. That is an observation, not a diagnosis. A neighbour's daemon, another project, or an
7
+ * unrelated process on 4460 all look identical from here, so the sentence that reports it must not
8
+ * conclude that it is the daemon this app wants. Maintainer constraint on the issue: report the
9
+ * observation without the conclusion.
10
+ */
11
+ import { RETICLE_DEFAULT_PORT } from '@reticlehq/core';
12
+ /**
13
+ * The port the bench fixture's SDK dials.
14
+ *
15
+ * Deliberate, and must not be "fixed" onto the default: `bench/harness/ports.mjs` documents a past
16
+ * incident caused by having more than one source of truth for it. It exists so a default daemon on
17
+ * 4400 and a bench run never collide. Doctor and the no-session diagnosis probe it as a sibling
18
+ * of 4400 for that reason — it is the port the original silent no-connect was on.
19
+ */
20
+ export const RETICLE_BENCH_PORT = 4460;
21
+ /** Ports a Reticle daemon is known to occupy. Probe every one except the port we ourselves bound. */
22
+ export const WELL_KNOWN_RETICLE_PORTS = [
23
+ RETICLE_DEFAULT_PORT,
24
+ RETICLE_BENCH_PORT,
25
+ ];
26
+ /** Well-known Reticle ports other than `boundPort`. Empty when we are the only well-known occupant. */
27
+ export function siblingPorts(boundPort, known = WELL_KNOWN_RETICLE_PORTS) {
28
+ return known.filter((port) => port !== boundPort);
29
+ }
30
+ /**
31
+ * One sentence for a listener on a sibling Reticle port. `undefined` when there is nothing to say.
32
+ *
33
+ * Wording is load-bearing: it names the observation and refuses the conclusion. A test pins that it
34
+ * does not claim the listener is the daemon this app wants, or that the SDK is dialling it.
35
+ */
36
+ export function siblingListenerNote(boundPort, occupiedSiblings) {
37
+ if (0 === occupiedSiblings.length)
38
+ return undefined;
39
+ const listed = occupiedSiblings.map((port) => `:${String(port)}`).join(', ');
40
+ return (`something is listening on ${listed}, which this daemon is not` +
41
+ ` (this daemon is on :${String(boundPort)}); that may or may not be related.`);
42
+ }
43
+ /**
44
+ * Which well-known siblings currently accept a TCP connection.
45
+ *
46
+ * Pure in the rule, impure at the edge: `isListening` is injected so this can be tested without a
47
+ * socket, and so a lying probe is a separate failure from a lying sentence.
48
+ */
49
+ export async function findOccupiedSiblings(boundPort, isListening) {
50
+ const occupied = [];
51
+ for (const port of siblingPorts(boundPort)) {
52
+ if (await isListening(port))
53
+ occupied.push(port);
54
+ }
55
+ return occupied;
56
+ }