@reticlehq/server 2.6.0 → 2.8.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 (276) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +251 -33
  3. package/dist/bridge/bridge.d.ts +6 -1
  4. package/dist/bridge/bridge.js +27 -4
  5. package/dist/capsule/causal-summary.d.ts +20 -0
  6. package/dist/capsule/causal-summary.js +31 -6
  7. package/dist/capsule/divergence.js +7 -1
  8. package/dist/cli/chromium-hint.d.ts +68 -0
  9. package/dist/cli/chromium-hint.js +145 -0
  10. package/dist/cli/cli-doctor.js +31 -36
  11. package/dist/cli/cli-kill.d.ts +58 -0
  12. package/dist/cli/cli-kill.js +162 -0
  13. package/dist/cli/cli-launch.d.ts +18 -0
  14. package/dist/cli/cli-launch.js +32 -1
  15. package/dist/cli/cli-parse.d.ts +16 -1
  16. package/dist/cli/cli-parse.js +32 -9
  17. package/dist/cli/cli-port.d.ts +8 -0
  18. package/dist/cli/cli-port.js +36 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/doctor-daemon-line.js +10 -1
  21. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  22. package/dist/cli/doctor-sessions-line.js +66 -0
  23. package/dist/cli/port-holder.d.ts +10 -0
  24. package/dist/cli/port-holder.js +26 -2
  25. package/dist/cli.js +139 -23
  26. package/dist/crawl/crawl-tools.js +12 -2
  27. package/dist/crawl/crawl.d.ts +13 -0
  28. package/dist/crawl/crawl.js +50 -2
  29. package/dist/daemon/daemon-resilience.js +2 -2
  30. package/dist/daemon/loopback-alias.d.ts +32 -0
  31. package/dist/daemon/loopback-alias.js +80 -0
  32. package/dist/events/contradictions.d.ts +14 -0
  33. package/dist/events/contradictions.js +26 -17
  34. package/dist/events/predicate-eval.d.ts +26 -3
  35. package/dist/events/predicate-eval.js +284 -95
  36. package/dist/events/predicate-parse.d.ts +1 -1
  37. package/dist/events/predicate-parse.js +18 -4
  38. package/dist/events/predicate.js +170 -18
  39. package/dist/events/ring-buffer.d.ts +8 -0
  40. package/dist/events/ring-buffer.js +34 -1
  41. package/dist/flows/flow-classify.js +1 -1
  42. package/dist/flows/flow-tools.js +12 -4
  43. package/dist/flows/heal-run.js +1 -1
  44. package/dist/honesty/blind-spots.js +6 -2
  45. package/dist/honesty/honesty.d.ts +11 -0
  46. package/dist/honesty/honesty.js +9 -1
  47. package/dist/honesty/unsettled.d.ts +40 -0
  48. package/dist/honesty/unsettled.js +78 -0
  49. package/dist/honesty/verified.d.ts +7 -0
  50. package/dist/honesty/verified.js +27 -4
  51. package/dist/http-server.js +11 -1
  52. package/dist/index.js +29 -2
  53. package/dist/init/agent-rules.d.ts +16 -0
  54. package/dist/init/agent-rules.js +96 -17
  55. package/dist/init/astro-patch.d.ts +2 -1
  56. package/dist/init/astro-patch.js +11 -5
  57. package/dist/init/capabilities.d.ts +22 -0
  58. package/dist/init/capabilities.js +76 -0
  59. package/dist/init/confirm.d.ts +96 -0
  60. package/dist/init/confirm.js +138 -0
  61. package/dist/init/cra.js +3 -2
  62. package/dist/init/detect.d.ts +8 -0
  63. package/dist/init/detect.js +15 -0
  64. package/dist/init/non-js-project.d.ts +27 -0
  65. package/dist/init/non-js-project.js +84 -0
  66. package/dist/init/plan-framework.d.ts +9 -0
  67. package/dist/init/plan-framework.js +72 -16
  68. package/dist/init/plan.d.ts +40 -4
  69. package/dist/init/plan.js +183 -43
  70. package/dist/init/run.d.ts +24 -2
  71. package/dist/init/run.js +181 -46
  72. package/dist/init/slash-command.d.ts +1 -1
  73. package/dist/init/slash-command.js +46 -6
  74. package/dist/init/snippets.d.ts +64 -5
  75. package/dist/init/snippets.js +220 -31
  76. package/dist/init/vite-config.js +29 -3
  77. package/dist/input/network-mock-tools.d.ts +19 -0
  78. package/dist/input/network-mock-tools.js +29 -4
  79. package/dist/license/license.js +1 -1
  80. package/dist/mcp/mcp-proxy.d.ts +69 -3
  81. package/dist/mcp/mcp-proxy.js +215 -23
  82. package/dist/mcp/mcp.js +58 -7
  83. package/dist/mcp/proxy-lifecycle.d.ts +20 -1
  84. package/dist/mcp/proxy-lifecycle.js +22 -1
  85. package/dist/pool/browser-pool.d.ts +22 -2
  86. package/dist/pool/browser-pool.js +44 -4
  87. package/dist/pool/playwright-launcher.js +10 -2
  88. package/dist/project/learned-routes.d.ts +24 -0
  89. package/dist/project/learned-routes.js +63 -0
  90. package/dist/project/project-store.d.ts +6 -0
  91. package/dist/project/project-store.js +25 -1
  92. package/dist/session/live-control-tools.js +39 -2
  93. package/dist/session/live-control.d.ts +7 -9
  94. package/dist/session/live-control.js +34 -1
  95. package/dist/session/no-session-diagnosis.d.ts +23 -4
  96. package/dist/session/no-session-diagnosis.js +128 -44
  97. package/dist/session/no-session-watch.js +12 -0
  98. package/dist/session/ref-provenance.d.ts +54 -0
  99. package/dist/session/ref-provenance.js +61 -0
  100. package/dist/session/review-store.d.ts +19 -0
  101. package/dist/session/review-store.js +34 -5
  102. package/dist/session/session-lease.js +1 -1
  103. package/dist/session/session-manager.d.ts +9 -0
  104. package/dist/session/session-manager.js +11 -0
  105. package/dist/session/session.d.ts +8 -0
  106. package/dist/session/session.js +12 -0
  107. package/dist/status-payload.d.ts +13 -1
  108. package/dist/status-payload.js +6 -1
  109. package/dist/telemetry/app-instrumented.d.ts +37 -0
  110. package/dist/telemetry/app-instrumented.js +54 -0
  111. package/dist/telemetry/automation-hint.d.ts +17 -0
  112. package/dist/telemetry/automation-hint.js +71 -0
  113. package/dist/telemetry/bug-found.d.ts +0 -20
  114. package/dist/telemetry/bug-found.js +38 -21
  115. package/dist/telemetry/cli-telemetry.js +7 -1
  116. package/dist/telemetry/daemon-telemetry.d.ts +4 -4
  117. package/dist/telemetry/daemon-telemetry.js +8 -4
  118. package/dist/telemetry/feedback.js +1 -1
  119. package/dist/telemetry/init-telemetry.js +9 -1
  120. package/dist/telemetry/install-source.d.ts +36 -0
  121. package/dist/telemetry/install-source.js +40 -0
  122. package/dist/telemetry/run-telemetry.js +4 -1
  123. package/dist/telemetry/session-metrics.d.ts +3 -3
  124. package/dist/telemetry/session-metrics.js +9 -4
  125. package/dist/telemetry/telemetry.d.ts +7 -1
  126. package/dist/telemetry/telemetry.js +17 -2
  127. package/dist/telemetry/tool-refused.d.ts +19 -0
  128. package/dist/telemetry/tool-refused.js +55 -0
  129. package/dist/telemetry/verification-of.js +72 -8
  130. package/dist/temp-dir.d.ts +27 -0
  131. package/dist/temp-dir.js +46 -0
  132. package/dist/tools/act-tools.js +77 -11
  133. package/dist/tools/assert-grade.d.ts +1 -1
  134. package/dist/tools/assert-grade.js +27 -5
  135. package/dist/tools/assert-verdict.js +25 -1
  136. package/dist/tools/contract-tools.js +4 -0
  137. package/dist/tools/dynamic-tools.js +1 -1
  138. package/dist/tools/error-recovery.d.ts +10 -0
  139. package/dist/tools/error-recovery.js +70 -5
  140. package/dist/tools/feedback-invite.js +1 -1
  141. package/dist/tools/invoke-tool.js +149 -32
  142. package/dist/tools/lease-tools.d.ts +29 -1
  143. package/dist/tools/lease-tools.js +58 -5
  144. package/dist/tools/navigate-result.js +1 -1
  145. package/dist/tools/observe-tools.js +1 -1
  146. package/dist/tools/observed-nothing.js +1 -1
  147. package/dist/tools/react-grace.d.ts +48 -0
  148. package/dist/tools/react-grace.js +87 -0
  149. package/dist/tools/read-tools.js +20 -4
  150. package/dist/tools/settle-in-flight.d.ts +79 -0
  151. package/dist/tools/settle-in-flight.js +106 -0
  152. package/dist/tools/snapshot-delta.d.ts +7 -0
  153. package/dist/tools/snapshot-delta.js +130 -6
  154. package/dist/tools/tool-kit.d.ts +52 -5
  155. package/dist/tools/tool-kit.js +55 -24
  156. package/dist/tools/tool-surface.js +28 -9
  157. package/dist/tools/tools.js +38 -4
  158. package/dist/visual/capture-cleanup.d.ts +16 -0
  159. package/dist/visual/capture-cleanup.js +39 -0
  160. package/dist/visual/visual-tools.d.ts +18 -0
  161. package/dist/visual/visual-tools.js +24 -5
  162. package/docs/README.md +56 -11
  163. package/docs/actions.mdx +87 -0
  164. package/docs/agent-cheatsheet.md +102 -26
  165. package/docs/architecture.md +46 -35
  166. package/docs/benchmarks.md +76 -72
  167. package/docs/best-practices.mdx +211 -0
  168. package/docs/capabilities.mdx +387 -0
  169. package/docs/cli/affected.mdx +51 -0
  170. package/docs/cli/capsules.mdx +69 -0
  171. package/docs/cli/cloud.mdx +127 -0
  172. package/docs/cli/doctor.mdx +76 -0
  173. package/docs/cli/drive.mdx +55 -0
  174. package/docs/cli/feedback.mdx +67 -0
  175. package/docs/cli/gate.mdx +64 -0
  176. package/docs/cli/help.mdx +92 -0
  177. package/docs/cli/hunt.mdx +57 -0
  178. package/docs/cli/identify.mdx +62 -0
  179. package/docs/cli/init.mdx +94 -0
  180. package/docs/cli/kill.mdx +91 -0
  181. package/docs/cli/license.mdx +48 -0
  182. package/docs/cli/mcp.mdx +61 -0
  183. package/docs/cli/open.mdx +68 -0
  184. package/docs/cli/restart.mdx +60 -0
  185. package/docs/cli/rollback.mdx +35 -0
  186. package/docs/cli/serve.mdx +62 -0
  187. package/docs/cli/status.mdx +64 -0
  188. package/docs/cli/stop.mdx +56 -0
  189. package/docs/cli/telemetry.mdx +57 -0
  190. package/docs/cli/update.mdx +52 -0
  191. package/docs/cli/verify.mdx +57 -0
  192. package/docs/cli/version.mdx +39 -0
  193. package/docs/cli/watch.mdx +55 -0
  194. package/docs/cli.mdx +219 -0
  195. package/docs/debugging.md +13 -9
  196. package/docs/deploy-checks.md +15 -11
  197. package/docs/desktop-apps.md +49 -40
  198. package/docs/desktop.mdx +188 -0
  199. package/docs/docs.json +363 -0
  200. package/docs/enterprise.md +20 -16
  201. package/docs/faq.mdx +88 -0
  202. package/docs/fixtures.md +11 -7
  203. package/docs/flows.md +26 -22
  204. package/docs/for-agents.md +71 -0
  205. package/docs/frameworks.mdx +199 -0
  206. package/docs/gate-plan.md +45 -41
  207. package/docs/gates.md +23 -19
  208. package/docs/getting-started.md +76 -65
  209. package/docs/human-control.md +22 -18
  210. package/docs/index.mdx +101 -0
  211. package/docs/install-agentic.mdx +215 -0
  212. package/docs/install-manual.mdx +194 -0
  213. package/docs/instrumentation.mdx +232 -0
  214. package/docs/integration-patterns.md +38 -34
  215. package/docs/local-registry.md +16 -12
  216. package/docs/multi-agent-testing.md +16 -12
  217. package/docs/packages/babel-plugin.mdx +70 -0
  218. package/docs/packages/browser.mdx +111 -0
  219. package/docs/packages/core.mdx +106 -0
  220. package/docs/packages/electron.mdx +87 -0
  221. package/docs/packages/eslint-plugin.mdx +84 -0
  222. package/docs/packages/next.mdx +91 -0
  223. package/docs/packages/react.mdx +91 -0
  224. package/docs/packages/server.mdx +98 -0
  225. package/docs/packages/tauri.mdx +63 -0
  226. package/docs/packages/test.mdx +108 -0
  227. package/docs/packages/vite-plugin.mdx +96 -0
  228. package/docs/packages.mdx +217 -0
  229. package/docs/platform-integration.md +33 -26
  230. package/docs/predicates.mdx +179 -0
  231. package/docs/quickstart.mdx +211 -0
  232. package/docs/recipes.mdx +242 -0
  233. package/docs/skill-file.mdx +105 -0
  234. package/docs/state-management.mdx +197 -0
  235. package/docs/style.css +14 -0
  236. package/docs/system-map.md +47 -32
  237. package/docs/telemetry-contract.md +101 -46
  238. package/docs/telemetry-events.mdx +430 -0
  239. package/docs/telemetry.md +63 -50
  240. package/docs/testing.md +10 -6
  241. package/docs/token-efficiency.md +30 -26
  242. package/docs/tools-act-and-wait.mdx +123 -0
  243. package/docs/tools-act-sequence.mdx +120 -0
  244. package/docs/tools-act.mdx +82 -0
  245. package/docs/tools-assert.mdx +70 -0
  246. package/docs/tools-console.mdx +59 -0
  247. package/docs/tools-inspect.mdx +85 -0
  248. package/docs/tools-navigate.mdx +63 -0
  249. package/docs/tools-network.mdx +86 -0
  250. package/docs/tools-observe.mdx +89 -0
  251. package/docs/tools-overview.mdx +157 -0
  252. package/docs/tools-query.mdx +82 -0
  253. package/docs/tools-session-and-feedback.mdx +91 -0
  254. package/docs/tools-sessions.mdx +65 -0
  255. package/docs/tools-snapshot.mdx +75 -0
  256. package/docs/tools-state.mdx +81 -0
  257. package/docs/tools-tools-and-run.mdx +125 -0
  258. package/docs/tools-wait-for.mdx +66 -0
  259. package/docs/troubleshooting.mdx +193 -0
  260. package/docs/usage.md +175 -153
  261. package/docs/vs-chrome-devtools-mcp.mdx +82 -0
  262. package/docs/vs-playwright-mcp.mdx +85 -0
  263. package/docs/vs-screenshots.mdx +96 -0
  264. package/docs/what-is-reticle.mdx +63 -0
  265. package/docs/why-reticle.mdx +107 -0
  266. package/package.json +2 -2
  267. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  268. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  269. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  270. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  271. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  272. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  273. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  274. package/docs/matrix/MATRIX.md +0 -26
  275. package/docs/matrix/README.md +0 -59
  276. package/docs/mint.json +0 -61
@@ -1,3 +1,4 @@
1
+ import { PortPresence } from '../daemon/port-presence.js';
1
2
  /**
2
3
  * CLI launch + status helpers — the daemon-introspection (`reticle status`) and the one-command
3
4
  * "show me the app" flow (`reticle open`). Split out of cli.ts so that file stays under the size cap.
@@ -19,6 +20,7 @@ interface StatusSession {
19
20
  export declare function summarizeStatus(payload: unknown): {
20
21
  sessionCount: number;
21
22
  sessions: StatusSession[];
23
+ why?: string;
22
24
  };
23
25
  /**
24
26
  * Warn when the daemon already on this port is a different version than this CLI, and attach anyway.
@@ -28,6 +30,22 @@ export declare function summarizeStatus(payload: unknown): {
28
30
  * another agent's session on a version bump, which is worse than a loud line.
29
31
  */
30
32
  export declare function warnOnDaemonSkew(port: number): Promise<void>;
33
+ /**
34
+ * Why `reticle drive` cannot have the port, or undefined when it can. Pure.
35
+ *
36
+ * `drive` bound the port unconditionally, and a daemon already listening on it is the NORMAL state
37
+ * once any MCP client has started one. The bind died with a raw `node:net` stack trace: the one
38
+ * failure in this CLI that answered a user with a Node core dump rather than a structured Reticle
39
+ * line naming the port. It is also the command Reticle itself recommends — `reticle_sessions`
40
+ * returns "run `reticle drive <url>`" whenever a tab is throttled, and a throttled tab nearly always
41
+ * coexists with the running daemon that makes the bind impossible.
42
+ *
43
+ * The `reticle_lease` suggestion is deliberately first: it needs no free port, it is what the
44
+ * daemon's own no-session diagnostic already points at, and it works while the collision stands.
45
+ */
46
+ export declare function drivePortConflict(presence: PortPresence, port: number, holder?: {
47
+ ourPid: number | null;
48
+ }): string | undefined;
31
49
  /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
32
50
  export declare function fetchStatus(port: number): Promise<unknown>;
33
51
  /** What `reticle open` should do: reuse an already-connected tab, open a new one, or ask for a url. */
@@ -6,6 +6,7 @@ import { describeSkew, DAEMON_FIX } from '../version/version-skew.js';
6
6
  import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
7
7
  import { SERVER_VERSION } from '../version/server-version.js';
8
8
  import { log } from '../log.js';
9
+ import { PortPresence, describePresence } from '../daemon/port-presence.js';
9
10
  /**
10
11
  * Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
11
12
  * untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
@@ -15,6 +16,9 @@ export function summarizeStatus(payload) {
15
16
  if (typeof payload !== 'object' || null === payload)
16
17
  return { sessionCount: 0, sessions: [] };
17
18
  const obj = payload;
19
+ // Carried through to the printed line: with no sessions this is the whole answer, and dropping it
20
+ // here would silently undo the reason it is on the wire.
21
+ const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
18
22
  const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
19
23
  const sessions = raw
20
24
  .map((s) => {
@@ -34,7 +38,7 @@ export function summarizeStatus(payload) {
34
38
  })
35
39
  .filter((s) => s !== null);
36
40
  const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
37
- return { sessionCount, sessions };
41
+ return { sessionCount, sessions, ...(why === undefined ? {} : { why }) };
38
42
  }
39
43
  /** A string field off the /status body, or undefined on a daemon too old to report it. */
40
44
  function statusField(payload, key) {
@@ -61,6 +65,33 @@ export async function warnOnDaemonSkew(port) {
61
65
  if (skew !== undefined)
62
66
  log('reticle_daemon_skew', { port, warning: skew });
63
67
  }
68
+ /**
69
+ * Why `reticle drive` cannot have the port, or undefined when it can. Pure.
70
+ *
71
+ * `drive` bound the port unconditionally, and a daemon already listening on it is the NORMAL state
72
+ * once any MCP client has started one. The bind died with a raw `node:net` stack trace: the one
73
+ * failure in this CLI that answered a user with a Node core dump rather than a structured Reticle
74
+ * line naming the port. It is also the command Reticle itself recommends — `reticle_sessions`
75
+ * returns "run `reticle drive <url>`" whenever a tab is throttled, and a throttled tab nearly always
76
+ * coexists with the running daemon that makes the bind impossible.
77
+ *
78
+ * The `reticle_lease` suggestion is deliberately first: it needs no free port, it is what the
79
+ * daemon's own no-session diagnostic already points at, and it works while the collision stands.
80
+ */
81
+ export function drivePortConflict(presence, port, holder) {
82
+ if (presence !== PortPresence.DAEMON && presence !== PortPresence.FOREIGN)
83
+ return undefined;
84
+ if (PortPresence.FOREIGN === presence)
85
+ return describePresence(presence, port);
86
+ const pid = holder?.ourPid;
87
+ const who = pid !== null && pid !== undefined
88
+ ? `a Reticle daemon (pid ${String(pid)}) is already serving :${String(port)}`
89
+ : `a Reticle daemon is already serving :${String(port)}`;
90
+ return (`${who}, so \`drive\` cannot bind that port. Use the daemon that is already there — its ` +
91
+ '`reticle_lease` tool opens a browser Reticle drives itself and needs no free port — or stop ' +
92
+ 'it with `reticle stop` and retry, or drive on another port with `RETICLE_PORT=N` (`drive` ' +
93
+ 'takes the port from the environment or .reticle.json, not from a flag).');
94
+ }
64
95
  /** How long the daemon /status probe waits before giving up — a local loopback call is near-instant. */
65
96
  const STATUS_PROBE_TIMEOUT_MS = 1000;
66
97
  /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
@@ -6,7 +6,14 @@
6
6
  */
7
7
  import { type ParsedFeedback } from './cli-parse-feedback.js';
8
8
  export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
9
- export declare const CLI_USAGE = "usage:\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]\n reticle status [--port N]\n reticle doctor [--port N] (diagnose setup: Chromium, daemon, port \u2014 one command)\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 \u2014 for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy \u2014 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 \u2014 status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't \u2014 prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> \"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 \u2014 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 \u2014 batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
9
+ /**
10
+ * The lines below say `reticle <command>`, which is the bin this package installs and is correct
11
+ * once it is on PATH. Copied into `npx`, it is not: `npx reticle` resolves the PACKAGE named
12
+ * `reticle`, which belongs to somebody else, and npx will happily fetch and run it. The docs were
13
+ * telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
14
+ * the usage block rather than being a footnote somewhere else.
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> \"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.";
10
17
  /** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
11
18
  export declare const TelemetryAction: {
12
19
  readonly STATUS: "status";
@@ -52,6 +59,14 @@ export type CliResult = {
52
59
  kind: 'stop';
53
60
  port: number;
54
61
  quiet: boolean;
62
+ } | {
63
+ kind: 'kill';
64
+ port: number;
65
+ force: boolean;
66
+ } | {
67
+ kind: 'restart';
68
+ port: number;
69
+ force: boolean;
55
70
  } | {
56
71
  kind: 'status';
57
72
  port: number;
@@ -5,36 +5,47 @@
5
5
  * imports are unchanged.
6
6
  */
7
7
  import { parseFeedbackArgs } from './cli-parse-feedback.js';
8
+ import { FORCE_FLAG } from './cli-kill.js';
8
9
  // Re-exported so every existing importer of these flags is unaffected by the file split.
9
10
  export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
10
- export const CLI_USAGE = `usage:
11
+ /**
12
+ * The lines below say `reticle <command>`, which is the bin this package installs and is correct
13
+ * once it is on PATH. Copied into `npx`, it is not: `npx reticle` resolves the PACKAGE named
14
+ * `reticle`, which belongs to somebody else, and npx will happily fetch and run it. The docs were
15
+ * telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
16
+ * the usage block rather than being a footnote somewhere else.
17
+ */
18
+ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticle <command>\` once the bin is on your PATH)
19
+
11
20
  reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>] (wire Reticle into the project in this directory)
12
21
  --app picks WHICH app in a monorepo, when several are found
13
22
  --no-mcp skips MORE than the server registration: also the agent rule files
14
23
  (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three
15
24
  only make sense once the tools are reachable.
16
25
  reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]
17
- reticle stop [--port N] [--quiet]
26
+ reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)
27
+ reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)
28
+ reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)
18
29
  reticle status [--port N]
19
- reticle doctor [--port N] (diagnose setup: Chromium, daemon, port — one command)
30
+ reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)
20
31
  reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)
21
32
  reticle verify <url> [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)
22
33
  reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)
23
34
  reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)
24
35
  reticle watch [url] (on save, report which saved flows must re-verify)
25
- reticle drive <url> [--headless] (foreground mode — for debugging)
26
- reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy — auto-starts daemon if needed)
36
+ reticle drive <url> [--headless] (foreground mode, for debugging)
37
+ reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)
27
38
  reticle update (install the latest server version and restart)
28
39
  reticle rollback (restore the previous server version and restart)
29
40
  reticle license (show enterprise license status: active | eval | missing)
30
- reticle telemetry [status|enable|disable] (anonymous usage metrics — status shows what's sent + the policy)
31
- reticle feedback [--rating 1-5] [--bug] "message" (tell us what worked and what didn't — prints exactly what it sends)
41
+ reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)
42
+ reticle feedback [--rating 1-5] [--bug] "message" (tell us what worked and what didn't; prints exactly what it sends)
32
43
  reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "message"
33
44
  (agents: file from anywhere, including a setup that never finished)
34
45
  reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]
35
46
  (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)
36
47
 
37
- Cloud (link this project to Reticle Cloud — runs/flows recorded on the dashboard):
48
+ Cloud (link this project to Reticle Cloud; runs/flows recorded on the dashboard):
38
49
  reticle login --email <e> [--code <c>] [--org <n>] (sign in: mails a code, then exchanges it)
39
50
  reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)
40
51
  reticle whoami (who am I signed in as, and is this repo attached?)
@@ -44,10 +55,12 @@ Cloud (link this project to Reticle Cloud — runs/flows recorded on the dashboa
44
55
  reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)
45
56
 
46
57
  'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay
47
- and the spec runner — batch work). --headed opts any of them in, --headless hides drive, CI hides it.`;
58
+ and the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.`;
48
59
  const INIT_COMMAND = 'init';
49
60
  const SERVE_COMMAND = 'serve';
50
61
  const STOP_COMMAND = 'stop';
62
+ const KILL_COMMAND = 'kill';
63
+ const RESTART_COMMAND = 'restart';
51
64
  const STATUS_COMMAND = 'status';
52
65
  const OPEN_COMMAND = 'open';
53
66
  const DRIVE_COMMAND = 'drive';
@@ -90,6 +103,8 @@ const KNOWN_COMMANDS = new Set([
90
103
  INIT_COMMAND,
91
104
  SERVE_COMMAND,
92
105
  STOP_COMMAND,
106
+ KILL_COMMAND,
107
+ RESTART_COMMAND,
93
108
  STATUS_COMMAND,
94
109
  OPEN_COMMAND,
95
110
  DRIVE_COMMAND,
@@ -463,6 +478,14 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
463
478
  const quiet = rest.includes(QUIET_FLAG);
464
479
  return { kind: 'stop', port, quiet };
465
480
  }
481
+ case KILL_COMMAND: {
482
+ const port = parsePortFlag(rest, defaultPort);
483
+ return { kind: 'kill', port, force: rest.includes(FORCE_FLAG) };
484
+ }
485
+ case RESTART_COMMAND: {
486
+ const port = parsePortFlag(rest, defaultPort);
487
+ return { kind: 'restart', port, force: rest.includes(FORCE_FLAG) };
488
+ }
466
489
  case STATUS_COMMAND: {
467
490
  const port = parsePortFlag(rest, defaultPort);
468
491
  return { kind: 'status', port };
@@ -44,3 +44,11 @@ export declare function readJournalEnabled(cwd: string, env: string | undefined)
44
44
  * Pass `portFlag` when the user explicitly supplied --port; pass `undefined` to fall through.
45
45
  */
46
46
  export declare function resolvePort(portFlag: number | undefined, envPort: number | undefined, projectPort: number | undefined, defaultPort: number): number;
47
+ /**
48
+ * Detect a port mismatch between the daemon and the project's SDK configuration.
49
+ *
50
+ * Returns a diagnostic string when the daemon port differs from what `.reticle.json` declares.
51
+ * The SDK reads `.reticle.json` at build/connect time, so a mismatch means the app will dial a
52
+ * port nothing is listening on — the silent no-connect that #261 documents.
53
+ */
54
+ export declare function diagnosePortMismatch(daemonPort: number, projectPort: number | undefined): string | undefined;
@@ -20,7 +20,26 @@ import { readFileSync } from 'node:fs';
20
20
  * the confusion got manufactured in the first place.
21
21
  */
22
22
  export const DEV_SERVER_PORTS = new Set([
23
- 3000, 3001, 4200, 4321, 5000, 5173, 5174, 8000, 8080, 8100, 9000,
23
+ 3000,
24
+ 3001,
25
+ 4200,
26
+ 4321,
27
+ 5000,
28
+ 5173,
29
+ 5174,
30
+ 8000,
31
+ 8080,
32
+ 8100,
33
+ 9000,
34
+ // Added after the scan reported "nothing is listening" at people whose app was plainly running.
35
+ // A narrow list is not just unhelpful, it is CONFIDENTLY wrong — the message states absence as a
36
+ // fact — so the defaults of the frameworks agents actually meet belong in it.
37
+ 1420, // Tauri
38
+ 3100, // Next, second instance
39
+ 4173, // vite preview
40
+ 5175, // Vite, third instance
41
+ 7860, // Gradio
42
+ 8501, // Streamlit
24
43
  ]);
25
44
  export function isLikelyDevServerPort(port) {
26
45
  return DEV_SERVER_PORTS.has(port);
@@ -101,3 +120,19 @@ export function readJournalEnabled(cwd, env) {
101
120
  export function resolvePort(portFlag, envPort, projectPort, defaultPort) {
102
121
  return portFlag ?? envPort ?? projectPort ?? defaultPort;
103
122
  }
123
+ /**
124
+ * Detect a port mismatch between the daemon and the project's SDK configuration.
125
+ *
126
+ * Returns a diagnostic string when the daemon port differs from what `.reticle.json` declares.
127
+ * The SDK reads `.reticle.json` at build/connect time, so a mismatch means the app will dial a
128
+ * port nothing is listening on — the silent no-connect that #261 documents.
129
+ */
130
+ export function diagnosePortMismatch(daemonPort, projectPort) {
131
+ if (projectPort === undefined)
132
+ return undefined;
133
+ if (projectPort === daemonPort)
134
+ return undefined;
135
+ return (`.reticle.json says "port": ${String(projectPort)} but this daemon is on :${String(daemonPort)}` +
136
+ ` — the SDK will dial :${String(projectPort)} and never connect. ` +
137
+ `Either start the daemon with --port ${String(projectPort)}, or update .reticle.json to match.`);
138
+ }
@@ -30,10 +30,10 @@ const DEFAULT_SESSION_TIMEOUT_MS = 15_000;
30
30
  const SESSION_POLL_MS = 250;
31
31
  const DEFAULT_PROJECT_NAME = 'app';
32
32
  const VERIFY_AGENT_ID = 'reticle-cli';
33
- const MSG_NO_SESSION = 'No app connected — Reticle drove the URL but no @reticlehq/browser session dialed back.\n' +
33
+ const MSG_NO_SESSION = 'No app connected: Reticle drove the URL but no @reticlehq/browser session dialed back.\n' +
34
34
  ' Make sure the SDK is in the build and reticle.connect() runs on the preview page' +
35
35
  ' (for a non-localhost preview: allowNonLocalhost + a pairing token).';
36
- const MSG_NO_FLOWS = 'No saved flows to verify (.reticle/flows is empty) — refusing to report a pass for verifying nothing.\n' +
36
+ const MSG_NO_FLOWS = 'No saved flows to verify (.reticle/flows is empty), so refusing to report a pass for verifying nothing.\n' +
37
37
  ' Flows are recorded interactively by an agent (reticle_record{action:"start"} → act → reticle_flow_save via the\n' +
38
38
  ' MCP tools), then committed to .reticle/flows/. In CI, check those files in and re-run `reticle verify`.';
39
39
  const MSG_VERIFY_PREFIX = 'verify failed: ';
@@ -14,7 +14,16 @@
14
14
  * at the call site.
15
15
  */
16
16
  import { describeSkew } from '../version/version-skew.js';
17
- const FIX = 'Restart it so both ends are the same build: `reticle stop`, then let your agent reconnect.';
17
+ /**
18
+ * `reticle stop`, not `reticle kill`.
19
+ *
20
+ * The first version of this said `reticle kill`, which this CLI does not dispatch — it is a proposal
21
+ * (#114), described as a gap in docs/system-map.md, and I read it there as if it shipped. A remedy
22
+ * naming a command that errors is a second dead end handed to someone already stuck, on the one
23
+ * command they run when they are confused.
24
+ */
25
+ const FIX = 'Restart it so both ends are the same build: `reticle stop`, then let your agent reconnect (it ' +
26
+ 'spawns a fresh daemon on the next tool call).';
18
27
  /**
19
28
  * Build the daemon line for a port that answered `/status`.
20
29
  *
@@ -0,0 +1,12 @@
1
+ /** What this reads off `/status`. Everything is optional — an older daemon may send none of it. */
2
+ export interface SessionFacts {
3
+ sessionCount?: unknown;
4
+ why?: unknown;
5
+ }
6
+ export interface SessionsLine {
7
+ /** The `sessions` row itself. */
8
+ text: string;
9
+ /** The diagnosis to print underneath, when there is one and it is not contradicted by a session. */
10
+ why?: string;
11
+ }
12
+ export declare function sessionsLine(facts: SessionFacts): SessionsLine;
@@ -0,0 +1,66 @@
1
+ import { SELF_RECOVERING_MARKER } from '../session/no-session-diagnosis.js';
2
+ /**
3
+ * The line `doctor` was missing: is anything actually CONNECTED?
4
+ *
5
+ * `doctor` collapses the first-run failure modes into one command, and it checked every one of them
6
+ * except the one the funnel actually stalls on. A user who has installed the SDK, wired the plugin
7
+ * and started the daemon runs `doctor`, gets node, chromium, daemon, bridge port and desktop wiring
8
+ * all ✓, and is no better off — because the thing that is wrong is that no page has ever dialled in,
9
+ * and nothing on that screen mentions pages.
10
+ *
11
+ * The daemon has always known. `/status` carries `sessionCount`, the session list, and `why` — the
12
+ * same no-session diagnosis an agent gets from an empty `reticle_sessions`, which is careful about
13
+ * what it can and cannot prove. `reticle status` prints it. `doctor` fetched the identical payload
14
+ * to read `version` and `contract` off it and dropped the rest on the floor.
15
+ *
16
+ * So this invents no new diagnosis. It surfaces one that already exists, on the command a stuck human
17
+ * runs, which is the difference between a checklist that ends in "everything is fine" and one that
18
+ * ends in a next action.
19
+ *
20
+ * Pure and payload-shaped rather than reaching for the bridge: `doctor` runs in a separate process
21
+ * from the daemon and can only ever know what `/status` told it.
22
+ */
23
+ /**
24
+ * The diagnosis is written for an AGENT, and `doctor` has a human in front of it.
25
+ *
26
+ * Every branch of it ends by telling the reader to call `reticle_sessions` again, which is right for
27
+ * the caller it was written for and useless to somebody in a shell: an MCP tool is not something you
28
+ * can type. Printing it unchanged would make `doctor` end on a recovery its reader cannot perform,
29
+ * which is the same defect as the aged-out lease message that told an agent to ask a human to reopen
30
+ * a tab that never existed.
31
+ *
32
+ * So the last sentence is swapped for the shell equivalent of itself. Only that sentence — the rest
33
+ * of the diagnosis is about the app and reads the same to either audience.
34
+ */
35
+ const HUMAN_RETRY = 'Then run `reticle doctor` again — it will appear within a second of the page loading.';
36
+ function forAHuman(why) {
37
+ // Imported, not re-typed: a copy of a sentence that has to match another file's byte-for-byte is a
38
+ // drift bug with a delay on it, and the failure is silent — the swap simply stops happening and
39
+ // doctor goes back to telling a human to call an MCP tool.
40
+ return why.replace(SELF_RECOVERING_MARKER, HUMAN_RETRY);
41
+ }
42
+ function countOf(facts) {
43
+ const raw = facts.sessionCount;
44
+ return 'number' === typeof raw && Number.isFinite(raw) && raw >= 0 ? raw : undefined;
45
+ }
46
+ export function sessionsLine(facts) {
47
+ const count = countOf(facts);
48
+ if (count === undefined) {
49
+ // An older daemon that does not report it. Saying "0 connected" here would be a claim about the
50
+ // app made from a payload that never mentioned the app — the exact kind of confident wrong
51
+ // sentence the no-session diagnosis exists to avoid.
52
+ return { text: ' sessions ? this daemon did not report connected pages (older build)' };
53
+ }
54
+ if (count > 0) {
55
+ const plural = 1 === count ? 'page' : 'pages';
56
+ return { text: ` sessions ✓ ${String(count)} ${plural} connected` };
57
+ }
58
+ const why = 'string' === typeof facts.why && facts.why.length > 0 ? forAHuman(facts.why) : undefined;
59
+ return {
60
+ // Deliberately ✗ rather than a neutral note. Zero connected pages is why somebody is running
61
+ // doctor, and a ✓-coloured checklist that hides the one failing thing is how the old output
62
+ // convinced people their setup was fine.
63
+ text: ' sessions ✗ no page has connected to this daemon',
64
+ ...(why === undefined ? {} : { why }),
65
+ };
66
+ }
@@ -17,6 +17,16 @@ export interface PortHolder {
17
17
  pid: number;
18
18
  command: string;
19
19
  }
20
+ /**
21
+ * Run a lookup and return its stdout, or null if it could not run.
22
+ *
23
+ * Null on ANY failure — no lsof (every Windows user, a slim container), a non-zero exit, a throw.
24
+ * This is a diagnostic nicety inside the commands people run when things are already broken; it must
25
+ * never be the reason one of them fails. Every caller handles the null by saying it could not
26
+ * identify the holder, rather than asserting the holder is not ours — a claim it has no evidence
27
+ * for, and one that was wrong for every Windows user.
28
+ */
29
+ export declare function captureLookup(command: string, args: readonly string[]): string | null;
20
30
  /**
21
31
  * The FIRST listener in `lsof -nP -iTCP:<port> -sTCP:LISTEN -F pc` output, or null.
22
32
  *
@@ -13,6 +13,27 @@
13
13
  * The parser is pure and the lookup takes an injected exec, so the rule is tested on every platform
14
14
  * while the shelling out stays at the call site.
15
15
  */
16
+ import { spawnSync } from 'node:child_process';
17
+ /** Bound on the holder lookup. A hung `lsof` must not hang the commands that diagnose hangs. */
18
+ const LOOKUP_TIMEOUT_MS = 2_000;
19
+ /**
20
+ * Run a lookup and return its stdout, or null if it could not run.
21
+ *
22
+ * Null on ANY failure — no lsof (every Windows user, a slim container), a non-zero exit, a throw.
23
+ * This is a diagnostic nicety inside the commands people run when things are already broken; it must
24
+ * never be the reason one of them fails. Every caller handles the null by saying it could not
25
+ * identify the holder, rather than asserting the holder is not ours — a claim it has no evidence
26
+ * for, and one that was wrong for every Windows user.
27
+ */
28
+ export function captureLookup(command, args) {
29
+ try {
30
+ const result = spawnSync(command, [...args], { encoding: 'utf8', timeout: LOOKUP_TIMEOUT_MS });
31
+ return 0 === result.status && 'string' === typeof result.stdout ? result.stdout : null;
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
16
37
  /** `lsof -F pc` emits one field per line, prefixed by its selector: `p<pid>`, `c<command>`. */
17
38
  const PID_FIELD = 'p';
18
39
  const COMMAND_FIELD = 'c';
@@ -59,8 +80,11 @@ export function describeForeignHolder(port, holder,
59
80
  * stranger, and the fix that works (`reticle stop`, it respawns on the next call) is never named.
60
81
  */
61
82
  ourPid) {
62
- const tail = 'a daemon cannot bind it. Stop that process, or run Reticle on a different port ' +
63
- '(`--port`), then retry.';
83
+ // Naming the command matters more than naming the process. "Stop that process" describes an
84
+ // outcome and leaves the reader to invent a route to it, and the route they invent is the `-ti`
85
+ // pipeline that takes their agent's proxy down with the holder.
86
+ const tail = 'a daemon cannot bind it. Free it with `reticle kill --force` (it signals the listener only, ' +
87
+ "never the agent's mcp proxy), or run Reticle on a different port (`--port`), then retry.";
64
88
  if (null === holder) {
65
89
  /**
66
90
  * We could not identify the holder — no `lsof` (every Windows user, and 35% of them by the