@reticlehq/server 2.7.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 (236) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +64 -29
  3. package/dist/capsule/causal-summary.d.ts +20 -0
  4. package/dist/capsule/causal-summary.js +31 -6
  5. package/dist/capsule/divergence.js +7 -1
  6. package/dist/cli/chromium-hint.d.ts +33 -5
  7. package/dist/cli/chromium-hint.js +82 -12
  8. package/dist/cli/cli-doctor.js +28 -25
  9. package/dist/cli/cli-kill.d.ts +58 -0
  10. package/dist/cli/cli-kill.js +162 -0
  11. package/dist/cli/cli-launch.d.ts +17 -0
  12. package/dist/cli/cli-launch.js +28 -0
  13. package/dist/cli/cli-parse.d.ts +16 -1
  14. package/dist/cli/cli-parse.js +32 -9
  15. package/dist/cli/cli-port.d.ts +8 -0
  16. package/dist/cli/cli-port.js +16 -0
  17. package/dist/cli/cli-verify.js +2 -2
  18. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  19. package/dist/cli/doctor-sessions-line.js +66 -0
  20. package/dist/cli/port-holder.d.ts +10 -0
  21. package/dist/cli/port-holder.js +26 -2
  22. package/dist/cli.js +99 -14
  23. package/dist/crawl/crawl-tools.js +12 -2
  24. package/dist/crawl/crawl.d.ts +13 -0
  25. package/dist/crawl/crawl.js +50 -2
  26. package/dist/events/contradictions.d.ts +14 -0
  27. package/dist/events/contradictions.js +26 -17
  28. package/dist/events/predicate-eval.d.ts +20 -1
  29. package/dist/events/predicate-eval.js +125 -3
  30. package/dist/events/predicate.js +170 -18
  31. package/dist/flows/flow-classify.js +1 -1
  32. package/dist/flows/flow-tools.js +12 -4
  33. package/dist/flows/heal-run.js +1 -1
  34. package/dist/honesty/blind-spots.js +6 -2
  35. package/dist/honesty/unsettled.d.ts +40 -0
  36. package/dist/honesty/unsettled.js +78 -0
  37. package/dist/honesty/verified.d.ts +7 -0
  38. package/dist/honesty/verified.js +3 -3
  39. package/dist/index.js +3 -0
  40. package/dist/init/agent-rules.d.ts +16 -0
  41. package/dist/init/agent-rules.js +96 -22
  42. package/dist/init/astro-patch.d.ts +2 -1
  43. package/dist/init/astro-patch.js +11 -5
  44. package/dist/init/capabilities.d.ts +22 -0
  45. package/dist/init/capabilities.js +76 -0
  46. package/dist/init/confirm.d.ts +96 -0
  47. package/dist/init/confirm.js +138 -0
  48. package/dist/init/cra.js +3 -2
  49. package/dist/init/non-js-project.js +29 -5
  50. package/dist/init/plan-framework.js +53 -15
  51. package/dist/init/plan.d.ts +40 -4
  52. package/dist/init/plan.js +163 -40
  53. package/dist/init/run.d.ts +24 -2
  54. package/dist/init/run.js +148 -42
  55. package/dist/init/slash-command.d.ts +1 -1
  56. package/dist/init/slash-command.js +46 -6
  57. package/dist/init/snippets.d.ts +44 -4
  58. package/dist/init/snippets.js +161 -37
  59. package/dist/init/vite-config.js +29 -3
  60. package/dist/input/network-mock-tools.d.ts +19 -0
  61. package/dist/input/network-mock-tools.js +29 -4
  62. package/dist/license/license.js +1 -1
  63. package/dist/mcp/mcp-proxy.d.ts +69 -3
  64. package/dist/mcp/mcp-proxy.js +205 -20
  65. package/dist/mcp/mcp.js +23 -7
  66. package/dist/mcp/proxy-lifecycle.d.ts +1 -1
  67. package/dist/mcp/proxy-lifecycle.js +1 -1
  68. package/dist/pool/browser-pool.d.ts +22 -2
  69. package/dist/pool/browser-pool.js +44 -4
  70. package/dist/project/learned-routes.d.ts +24 -0
  71. package/dist/project/learned-routes.js +63 -0
  72. package/dist/project/project-store.d.ts +6 -0
  73. package/dist/project/project-store.js +25 -1
  74. package/dist/session/no-session-diagnosis.d.ts +19 -0
  75. package/dist/session/no-session-diagnosis.js +71 -18
  76. package/dist/session/no-session-watch.js +12 -0
  77. package/dist/session/ref-provenance.d.ts +54 -0
  78. package/dist/session/ref-provenance.js +61 -0
  79. package/dist/session/session-lease.js +1 -1
  80. package/dist/telemetry/automation-hint.d.ts +17 -0
  81. package/dist/telemetry/automation-hint.js +71 -0
  82. package/dist/telemetry/bug-found.d.ts +0 -20
  83. package/dist/telemetry/bug-found.js +38 -21
  84. package/dist/telemetry/cli-telemetry.js +7 -1
  85. package/dist/telemetry/feedback.js +1 -1
  86. package/dist/telemetry/init-telemetry.js +9 -1
  87. package/dist/telemetry/install-source.d.ts +36 -0
  88. package/dist/telemetry/install-source.js +40 -0
  89. package/dist/telemetry/run-telemetry.js +4 -1
  90. package/dist/telemetry/session-metrics.js +5 -0
  91. package/dist/telemetry/telemetry.d.ts +5 -1
  92. package/dist/telemetry/telemetry.js +15 -2
  93. package/dist/telemetry/tool-refused.d.ts +19 -0
  94. package/dist/telemetry/tool-refused.js +55 -0
  95. package/dist/telemetry/verification-of.js +46 -7
  96. package/dist/temp-dir.d.ts +27 -0
  97. package/dist/temp-dir.js +46 -0
  98. package/dist/tools/act-tools.js +45 -1
  99. package/dist/tools/assert-grade.d.ts +1 -1
  100. package/dist/tools/assert-grade.js +27 -5
  101. package/dist/tools/assert-verdict.js +18 -1
  102. package/dist/tools/contract-tools.js +4 -0
  103. package/dist/tools/dynamic-tools.js +1 -1
  104. package/dist/tools/error-recovery.d.ts +10 -0
  105. package/dist/tools/error-recovery.js +64 -4
  106. package/dist/tools/feedback-invite.js +1 -1
  107. package/dist/tools/invoke-tool.js +139 -23
  108. package/dist/tools/lease-tools.d.ts +29 -1
  109. package/dist/tools/lease-tools.js +58 -5
  110. package/dist/tools/navigate-result.js +1 -1
  111. package/dist/tools/observe-tools.js +1 -1
  112. package/dist/tools/observed-nothing.js +1 -1
  113. package/dist/tools/react-grace.d.ts +48 -0
  114. package/dist/tools/react-grace.js +87 -0
  115. package/dist/tools/read-tools.js +20 -4
  116. package/dist/tools/settle-in-flight.d.ts +79 -0
  117. package/dist/tools/settle-in-flight.js +106 -0
  118. package/dist/tools/snapshot-delta.d.ts +7 -0
  119. package/dist/tools/snapshot-delta.js +130 -6
  120. package/dist/tools/tool-surface.js +19 -0
  121. package/dist/tools/tools.js +16 -4
  122. package/docs/README.md +56 -11
  123. package/docs/actions.mdx +87 -0
  124. package/docs/agent-cheatsheet.md +93 -29
  125. package/docs/architecture.md +46 -35
  126. package/docs/benchmarks.md +76 -72
  127. package/docs/best-practices.mdx +211 -0
  128. package/docs/capabilities.mdx +387 -0
  129. package/docs/cli/affected.mdx +51 -0
  130. package/docs/cli/capsules.mdx +69 -0
  131. package/docs/cli/cloud.mdx +127 -0
  132. package/docs/cli/doctor.mdx +76 -0
  133. package/docs/cli/drive.mdx +55 -0
  134. package/docs/cli/feedback.mdx +67 -0
  135. package/docs/cli/gate.mdx +64 -0
  136. package/docs/cli/help.mdx +92 -0
  137. package/docs/cli/hunt.mdx +57 -0
  138. package/docs/cli/identify.mdx +62 -0
  139. package/docs/cli/init.mdx +94 -0
  140. package/docs/cli/kill.mdx +91 -0
  141. package/docs/cli/license.mdx +48 -0
  142. package/docs/cli/mcp.mdx +61 -0
  143. package/docs/cli/open.mdx +68 -0
  144. package/docs/cli/restart.mdx +60 -0
  145. package/docs/cli/rollback.mdx +35 -0
  146. package/docs/cli/serve.mdx +62 -0
  147. package/docs/cli/status.mdx +64 -0
  148. package/docs/cli/stop.mdx +56 -0
  149. package/docs/cli/telemetry.mdx +57 -0
  150. package/docs/cli/update.mdx +52 -0
  151. package/docs/cli/verify.mdx +57 -0
  152. package/docs/cli/version.mdx +39 -0
  153. package/docs/cli/watch.mdx +55 -0
  154. package/docs/cli.mdx +219 -0
  155. package/docs/debugging.md +13 -9
  156. package/docs/deploy-checks.md +15 -11
  157. package/docs/desktop-apps.md +49 -40
  158. package/docs/desktop.mdx +188 -0
  159. package/docs/docs.json +363 -0
  160. package/docs/enterprise.md +20 -16
  161. package/docs/faq.mdx +88 -0
  162. package/docs/fixtures.md +11 -7
  163. package/docs/flows.md +26 -22
  164. package/docs/for-agents.md +71 -0
  165. package/docs/frameworks.mdx +199 -0
  166. package/docs/gate-plan.md +45 -41
  167. package/docs/gates.md +22 -18
  168. package/docs/getting-started.md +76 -65
  169. package/docs/human-control.md +22 -18
  170. package/docs/index.mdx +101 -0
  171. package/docs/install-agentic.mdx +215 -0
  172. package/docs/install-manual.mdx +194 -0
  173. package/docs/instrumentation.mdx +232 -0
  174. package/docs/integration-patterns.md +38 -34
  175. package/docs/local-registry.md +16 -12
  176. package/docs/multi-agent-testing.md +16 -12
  177. package/docs/packages/babel-plugin.mdx +70 -0
  178. package/docs/packages/browser.mdx +111 -0
  179. package/docs/packages/core.mdx +106 -0
  180. package/docs/packages/electron.mdx +87 -0
  181. package/docs/packages/eslint-plugin.mdx +84 -0
  182. package/docs/packages/next.mdx +91 -0
  183. package/docs/packages/react.mdx +91 -0
  184. package/docs/packages/server.mdx +98 -0
  185. package/docs/packages/tauri.mdx +63 -0
  186. package/docs/packages/test.mdx +108 -0
  187. package/docs/packages/vite-plugin.mdx +96 -0
  188. package/docs/packages.mdx +217 -0
  189. package/docs/platform-integration.md +33 -26
  190. package/docs/predicates.mdx +179 -0
  191. package/docs/quickstart.mdx +211 -0
  192. package/docs/recipes.mdx +242 -0
  193. package/docs/skill-file.mdx +105 -0
  194. package/docs/state-management.mdx +197 -0
  195. package/docs/style.css +14 -0
  196. package/docs/system-map.md +47 -32
  197. package/docs/telemetry-contract.md +93 -50
  198. package/docs/telemetry-events.mdx +430 -0
  199. package/docs/telemetry.md +57 -52
  200. package/docs/testing.md +10 -6
  201. package/docs/token-efficiency.md +30 -26
  202. package/docs/tools-act-and-wait.mdx +123 -0
  203. package/docs/tools-act-sequence.mdx +120 -0
  204. package/docs/tools-act.mdx +82 -0
  205. package/docs/tools-assert.mdx +70 -0
  206. package/docs/tools-console.mdx +59 -0
  207. package/docs/tools-inspect.mdx +85 -0
  208. package/docs/tools-navigate.mdx +63 -0
  209. package/docs/tools-network.mdx +86 -0
  210. package/docs/tools-observe.mdx +89 -0
  211. package/docs/tools-overview.mdx +157 -0
  212. package/docs/tools-query.mdx +82 -0
  213. package/docs/tools-session-and-feedback.mdx +91 -0
  214. package/docs/tools-sessions.mdx +65 -0
  215. package/docs/tools-snapshot.mdx +75 -0
  216. package/docs/tools-state.mdx +81 -0
  217. package/docs/tools-tools-and-run.mdx +125 -0
  218. package/docs/tools-wait-for.mdx +66 -0
  219. package/docs/troubleshooting.mdx +193 -0
  220. package/docs/usage.md +175 -153
  221. package/docs/vs-chrome-devtools-mcp.mdx +82 -0
  222. package/docs/vs-playwright-mcp.mdx +85 -0
  223. package/docs/vs-screenshots.mdx +96 -0
  224. package/docs/what-is-reticle.mdx +63 -0
  225. package/docs/why-reticle.mdx +107 -0
  226. package/package.json +2 -2
  227. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  228. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  229. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  230. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  231. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  232. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  233. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  234. package/docs/matrix/MATRIX.md +0 -26
  235. package/docs/matrix/README.md +0 -59
  236. package/docs/mint.json +0 -61
@@ -6,37 +6,18 @@ import { PortPresence, probePresence } from '../daemon/port-presence.js';
6
6
  import { probeDaemon } from '../mcp/mcp-proxy.js';
7
7
  import { fetchStatus } from './cli-launch.js';
8
8
  import { daemonLine } from './doctor-daemon-line.js';
9
- import { describeForeignHolder, findPortHolder } from './port-holder.js';
9
+ import { sessionsLine } from './doctor-sessions-line.js';
10
+ import { captureLookup, describeForeignHolder, findPortHolder } from './port-holder.js';
10
11
  import { chromiumHint, probeChromium } from './chromium-hint.js';
11
- import { spawnSync } from 'node:child_process';
12
12
  import { SERVER_VERSION } from '../version/server-version.js';
13
13
  import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
14
14
  import { diagnoseDesktop, isDesktopProject } from '../init/desktop-doctor.js';
15
+ import { diagnosePortMismatch, readProjectPort } from './cli-port.js';
15
16
  /**
16
17
  * `reticle doctor` — collapse the ~6 independent first-run failure modes into one command. Checks the
17
18
  * Chromium install (the #1 silent failure), whether a daemon is up on the resolved bridge port, and
18
19
  * reminds the user which port the app must dial. Human-readable to stdout (not the JSON log).
19
20
  */
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 command people run when things are already broken; it must
25
- * never be the reason `doctor` fails. `describeForeignHolder` handles the null by saying it could
26
- * not identify the holder and naming our recorded pid, rather than asserting the holder is not ours
27
- * — a claim it has no evidence for, and one that was wrong for every Windows user.
28
- */
29
- function runCapture(command, args) {
30
- try {
31
- const result = spawnSync(command, [...args], { encoding: 'utf8', timeout: LOOKUP_TIMEOUT_MS });
32
- return 0 === result.status && 'string' === typeof result.stdout ? result.stdout : null;
33
- }
34
- catch {
35
- return null;
36
- }
37
- }
38
- /** Bound on the holder lookup. A hung `lsof` must not hang the command that diagnoses hangs. */
39
- const LOOKUP_TIMEOUT_MS = 2_000;
40
21
  /** Narrow the `/status` payload to the two fields the daemon line reads. */
41
22
  function asIdentity(payload) {
42
23
  if (typeof payload !== 'object' || null === payload)
@@ -61,6 +42,9 @@ export async function handleDoctor(port) {
61
42
  };
62
43
  line('reticle doctor');
63
44
  line(` node ${process.version}`);
45
+ // Filled in only on the daemon branch — there is nothing to ask when no daemon is answering, and
46
+ // the branches below already say so in their own terms.
47
+ let sessions;
64
48
  line(` chromium ${chromiumHint(await probeChromium())}`);
65
49
  // Ask the PORT, not just the pid file. "not running on :4400" has been printed about a port that
66
50
  // was demonstrably occupied, which sends the reader to start a daemon that cannot bind. The three
@@ -71,7 +55,8 @@ export async function handleDoctor(port) {
71
55
  // Name WHICH daemon. The /status payload already carries version + contract, and doctor was
72
56
  // throwing both away — while skew is invisible everywhere else, reaching the agent as a bare
73
57
  // -32000 naming no version. This is the command a human runs at exactly that moment.
74
- const status = asIdentity(await fetchStatus(port));
58
+ const payload = await fetchStatus(port);
59
+ const status = asIdentity(payload);
75
60
  const built = daemonLine(port, pid, status, {
76
61
  version: SERVER_VERSION,
77
62
  contract: CONTRACT_FINGERPRINT,
@@ -79,6 +64,10 @@ export async function handleDoctor(port) {
79
64
  line(built.text);
80
65
  if (built.skew !== undefined)
81
66
  line(` version ✗ ${built.skew}`);
67
+ // The same payload already carries whether anything has CONNECTED, and doctor was reading
68
+ // `version` off it and dropping the rest. Without this the command is silent on the one step the
69
+ // funnel stalls on: wired correctly, daemon up, and no page has ever dialled in.
70
+ sessions = sessionsLine('object' === typeof payload && null !== payload ? payload : {});
82
71
  }
83
72
  else if (presence === PortPresence.FOREIGN) {
84
73
  // Name the holder when we can. `doctor` exists for exactly this moment, and "another process"
@@ -86,18 +75,32 @@ export async function handleDoctor(port) {
86
75
  // pipeline that also kills the agent's own MCP proxy.
87
76
  // `pid` is what our pid file recorded for this port. When it matches the process actually
88
77
  // holding it, this is our OWN daemon wedged, not a stranger — and the fix is different.
89
- line(` daemon ✗ ${describeForeignHolder(port, findPortHolder(port, runCapture), pid)}`);
78
+ line(` daemon ✗ ${describeForeignHolder(port, findPortHolder(port, captureLookup), pid)}`);
90
79
  }
91
80
  else {
92
81
  line(` daemon ✗ not running on :${port} — your agent runs \`reticle mcp\` (or \`reticle serve\`)`);
93
82
  }
94
- line(` bridge port ${port} (your app must dial THIS port — not your dev-server port)`);
83
+ if (sessions !== undefined)
84
+ line(sessions.text);
85
+ line(` bridge port ${port} (your app must dial THIS port, not your dev-server port)`);
86
+ const projectPort = readProjectPort(process.cwd());
87
+ const mismatch = diagnosePortMismatch(port, projectPort);
88
+ if (mismatch !== undefined)
89
+ line(` port check ✗ ${mismatch}`);
95
90
  // Where to LOOK when something is wrong. The daemon has always written a structured log here and
96
91
  // nothing ever said so, so the first move in every investigation was reading source instead of
97
92
  // reading the log. `RETICLE_TRACE=1` turns the same stream into a per-stage trace — see
98
93
  // docs/debugging.md.
99
94
  line(` daemon log ${join(reticleStateHome(), `daemon-${String(port)}.log`)}`);
100
95
  line(` tracing ${ReticleEnv.TRACE}=1 on the daemon for per-stage timings in that log`);
96
+ // Below the checklist rather than inline: it is a paragraph, and a paragraph in the middle of a
97
+ // column of one-line checks buries the checks under it. This is the daemon's own no-session
98
+ // diagnosis, the same one an agent gets from an empty reticle_sessions.
99
+ if (sessions?.why !== undefined) {
100
+ line('');
101
+ line(' nothing has connected. What the daemon can tell from here:');
102
+ line(` ${sessions.why}`);
103
+ }
101
104
  // Desktop setup RCA. Every one of these fails SILENTLY — a Tauri app with the default CSP runs
102
105
  // perfectly and never connects; an Electron app without the preload line reports zero network
103
106
  // activity forever, which reads as "makes no backend calls" rather than "you are blind to them".
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `reticle kill` — free the bridge port without taking the agent's transport with it.
3
+ *
4
+ * The obvious incantation, `lsof -ti tcp:4400 | xargs kill -9`, also kills `reticle mcp`: the proxy
5
+ * holds a CLIENT socket on that port, `-ti` does not filter to listeners, and so the one command
6
+ * people reach for when the bridge is stuck destroys the channel they were trying to fix. What
7
+ * follows is worse than an error — the client gets no reply at all, and nothing reaches the proxy log
8
+ * because the process that writes it is the one that died. That is the mechanism behind most
9
+ * "my MCP disconnected" reports (#114, #110).
10
+ *
11
+ * The safe form was written down, and a documented incantation is a worse answer than a command: it
12
+ * has to be found, remembered, and typed correctly at the moment someone is already stuck. So this is
13
+ * the command, and it signals exactly one pid — the LISTENER.
14
+ *
15
+ * It is not a rename of `stop`. `stop` reads the pid file, which is the right source when the daemon
16
+ * we started is the daemon that is wedged, and no source at all when the pid file is stale, was never
17
+ * written, or belongs to a daemon from another checkout. `kill` asks the PORT, which is the thing the
18
+ * user actually wants freed, and falls back to the pid file only where the lookup cannot run.
19
+ */
20
+ import { type PortHolder } from './port-holder.js';
21
+ /** What the plan says to do with the port. */
22
+ export declare const KillAction: {
23
+ readonly NOTHING: "nothing";
24
+ readonly KILL: "kill";
25
+ readonly REFUSE: "refuse";
26
+ };
27
+ export type KillAction = (typeof KillAction)[keyof typeof KillAction];
28
+ export interface KillPlan {
29
+ action: KillAction;
30
+ /** The single pid to signal. Present only for `kill`. */
31
+ pid?: number;
32
+ /** True when the target was not identifiable as Reticle's and `--force` overrode the refusal. */
33
+ forced: boolean;
34
+ /** False when the listener lookup could not run, so the pid came from our own records instead. */
35
+ identifiedListener: boolean;
36
+ /** Why we will not act, for `refuse`. */
37
+ reason?: string;
38
+ holder?: PortHolder;
39
+ }
40
+ export declare const FORCE_FLAG = "--force";
41
+ /**
42
+ * Decide what to signal. Pure.
43
+ *
44
+ * `answersStatus` is the strongest evidence available and deliberately outranks the pid file: a
45
+ * daemon started from another checkout owns the port, is a Reticle daemon, and appears nowhere in our
46
+ * records. Refusing it would leave the port held by exactly the process this command exists to clear.
47
+ */
48
+ export declare function planKill(input: {
49
+ /** The listener on the port, from a `-sTCP:LISTEN` lookup. Null when there is none, or no `lsof`. */
50
+ listener: PortHolder | null;
51
+ /** The pid our records hold for this port, or null when there is none or it is dead. */
52
+ recordedPid: number | null;
53
+ /** Whether the port answers `/status`, i.e. whatever holds it is a Reticle daemon. */
54
+ answersStatus: boolean;
55
+ force: boolean;
56
+ }): KillPlan;
57
+ /** `reticle kill` — free the port. Resolves to whether the port is now free. */
58
+ export declare function runKill(port: number, force: boolean): Promise<boolean>;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * `reticle kill` — free the bridge port without taking the agent's transport with it.
3
+ *
4
+ * The obvious incantation, `lsof -ti tcp:4400 | xargs kill -9`, also kills `reticle mcp`: the proxy
5
+ * holds a CLIENT socket on that port, `-ti` does not filter to listeners, and so the one command
6
+ * people reach for when the bridge is stuck destroys the channel they were trying to fix. What
7
+ * follows is worse than an error — the client gets no reply at all, and nothing reaches the proxy log
8
+ * because the process that writes it is the one that died. That is the mechanism behind most
9
+ * "my MCP disconnected" reports (#114, #110).
10
+ *
11
+ * The safe form was written down, and a documented incantation is a worse answer than a command: it
12
+ * has to be found, remembered, and typed correctly at the moment someone is already stuck. So this is
13
+ * the command, and it signals exactly one pid — the LISTENER.
14
+ *
15
+ * It is not a rename of `stop`. `stop` reads the pid file, which is the right source when the daemon
16
+ * we started is the daemon that is wedged, and no source at all when the pid file is stale, was never
17
+ * written, or belongs to a daemon from another checkout. `kill` asks the PORT, which is the thing the
18
+ * user actually wants freed, and falls back to the pid file only where the lookup cannot run.
19
+ */
20
+ import { PortPresence, probePresence } from '../daemon/port-presence.js';
21
+ import { isAlive, readPid, removePid } from '../daemon/daemon.js';
22
+ import { probeDaemon } from '../mcp/mcp-proxy.js';
23
+ import { log } from '../log.js';
24
+ import { fetchStatus } from './cli-launch.js';
25
+ import { captureLookup, findPortHolder } from './port-holder.js';
26
+ /** What the plan says to do with the port. */
27
+ export const KillAction = {
28
+ NOTHING: 'nothing',
29
+ KILL: 'kill',
30
+ REFUSE: 'refuse',
31
+ };
32
+ export const FORCE_FLAG = '--force';
33
+ /**
34
+ * Decide what to signal. Pure.
35
+ *
36
+ * `answersStatus` is the strongest evidence available and deliberately outranks the pid file: a
37
+ * daemon started from another checkout owns the port, is a Reticle daemon, and appears nowhere in our
38
+ * records. Refusing it would leave the port held by exactly the process this command exists to clear.
39
+ */
40
+ export function planKill(input) {
41
+ const { listener, recordedPid, answersStatus, force } = input;
42
+ if (null === listener) {
43
+ // No listener identified. On Windows and in slim containers that means the lookup could not run
44
+ // rather than that the port is free, so the recorded pid is the best target we have — and the
45
+ // report says which of the two it is rather than implying a lookup we never made.
46
+ if (null === recordedPid)
47
+ return { action: KillAction.NOTHING, forced: false, identifiedListener: false };
48
+ return { action: KillAction.KILL, pid: recordedPid, forced: false, identifiedListener: false };
49
+ }
50
+ const ours = answersStatus || listener.pid === recordedPid;
51
+ if (ours || force) {
52
+ return {
53
+ action: KillAction.KILL,
54
+ pid: listener.pid,
55
+ forced: !ours,
56
+ identifiedListener: true,
57
+ holder: listener,
58
+ };
59
+ }
60
+ return {
61
+ action: KillAction.REFUSE,
62
+ forced: false,
63
+ identifiedListener: true,
64
+ holder: listener,
65
+ reason: `pid ${String(listener.pid)} ("${listener.command}") is listening, and it is not a Reticle ` +
66
+ `daemon — it never answered /status and it is not the pid Reticle recorded. Killing it would ` +
67
+ `be killing someone else's process. Run it again with ${FORCE_FLAG} if that is what you ` +
68
+ `meant, or start Reticle on another port.`,
69
+ };
70
+ }
71
+ /** How long the target gets to exit on SIGTERM before we stop asking politely. */
72
+ const GRACEFUL_KILL_MS = 5_000;
73
+ /** How long after SIGKILL before we accept the process is not ours to kill and say so. */
74
+ const FORCED_KILL_MS = 2_000;
75
+ const KILL_POLL_MS = 100;
76
+ /**
77
+ * Signal ONE pid and wait for it to go: SIGTERM, then SIGKILL, then give up and say it survived.
78
+ *
79
+ * The escalation is the reason this exists rather than a bare SIGTERM — there is a field report of a
80
+ * daemon that ignored SIGTERM until a human ran `kill -9` by hand, which is the moment the `lsof -ti`
81
+ * pipeline gets typed and the agent's proxy dies with the daemon.
82
+ */
83
+ async function terminate(pid) {
84
+ const signal = (name) => {
85
+ try {
86
+ process.kill(pid, name);
87
+ }
88
+ catch {
89
+ // Gone between the liveness check and here. The next poll reports it as gone.
90
+ }
91
+ };
92
+ signal('SIGTERM');
93
+ const started = Date.now();
94
+ let escalated = false;
95
+ for (;;) {
96
+ await new Promise((resolve) => setTimeout(resolve, KILL_POLL_MS));
97
+ if (!isAlive(pid))
98
+ return { gone: true, escalated };
99
+ const waited = Date.now() - started;
100
+ if (!escalated && waited > GRACEFUL_KILL_MS) {
101
+ escalated = true;
102
+ signal('SIGKILL');
103
+ continue;
104
+ }
105
+ if (waited > GRACEFUL_KILL_MS + FORCED_KILL_MS)
106
+ return { gone: false, escalated };
107
+ }
108
+ }
109
+ /**
110
+ * What was NOT touched, said out loud on every kill.
111
+ *
112
+ * The command exists because of a pipeline that killed more than the reader intended and never told
113
+ * them, so a report that lists only the casualty repeats half the mistake. An `reticle mcp` proxy on
114
+ * this port survives, and its own recovery from a dead daemon is good: it goes dormant and restarts
115
+ * one on the next request.
116
+ */
117
+ const PROXY_SPARED_NOTE = 'only the listener was signalled. Any `reticle mcp` proxy on this port holds a client connection, ' +
118
+ 'not the port, and was left alone — it goes dormant and starts a fresh daemon on the next tool call.';
119
+ /** `reticle kill` — free the port. Resolves to whether the port is now free. */
120
+ export async function runKill(port, force) {
121
+ const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
122
+ const recorded = readPid(port);
123
+ const plan = planKill({
124
+ listener: findPortHolder(port, captureLookup),
125
+ recordedPid: null !== recorded && isAlive(recorded) ? recorded : null,
126
+ answersStatus: presence === PortPresence.DAEMON,
127
+ force,
128
+ });
129
+ if (KillAction.NOTHING === plan.action) {
130
+ removePid(port);
131
+ log('reticle_kill_nothing_to_do', { port, presence });
132
+ return true;
133
+ }
134
+ if (KillAction.REFUSE === plan.action) {
135
+ log('reticle_kill_refused', { port, reason: plan.reason, holder: plan.holder?.command });
136
+ return false;
137
+ }
138
+ const pid = plan.pid;
139
+ if (pid === undefined) {
140
+ // Unreachable by construction (planKill never returns KILL without a pid, and a test pins that),
141
+ // but the alternative to this branch is a non-null assertion, which this codebase does not use.
142
+ log('reticle_kill_nothing_to_do', { port, presence });
143
+ return true;
144
+ }
145
+ const { gone, escalated } = await terminate(pid);
146
+ if (gone)
147
+ removePid(port);
148
+ log(gone ? 'reticle_killed' : 'reticle_kill_survived', {
149
+ port,
150
+ pid,
151
+ escalated,
152
+ ...(plan.forced ? { forced: true } : {}),
153
+ // Say where the pid came from. Without `lsof` this is the pid we recorded, not the pid we
154
+ // observed listening, and those are different claims.
155
+ listenerIdentified: plan.identifiedListener,
156
+ note: gone
157
+ ? PROXY_SPARED_NOTE
158
+ : `pid ${String(pid)} survived SIGKILL, so it is not yours to kill (permissions) or is an ` +
159
+ `unkillable zombie. The port is still held. ${PROXY_SPARED_NOTE}`,
160
+ });
161
+ return gone;
162
+ }
@@ -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.
@@ -29,6 +30,22 @@ export declare function summarizeStatus(payload: unknown): {
29
30
  * another agent's session on a version bump, which is worse than a loud line.
30
31
  */
31
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;
32
49
  /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
33
50
  export declare function fetchStatus(port: number): Promise<unknown>;
34
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
@@ -64,6 +65,33 @@ export async function warnOnDaemonSkew(port) {
64
65
  if (skew !== undefined)
65
66
  log('reticle_daemon_skew', { port, warning: skew });
66
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
+ }
67
95
  /** How long the daemon /status probe waits before giving up — a local loopback call is near-instant. */
68
96
  const STATUS_PROBE_TIMEOUT_MS = 1000;
69
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;
@@ -120,3 +120,19 @@ export function readJournalEnabled(cwd, env) {
120
120
  export function resolvePort(portFlag, envPort, projectPort, defaultPort) {
121
121
  return portFlag ?? envPort ?? projectPort ?? defaultPort;
122
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: ';
@@ -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;