@reticlehq/server 2.7.0 → 2.9.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 (357) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +130 -778
  3. package/dist/bridge/bridge.d.ts +23 -0
  4. package/dist/bridge/bridge.js +80 -2
  5. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  6. package/dist/bridge/bridge.test-harness.js +6 -4
  7. package/dist/capsule/causal-summary.d.ts +36 -1
  8. package/dist/capsule/causal-summary.js +33 -7
  9. package/dist/capsule/divergence.js +7 -1
  10. package/dist/cli/chromium-hint.d.ts +33 -5
  11. package/dist/cli/chromium-hint.js +82 -12
  12. package/dist/cli/cli-doctor.js +65 -34
  13. package/dist/cli/cli-flow-commands.js +8 -8
  14. package/dist/cli/cli-kill.d.ts +58 -0
  15. package/dist/cli/cli-kill.js +162 -0
  16. package/dist/cli/cli-parse.d.ts +16 -1
  17. package/dist/cli/cli-parse.js +32 -9
  18. package/dist/cli/cli-port.d.ts +32 -0
  19. package/dist/cli/cli-port.js +82 -35
  20. package/dist/cli/cli-verify.d.ts +21 -0
  21. package/dist/cli/cli-verify.js +47 -3
  22. package/dist/cli/config-discovery.d.ts +35 -0
  23. package/dist/cli/config-discovery.js +163 -0
  24. package/dist/cli/doctor-daemon-line.js +4 -1
  25. package/dist/cli/doctor-project-line.d.ts +37 -0
  26. package/dist/cli/doctor-project-line.js +35 -0
  27. package/dist/cli/doctor-rows.d.ts +51 -0
  28. package/dist/cli/doctor-rows.js +54 -0
  29. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  30. package/dist/cli/doctor-sessions-line.js +69 -0
  31. package/dist/cli/drive-attach.d.ts +99 -0
  32. package/dist/cli/drive-attach.js +203 -0
  33. package/dist/cli/drive-command.d.ts +13 -0
  34. package/dist/cli/drive-command.js +103 -0
  35. package/dist/cli/open-note.d.ts +20 -0
  36. package/dist/cli/open-note.js +37 -0
  37. package/dist/cli/port-holder.d.ts +10 -0
  38. package/dist/cli/port-holder.js +26 -2
  39. package/dist/cli/status-next-action.d.ts +37 -0
  40. package/dist/cli/status-next-action.js +61 -0
  41. package/dist/cli.js +119 -39
  42. package/dist/crawl/crawl-tools.js +12 -2
  43. package/dist/crawl/crawl.d.ts +13 -0
  44. package/dist/crawl/crawl.js +50 -2
  45. package/dist/events/contradictions.d.ts +46 -0
  46. package/dist/events/contradictions.js +35 -22
  47. package/dist/events/declared.d.ts +45 -0
  48. package/dist/events/declared.js +73 -0
  49. package/dist/events/echo-mismatch.d.ts +8 -2
  50. package/dist/events/echo-mismatch.js +33 -5
  51. package/dist/events/event-filters.d.ts +1 -1
  52. package/dist/events/event-filters.js +7 -4
  53. package/dist/events/predicate-eval.d.ts +17 -72
  54. package/dist/events/predicate-eval.js +117 -236
  55. package/dist/events/predicate-parse.js +31 -3
  56. package/dist/events/predicate-route.d.ts +32 -0
  57. package/dist/events/predicate-route.js +102 -0
  58. package/dist/events/predicate-schema.d.ts +99 -0
  59. package/dist/events/predicate-schema.js +309 -0
  60. package/dist/events/predicate.d.ts +8 -0
  61. package/dist/events/predicate.js +296 -22
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-classify.js +1 -1
  69. package/dist/flows/flow-replay-run.js +8 -1
  70. package/dist/flows/flow-tools.js +21 -5
  71. package/dist/flows/heal-run.js +1 -1
  72. package/dist/flows/replay.js +3 -0
  73. package/dist/honesty/accepted-write.d.ts +3 -0
  74. package/dist/honesty/accepted-write.js +8 -3
  75. package/dist/honesty/already-true.js +15 -0
  76. package/dist/honesty/blind-spots.js +10 -2
  77. package/dist/honesty/honesty.d.ts +11 -0
  78. package/dist/honesty/honesty.js +1 -0
  79. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  80. package/dist/honesty/uncaptured-bodies.js +9 -2
  81. package/dist/honesty/unread-outcome.d.ts +8 -0
  82. package/dist/honesty/unread-outcome.js +36 -9
  83. package/dist/honesty/unsettled.d.ts +49 -0
  84. package/dist/honesty/unsettled.js +82 -0
  85. package/dist/honesty/verified.d.ts +36 -3
  86. package/dist/honesty/verified.js +54 -11
  87. package/dist/http-server.d.ts +19 -0
  88. package/dist/http-server.js +202 -4
  89. package/dist/impact/impact-recorder.d.ts +23 -0
  90. package/dist/impact/impact-recorder.js +76 -0
  91. package/dist/impact/impact-store.d.ts +47 -0
  92. package/dist/impact/impact-store.js +171 -0
  93. package/dist/index.d.ts +7 -0
  94. package/dist/index.js +47 -10
  95. package/dist/init/agent-rules.d.ts +36 -0
  96. package/dist/init/agent-rules.js +132 -22
  97. package/dist/init/astro-patch.d.ts +2 -1
  98. package/dist/init/astro-patch.js +38 -6
  99. package/dist/init/capabilities.d.ts +22 -0
  100. package/dist/init/capabilities.js +76 -0
  101. package/dist/init/confirm.d.ts +114 -0
  102. package/dist/init/confirm.js +165 -0
  103. package/dist/init/connect-steps.d.ts +2 -0
  104. package/dist/init/connect-steps.js +50 -0
  105. package/dist/init/cra.js +3 -2
  106. package/dist/init/csp-check.d.ts +38 -0
  107. package/dist/init/csp-check.js +92 -0
  108. package/dist/init/csp-doctor.d.ts +25 -0
  109. package/dist/init/csp-doctor.js +49 -0
  110. package/dist/init/cursor.js +6 -9
  111. package/dist/init/detect.d.ts +8 -0
  112. package/dist/init/detect.js +33 -3
  113. package/dist/init/mcp-clients.d.ts +1 -0
  114. package/dist/init/mcp-clients.js +51 -7
  115. package/dist/init/mcp.d.ts +15 -0
  116. package/dist/init/mcp.js +19 -0
  117. package/dist/init/non-js-project.js +29 -5
  118. package/dist/init/plan-framework.d.ts +10 -0
  119. package/dist/init/plan-framework.js +78 -16
  120. package/dist/init/plan.d.ts +47 -6
  121. package/dist/init/plan.js +228 -69
  122. package/dist/init/run.d.ts +24 -2
  123. package/dist/init/run.js +157 -45
  124. package/dist/init/slash-command.d.ts +1 -1
  125. package/dist/init/slash-command.js +54 -6
  126. package/dist/init/snippets.d.ts +45 -5
  127. package/dist/init/snippets.js +194 -42
  128. package/dist/init/vite-config.js +53 -5
  129. package/dist/input/network-mock-tools.d.ts +19 -0
  130. package/dist/input/network-mock-tools.js +37 -5
  131. package/dist/input/scroll-find.d.ts +6 -0
  132. package/dist/input/scroll-find.js +17 -2
  133. package/dist/input/scroll-tools.js +4 -0
  134. package/dist/journal/attach-journal.d.ts +9 -1
  135. package/dist/journal/attach-journal.js +13 -2
  136. package/dist/journal/journal-query.d.ts +13 -0
  137. package/dist/journal/journal-query.js +28 -3
  138. package/dist/journal/workspace-gitignore.d.ts +10 -0
  139. package/dist/journal/workspace-gitignore.js +59 -0
  140. package/dist/license/license.js +1 -1
  141. package/dist/mcp/mcp-proxy.d.ts +35 -30
  142. package/dist/mcp/mcp-proxy.js +306 -104
  143. package/dist/mcp/mcp.d.ts +10 -1
  144. package/dist/mcp/mcp.js +70 -34
  145. package/dist/mcp/proxy-handshake.d.ts +82 -1
  146. package/dist/mcp/proxy-handshake.js +109 -2
  147. package/dist/mcp/proxy-lifecycle.d.ts +11 -2
  148. package/dist/mcp/proxy-lifecycle.js +12 -3
  149. package/dist/mcp/proxy-log.d.ts +63 -0
  150. package/dist/mcp/proxy-log.js +132 -0
  151. package/dist/mcp/server-instructions.d.ts +17 -0
  152. package/dist/mcp/server-instructions.js +56 -0
  153. package/dist/mcp/shared-params.d.ts +27 -0
  154. package/dist/mcp/shared-params.js +36 -0
  155. package/dist/pool/browser-pool.d.ts +22 -2
  156. package/dist/pool/browser-pool.js +44 -4
  157. package/dist/project/learned-routes.d.ts +24 -0
  158. package/dist/project/learned-routes.js +63 -0
  159. package/dist/project/project-store.d.ts +6 -0
  160. package/dist/project/project-store.js +25 -1
  161. package/dist/project/reticle-dir.d.ts +2 -0
  162. package/dist/project/reticle-dir.js +1 -0
  163. package/dist/session/connection-memory.d.ts +59 -0
  164. package/dist/session/connection-memory.js +108 -0
  165. package/dist/session/dev-command.d.ts +26 -0
  166. package/dist/session/dev-command.js +89 -0
  167. package/dist/session/dev-server-probe.d.ts +68 -0
  168. package/dist/session/dev-server-probe.js +102 -7
  169. package/dist/session/last-act.d.ts +26 -0
  170. package/dist/session/last-act.js +35 -1
  171. package/dist/session/live-control-tools.js +42 -5
  172. package/dist/session/no-session-diagnosis.d.ts +67 -2
  173. package/dist/session/no-session-diagnosis.js +236 -27
  174. package/dist/session/no-session-next-action.d.ts +46 -0
  175. package/dist/session/no-session-next-action.js +85 -0
  176. package/dist/session/no-session-watch.d.ts +40 -1
  177. package/dist/session/no-session-watch.js +174 -8
  178. package/dist/session/pending-commands.d.ts +12 -0
  179. package/dist/session/pending-commands.js +13 -1
  180. package/dist/session/ref-provenance.d.ts +54 -0
  181. package/dist/session/ref-provenance.js +61 -0
  182. package/dist/session/session-lease.js +1 -1
  183. package/dist/session/session-manager.d.ts +4 -0
  184. package/dist/session/session-manager.js +35 -5
  185. package/dist/session/session.d.ts +10 -1
  186. package/dist/session/session.js +146 -5
  187. package/dist/telemetry/app-instrumented.d.ts +8 -0
  188. package/dist/telemetry/app-instrumented.js +10 -0
  189. package/dist/telemetry/automation-hint.d.ts +17 -0
  190. package/dist/telemetry/automation-hint.js +71 -0
  191. package/dist/telemetry/bug-found.d.ts +15 -17
  192. package/dist/telemetry/bug-found.js +52 -21
  193. package/dist/telemetry/cli-telemetry.js +7 -1
  194. package/dist/telemetry/daemon-telemetry.js +24 -0
  195. package/dist/telemetry/feedback.js +1 -1
  196. package/dist/telemetry/init-telemetry.js +9 -1
  197. package/dist/telemetry/install-source.d.ts +56 -0
  198. package/dist/telemetry/install-source.js +74 -0
  199. package/dist/telemetry/instrumentation-stall.d.ts +50 -0
  200. package/dist/telemetry/instrumentation-stall.js +90 -0
  201. package/dist/telemetry/run-telemetry.js +4 -1
  202. package/dist/telemetry/session-metrics.d.ts +8 -0
  203. package/dist/telemetry/session-metrics.js +15 -0
  204. package/dist/telemetry/telemetry.d.ts +7 -1
  205. package/dist/telemetry/telemetry.js +26 -2
  206. package/dist/telemetry/tool-refused.d.ts +19 -0
  207. package/dist/telemetry/tool-refused.js +55 -0
  208. package/dist/telemetry/verification-of.js +46 -7
  209. package/dist/temp-dir.d.ts +27 -0
  210. package/dist/temp-dir.js +46 -0
  211. package/dist/tools/act-tools.js +209 -28
  212. package/dist/tools/assert-grade.d.ts +1 -1
  213. package/dist/tools/assert-grade.js +27 -5
  214. package/dist/tools/assert-verdict.js +49 -7
  215. package/dist/tools/browser-tools.js +10 -0
  216. package/dist/tools/contract-tools.js +4 -0
  217. package/dist/tools/dynamic-tools.js +16 -7
  218. package/dist/tools/error-recovery.d.ts +10 -0
  219. package/dist/tools/error-recovery.js +73 -9
  220. package/dist/tools/feedback-invite.js +1 -1
  221. package/dist/tools/invoke-tool.js +159 -26
  222. package/dist/tools/lease-hint.d.ts +30 -8
  223. package/dist/tools/lease-hint.js +65 -14
  224. package/dist/tools/lease-tools.d.ts +40 -1
  225. package/dist/tools/lease-tools.js +181 -83
  226. package/dist/tools/navigate-result.js +1 -1
  227. package/dist/tools/observe-tools.js +10 -4
  228. package/dist/tools/observed-nothing.js +1 -1
  229. package/dist/tools/react-grace.d.ts +52 -0
  230. package/dist/tools/react-grace.js +90 -0
  231. package/dist/tools/read-tools.js +67 -5
  232. package/dist/tools/resolve-target.d.ts +33 -0
  233. package/dist/tools/resolve-target.js +53 -0
  234. package/dist/tools/settle-in-flight.d.ts +94 -0
  235. package/dist/tools/settle-in-flight.js +146 -0
  236. package/dist/tools/snapshot-delta.d.ts +10 -0
  237. package/dist/tools/snapshot-delta.js +194 -9
  238. package/dist/tools/tool-kit.d.ts +8 -0
  239. package/dist/tools/tool-surface.d.ts +60 -0
  240. package/dist/tools/tool-surface.js +86 -1
  241. package/dist/tools/tools.js +61 -8
  242. package/dist/tools/unadvertised-help.js +9 -6
  243. package/docs/README.md +55 -11
  244. package/docs/actions.mdx +120 -0
  245. package/docs/agent-cheatsheet.md +95 -29
  246. package/docs/architecture.md +52 -35
  247. package/docs/benchmarks.md +127 -75
  248. package/docs/best-practices.mdx +216 -0
  249. package/docs/capabilities.mdx +534 -0
  250. package/docs/cli/affected.mdx +53 -0
  251. package/docs/cli/capsules.mdx +70 -0
  252. package/docs/cli/cloud.mdx +153 -0
  253. package/docs/cli/doctor.mdx +109 -0
  254. package/docs/cli/drive.mdx +74 -0
  255. package/docs/cli/feedback.mdx +67 -0
  256. package/docs/cli/gate.mdx +64 -0
  257. package/docs/cli/help.mdx +94 -0
  258. package/docs/cli/hunt.mdx +73 -0
  259. package/docs/cli/identify.mdx +76 -0
  260. package/docs/cli/init.mdx +121 -0
  261. package/docs/cli/kill.mdx +91 -0
  262. package/docs/cli/license.mdx +48 -0
  263. package/docs/cli/mcp.mdx +61 -0
  264. package/docs/cli/open.mdx +74 -0
  265. package/docs/cli/restart.mdx +60 -0
  266. package/docs/cli/rollback.mdx +35 -0
  267. package/docs/cli/serve.mdx +69 -0
  268. package/docs/cli/status.mdx +88 -0
  269. package/docs/cli/stop.mdx +64 -0
  270. package/docs/cli/telemetry.mdx +57 -0
  271. package/docs/cli/update.mdx +52 -0
  272. package/docs/cli/verify.mdx +76 -0
  273. package/docs/cli/version.mdx +39 -0
  274. package/docs/cli/watch.mdx +62 -0
  275. package/docs/cli.mdx +241 -0
  276. package/docs/debugging.md +16 -10
  277. package/docs/deploy-checks.md +45 -12
  278. package/docs/desktop.mdx +384 -0
  279. package/docs/docs.json +364 -0
  280. package/docs/enterprise.md +22 -16
  281. package/docs/faq.mdx +102 -0
  282. package/docs/fixtures.md +13 -7
  283. package/docs/flows.md +29 -23
  284. package/docs/for-agents.md +71 -0
  285. package/docs/frameworks.mdx +238 -0
  286. package/docs/gate-plan.md +48 -42
  287. package/docs/gates.md +28 -22
  288. package/docs/getting-started.md +129 -77
  289. package/docs/human-control.md +24 -18
  290. package/docs/index.mdx +143 -0
  291. package/docs/install-agentic.mdx +224 -0
  292. package/docs/install-manual.mdx +196 -0
  293. package/docs/instrumentation.mdx +238 -0
  294. package/docs/integration-patterns.md +41 -35
  295. package/docs/local-registry.md +19 -13
  296. package/docs/multi-agent-testing.md +19 -13
  297. package/docs/packages/babel-plugin.mdx +79 -0
  298. package/docs/packages/browser.mdx +111 -0
  299. package/docs/packages/core.mdx +116 -0
  300. package/docs/packages/electron.mdx +85 -0
  301. package/docs/packages/eslint-plugin.mdx +86 -0
  302. package/docs/packages/next.mdx +89 -0
  303. package/docs/packages/react.mdx +91 -0
  304. package/docs/packages/server.mdx +98 -0
  305. package/docs/packages/tauri.mdx +63 -0
  306. package/docs/packages/test.mdx +119 -0
  307. package/docs/packages/vite-plugin.mdx +94 -0
  308. package/docs/packages.mdx +218 -0
  309. package/docs/platform-integration.md +35 -26
  310. package/docs/predicates.mdx +248 -0
  311. package/docs/quickstart.mdx +257 -0
  312. package/docs/recipes.mdx +282 -0
  313. package/docs/skill-file.mdx +107 -0
  314. package/docs/state-management.mdx +199 -0
  315. package/docs/style.css +14 -0
  316. package/docs/system-map.md +50 -33
  317. package/docs/telemetry-contract.md +106 -52
  318. package/docs/telemetry-events.mdx +475 -0
  319. package/docs/telemetry.md +63 -55
  320. package/docs/testing.md +44 -6
  321. package/docs/token-efficiency.md +37 -27
  322. package/docs/tools-act-and-wait.mdx +173 -0
  323. package/docs/tools-act-sequence.mdx +137 -0
  324. package/docs/tools-act.mdx +95 -0
  325. package/docs/tools-assert.mdx +129 -0
  326. package/docs/tools-console.mdx +63 -0
  327. package/docs/tools-inspect.mdx +88 -0
  328. package/docs/tools-navigate.mdx +99 -0
  329. package/docs/tools-network.mdx +119 -0
  330. package/docs/tools-observe.mdx +145 -0
  331. package/docs/tools-overview.mdx +160 -0
  332. package/docs/tools-query.mdx +126 -0
  333. package/docs/tools-session-and-feedback.mdx +112 -0
  334. package/docs/tools-sessions.mdx +94 -0
  335. package/docs/tools-snapshot.mdx +129 -0
  336. package/docs/tools-state.mdx +104 -0
  337. package/docs/tools-tools-and-run.mdx +156 -0
  338. package/docs/tools-wait-for.mdx +85 -0
  339. package/docs/troubleshooting.mdx +263 -0
  340. package/docs/usage.md +209 -165
  341. package/docs/vs-chrome-devtools-mcp.mdx +123 -0
  342. package/docs/vs-playwright-mcp.mdx +125 -0
  343. package/docs/vs-screenshots.mdx +137 -0
  344. package/docs/what-is-reticle.mdx +63 -0
  345. package/docs/why-reticle.mdx +109 -0
  346. package/package.json +5 -5
  347. package/docs/desktop-apps.md +0 -256
  348. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  349. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  350. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  351. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  352. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  353. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  354. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  355. package/docs/matrix/MATRIX.md +0 -26
  356. package/docs/matrix/README.md +0 -59
  357. package/docs/mint.json +0 -61
@@ -1,4 +1,4 @@
1
- import { readFileSync } from 'node:fs';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ReticleEnv } from '@reticlehq/core';
4
4
  import { readPid, reticleStateHome } from '../daemon/daemon.js';
@@ -6,37 +6,22 @@ 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 { projectWiringLine } from './doctor-project-line.js';
10
+ import { hasProjectConnectedBefore } from '../session/connection-memory.js';
11
+ import { sessionsLine } from './doctor-sessions-line.js';
12
+ import { captureLookup, describeForeignHolder, findPortHolder } from './port-holder.js';
10
13
  import { chromiumHint, probeChromium } from './chromium-hint.js';
11
- import { spawnSync } from 'node:child_process';
12
14
  import { SERVER_VERSION } from '../version/server-version.js';
13
15
  import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
14
16
  import { diagnoseDesktop, isDesktopProject } from '../init/desktop-doctor.js';
17
+ import { diagnoseWebCsp } from '../init/csp-doctor.js';
18
+ import { RETICLE_CONFIG_BASENAME, diagnosePortMismatch, readProjectId, readProjectPort, } from './cli-port.js';
19
+ import { DoctorRow, doctorRow } from './doctor-rows.js';
15
20
  /**
16
21
  * `reticle doctor` — collapse the ~6 independent first-run failure modes into one command. Checks the
17
22
  * Chromium install (the #1 silent failure), whether a daemon is up on the resolved bridge port, and
18
23
  * reminds the user which port the app must dial. Human-readable to stdout (not the JSON log).
19
24
  */
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
25
  /** Narrow the `/status` payload to the two fields the daemon line reads. */
41
26
  function asIdentity(payload) {
42
27
  if (typeof payload !== 'object' || null === payload)
@@ -60,8 +45,11 @@ export async function handleDoctor(port) {
60
45
  process.stdout.write(`${s}\n`);
61
46
  };
62
47
  line('reticle doctor');
63
- line(` node ${process.version}`);
64
- line(` chromium ${chromiumHint(await probeChromium())}`);
48
+ line(doctorRow(DoctorRow.NODE, process.version));
49
+ // Filled in only on the daemon branch — there is nothing to ask when no daemon is answering, and
50
+ // the branches below already say so in their own terms.
51
+ let sessions;
52
+ line(doctorRow(DoctorRow.CHROMIUM, chromiumHint(await probeChromium())));
65
53
  // Ask the PORT, not just the pid file. "not running on :4400" has been printed about a port that
66
54
  // was demonstrably occupied, which sends the reader to start a daemon that cannot bind. The three
67
55
  // states are genuinely different problems with different fixes, so doctor names which one it is.
@@ -71,14 +59,19 @@ export async function handleDoctor(port) {
71
59
  // Name WHICH daemon. The /status payload already carries version + contract, and doctor was
72
60
  // throwing both away — while skew is invisible everywhere else, reaching the agent as a bare
73
61
  // -32000 naming no version. This is the command a human runs at exactly that moment.
74
- const status = asIdentity(await fetchStatus(port));
62
+ const payload = await fetchStatus(port);
63
+ const status = asIdentity(payload);
75
64
  const built = daemonLine(port, pid, status, {
76
65
  version: SERVER_VERSION,
77
66
  contract: CONTRACT_FINGERPRINT,
78
67
  });
79
68
  line(built.text);
80
69
  if (built.skew !== undefined)
81
- line(` version ✗ ${built.skew}`);
70
+ line(doctorRow(DoctorRow.VERSION, `✗ ${built.skew}`));
71
+ // The same payload already carries whether anything has CONNECTED, and doctor was reading
72
+ // `version` off it and dropping the rest. Without this the command is silent on the one step the
73
+ // funnel stalls on: wired correctly, daemon up, and no page has ever dialled in.
74
+ sessions = sessionsLine('object' === typeof payload && null !== payload ? payload : {});
82
75
  }
83
76
  else if (presence === PortPresence.FOREIGN) {
84
77
  // Name the holder when we can. `doctor` exists for exactly this moment, and "another process"
@@ -86,18 +79,43 @@ export async function handleDoctor(port) {
86
79
  // pipeline that also kills the agent's own MCP proxy.
87
80
  // `pid` is what our pid file recorded for this port. When it matches the process actually
88
81
  // 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)}`);
82
+ line(doctorRow(DoctorRow.DAEMON, `✗ ${describeForeignHolder(port, findPortHolder(port, captureLookup), pid)}`));
90
83
  }
91
84
  else {
92
- line(` daemon ✗ not running on :${port} — your agent runs \`reticle mcp\` (or \`reticle serve\`)`);
85
+ line(doctorRow(DoctorRow.DAEMON, `✗ not running on :${port} — your agent runs \`reticle mcp\` (or \`reticle serve\`)`));
93
86
  }
94
- line(` bridge port ${port} (your app must dial THIS port — not your dev-server port)`);
87
+ if (sessions !== undefined)
88
+ line(sessions.text);
89
+ line(doctorRow(DoctorRow.BRIDGE_PORT, `${port} (your app must dial THIS port, not your dev-server port)`));
90
+ const projectPort = readProjectPort(process.cwd());
91
+ const mismatch = diagnosePortMismatch(port, projectPort);
92
+ if (mismatch !== undefined)
93
+ line(doctorRow(DoctorRow.PORT_CHECK, `✗ ${mismatch}`));
94
+ // The check this checklist was missing: is the APP wired, not just the tools. Everything above can
95
+ // be green in a project that has never been through `init`, and that combination is precisely the
96
+ // one `doctor` gets run to explain.
97
+ const projectId = readProjectId(process.cwd());
98
+ line(projectWiringLine({
99
+ projectId,
100
+ previouslyConnected: hasProjectConnectedBefore(reticleStateHome(), port, projectId),
101
+ // An absent config and a corrupt one both read back as "no id"; only this tells them apart,
102
+ // and telling somebody a file is missing while it sits in front of them is its own dead end.
103
+ configPresent: existsSync(join(process.cwd(), RETICLE_CONFIG_BASENAME)),
104
+ }));
95
105
  // Where to LOOK when something is wrong. The daemon has always written a structured log here and
96
106
  // nothing ever said so, so the first move in every investigation was reading source instead of
97
107
  // reading the log. `RETICLE_TRACE=1` turns the same stream into a per-stage trace — see
98
108
  // docs/debugging.md.
99
- line(` daemon log ${join(reticleStateHome(), `daemon-${String(port)}.log`)}`);
100
- line(` tracing ${ReticleEnv.TRACE}=1 on the daemon for per-stage timings in that log`);
109
+ line(doctorRow(DoctorRow.DAEMON_LOG, join(reticleStateHome(), `daemon-${String(port)}.log`)));
110
+ line(doctorRow(DoctorRow.TRACING, `${ReticleEnv.TRACE}=1 on the daemon for per-stage timings in that log`));
111
+ // Below the checklist rather than inline: it is a paragraph, and a paragraph in the middle of a
112
+ // column of one-line checks buries the checks under it. This is the daemon's own no-session
113
+ // diagnosis, the same one an agent gets from an empty reticle_sessions.
114
+ if (sessions?.why !== undefined) {
115
+ line('');
116
+ line(' nothing has connected. What the daemon can tell from here:');
117
+ line(` ${sessions.why}`);
118
+ }
101
119
  // Desktop setup RCA. Every one of these fails SILENTLY — a Tauri app with the default CSP runs
102
120
  // perfectly and never connects; an Electron app without the preload line reports zero network
103
121
  // activity forever, which reads as "makes no backend calls" rather than "you are blind to them".
@@ -109,10 +127,23 @@ export async function handleDoctor(port) {
109
127
  return undefined;
110
128
  }
111
129
  };
130
+ // The web sibling of the desktop findings below: a `connect-src` that excludes the bridge makes
131
+ // the browser refuse the WebSocket and report it in ITS console only, so every check above passes
132
+ // at an app that can never connect. Named, with the exact text to paste.
133
+ const csp = diagnoseWebCsp(readProjectFile, port);
134
+ if (csp.length > 0) {
135
+ line('');
136
+ line(` csp ✗ a Content-Security-Policy is blocking the Reticle bridge:`);
137
+ for (const finding of csp) {
138
+ line(` ${finding.file}`);
139
+ line(` ${finding.problem}`);
140
+ line(` fix: ${finding.fix}`);
141
+ }
142
+ }
112
143
  const desktop = diagnoseDesktop(readProjectFile, port);
113
144
  if (desktop.length > 0) {
114
145
  line('');
115
- line(` desktop ✗ ${String(desktop.length)} issue(s) — the app will look fine and not work:`);
146
+ line(doctorRow(DoctorRow.DESKTOP, `✗ ${String(desktop.length)} issue(s) — the app will look fine and not work:`));
116
147
  for (const finding of desktop) {
117
148
  line(` ${finding.file}`);
118
149
  line(` ${finding.problem}`);
@@ -122,6 +153,6 @@ export async function handleDoctor(port) {
122
153
  else if (isDesktopProject(readProjectFile)) {
123
154
  // Say so explicitly. Silence would read as "not checked", which is the same ambiguity the
124
155
  // findings above exist to remove.
125
- line(' desktop ✓ desktop wiring looks right');
156
+ line(doctorRow(DoctorRow.DESKTOP, '✓ desktop wiring looks right'));
126
157
  }
127
158
  }
@@ -19,7 +19,7 @@ import { formatBuddyStatus } from '../flows/buddy-status.js';
19
19
  import { CapsuleStore } from '../capsule/capsule-store.js';
20
20
  import { AssertionTiersStore } from '../flows/assertion-tiers-store.js';
21
21
  import { detectDowngrades } from '../flows/assertion-integrity.js';
22
- import { computeCoverage } from '../flows/coverage.js';
22
+ import { computeCoverage, flowCoverageReport } from '../flows/coverage.js';
23
23
  import { createWatchBatcher } from '../flows/watch-batcher.js';
24
24
  import { watch } from 'node:fs';
25
25
  import { log } from '../log.js';
@@ -184,19 +184,19 @@ export async function handleGate(files, since) {
184
184
  const result = gateDecision({ affected, passing, flaky, downgraded, deleted });
185
185
  // Verified-surface coverage over flows: how much of the saved suite this run actually exercised.
186
186
  const coverage = computeCoverage({ testids: [], signals: [], flows: allFlows.map((f) => f.name) }, { testids: [], signals: [], flows: passing });
187
+ // A gate over an empty suite has not passed — it had nothing to check. Same rule the suite
188
+ // verdict already applies to `reticle verify`; see flowCoverageReport.
189
+ const flowCoverage = flowCoverageReport(coverage.flows);
190
+ const pass = result.pass && flowCoverage.outcome === undefined;
187
191
  log('reticle_gate', {
188
- pass: result.pass,
192
+ pass,
189
193
  uncovered: result.uncovered,
190
194
  quarantined: result.quarantined,
191
195
  ...(result.downgraded.length > 0 ? { downgraded: result.downgraded } : {}),
192
196
  ...(result.deleted.length > 0 ? { deletedCoverage: result.deleted } : {}),
193
- coverage: {
194
- pct: coverage.flows.pct,
195
- covered: coverage.flows.covered,
196
- total: coverage.flows.total,
197
- },
197
+ coverage: flowCoverage,
198
198
  });
199
- if (!result.pass)
199
+ if (!pass)
200
200
  process.exitCode = 1;
201
201
  }
202
202
  catch (error) {
@@ -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
+ }
@@ -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 };
@@ -7,6 +7,21 @@
7
7
  * 3..reticle.json "port" field in the cwd ← per-project isolation
8
8
  * 4. RETICLE_DEFAULT_PORT (4400)
9
9
  */
10
+ /** The project config `reticle init` writes. */
11
+ export declare const RETICLE_CONFIG_BASENAME = ".reticle.json";
12
+ /**
13
+ * Find the nearest `.reticle.json` at or above `cwd`, and return its parsed contents.
14
+ *
15
+ * Every reader below used to join the basename onto `cwd` and stop there, so a process whose
16
+ * working directory was one level away from the config behaved exactly like a project that had
17
+ * never been through `init`: no port, no projectId, no journal setting, and a diagnostic that told
18
+ * the user to run an install they had already run. That is the shape of several field reports at
19
+ * once — an app wired in a subdirectory, an agent running from a repo root, a worktree beside its
20
+ * main checkout — and they are all the same missing walk.
21
+ *
22
+ * Returns undefined when nothing is found, which keeps every caller's existing default intact.
23
+ */
24
+ export declare function findProjectConfig(cwd: string): Record<string, unknown> | undefined;
10
25
  /**
11
26
  * Ports that belong to a DEV SERVER, not to Reticle's bridge. Next defaults to 3000, Vite to 5173,
12
27
  * Astro to 4321, and so on.
@@ -33,6 +48,15 @@ export declare function readProjectPort(cwd: string): number | undefined;
33
48
  * undefined if the file is absent/unreadable or has no non-empty string projectId.
34
49
  */
35
50
  export declare function readProjectId(cwd: string): string | undefined;
51
+ /**
52
+ * The framework `reticle init` stamped into `.reticle.json`, when there is one.
53
+ *
54
+ * Read so the no-session diagnosis can RANK its causes instead of printing one static differential.
55
+ * Nuxt is the case that pays for this: it does not register a newly added plugin on HMR, so a dev
56
+ * server older than the wiring is its single most likely cause — `init` warns about it at install
57
+ * time and the hint the agent reads hours later never mentioned it.
58
+ */
59
+ export declare function readProjectFramework(cwd: string): string | undefined;
36
60
  /**
37
61
  * Whether the durable causal journal is enabled. On by default (the journal IS the loop); off only via
38
62
  * explicit opt-out — `.reticle.json` `"journal": false`, or `RETICLE_JOURNAL` set to `0`/`false`. The env
@@ -44,3 +68,11 @@ export declare function readJournalEnabled(cwd: string, env: string | undefined)
44
68
  * Pass `portFlag` when the user explicitly supplied --port; pass `undefined` to fall through.
45
69
  */
46
70
  export declare function resolvePort(portFlag: number | undefined, envPort: number | undefined, projectPort: number | undefined, defaultPort: number): number;
71
+ /**
72
+ * Detect a port mismatch between the daemon and the project's SDK configuration.
73
+ *
74
+ * Returns a diagnostic string when the daemon port differs from what `.reticle.json` declares.
75
+ * The SDK reads `.reticle.json` at build/connect time, so a mismatch means the app will dial a
76
+ * port nothing is listening on — the silent no-connect that #261 documents.
77
+ */
78
+ export declare function diagnosePortMismatch(daemonPort: number, projectPort: number | undefined): string | undefined;