@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
@@ -8,6 +8,50 @@
8
8
  * 4. RETICLE_DEFAULT_PORT (4400)
9
9
  */
10
10
  import { readFileSync } from 'node:fs';
11
+ import { dirname, resolve } from 'node:path';
12
+ /** The project config `reticle init` writes. */
13
+ export const RETICLE_CONFIG_BASENAME = '.reticle.json';
14
+ /**
15
+ * How far up to look for the project config before giving up.
16
+ *
17
+ * A cap rather than "walk to the filesystem root" so a process started somewhere unrelated cannot
18
+ * silently adopt a config from a distant ancestor and dial a port that belongs to another project.
19
+ * Deep enough for the layouts that actually broke: an app in `frontend/` or `apps/web/`, a git
20
+ * worktree checked out beside its main repo, and an agent whose cwd is a package inside a monorepo.
21
+ */
22
+ const MAX_CONFIG_SEARCH_DEPTH = 6;
23
+ /**
24
+ * Find the nearest `.reticle.json` at or above `cwd`, and return its parsed contents.
25
+ *
26
+ * Every reader below used to join the basename onto `cwd` and stop there, so a process whose
27
+ * working directory was one level away from the config behaved exactly like a project that had
28
+ * never been through `init`: no port, no projectId, no journal setting, and a diagnostic that told
29
+ * the user to run an install they had already run. That is the shape of several field reports at
30
+ * once — an app wired in a subdirectory, an agent running from a repo root, a worktree beside its
31
+ * main checkout — and they are all the same missing walk.
32
+ *
33
+ * Returns undefined when nothing is found, which keeps every caller's existing default intact.
34
+ */
35
+ export function findProjectConfig(cwd) {
36
+ let dir = resolve(cwd);
37
+ for (let depth = 0; depth <= MAX_CONFIG_SEARCH_DEPTH; depth += 1) {
38
+ try {
39
+ const raw = readFileSync(`${dir}/${RETICLE_CONFIG_BASENAME}`, 'utf8');
40
+ const config = JSON.parse(raw);
41
+ if ('object' === typeof config && config !== null && !Array.isArray(config)) {
42
+ return config;
43
+ }
44
+ }
45
+ catch {
46
+ // Absent, unreadable, or not JSON at this level — keep walking.
47
+ }
48
+ const parent = dirname(dir);
49
+ if (parent === dir)
50
+ break;
51
+ dir = parent;
52
+ }
53
+ return undefined;
54
+ }
11
55
  /**
12
56
  * Ports that belong to a DEV SERVER, not to Reticle's bridge. Next defaults to 3000, Vite to 5173,
13
57
  * Astro to 4321, and so on.
@@ -53,18 +97,9 @@ export function devServerPortWarning(port) {
53
97
  * Returns undefined if the file is absent, unreadable, or has no valid numeric port.
54
98
  */
55
99
  export function readProjectPort(cwd) {
56
- try {
57
- const raw = readFileSync(`${cwd}/.reticle.json`, 'utf8');
58
- const config = JSON.parse(raw);
59
- if ('object' === typeof config && config !== null) {
60
- const p = config['port'];
61
- if ('number' === typeof p && Number.isInteger(p) && p > 0 && p < 65536)
62
- return p;
63
- }
64
- }
65
- catch {
66
- //.reticle.json absent or unreadable — fall through to default
67
- }
100
+ const p = findProjectConfig(cwd)?.['port'];
101
+ if ('number' === typeof p && Number.isInteger(p) && p > 0 && p < 65536)
102
+ return p;
68
103
  return undefined;
69
104
  }
70
105
  /**
@@ -73,18 +108,23 @@ export function readProjectPort(cwd) {
73
108
  * undefined if the file is absent/unreadable or has no non-empty string projectId.
74
109
  */
75
110
  export function readProjectId(cwd) {
76
- try {
77
- const raw = readFileSync(`${cwd}/.reticle.json`, 'utf8');
78
- const config = JSON.parse(raw);
79
- if ('object' === typeof config && config !== null) {
80
- const id = config['projectId'];
81
- if ('string' === typeof id && id.length > 0)
82
- return id;
83
- }
84
- }
85
- catch {
86
- //.reticle.json absent or unreadable — no default scope
87
- }
111
+ const id = findProjectConfig(cwd)?.['projectId'];
112
+ if ('string' === typeof id && id.length > 0)
113
+ return id;
114
+ return undefined;
115
+ }
116
+ /**
117
+ * The framework `reticle init` stamped into `.reticle.json`, when there is one.
118
+ *
119
+ * Read so the no-session diagnosis can RANK its causes instead of printing one static differential.
120
+ * Nuxt is the case that pays for this: it does not register a newly added plugin on HMR, so a dev
121
+ * server older than the wiring is its single most likely cause — `init` warns about it at install
122
+ * time and the hint the agent reads hours later never mentioned it.
123
+ */
124
+ export function readProjectFramework(cwd) {
125
+ const framework = findProjectConfig(cwd)?.['framework'];
126
+ if ('string' === typeof framework && framework.length > 0)
127
+ return framework;
88
128
  return undefined;
89
129
  }
90
130
  /**
@@ -100,17 +140,8 @@ export function readJournalEnabled(cwd, env) {
100
140
  if ('1' === v || 'true' === v || 'on' === v)
101
141
  return true;
102
142
  }
103
- try {
104
- const raw = readFileSync(`${cwd}/.reticle.json`, 'utf8');
105
- const config = JSON.parse(raw);
106
- if ('object' === typeof config && config !== null) {
107
- if (false === config['journal'])
108
- return false;
109
- }
110
- }
111
- catch {
112
- //.reticle.json absent or unreadable — journaling stays on by default
113
- }
143
+ if (false === findProjectConfig(cwd)?.['journal'])
144
+ return false;
114
145
  return true;
115
146
  }
116
147
  /**
@@ -120,3 +151,19 @@ export function readJournalEnabled(cwd, env) {
120
151
  export function resolvePort(portFlag, envPort, projectPort, defaultPort) {
121
152
  return portFlag ?? envPort ?? projectPort ?? defaultPort;
122
153
  }
154
+ /**
155
+ * Detect a port mismatch between the daemon and the project's SDK configuration.
156
+ *
157
+ * Returns a diagnostic string when the daemon port differs from what `.reticle.json` declares.
158
+ * The SDK reads `.reticle.json` at build/connect time, so a mismatch means the app will dial a
159
+ * port nothing is listening on — the silent no-connect that #261 documents.
160
+ */
161
+ export function diagnosePortMismatch(daemonPort, projectPort) {
162
+ if (projectPort === undefined)
163
+ return undefined;
164
+ if (projectPort === daemonPort)
165
+ return undefined;
166
+ return (`.reticle.json says "port": ${String(projectPort)} but this daemon is on :${String(daemonPort)}` +
167
+ ` — the SDK will dial :${String(projectPort)} and never connect. ` +
168
+ `Either start the daemon with --port ${String(projectPort)}, or update .reticle.json to match.`);
169
+ }
@@ -36,6 +36,27 @@ export declare function urlParts(url: string): {
36
36
  loopback: boolean;
37
37
  };
38
38
  /** CLI entry — wires the live ports and runs the one-shot verification. Exits the process itself. */
39
+ /**
40
+ * What a user reads when the bridge port is already taken by a daemon.
41
+ *
42
+ * `verify` boots its OWN daemon, and it has to do so on the port the APP dials — a loopback page
43
+ * connects natively to the port baked into its config, so verify cannot simply pick a free one and
44
+ * still be found. On any machine with Reticle set up, that port already belongs to a running daemon.
45
+ *
46
+ * It used to die there. The listen failure surfaces asynchronously on the server object, after
47
+ * `start` has resolved, so nothing caught it and the process printed a raw `node:net` stack — the
48
+ * worst answer available, and reached most often by the people with the fewest other options, since
49
+ * the skill offers this command as the way to a verdict with no MCP at all.
50
+ *
51
+ * Three ways out, in the order they are worth trying, and the message says all three rather than
52
+ * picking one: an agent that already has the tools should ask the daemon that is running instead of
53
+ * starting a second one; somebody who wants this command specifically can stop the daemon; and a
54
+ * second port works when the app is configured for it.
55
+ *
56
+ * Attaching to the running daemon the way `reticle drive` now does is the better answer and is not
57
+ * this change — see cli/drive-attach.ts for the shape it should take.
58
+ */
59
+ export declare function portBusyMessage(port: number): string;
39
60
  export declare function handleVerify(parsed: {
40
61
  url: string;
41
62
  headless: boolean;
@@ -13,6 +13,9 @@ import { join, basename } from 'node:path';
13
13
  import { randomUUID } from 'node:crypto';
14
14
  import { RETICLE_DEFAULT_PORT, ReticleEnv, bridgeWsUrl, isLoopbackHostname, ReticleDir, RunAgentKind, RunFramework, RunProfile, RunTrigger, VerdictStatus, } from '@reticlehq/core';
15
15
  import { start } from '../index.js';
16
+ import { probePresence, PortPresence } from '../daemon/port-presence.js';
17
+ import { probeDaemon } from '../mcp/mcp-proxy.js';
18
+ import { fetchStatus } from './cli-launch.js';
16
19
  import { resolveCloudConfig, syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
17
20
  import { ReticleRunner } from '../runs/reticle-runner.js';
18
21
  import { createRunnerPort } from '../runs/runner-port.js';
@@ -30,10 +33,10 @@ const DEFAULT_SESSION_TIMEOUT_MS = 15_000;
30
33
  const SESSION_POLL_MS = 250;
31
34
  const DEFAULT_PROJECT_NAME = 'app';
32
35
  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' +
36
+ const MSG_NO_SESSION = 'No app connected: Reticle drove the URL but no @reticlehq/browser session dialed back.\n' +
34
37
  ' Make sure the SDK is in the build and reticle.connect() runs on the preview page' +
35
38
  ' (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' +
39
+ const MSG_NO_FLOWS = 'No saved flows to verify (.reticle/flows is empty), so refusing to report a pass for verifying nothing.\n' +
37
40
  ' Flows are recorded interactively by an agent (reticle_record{action:"start"} → act → reticle_flow_save via the\n' +
38
41
  ' MCP tools), then committed to .reticle/flows/. In CI, check those files in and re-run `reticle verify`.';
39
42
  const MSG_VERIFY_PREFIX = 'verify failed: ';
@@ -199,6 +202,35 @@ async function openLiveConnection(opts) {
199
202
  };
200
203
  }
201
204
  /** CLI entry — wires the live ports and runs the one-shot verification. Exits the process itself. */
205
+ /**
206
+ * What a user reads when the bridge port is already taken by a daemon.
207
+ *
208
+ * `verify` boots its OWN daemon, and it has to do so on the port the APP dials — a loopback page
209
+ * connects natively to the port baked into its config, so verify cannot simply pick a free one and
210
+ * still be found. On any machine with Reticle set up, that port already belongs to a running daemon.
211
+ *
212
+ * It used to die there. The listen failure surfaces asynchronously on the server object, after
213
+ * `start` has resolved, so nothing caught it and the process printed a raw `node:net` stack — the
214
+ * worst answer available, and reached most often by the people with the fewest other options, since
215
+ * the skill offers this command as the way to a verdict with no MCP at all.
216
+ *
217
+ * Three ways out, in the order they are worth trying, and the message says all three rather than
218
+ * picking one: an agent that already has the tools should ask the daemon that is running instead of
219
+ * starting a second one; somebody who wants this command specifically can stop the daemon; and a
220
+ * second port works when the app is configured for it.
221
+ *
222
+ * Attaching to the running daemon the way `reticle drive` now does is the better answer and is not
223
+ * this change — see cli/drive-attach.ts for the shape it should take.
224
+ */
225
+ export function portBusyMessage(port) {
226
+ return (`reticle verify needs port ${String(port)} — the port your app dials — and a Reticle daemon is ` +
227
+ 'already listening on it. It did not start a second one.\n\n' +
228
+ ' • If you have the Reticle tools, ask the daemon that is already running instead: ' +
229
+ 'reticle_run { tool: "reticle_verify_change", args: { files: ["..."] } }\n' +
230
+ ` • Or stop it and re-run: npx @reticlehq/server stop --port ${String(port)}\n` +
231
+ ` • Or run both on another port, if your app is configured for it: RETICLE_PORT=<port> ` +
232
+ 'npx @reticlehq/server verify <url>');
233
+ }
202
234
  export function handleVerify(parsed) {
203
235
  const now = () => Date.now();
204
236
  const reticleRoot = join(process.cwd(), ReticleDir.ROOT);
@@ -217,5 +249,17 @@ export function handleVerify(parsed) {
217
249
  fail: (line) => process.stderr.write(`${line}\n`),
218
250
  exit: (code) => process.exit(code),
219
251
  };
220
- void runVerify({ url: parsed.url, timeoutMs: parsed.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS }, ports);
252
+ // Asked BEFORE anything binds. The listen failure arrives asynchronously on the server object,
253
+ // long after `start` has resolved, so no `.catch` on that promise can ever see it — which is why
254
+ // this used to reach the user as a raw node stack rather than as an answer.
255
+ void (async () => {
256
+ const port = parsed.port ?? RETICLE_DEFAULT_PORT;
257
+ if ((await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })) ===
258
+ PortPresence.DAEMON) {
259
+ ports.fail(portBusyMessage(port));
260
+ ports.exit(1);
261
+ return;
262
+ }
263
+ await runVerify({ url: parsed.url, timeoutMs: parsed.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS }, ports);
264
+ })();
221
265
  }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Where the project config actually is, and — when we cannot find it — where we looked.
3
+ *
4
+ * `findProjectConfig` walks UP from the daemon's working directory, which is right for a developer
5
+ * who ran `reticle` in their project and wrong for the way most daemons are actually started.
6
+ * Editors launch MCP servers from wherever they like, and in at least one popular one that is the
7
+ * user's home directory: the config then sits at `<repo>/apps/web/.reticle.json`, the up-walk from
8
+ * home never touches it, and Reticle confidently reports the project unwired. Reported twice, plus a
9
+ * git-worktree variant of the same shape.
10
+ *
11
+ * So: walk up, then — from the repo root, if there is one — look in the workspace directories the
12
+ * project's OWN manifest declares, falling back to the two conventions everybody uses.
13
+ *
14
+ * The second half is the `searched` list. "There is no `.reticle.json`" is unfalsifiable without it,
15
+ * and this file exists because that claim was made, confidently, about configs that were right
16
+ * there. Every result carries the places that were checked.
17
+ *
18
+ * Deliberately NOT a "pick one" function. Several configs means several projects, and choosing one
19
+ * silently is how an agent ends up with a confident verdict about an app it never touched. Callers
20
+ * are handed the list and must name it.
21
+ */
22
+ export interface FoundConfig {
23
+ /** Absolute path to the `.reticle.json`. */
24
+ path: string;
25
+ /** The directory it configures — what a reader needs in order to act. */
26
+ directory: string;
27
+ /** Its projectId, when it declares a usable one. */
28
+ projectId?: string;
29
+ }
30
+ export interface ConfigDiscovery {
31
+ found: FoundConfig[];
32
+ /** Every directory checked, in the order checked. The answer to "are you sure?". */
33
+ searched: string[];
34
+ }
35
+ export declare function discoverProjectConfigs(cwd: string): ConfigDiscovery;
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Where the project config actually is, and — when we cannot find it — where we looked.
3
+ *
4
+ * `findProjectConfig` walks UP from the daemon's working directory, which is right for a developer
5
+ * who ran `reticle` in their project and wrong for the way most daemons are actually started.
6
+ * Editors launch MCP servers from wherever they like, and in at least one popular one that is the
7
+ * user's home directory: the config then sits at `<repo>/apps/web/.reticle.json`, the up-walk from
8
+ * home never touches it, and Reticle confidently reports the project unwired. Reported twice, plus a
9
+ * git-worktree variant of the same shape.
10
+ *
11
+ * So: walk up, then — from the repo root, if there is one — look in the workspace directories the
12
+ * project's OWN manifest declares, falling back to the two conventions everybody uses.
13
+ *
14
+ * The second half is the `searched` list. "There is no `.reticle.json`" is unfalsifiable without it,
15
+ * and this file exists because that claim was made, confidently, about configs that were right
16
+ * there. Every result carries the places that were checked.
17
+ *
18
+ * Deliberately NOT a "pick one" function. Several configs means several projects, and choosing one
19
+ * silently is how an agent ends up with a confident verdict about an app it never touched. Callers
20
+ * are handed the list and must name it.
21
+ */
22
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
23
+ import { dirname, join, resolve } from 'node:path';
24
+ import { RETICLE_CONFIG_BASENAME } from './cli-port.js';
25
+ /** How far up to walk. Matches the existing config search so the two cannot disagree. */
26
+ const MAX_UP_LEVELS = 6;
27
+ /**
28
+ * Workspace directories checked when the manifest declares none.
29
+ *
30
+ * Not a guess at where an app might be — these are the two layouts the JS ecosystem converged on,
31
+ * and both reported cases used one of them.
32
+ */
33
+ const CONVENTIONAL_WORKSPACES = ['apps', 'packages'];
34
+ /** How many entries of one workspace directory to stat. A backstop, not a policy. */
35
+ const MAX_WORKSPACE_ENTRIES = 64;
36
+ function readConfig(directory) {
37
+ const path = join(directory, RETICLE_CONFIG_BASENAME);
38
+ try {
39
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
40
+ if ('object' !== typeof parsed || null === parsed || Array.isArray(parsed))
41
+ return undefined;
42
+ const projectId = parsed['projectId'];
43
+ return {
44
+ path,
45
+ directory,
46
+ ...('string' === typeof projectId && projectId.length > 0 ? { projectId } : {}),
47
+ };
48
+ }
49
+ catch {
50
+ // Absent, unreadable, or not JSON. A malformed config is reported elsewhere; here it is simply
51
+ // not a config we can name a project from.
52
+ return undefined;
53
+ }
54
+ }
55
+ /** The nearest ancestor holding a `.git`, or undefined. The repo root is where workspaces hang off. */
56
+ function repoRootOf(start) {
57
+ let dir = start;
58
+ for (let level = 0; level <= MAX_UP_LEVELS; level += 1) {
59
+ if (existsSync(join(dir, '.git')))
60
+ return dir;
61
+ const parent = dirname(dir);
62
+ if (parent === dir)
63
+ return undefined;
64
+ dir = parent;
65
+ }
66
+ return undefined;
67
+ }
68
+ /**
69
+ * The workspace directories this repo declares, from its own manifest where it has one.
70
+ *
71
+ * Reading `pnpm-workspace.yaml` with a substring scan rather than a YAML parser is deliberate: the
72
+ * only thing wanted from it is the leading path segment of each glob, the file is machine-written in
73
+ * practice, and a parser dependency to read one list of strings does not pay for itself.
74
+ */
75
+ function declaredWorkspaceDirs(repoRoot) {
76
+ const dirs = new Set();
77
+ try {
78
+ const pkg = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'));
79
+ if ('object' === typeof pkg && null !== pkg) {
80
+ const workspaces = pkg['workspaces'];
81
+ const globs = Array.isArray(workspaces)
82
+ ? workspaces
83
+ : 'object' === typeof workspaces && null !== workspaces
84
+ ? (workspaces['packages'] ?? [])
85
+ : [];
86
+ if (Array.isArray(globs)) {
87
+ for (const glob of globs)
88
+ if ('string' === typeof glob)
89
+ dirs.add(firstSegment(glob));
90
+ }
91
+ }
92
+ }
93
+ catch {
94
+ // No manifest, or one we cannot read. The conventions below still apply.
95
+ }
96
+ try {
97
+ const yaml = readFileSync(join(repoRoot, 'pnpm-workspace.yaml'), 'utf8');
98
+ for (const line of yaml.split('\n')) {
99
+ const match = /^\s*-\s*['"]?([^'"\s]+)/.exec(line);
100
+ const glob = match?.[1];
101
+ if (glob !== undefined)
102
+ dirs.add(firstSegment(glob));
103
+ }
104
+ }
105
+ catch {
106
+ // Not a pnpm workspace.
107
+ }
108
+ for (const conventional of CONVENTIONAL_WORKSPACES)
109
+ dirs.add(conventional);
110
+ return [...dirs].filter((dir) => dir.length > 0 && !dir.startsWith('.'));
111
+ }
112
+ /** `apps/*` -> `apps`. The glob machinery is not needed; the leading directory is all we scan. */
113
+ function firstSegment(glob) {
114
+ const [first] = glob.split('/');
115
+ return first ?? '';
116
+ }
117
+ export function discoverProjectConfigs(cwd) {
118
+ const searched = [];
119
+ const found = [];
120
+ const seen = new Set();
121
+ const check = (directory) => {
122
+ if (seen.has(directory))
123
+ return;
124
+ seen.add(directory);
125
+ searched.push(directory);
126
+ const config = readConfig(directory);
127
+ if (config !== undefined)
128
+ found.push(config);
129
+ };
130
+ let dir = resolve(cwd);
131
+ for (let level = 0; level <= MAX_UP_LEVELS; level += 1) {
132
+ check(dir);
133
+ const parent = dirname(dir);
134
+ if (parent === dir)
135
+ break;
136
+ dir = parent;
137
+ }
138
+ const repoRoot = repoRootOf(resolve(cwd));
139
+ if (repoRoot === undefined)
140
+ return { found, searched };
141
+ for (const workspace of declaredWorkspaceDirs(repoRoot)) {
142
+ const base = join(repoRoot, workspace);
143
+ let entries = [];
144
+ try {
145
+ entries = readdirSync(base).slice(0, MAX_WORKSPACE_ENTRIES);
146
+ }
147
+ catch {
148
+ continue; // A declared workspace directory that does not exist is not an error here.
149
+ }
150
+ for (const entry of entries) {
151
+ const candidate = join(base, entry);
152
+ try {
153
+ if (!statSync(candidate).isDirectory())
154
+ continue;
155
+ }
156
+ catch {
157
+ continue;
158
+ }
159
+ check(candidate);
160
+ }
161
+ }
162
+ return { found, searched };
163
+ }
@@ -14,6 +14,7 @@
14
14
  * at the call site.
15
15
  */
16
16
  import { describeSkew } from '../version/version-skew.js';
17
+ import { DoctorRow, doctorRow } from './doctor-rows.js';
17
18
  /**
18
19
  * `reticle stop`, not `reticle kill`.
19
20
  *
@@ -39,6 +40,8 @@ export function daemonLine(port, pid, peer, self) {
39
40
  parts.push(`v${peer.version}`);
40
41
  const detail = 0 === parts.length ? '' : ` (${parts.join(', ')})`;
41
42
  const skew = describeSkew({ what: 'the daemon on this port', version: peer.version, contract: peer.contract, fix: FIX }, self);
42
- const line = { text: ` daemon ✓ running on :${String(port)}${detail}` };
43
+ const line = {
44
+ text: doctorRow(DoctorRow.DAEMON, `✓ running on :${String(port)}${detail}`),
45
+ };
43
46
  return skew === undefined ? line : { ...line, skew };
44
47
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Whether `init` has wired THIS directory — the check `doctor` was missing.
3
+ *
4
+ * `doctor` already read `.reticle.json` for the port, so it knew whether the file existed and never
5
+ * said so. In a project where `init` has not run the checklist came out clean: node fine, chromium
6
+ * fine, a daemon that starts on demand, a bridge port. Every line true, and the app carrying no SDK
7
+ * went unremarked — which is exactly the state that produces "the tools are here and nothing ever
8
+ * verifies", and the state `doctor` is run to explain.
9
+ *
10
+ * Registering the MCP server and instrumenting the app are two different halves of the install, and
11
+ * more than one route does the first without the second.
12
+ */
13
+ export interface ProjectWiringFacts {
14
+ /** The id from `.reticle.json`, or undefined when there is no config here. */
15
+ projectId: string | undefined;
16
+ /** Has an app for this project ever connected on this port? Durable across daemon restarts. */
17
+ previouslyConnected: boolean;
18
+ /**
19
+ * Is there a `.reticle.json` on disk here at all?
20
+ *
21
+ * Separate from `projectId` because the two failures are different facts with the same symptom: an
22
+ * absent config and a corrupt one both read back as "no id". Saying "no .reticle.json here" about a
23
+ * file that is sitting right there sends somebody hunting a missing file when what they have is a
24
+ * broken one — and a corrupt config is itself worth naming, since it is how a project ends up
25
+ * pointed at the wrong daemon.
26
+ */
27
+ configPresent?: boolean;
28
+ }
29
+ /**
30
+ * One line, in the same shape as the checks beside it.
31
+ *
32
+ * `previouslyConnected` outranks the missing file, and that ordering is the important part: the Vite
33
+ * and Babel plugins instrument an app without writing a config, so a project that has connected here
34
+ * is wired whatever the file says. Sending it to `init` would be the same wrong answer pointing the
35
+ * other way — and `init` is the one action that can overwrite a setup that already works.
36
+ */
37
+ export declare function projectWiringLine(facts: ProjectWiringFacts): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Whether `init` has wired THIS directory — the check `doctor` was missing.
3
+ *
4
+ * `doctor` already read `.reticle.json` for the port, so it knew whether the file existed and never
5
+ * said so. In a project where `init` has not run the checklist came out clean: node fine, chromium
6
+ * fine, a daemon that starts on demand, a bridge port. Every line true, and the app carrying no SDK
7
+ * went unremarked — which is exactly the state that produces "the tools are here and nothing ever
8
+ * verifies", and the state `doctor` is run to explain.
9
+ *
10
+ * Registering the MCP server and instrumenting the app are two different halves of the install, and
11
+ * more than one route does the first without the second.
12
+ */
13
+ /**
14
+ * One line, in the same shape as the checks beside it.
15
+ *
16
+ * `previouslyConnected` outranks the missing file, and that ordering is the important part: the Vite
17
+ * and Babel plugins instrument an app without writing a config, so a project that has connected here
18
+ * is wired whatever the file says. Sending it to `init` would be the same wrong answer pointing the
19
+ * other way — and `init` is the one action that can overwrite a setup that already works.
20
+ */
21
+ export function projectWiringLine(facts) {
22
+ if (facts.projectId !== undefined) {
23
+ return ` project ✓ wired here (${facts.projectId})`;
24
+ }
25
+ if (facts.previouslyConnected) {
26
+ return ' project ✓ an app has connected here before, so it is wired (no .reticle.json — a build plugin can wire an app without one)';
27
+ }
28
+ if (true === facts.configPresent) {
29
+ return (' project ✗ .reticle.json is here but carries no readable projectId — a corrupt or ' +
30
+ 'hand-edited config reads the same as no config, and points this project at the default ' +
31
+ 'daemon. Run `npx @reticlehq/server init` in the app directory to rewrite it');
32
+ }
33
+ return (' project ✗ no .reticle.json here and nothing has ever connected — the tools may be ' +
34
+ 'registered, but this app is not instrumented. Run `npx @reticlehq/server init` in the app directory');
35
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The row labels `reticle doctor` can print, and the one place that pads them into a column.
3
+ *
4
+ * These were free strings before, hand-padded at every call site across three modules
5
+ * (`cli-doctor.ts`, `doctor-daemon-line.ts`, `doctor-sessions-line.ts`). Two problems with that,
6
+ * and the second is the one that motivated #340:
7
+ *
8
+ * 1. The padding was maintained by eye. Every label happened to be padded to the same column, but
9
+ * nothing enforced it, and a label longer than the column would have quietly broken the
10
+ * alignment of one row in a checklist whose whole readability is the column.
11
+ * 2. Nothing connected the labels to the docs pages that reproduce doctor's output. A row could be
12
+ * added, renamed or removed and the three pages showing sample output would go on describing the
13
+ * old vocabulary, with no gate noticing. `doctor` is the command a stuck user runs, and the docs
14
+ * are what they read when its output does not match what they were told to expect.
15
+ *
16
+ * The label set is exported so the docs guard can derive its expectation from the command rather
17
+ * than from a hand-kept list, which is the direction that catches the case that actually bites: a
18
+ * row added and never documented.
19
+ *
20
+ * NOT every row prints on every run. `version` prints only on skew, `port check` only on a
21
+ * mismatch, `desktop` only on a desktop project, and `sessions` only when a daemon answered. The
22
+ * vocabulary is therefore "what this command can print", not "what one run printed" — a guard built
23
+ * by scraping a single invocation would silently stop covering the conditional rows, which are
24
+ * exactly the ones a reader is least likely to have seen before.
25
+ */
26
+ /** Every row label `doctor` can emit. Values are the literal text at the head of the row. */
27
+ export declare const DoctorRow: {
28
+ readonly NODE: "node";
29
+ readonly CHROMIUM: "chromium";
30
+ readonly DAEMON: "daemon";
31
+ readonly VERSION: "version";
32
+ readonly SESSIONS: "sessions";
33
+ readonly BRIDGE_PORT: "bridge port";
34
+ readonly PORT_CHECK: "port check";
35
+ readonly DAEMON_LOG: "daemon log";
36
+ readonly TRACING: "tracing";
37
+ readonly DESKTOP: "desktop";
38
+ };
39
+ export type DoctorRowLabel = (typeof DoctorRow)[keyof typeof DoctorRow];
40
+ export declare const DOCTOR_ROW_LABELS: readonly DoctorRowLabel[];
41
+ /**
42
+ * Width the label column pads to.
43
+ *
44
+ * Derived from the longest label rather than hardcoded, so adding a longer row realigns the whole
45
+ * checklist instead of breaking one line of it. With the current set the longest is `bridge port`
46
+ * (11), so this is 13 — byte-identical to the hand-padding it replaces. A test pins that, so a
47
+ * change to the column is a deliberate, visible change rather than an invisible reflow.
48
+ */
49
+ export declare const LABEL_COLUMN: number;
50
+ /** Build one `doctor` row: the standard indent, the label padded to the column, then `rest`. */
51
+ export declare function doctorRow(label: DoctorRowLabel, rest: string): string;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The row labels `reticle doctor` can print, and the one place that pads them into a column.
3
+ *
4
+ * These were free strings before, hand-padded at every call site across three modules
5
+ * (`cli-doctor.ts`, `doctor-daemon-line.ts`, `doctor-sessions-line.ts`). Two problems with that,
6
+ * and the second is the one that motivated #340:
7
+ *
8
+ * 1. The padding was maintained by eye. Every label happened to be padded to the same column, but
9
+ * nothing enforced it, and a label longer than the column would have quietly broken the
10
+ * alignment of one row in a checklist whose whole readability is the column.
11
+ * 2. Nothing connected the labels to the docs pages that reproduce doctor's output. A row could be
12
+ * added, renamed or removed and the three pages showing sample output would go on describing the
13
+ * old vocabulary, with no gate noticing. `doctor` is the command a stuck user runs, and the docs
14
+ * are what they read when its output does not match what they were told to expect.
15
+ *
16
+ * The label set is exported so the docs guard can derive its expectation from the command rather
17
+ * than from a hand-kept list, which is the direction that catches the case that actually bites: a
18
+ * row added and never documented.
19
+ *
20
+ * NOT every row prints on every run. `version` prints only on skew, `port check` only on a
21
+ * mismatch, `desktop` only on a desktop project, and `sessions` only when a daemon answered. The
22
+ * vocabulary is therefore "what this command can print", not "what one run printed" — a guard built
23
+ * by scraping a single invocation would silently stop covering the conditional rows, which are
24
+ * exactly the ones a reader is least likely to have seen before.
25
+ */
26
+ /** Every row label `doctor` can emit. Values are the literal text at the head of the row. */
27
+ export const DoctorRow = {
28
+ NODE: 'node',
29
+ CHROMIUM: 'chromium',
30
+ DAEMON: 'daemon',
31
+ VERSION: 'version',
32
+ SESSIONS: 'sessions',
33
+ BRIDGE_PORT: 'bridge port',
34
+ PORT_CHECK: 'port check',
35
+ DAEMON_LOG: 'daemon log',
36
+ TRACING: 'tracing',
37
+ DESKTOP: 'desktop',
38
+ };
39
+ export const DOCTOR_ROW_LABELS = Object.values(DoctorRow);
40
+ /** Indent every row carries, before the label. */
41
+ const INDENT = ' ';
42
+ /**
43
+ * Width the label column pads to.
44
+ *
45
+ * Derived from the longest label rather than hardcoded, so adding a longer row realigns the whole
46
+ * checklist instead of breaking one line of it. With the current set the longest is `bridge port`
47
+ * (11), so this is 13 — byte-identical to the hand-padding it replaces. A test pins that, so a
48
+ * change to the column is a deliberate, visible change rather than an invisible reflow.
49
+ */
50
+ export const LABEL_COLUMN = DOCTOR_ROW_LABELS.reduce((widest, label) => Math.max(widest, label.length), 0) + 2;
51
+ /** Build one `doctor` row: the standard indent, the label padded to the column, then `rest`. */
52
+ export function doctorRow(label, rest) {
53
+ return `${INDENT}${label.padEnd(LABEL_COLUMN)}${rest}`;
54
+ }