@reticlehq/server 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/SKILL.md +73 -11
  2. package/dist/bridge/bridge.d.ts +0 -13
  3. package/dist/bridge/bridge.js +1 -1
  4. package/dist/bridge/bridge.test-harness.d.ts +2 -0
  5. package/dist/bridge/bridge.test-harness.js +8 -1
  6. package/dist/capsule/capsule-store.d.ts +2 -1
  7. package/dist/capsule/capsule-store.js +1 -1
  8. package/dist/capsule/causal-summary.d.ts +2 -2
  9. package/dist/chromium-launch-options.d.ts +28 -0
  10. package/dist/chromium-launch-options.js +29 -0
  11. package/dist/cli/cli-doctor.js +8 -0
  12. package/dist/cli/cli-flow-commands.d.ts +3 -1
  13. package/dist/cli/cli-flow-commands.js +34 -4
  14. package/dist/cli/cli-kill.d.ts +2 -1
  15. package/dist/cli/cli-parse.d.ts +3 -1
  16. package/dist/cli/cli-parse.js +29 -7
  17. package/dist/cli/cli-update-commands.js +20 -1
  18. package/dist/cli/cli-verify.d.ts +1 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/cloud-cli.js +12 -6
  21. package/dist/cli/config-discovery.d.ts +2 -1
  22. package/dist/cli/doctor-rows.d.ts +3 -1
  23. package/dist/cli/doctor-rows.js +3 -1
  24. package/dist/cli/gate-exit.d.ts +21 -0
  25. package/dist/cli/gate-exit.js +21 -0
  26. package/dist/cli/gate-hook-message.d.ts +33 -0
  27. package/dist/cli/gate-hook-message.js +46 -0
  28. package/dist/cli/mcp-command.d.ts +25 -0
  29. package/dist/cli/mcp-command.js +143 -0
  30. package/dist/cli/sibling-ports.d.ts +37 -0
  31. package/dist/cli/sibling-ports.js +56 -0
  32. package/dist/cli.js +37 -110
  33. package/dist/cloud/cloud-config.d.ts +2 -1
  34. package/dist/cloud/cloud-sync.d.ts +11 -0
  35. package/dist/cloud/cloud-sync.js +32 -0
  36. package/dist/crawl/crawl.d.ts +9 -13
  37. package/dist/crawl/crawl.js +20 -3
  38. package/dist/daemon/daemon-resolve.d.ts +79 -0
  39. package/dist/daemon/daemon-resolve.js +118 -0
  40. package/dist/daemon/free-port.d.ts +29 -0
  41. package/dist/daemon/free-port.js +55 -0
  42. package/dist/daemon/heartbeat.d.ts +0 -4
  43. package/dist/daemon/heartbeat.js +2 -2
  44. package/dist/dev/stale-issue-guard.d.ts +41 -0
  45. package/dist/dev/stale-issue-guard.js +75 -0
  46. package/dist/domain/domain-tools.js +3 -2
  47. package/dist/events/contradictions.d.ts +41 -0
  48. package/dist/events/contradictions.js +125 -12
  49. package/dist/events/declared.d.ts +14 -0
  50. package/dist/events/declared.js +33 -0
  51. package/dist/events/echo-mismatch.js +14 -0
  52. package/dist/events/observed-in-window.d.ts +12 -0
  53. package/dist/events/observed-in-window.js +17 -0
  54. package/dist/events/predicate-asks.d.ts +30 -0
  55. package/dist/events/predicate-asks.js +48 -0
  56. package/dist/events/predicate-eval.js +254 -25
  57. package/dist/events/predicate-parse.d.ts +0 -6
  58. package/dist/events/predicate-parse.js +51 -5
  59. package/dist/events/predicate-schema.d.ts +62 -0
  60. package/dist/events/predicate-schema.js +162 -4
  61. package/dist/events/predicate.d.ts +15 -0
  62. package/dist/events/predicate.js +41 -2
  63. package/dist/flows/coverage.d.ts +0 -8
  64. package/dist/flows/coverage.js +1 -1
  65. package/dist/flows/decision.d.ts +5 -1
  66. package/dist/flows/decision.js +33 -7
  67. package/dist/flows/flake.d.ts +0 -2
  68. package/dist/flows/flake.js +1 -1
  69. package/dist/flows/flow-intent.d.ts +52 -0
  70. package/dist/flows/flow-intent.js +123 -0
  71. package/dist/flows/flow-replay-run.js +27 -4
  72. package/dist/flows/flow-success.js +15 -3
  73. package/dist/flows/flow-tools.js +65 -8
  74. package/dist/flows/flows.d.ts +6 -1
  75. package/dist/flows/flows.js +34 -22
  76. package/dist/flows/predicate-to-expect.js +14 -3
  77. package/dist/flows/server-verify.js +16 -2
  78. package/dist/flows/verify-change-tools.js +9 -2
  79. package/dist/honesty/blind-spots.d.ts +8 -16
  80. package/dist/honesty/blind-spots.js +18 -5
  81. package/dist/honesty/feature-capture.d.ts +150 -0
  82. package/dist/honesty/feature-capture.js +233 -0
  83. package/dist/honesty/gap-ledger.d.ts +52 -0
  84. package/dist/honesty/gap-ledger.js +63 -0
  85. package/dist/honesty/instrumentation-gaps.d.ts +79 -0
  86. package/dist/honesty/instrumentation-gaps.js +77 -0
  87. package/dist/honesty/observability.d.ts +37 -0
  88. package/dist/honesty/observability.js +40 -0
  89. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  90. package/dist/honesty/tool-hit-rate.js +90 -0
  91. package/dist/honesty/undeclared-change.d.ts +51 -0
  92. package/dist/honesty/undeclared-change.js +56 -0
  93. package/dist/honesty/verified.d.ts +15 -0
  94. package/dist/honesty/verified.js +68 -4
  95. package/dist/http-server.d.ts +0 -1
  96. package/dist/http-server.js +1 -1
  97. package/dist/hunt/hunt-report.d.ts +0 -2
  98. package/dist/hunt/hunt-report.js +1 -1
  99. package/dist/impact/impact-store.d.ts +0 -5
  100. package/dist/impact/impact-store.js +1 -1
  101. package/dist/index.d.ts +4 -2
  102. package/dist/index.js +41 -0
  103. package/dist/init/agent-rules.d.ts +17 -0
  104. package/dist/init/agent-rules.js +31 -1
  105. package/dist/init/capabilities.d.ts +0 -2
  106. package/dist/init/capabilities.js +1 -1
  107. package/dist/init/closing-hint.d.ts +36 -0
  108. package/dist/init/closing-hint.js +120 -0
  109. package/dist/init/confirm.d.ts +0 -25
  110. package/dist/init/confirm.js +3 -3
  111. package/dist/init/cra.d.ts +14 -1
  112. package/dist/init/cra.js +43 -12
  113. package/dist/init/dev-script.d.ts +48 -0
  114. package/dist/init/dev-script.js +63 -0
  115. package/dist/init/mcp.d.ts +16 -5
  116. package/dist/init/mcp.js +26 -10
  117. package/dist/init/node-io.js +3 -0
  118. package/dist/init/plan-framework.d.ts +0 -5
  119. package/dist/init/plan-framework.js +83 -9
  120. package/dist/init/plan.d.ts +10 -30
  121. package/dist/init/plan.js +30 -15
  122. package/dist/init/refresh-rules.d.ts +39 -0
  123. package/dist/init/refresh-rules.js +51 -0
  124. package/dist/init/run.d.ts +2 -10
  125. package/dist/init/run.js +26 -68
  126. package/dist/init/slash-command.d.ts +1 -1
  127. package/dist/init/slash-command.js +33 -10
  128. package/dist/init/snippets.js +53 -10
  129. package/dist/input/real-input.d.ts +2 -0
  130. package/dist/input/real-input.js +3 -2
  131. package/dist/input/scroll-find.d.ts +7 -4
  132. package/dist/input/scroll-find.js +63 -25
  133. package/dist/intent/inline-intent.d.ts +46 -0
  134. package/dist/intent/inline-intent.js +103 -0
  135. package/dist/intent/intent-store.d.ts +37 -0
  136. package/dist/intent/intent-store.js +112 -0
  137. package/dist/intent/intent-tools.d.ts +2 -0
  138. package/dist/intent/intent-tools.js +80 -0
  139. package/dist/intent/open-intents.d.ts +21 -0
  140. package/dist/intent/open-intents.js +25 -0
  141. package/dist/journal/envelope.d.ts +2 -2
  142. package/dist/journal/journal-recorder.d.ts +23 -0
  143. package/dist/journal/journal-recorder.js +35 -0
  144. package/dist/license/license.d.ts +8 -2
  145. package/dist/license/license.js +10 -15
  146. package/dist/mcp/attach-memory.d.ts +67 -0
  147. package/dist/mcp/attach-memory.js +142 -0
  148. package/dist/mcp/mcp-proxy.d.ts +0 -7
  149. package/dist/mcp/mcp-proxy.js +14 -2
  150. package/dist/mcp/mcp.d.ts +2 -0
  151. package/dist/mcp/mcp.js +45 -23
  152. package/dist/mcp/server-instructions.js +45 -3
  153. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  154. package/dist/mcp/tool-catalog-cache.js +10 -0
  155. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  156. package/dist/oracles/flow-instrument-gaps.js +4 -5
  157. package/dist/oracles/self-instrument.d.ts +9 -9
  158. package/dist/oracles/self-instrument.js +19 -13
  159. package/dist/pool/browser-pool.d.ts +12 -1
  160. package/dist/pool/browser-pool.js +31 -1
  161. package/dist/pool/playwright-launcher.js +3 -1
  162. package/dist/project/artifact-root.d.ts +83 -0
  163. package/dist/project/artifact-root.js +112 -0
  164. package/dist/project/fs-port.d.ts +3 -0
  165. package/dist/project/fs-port.js +3 -2
  166. package/dist/project/memory-fs.d.ts +9 -0
  167. package/dist/project/memory-fs.js +93 -0
  168. package/dist/project/project-store.d.ts +17 -0
  169. package/dist/project/project-store.js +33 -0
  170. package/dist/project/project-tools.js +3 -32
  171. package/dist/project/remember-project.d.ts +24 -0
  172. package/dist/project/remember-project.js +40 -0
  173. package/dist/project/reticle-dir.d.ts +2 -0
  174. package/dist/project/reticle-dir.js +1 -0
  175. package/dist/project/session-root.d.ts +15 -0
  176. package/dist/project/session-root.js +31 -0
  177. package/dist/runs/context-tools.d.ts +2 -0
  178. package/dist/runs/context-tools.js +89 -0
  179. package/dist/runs/run-context.d.ts +40 -0
  180. package/dist/runs/run-context.js +158 -0
  181. package/dist/runs/run-diff.d.ts +2 -1
  182. package/dist/runs/run-tools.js +2 -1
  183. package/dist/runs/verification-sync.js +2 -2
  184. package/dist/session/dev-server-probe.d.ts +0 -8
  185. package/dist/session/dev-server-probe.js +1 -1
  186. package/dist/session/last-act.d.ts +7 -1
  187. package/dist/session/last-act.js +2 -2
  188. package/dist/session/lease-availability.d.ts +40 -0
  189. package/dist/session/lease-availability.js +43 -0
  190. package/dist/session/live-control-tools.js +1 -42
  191. package/dist/session/no-session-diagnosis.d.ts +20 -0
  192. package/dist/session/no-session-diagnosis.js +41 -12
  193. package/dist/session/no-session-next-action.d.ts +5 -0
  194. package/dist/session/no-session-next-action.js +35 -3
  195. package/dist/session/no-session-watch.d.ts +8 -0
  196. package/dist/session/no-session-watch.js +83 -65
  197. package/dist/session/output-budget.js +4 -1
  198. package/dist/session/session-health.d.ts +9 -0
  199. package/dist/session/session-health.js +23 -0
  200. package/dist/session/session-manager.js +50 -1
  201. package/dist/session/session-successor.d.ts +37 -0
  202. package/dist/session/session-successor.js +78 -0
  203. package/dist/session/session.d.ts +40 -1
  204. package/dist/session/session.js +84 -2
  205. package/dist/session/stall-clock.d.ts +18 -0
  206. package/dist/session/stall-clock.js +48 -0
  207. package/dist/telemetry/automation-hint.d.ts +2 -1
  208. package/dist/telemetry/daemon-telemetry.js +12 -23
  209. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  210. package/dist/telemetry/project-profile.d.ts +0 -8
  211. package/dist/telemetry/project-profile.js +1 -1
  212. package/dist/telemetry/run-telemetry.d.ts +0 -2
  213. package/dist/telemetry/run-telemetry.js +1 -1
  214. package/dist/telemetry/session-metrics.d.ts +1 -1
  215. package/dist/telemetry/session-metrics.js +1 -1
  216. package/dist/telemetry/telemetry.d.ts +12 -3
  217. package/dist/telemetry/telemetry.js +25 -5
  218. package/dist/telemetry/verification-of.js +2 -2
  219. package/dist/tools/act-tools.js +159 -24
  220. package/dist/tools/assert-source.d.ts +22 -0
  221. package/dist/tools/assert-source.js +53 -0
  222. package/dist/tools/assert-verdict.d.ts +21 -2
  223. package/dist/tools/assert-verdict.js +59 -4
  224. package/dist/tools/browser-tools.js +0 -20
  225. package/dist/tools/complete-snapshot.d.ts +48 -0
  226. package/dist/tools/complete-snapshot.js +75 -0
  227. package/dist/tools/contract-tools.js +5 -3
  228. package/dist/tools/coverage-tools.d.ts +10 -1
  229. package/dist/tools/coverage-tools.js +124 -37
  230. package/dist/tools/dynamic-tools.js +29 -8
  231. package/dist/tools/error-recovery.d.ts +24 -14
  232. package/dist/tools/error-recovery.js +70 -9
  233. package/dist/tools/feedback-tools.d.ts +1 -1
  234. package/dist/tools/feedback-tools.js +2 -2
  235. package/dist/tools/invoke-tool.js +62 -2
  236. package/dist/tools/lease-hint.d.ts +10 -0
  237. package/dist/tools/lease-hint.js +53 -4
  238. package/dist/tools/lease-tools.d.ts +5 -1
  239. package/dist/tools/lease-tools.js +22 -8
  240. package/dist/tools/navigate-arrival.d.ts +0 -2
  241. package/dist/tools/navigate-arrival.js +1 -1
  242. package/dist/tools/observe-tools.js +70 -27
  243. package/dist/tools/read-tools.js +5 -3
  244. package/dist/tools/real-input-attempt.d.ts +22 -0
  245. package/dist/tools/real-input-attempt.js +153 -1
  246. package/dist/tools/reconcile-tools.js +18 -4
  247. package/dist/tools/resolve-target.d.ts +0 -7
  248. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  249. package/dist/tools/sdk-marker-probe.js +57 -0
  250. package/dist/tools/snapshot-delta.d.ts +1 -1
  251. package/dist/tools/tool-kit.d.ts +28 -1
  252. package/dist/tools/tool-kit.js +16 -0
  253. package/dist/tools/tool-names.d.ts +3 -0
  254. package/dist/tools/tool-names.js +3 -0
  255. package/dist/tools/tool-surface.d.ts +85 -6
  256. package/dist/tools/tool-surface.js +122 -2
  257. package/dist/tools/tools.js +22 -4
  258. package/dist/tools/unadvertised-help.js +4 -4
  259. package/dist/tools/verify-next-baton.d.ts +53 -0
  260. package/dist/tools/verify-next-baton.js +52 -0
  261. package/dist/update/update-nudge.d.ts +7 -10
  262. package/dist/update/update-nudge.js +12 -3
  263. package/dist/update/updater.d.ts +1 -1
  264. package/docs/README.md +3 -1
  265. package/docs/cli/cloud.mdx +1 -1
  266. package/docs/cli/doctor.mdx +1 -0
  267. package/docs/cli/verify.mdx +4 -5
  268. package/docs/desktop.mdx +1 -1
  269. package/docs/docs.json +3 -3
  270. package/docs/enterprise.md +3 -1
  271. package/docs/gates.md +1 -1
  272. package/docs/install-manual.mdx +1 -1
  273. package/docs/license-activation.mdx +145 -0
  274. package/docs/packages/browser.mdx +2 -2
  275. package/docs/packages/server.mdx +1 -1
  276. package/docs/packages.mdx +8 -6
  277. package/docs/platform-integration.md +17 -0
  278. package/docs/predicates.mdx +8 -0
  279. package/docs/reticle-manual.md +106 -0
  280. package/docs/telemetry-contract.md +1 -12
  281. package/docs/telemetry-events.mdx +1 -34
  282. package/docs/telemetry.md +0 -1
  283. package/docs/tools-tools-and-run.mdx +2 -4
  284. package/docs/troubleshooting.mdx +9 -0
  285. package/docs/usage.md +3 -2
  286. package/package.json +2 -2
  287. package/dist/session/session-readiness.d.ts +0 -34
  288. package/dist/session/session-readiness.js +0 -36
  289. package/dist/telemetry/instrumentation-stall.d.ts +0 -57
  290. package/dist/telemetry/instrumentation-stall.js +0 -103
package/dist/init/mcp.js CHANGED
@@ -19,11 +19,16 @@ import { MCP_SERVER_NAME } from '@reticlehq/core';
19
19
  /**
20
20
  * The registered command is STILL bare `npx` on every platform, deliberately.
21
21
  *
22
- * Windows is 66% of Reticle's users (65 of 99 in a day of telemetry) and has NO CI, no fixture, and
23
- * nothing in this repo has ever run there. Switching the majority platform to a launch command
24
- * nobody can test — where `.cmd` also has its own spawn caveats in some hosts — risks breaking the
25
- * users it is meant to help. So the fallback is DOCUMENTED (see mcpManual) rather than defaulted,
26
- * until somebody can run it on Windows.
22
+ * Windows is most of Reticle's users and, when this was written, had no CI. There is Windows CI
23
+ * now, so the `cmd /c` fallback is testable. Defaulting the majority platform to that launch
24
+ * command is still the riskier change: `.cmd` has spawn caveats in some hosts, and a registration
25
+ * that works on the machines we can test can still fail in the hosts we cannot. So the fallback
26
+ * stays documented rather than defaulted.
27
+ *
28
+ * What DID change is when it is printed. `mcpManual` used to run only when the `claude` CLI was
29
+ * absent. The reported Windows install had `claude mcp add` succeed, so the one paragraph that
30
+ * would have unblocked zero tools never appeared. `mcpWindowsNote` is that paragraph, and init
31
+ * prints it on every Windows run — including the ones that look like a clean registration.
27
32
  */
28
33
  export const NPX = 'npx';
29
34
  // The `reticle` bin lives in the server package, so `npx <pkg> mcp` runs the bridge without the
@@ -31,6 +36,9 @@ export const NPX = 'npx';
31
36
  const RETICLE_PACKAGE = RETICLE_NPM_PACKAGE;
32
37
  const MCP_SUBCOMMAND = 'mcp';
33
38
  const CLAUDE_CLI = 'claude';
39
+ /** Windows `cmd /c` — the spawn that PowerShell's execution policy does not gate. */
40
+ const WINDOWS_SHELL = 'cmd';
41
+ const WINDOWS_SHELL_C = '/c';
34
42
  /**
35
43
  * Args after `npx` that launch the bridge: `@reticlehq/server mcp`. Portless — the port comes from
36
44
  * the project's `.reticle.json` at runtime, so one global entry works for every project.
@@ -94,6 +102,18 @@ export function claudeExistsProbe() {
94
102
  export function claudeAvailableProbe() {
95
103
  return { command: CLAUDE_CLI, args: ['--version'] };
96
104
  }
105
+ /**
106
+ * The Windows `cmd /c npx` registration. Printed on every Windows init, not only when the `claude`
107
+ * CLI is missing — a successful `claude mcp add` of bare `npx` is exactly the failure that reported
108
+ * as a clean install with zero tools.
109
+ */
110
+ export function mcpWindowsNote() {
111
+ return `On Windows, if the agent cannot start Reticle because npx is blocked ("running scripts is disabled
112
+ on this system" — a PowerShell execution policy), register it through cmd instead, which that policy
113
+ does not gate:
114
+
115
+ "${MCP_SERVER_NAME}": { "command": "${WINDOWS_SHELL}", "args": ${JSON.stringify([WINDOWS_SHELL_C, NPX, ...serverInvocation().slice(1)])} }`;
116
+ }
97
117
  /** Printed when the `claude` CLI isn't available — register Reticle globally once, by hand. */
98
118
  export function mcpManual() {
99
119
  const tail = serverInvocation().join(' ');
@@ -105,9 +125,5 @@ Or, for another agent, add this to its global MCP config (e.g. Cursor's ~/.curso
105
125
 
106
126
  "${MCP_SERVER_NAME}": { "command": "${NPX}", "args": ${JSON.stringify(serverInvocation().slice(1))} }
107
127
 
108
- On Windows, if the agent cannot start Reticle because npx is blocked ("running scripts is disabled
109
- on this system" — a PowerShell execution policy), register it through cmd instead, which that policy
110
- does not gate:
111
-
112
- "${MCP_SERVER_NAME}": { "command": "cmd", "args": ${JSON.stringify(['/c', NPX, ...serverInvocation().slice(1)])} }`;
128
+ ${mcpWindowsNote()}`;
113
129
  }
@@ -52,6 +52,9 @@ export function buildNodeIo(cwd) {
52
52
  homeDir() {
53
53
  return homedir();
54
54
  },
55
+ cwd() {
56
+ return cwd;
57
+ },
55
58
  rootFiles() {
56
59
  return readdirSync(cwd).filter((name) => {
57
60
  try {
@@ -17,11 +17,6 @@ export declare const VITE_PLUGIN_DETAIL: {
17
17
  readonly SVELTEKIT: "add reticle() to plugins (stamps data-reticle-source in .svelte components)";
18
18
  };
19
19
  export declare function viteSteps(input: PlanInput, detail?: string): Step[];
20
- /**
21
- * Next used to be the ONLY stack with hand edits left over — and both of them fail silently when
22
- * skipped, so a Next user's app booted, connected to nothing, and said nothing about why. Both are
23
- * now patched by the same conservative rules the Vite config gets.
24
- */
25
20
  export declare function nextSteps(input: PlanInput): Step[];
26
21
  /**
27
22
  * SvelteKit is WIRED but not SUPPORTED, and the plan says so out loud.
@@ -3,6 +3,7 @@
3
3
  * Split out of `plan.ts` — that file is the plan's SHAPE (statuses, ordering, the agent/MCP steps),
4
4
  * this one is the per-framework detail, and they grow for different reasons.
5
5
  */
6
+ import { bridgeWsUrl } from '@reticlehq/core';
6
7
  import { patchViteConfig, VitePatchKind } from './vite-config.js';
7
8
  import { patchNextConfig, patchRootLayout, patchPagesApp } from './next-patch.js';
8
9
  import { patchAstroConfig, patchAstroLayout } from './astro-patch.js';
@@ -62,6 +63,22 @@ function capabilitiesTodo(path, stores) {
62
63
  * `__reticle_renders` — the state-truth read was unavailable on every app out of the box. Written
63
64
  * only when absent, because it is the one generated file a user is expected to EDIT.
64
65
  */
66
+ /**
67
+ * Does the generated dev module actually REGISTER anything?
68
+ *
69
+ * Shared by the Vite and Next paths because it is one question about one generated call, and the
70
+ * two answered it differently for as long as only Vite asked: `capabilitiesStep` was called from
71
+ * `viteSteps` and nowhere else, so a Next app whose store can only be SUGGESTED wrote
72
+ * `registerCapabilities({ testids: [], stores: [] })`, registered nothing, and said nothing.
73
+ * Measured on a real Next + mobx app: five steps, every one a checkmark, no capabilities line.
74
+ *
75
+ * `wired` are stores init resolved and wrote a live `registerStore` call for. Hints are not
76
+ * registrations: they land in the file as a commented line, and counting one as a registration
77
+ * silences the notice whose whole purpose is to say "act on the hint".
78
+ */
79
+ function registersNothing(testids, wired) {
80
+ return 0 === testids.length && 0 === wired.length;
81
+ }
65
82
  function capabilitiesStep(input) {
66
83
  if (true === input.viteDevModuleExists) {
67
84
  return [
@@ -79,8 +96,21 @@ function capabilitiesStep(input) {
79
96
  const found = testids.length > 0
80
97
  ? `${String(testids.length)} data-testid values`
81
98
  : 'no data-testid values yet';
82
- // A store we found and wired IS a registration, so the "registers nothing" notice must not fire.
83
- const nothingToRegister = 0 === testids.length && 0 === stores.length && 0 === wired.length;
99
+ // A store we found and WIRED is a registration, so the "registers nothing" notice must not fire.
100
+ // A store HINT is not: `stores` holds suggestions, written into the file as a commented line of
101
+ // the form `// import your store, then: registerStore(...)`. Counting a suggestion as a
102
+ // registration let the hint silence the notice whose entire job is to say "act on the hint".
103
+ //
104
+ // Measured against a real product UI (rowy — 70+ deps, jotai, a whole src/atoms tree): init
105
+ // detected jotai, offered one commented line, emitted NO notice, and wrote
106
+ // `registerCapabilities({ testids: [], signals: [], stores: [] })`. So `hasCapabilities` stayed
107
+ // false, `reticle_state` was empty forever, and the install gate reported "connected: 1,
108
+ // manual ⚠: none". Every check green, state observability zero.
109
+ //
110
+ // This is the common case rather than an edge one: a library whose registration needs an argument
111
+ // only reading the source supplies — jotai atoms, an XState actor, a TanStack queryClient — is
112
+ // exactly what init leaves commented.
113
+ const nothingToRegister = registersNothing(testids, wired);
84
114
  return [
85
115
  {
86
116
  title: CAPABILITIES_TITLE,
@@ -197,16 +227,44 @@ function patchStep(title, path, patch, applyDetail, manualDetail) {
197
227
  * skipped, so a Next user's app booted, connected to nothing, and said nothing about why. Both are
198
228
  * now patched by the same conservative rules the Vite config gets.
199
229
  */
230
+ /** The env var withReticle sets from the discovered daemon, and the component must read. */
231
+ const NEXT_DAEMON_URL_ENV = 'NEXT_PUBLIC_RETICLE_URL';
232
+ /**
233
+ * The two-line edit that unfreezes an existing install's port.
234
+ *
235
+ * Stated as the edit rather than as a problem, because the reader is an agent that can apply it in
236
+ * one write and is otherwise going to ask what to do.
237
+ */
238
+ const NEXT_DEV_FILE_STALE_DETAIL = 'predates daemon discovery, so it dials the port init saw when it ran. In this file add ' +
239
+ '`const url = process.env.NEXT_PUBLIC_RETICLE_URL;` and spread `...(url ? { url } : {})` into ' +
240
+ 'reticle.connect(), after any url already there. Nothing else needs to change.';
200
241
  export function nextSteps(input) {
201
242
  const configFile = input.nextConfigFile ?? 'next.config.mjs';
202
243
  const devPath = input.nextReticleDevPath ?? NEXT_RETICLE_DEV_PATH;
244
+ // An install that predates daemon discovery has a component that never reads the discovered URL,
245
+ // so it keeps dialling whatever port `init` saw on the day it ran. Re-running `init` used to call
246
+ // that "file exists" and move on, which is why the defect survives an upgrade. Named as work.
247
+ //
248
+ // Never overwritten: this file is the one an app owner edits — registered stores, capabilities,
249
+ // their own signals. Rewriting it to fix two lines would take the rest with it. Undefined source
250
+ // means it was not read, and an unread file stays ALREADY rather than becoming invented work.
251
+ const devStale = true === input.nextReticleDevExists &&
252
+ 'string' === typeof input.nextReticleDevSource &&
253
+ !input.nextReticleDevSource.includes(NEXT_DAEMON_URL_ENV);
203
254
  const devFile = input.nextReticleDevExists
204
- ? {
205
- title: 'ReticleDev component',
206
- target: devPath,
207
- status: StepStatus.ALREADY,
208
- detail: 'file exists',
209
- }
255
+ ? devStale
256
+ ? {
257
+ title: 'ReticleDev component',
258
+ target: devPath,
259
+ status: StepStatus.MANUAL,
260
+ detail: NEXT_DEV_FILE_STALE_DETAIL,
261
+ }
262
+ : {
263
+ title: 'ReticleDev component',
264
+ target: devPath,
265
+ status: StepStatus.ALREADY,
266
+ detail: 'file exists',
267
+ }
210
268
  : {
211
269
  title: 'ReticleDev component',
212
270
  target: devPath,
@@ -233,8 +291,22 @@ export function nextSteps(input) {
233
291
  : isPagesRouter
234
292
  ? patchPagesApp(layout.source, input.nextReticleDevImport)
235
293
  : patchRootLayout(layout.source);
294
+ // The same question the Vite path asks. Skipped when the module already exists: its contents are
295
+ // the user's, and re-nagging about a file we did not write is noise.
296
+ const nextTodo = true !== input.nextReticleDevExists &&
297
+ registersNothing(input.testids ?? [], input.nextFoundStores ?? [])
298
+ ? [
299
+ {
300
+ title: CAPABILITIES_TODO_TITLE,
301
+ target: devPath,
302
+ status: StepStatus.NOTICE,
303
+ detail: capabilitiesTodo(devPath, input.storeHints ?? []),
304
+ },
305
+ ]
306
+ : [];
236
307
  return [
237
308
  devFile,
309
+ ...nextTodo,
238
310
  patchStep('Next config (withReticle)', configFile, configPatch, 'wrap the export in withReticle (source mapping, dev-only)', nextConfigManual(configFile)),
239
311
  patchStep('Mount ReticleDev', layout?.path ?? NEXT_LAYOUT_PATH, layoutPatch, 'mount <ReticleDev /> in the root layout (dev-only)', NEXT_LAYOUT_MANUAL),
240
312
  ];
@@ -269,7 +341,9 @@ export function craSteps(input) {
269
341
  },
270
342
  ];
271
343
  const token = input.pairingToken ?? '';
272
- const env = craEnvPatch(input.craEnv ?? null, token);
344
+ // The daemon that is live NOW, not the one baked into the module at first install. CRA has no
345
+ // build hook, so this is refreshed by re-running `init` rather than by starting the dev server.
346
+ const env = craEnvPatch(input.craEnv ?? null, token, input.options.port === undefined ? undefined : bridgeWsUrl(input.options.port));
273
347
  if (env !== null) {
274
348
  steps.push({
275
349
  title: 'Pairing token',
@@ -6,16 +6,6 @@
6
6
  import { Framework, UiLibrary, type Detection } from './detect.js';
7
7
  import type { FoundStore } from './capabilities.js';
8
8
  import { type McpClient } from './mcp-clients.js';
9
- /**
10
- * Pin the SDK to the CLI's own version.
11
- *
12
- * `pnpm add -D @reticlehq/react` installed **2.2.1** in one project while npm and yarn took 2.3.0 in
13
- * the next — a stale registry metadata cache, invisible to the user and to us. A version-skewed SDK
14
- * talking to a newer daemon is the `-32000` failure path: the app connects, the protocol disagrees,
15
- * and nothing on either side names a version. Asking for the CLI's exact version makes the cache
16
- * irrelevant, and a skewed pair impossible to install by accident.
17
- */
18
- export declare function pinnedPackages(packages: readonly string[], version: string | undefined): readonly string[];
19
9
  /**
20
10
  * The dev-dependencies `reticle init` installs — kit (or sensor) first, build plugin next.
21
11
  *
@@ -101,6 +91,8 @@ export interface PlanInput {
101
91
  mcpExists: boolean;
102
92
  /** Whether Cursor is installed for this user (its global config dir exists). */
103
93
  cursorPresent: boolean;
94
+ /** `process.platform`. Injected so this module stays pure. Windows is the only branch. */
95
+ platform?: string;
104
96
  /** Whether THIS project has a .cursor/ directory — the signal that Cursor works on this repo. */
105
97
  cursorProjectPresent?: boolean | undefined;
106
98
  /** Current ~/.cursor/mcp.json content, or null if absent. */
@@ -154,6 +146,14 @@ export interface PlanInput {
154
146
  nextReticleDevImport?: string | undefined;
155
147
  /** Whether the ReticleDev component file already exists. */
156
148
  nextReticleDevExists: boolean;
149
+ /**
150
+ * The existing ReticleDev component's source, when there is one.
151
+ *
152
+ * Read so an install predating daemon discovery can be told apart from a current one. Undefined
153
+ * means NOT READ, and an unread file is reported as already-wired rather than as stale: inventing
154
+ * work from missing information is how a plan grows steps that can never be completed.
155
+ */
156
+ nextReticleDevSource?: string | null | undefined;
157
157
  /** `data-testid` values scanned from the app's source, for the generated capabilities block. */
158
158
  testids?: readonly string[] | undefined;
159
159
  /** Ready-to-uncomment `registerStore` lines for the state libraries the app actually depends on. */
@@ -226,24 +226,4 @@ export interface PlanInput {
226
226
  sdkVersion?: string;
227
227
  };
228
228
  }
229
- /**
230
- * Every coding agent `reticle init` knows how to wire itself into, and the three surfaces it wires:
231
- * the global MCP registration, the project rule file that makes the agent USE Reticle, and the
232
- * `/reticle` command.
233
- *
234
- * One list, because those three surfaces used to enumerate Claude and Cursor separately in three
235
- * places — so supporting a fourth agent meant finding all three and getting each right. Adding one
236
- * is now an entry here plus its builders; each builder answers null when that agent is not present
237
- * for this user or this project, which is also how "no agent detected" falls through to the
238
- * cross-agent AGENTS.md.
239
- *
240
- * Deliberately functions rather than data: registering with Claude runs its CLI while Cursor merges
241
- * a JSON file, and pretending those are the same shape would cost more than it saves.
242
- */
243
- declare const AgentId: {
244
- readonly CLAUDE: "claude";
245
- readonly CURSOR: "cursor";
246
- };
247
- export type AgentId = (typeof AgentId)[keyof typeof AgentId];
248
229
  export declare function buildPlan(input: PlanInput): Plan;
249
- export {};
package/dist/init/plan.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * runner performs the `write` side-effects; this module decides *what* should happen.
5
5
  */
6
6
  import { Framework, PackageManager, UiLibrary, installCommand, installCommandParts, } from './detect.js';
7
- import { claudeAddCommand, mcpManual } from './mcp.js';
7
+ import { claudeAddCommand, mcpManual, mcpWindowsNote } from './mcp.js';
8
+ import { NodePlatform } from '../platform.js';
8
9
  import { mergeCursorConfig, CursorMergeStatus, cursorServerEntry } from './cursor.js';
9
10
  import { mergeClientConfig, ClientMergeStatus, clientSnippet, clientSpec, } from './mcp-clients.js';
10
11
  import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH, SLASH_COMMAND_BODY, SLASH_COMMAND_SIGNATURE, } from './slash-command.js';
@@ -32,7 +33,7 @@ const RETICLE_NEXT_PLUGIN = '@reticlehq/next';
32
33
  * and nothing on either side names a version. Asking for the CLI's exact version makes the cache
33
34
  * irrelevant, and a skewed pair impossible to install by accident.
34
35
  */
35
- export function pinnedPackages(packages, version) {
36
+ function pinnedPackages(packages, version) {
36
37
  if (version === undefined || 0 === version.length)
37
38
  return packages;
38
39
  return packages.map((p) => `${p}@${version}`);
@@ -232,19 +233,33 @@ function mcpSteps(input) {
232
233
  // client. A machine with Cursor AND Windsurf gets both — registering only the first one found is
233
234
  // how a user ends up with Reticle in the editor they were not using.
234
235
  const steps = [...stepsForAgents(input, (a) => a.mcpStep), ...otherClientSteps(input)];
235
- if (steps.length > 0)
236
- return steps;
237
- // No supported agent detected — print the one-time global instructions. Reached when the `claude`
238
- // CLI is absent AND ~/.cursor does not exist, which includes Cursor installed with a fresh profile
239
- // that has not written its config directory yet, so the manual text covers both agents.
240
- return [
241
- {
242
- title: 'MCP server (global)',
243
- target: MCP_TARGET,
244
- status: StepStatus.MANUAL,
245
- detail: mcpManual(),
246
- },
247
- ];
236
+ if (0 === steps.length) {
237
+ // No agent detected. mcpManual already carries the Windows cmd fallback — do not append it again.
238
+ return [
239
+ {
240
+ title: 'MCP server (global)',
241
+ target: MCP_TARGET,
242
+ status: StepStatus.MANUAL,
243
+ detail: mcpManual(),
244
+ },
245
+ ];
246
+ }
247
+ const windowsNote = windowsMcpNoteStep(input);
248
+ if (windowsNote !== null)
249
+ steps.push(windowsNote);
250
+ return steps;
251
+ }
252
+ const WINDOWS_MCP_TITLE = 'Windows MCP spawn';
253
+ /** Windows only. The reported install never reached mcpManual because `claude mcp add` succeeded. */
254
+ function windowsMcpNoteStep(input) {
255
+ if (input.platform !== NodePlatform.WINDOWS)
256
+ return null;
257
+ return {
258
+ title: WINDOWS_MCP_TITLE,
259
+ target: MCP_TARGET,
260
+ status: StepStatus.NOTICE,
261
+ detail: mcpWindowsNote(),
262
+ };
248
263
  }
249
264
  const SLASH_COMMAND_TITLE = 'The /reticle command';
250
265
  /**
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Bring an already-installed project's agent rules up to the version that is running.
3
+ *
4
+ * `mergeMarkedInstruction` has always been able to do this: the managed block is marker-delimited,
5
+ * idempotence is decided by comparing CONTENT rather than by seeing a marker, and everything
6
+ * outside the markers is left exactly as the human wrote it. What was missing is anything that
7
+ * CALLS it after the first install.
8
+ *
9
+ * It is reachable only from `buildPlan`, so the rules refresh when somebody re-runs `init` and
10
+ * never otherwise. A project set up on an older release keeps that release's instructions forever,
11
+ * which means every improvement to them — what to do when a capabilities file registers nothing,
12
+ * that an install is not finished until a verdict exists, that intent is captured while building —
13
+ * reaches new projects only.
14
+ *
15
+ * That is the wrong way round. The people who most need better instructions are the ones already
16
+ * installed and not getting value, and `reticle update` is the moment they are already touching the
17
+ * install.
18
+ */
19
+ /** The instruction files the managed block lives in. Cursor's rule file is regenerated whole. */
20
+ export declare const RULE_FILES: readonly ["CLAUDE.md", "AGENTS.md"];
21
+ export interface RuleFileIo {
22
+ read: (path: string) => string | null;
23
+ write: (path: string, content: string) => void;
24
+ }
25
+ export interface RefreshResult {
26
+ /** Files whose managed block was replaced with the current one. */
27
+ updated: string[];
28
+ /** Files that were already current, or that carry no Reticle block to update. */
29
+ unchanged: string[];
30
+ }
31
+ /**
32
+ * Refresh every instruction file that already carries a Reticle block.
33
+ *
34
+ * Deliberately does NOT create one where none exists. A project with no Reticle block in its
35
+ * CLAUDE.md either never ran `init` here or had the block removed on purpose, and `update` is not
36
+ * the place to make that decision for somebody — it is a version command, and quietly writing
37
+ * instructions into a file a user curates would be a surprise.
38
+ */
39
+ export declare function refreshAgentRules(cwd: string, io: RuleFileIo): RefreshResult;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Bring an already-installed project's agent rules up to the version that is running.
3
+ *
4
+ * `mergeMarkedInstruction` has always been able to do this: the managed block is marker-delimited,
5
+ * idempotence is decided by comparing CONTENT rather than by seeing a marker, and everything
6
+ * outside the markers is left exactly as the human wrote it. What was missing is anything that
7
+ * CALLS it after the first install.
8
+ *
9
+ * It is reachable only from `buildPlan`, so the rules refresh when somebody re-runs `init` and
10
+ * never otherwise. A project set up on an older release keeps that release's instructions forever,
11
+ * which means every improvement to them — what to do when a capabilities file registers nothing,
12
+ * that an install is not finished until a verdict exists, that intent is captured while building —
13
+ * reaches new projects only.
14
+ *
15
+ * That is the wrong way round. The people who most need better instructions are the ones already
16
+ * installed and not getting value, and `reticle update` is the moment they are already touching the
17
+ * install.
18
+ */
19
+ import { join } from 'node:path';
20
+ import { mergeMarkedInstruction, AgentRuleStatus, hasManagedBlock } from './agent-rules.js';
21
+ /** The instruction files the managed block lives in. Cursor's rule file is regenerated whole. */
22
+ export const RULE_FILES = ['CLAUDE.md', 'AGENTS.md'];
23
+ /**
24
+ * Refresh every instruction file that already carries a Reticle block.
25
+ *
26
+ * Deliberately does NOT create one where none exists. A project with no Reticle block in its
27
+ * CLAUDE.md either never ran `init` here or had the block removed on purpose, and `update` is not
28
+ * the place to make that decision for somebody — it is a version command, and quietly writing
29
+ * instructions into a file a user curates would be a surprise.
30
+ */
31
+ export function refreshAgentRules(cwd, io) {
32
+ const updated = [];
33
+ const unchanged = [];
34
+ for (const name of RULE_FILES) {
35
+ const path = join(cwd, name);
36
+ const existing = io.read(path);
37
+ if (null === existing || !hasManagedBlock(existing)) {
38
+ unchanged.push(name);
39
+ continue;
40
+ }
41
+ const result = mergeMarkedInstruction(existing);
42
+ if (AgentRuleStatus.APPLY === result.status && result.content !== existing) {
43
+ io.write(path, result.content);
44
+ updated.push(name);
45
+ }
46
+ else {
47
+ unchanged.push(name);
48
+ }
49
+ }
50
+ return { updated, unchanged };
51
+ }
@@ -56,6 +56,8 @@ export interface InitIo {
56
56
  exists(relPath: string): boolean;
57
57
  /** The user's home directory (for global agent config like ~/.cursor/mcp.json). */
58
58
  homeDir(): string;
59
+ /** Absolute path of the directory `init` is running in — the project's own root. */
60
+ cwd(): string;
59
61
  /** Basenames present in the project root. */
60
62
  rootFiles(): readonly string[];
61
63
  /** Subdirectory names inside a project-relative directory; empty when it isn't one. */
@@ -89,14 +91,4 @@ export interface InitResult {
89
91
  * it knows it is in a workspace; this is the matching walk DOWN.
90
92
  */
91
93
  export declare function findWorkspaceApps(io: Pick<InitIo, 'exists' | 'readFile' | 'listDirs'>): string[];
92
- /**
93
- * The standing ask, printed at the end of every `init`.
94
- *
95
- * Addressed to the agent as much as the human: whichever of them just ran this command is the one
96
- * holding the experience, and neither has a Reticle tool surface to file through at this point.
97
- *
98
- * Printed by the `runInit` wrapper, NOT by `report()` — see the comment there for why, and for how
99
- * the workspace redirect is kept from asking twice.
100
- */
101
- export declare const FEEDBACK_HINT: string;
102
94
  export declare function runInit(options: InitOptions, io: InitIo): InitResult;
package/dist/init/run.js CHANGED
@@ -5,16 +5,19 @@
5
5
  */
6
6
  import { dirname, join } from 'node:path';
7
7
  import { noPackageJsonMessage } from './non-js-project.js';
8
+ import { devCommandFrom } from './dev-script.js';
9
+ import { restartHint, FEEDBACK_HINT } from './closing-hint.js';
8
10
  import { spanSync } from '../trace.js';
9
11
  import { readFileSync } from 'node:fs';
10
12
  import { homedir } from 'node:os';
13
+ import { projectIdOf, rememberProjectOnDisk } from '../project/remember-project.js';
11
14
  import { detect, Framework, namesAPackageManager, UiLibrary } from './detect.js';
12
15
  import { wasMcpRegistered } from './mcp-registered.js';
13
16
  import { pickAstroHost } from './astro-host.js';
14
17
  import { workspaceParents } from './workspace-apps.js';
15
18
  import { chooseWorkspaceApp } from './app-choice.js';
16
19
  import { isConnectStep } from './connect-steps.js';
17
- import { CLI, CURSOR_RULE_PATH, RETICLE_MD_PATH } from './agent-rules.js';
20
+ import { CURSOR_RULE_PATH, RETICLE_MD_PATH } from './agent-rules.js';
18
21
  import { CRA_ENV_PATH } from './cra.js';
19
22
  /** CRA's bundled entry, in the order create-react-app itself generates them. */
20
23
  const CRA_ENTRY_CANDIDATES = ['src/index.tsx', 'src/index.jsx', 'src/index.ts', 'src/index.js'];
@@ -351,6 +354,7 @@ function gatherPlanInput(options, io, pkgRaw) {
351
354
  claudeCli,
352
355
  mcpExists,
353
356
  cursorPresent,
357
+ platform: process.platform,
354
358
  detectedClients,
355
359
  cursorProjectPresent: io.exists(CURSOR_DIR_RELPATH),
356
360
  cursorConfig,
@@ -374,6 +378,7 @@ function gatherPlanInput(options, io, pkgRaw) {
374
378
  nextReticleDevPath: devLocation.path,
375
379
  nextReticleDevImport: devLocation.importSpecifier,
376
380
  nextReticleDevExists: io.exists(devLocation.path),
381
+ nextReticleDevSource: io.readFile(devLocation.path),
377
382
  svelteKitHooksExists: io.exists(SVELTEKIT_HOOKS),
378
383
  craEntry: craEntryOf(io),
379
384
  craEnv: io.readFile(CRA_ENV_PATH),
@@ -467,72 +472,11 @@ export function findWorkspaceApps(io) {
467
472
  }
468
473
  return [...new Set(found)];
469
474
  }
470
- /**
471
- * The standing ask, printed at the end of every `init`.
472
- *
473
- * Addressed to the agent as much as the human: whichever of them just ran this command is the one
474
- * holding the experience, and neither has a Reticle tool surface to file through at this point.
475
- *
476
- * Printed by the `runInit` wrapper, NOT by `report()` — see the comment there for why, and for how
477
- * the workspace redirect is kept from asking twice.
478
- */
479
- export const FEEDBACK_HINT = 'Anything wrong, missing, or awkward — in this setup or in Reticle itself? Tell us; it is the ' +
480
- 'one thing that decides what gets fixed:\n' +
481
- ` ${CLI} feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened" (agents)\n` +
482
- ` ${CLI} feedback "what worked, what didn't" (humans)`;
483
- function devServerRestart(framework) {
484
- if (framework === Framework.NEXT)
485
- return 'Restart `next dev`';
486
- if (framework === Framework.VITE)
487
- return 'Restart `vite`';
488
- if (framework === Framework.ASTRO)
489
- return 'Restart `astro dev`';
490
- if (framework === Framework.SVELTEKIT)
491
- return 'Restart your dev server (`npm run dev`)';
492
- return 'Reload your app on localhost';
493
- }
494
- /**
495
- * The last two lines a user reads, and the order matters.
496
- *
497
- * This used to say only "Restart <dev server>, then ask your agent: List Reticle sessions" — and
498
- * asking the agent was the one thing that could not work yet. `init` registers the MCP server, but
499
- * an agent client reads its tool list when it STARTS and never re-reads it, so the session that
500
- * just ran `init` has no `reticle_*` tools however clean the install was. The user follows the
501
- * instruction, the agent answers "unknown tool", and the obvious conclusion is that the install
502
- * failed.
503
- *
504
- * So the reload is named FIRST, before the sentence that depends on it. Omitted entirely when this
505
- * run did not register MCP (`--no-mcp`), where it would be advice about something we did not do.
506
- */
507
- function restartHint(framework, mcpRegistered) {
508
- const dev = `${devServerRestart(framework)}.`;
509
- // NAME THE COMMAND THAT PROVES IT, not one that merely asks.
510
- //
511
- // `init` writes files and stops; the install is not finished until an app carrying the SDK has
512
- // actually dialled the daemon, and nothing here confirmed that. The field shape is unambiguous:
513
- // people complete the agent half, never complete the app half, and keep a daemon running for
514
- // weeks with nothing to drive — so this is the last instruction most of them read.
515
- //
516
- // "Ask your agent: List Reticle sessions" asks a question whose failure is a dead end. `reticle
517
- // status` ANSWERS it: as of 2.7.0 it reports the session, or says why there is none — no app
518
- // running, an app running that never dialled us, a tab that closed — with the fix for each.
519
- const prove = 'Then run `npx @reticlehq/server status` — it confirms the app connected, or says exactly why ' +
520
- 'it has not.';
521
- if (!mcpRegistered)
522
- return `${dev} ${prove}`;
523
- return (`${dev}\n` +
524
- 'Then restart your agent so it picks up the new MCP server — restart Claude Code, reload the ' +
525
- 'window in Cursor, or hit Start in `.vscode/mcp.json` in VS Code.\n' +
526
- 'The tools only appear after that: your agent read its server list before Reticle existed, ' +
527
- 'and no slash command re-reads it (`/mcp` manages servers already loaded, so it cannot pick ' +
528
- 'up a new one). This is once per machine — Reticle is registered globally, so every later ' +
529
- 'project starts with the tools already there.\n' +
530
- `${prove}\n` +
531
- 'Once it shows a session, ask your agent to drive a flow — that is the install finished.');
532
- }
533
475
  const SKIPPED_DETAIL = 'skipped — the dependency install above failed, and wiring the app to a package that is not ' +
534
476
  'installed stops it booting. Run that install, then re-run `reticle init`.';
535
- function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRoot) {
477
+ function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRoot,
478
+ /** The project's own dev command, so the closing line names what a human would type. */
479
+ devCommand) {
536
480
  io.print(dryRun ? 'reticle init (dry run, no files written)' : 'reticle init');
537
481
  // Every path below is printed RELATIVE, and until now nothing said what to. Reported from the
538
482
  // field as "[✓] Reticle config → .reticle.json" followed by the file not being there: the app was
@@ -592,7 +536,7 @@ function report(plan, dryRun, failed, skipped, degraded, io, projectDir, agentRo
592
536
  '"no browser session connected" until then. Everything else is already in place.');
593
537
  io.print('');
594
538
  }
595
- io.print(restartHint(plan.framework, wasMcpRegistered(resolvedStatus(plan, MCP_TARGET, failed, skipped))));
539
+ io.print(restartHint(plan.framework, resolvedStatus(plan, MCP_TARGET, failed, skipped), devCommand));
596
540
  return { ok: !connectPending, applied, manual };
597
541
  }
598
542
  /**
@@ -684,6 +628,17 @@ function applyEffects(plan, io) {
684
628
  installFailed = true;
685
629
  }
686
630
  }
631
+ // Where this project lives, remembered for a daemon that will be started somewhere else.
632
+ //
633
+ // Deliberately AFTER the loop and unconditional on which steps ran: most init runs report "already
634
+ // wired", and a re-run in a re-cloned or moved checkout is exactly when the remembered path has
635
+ // gone stale — so the run that would otherwise be a no-op is the one that repairs the entry. The
636
+ // return value is ignored on purpose; this is a cache, and a read-only home directory must not
637
+ // turn a wired project into a failed init.
638
+ const projectId = projectIdOf(io.readFile(RETICLE_CONFIG_FILE));
639
+ if (projectId !== undefined) {
640
+ rememberProjectOnDisk(io, projectId, io.cwd(), Date.now());
641
+ }
687
642
  return { failed, skipped, degraded };
688
643
  }
689
644
  /**
@@ -803,12 +758,15 @@ function runInitSteps(options, io) {
803
758
  // Init is the flow a user experiences the wait of personally, and the fixture gate measures it at
804
759
  // 1–6s per app with no explanation of the spread. These three spans split that number into detect
805
760
  // (filesystem probing), plan (pure), and apply (writes + package-manager and CLI subprocesses).
806
- const plan = spanSync('init.plan', {}, () => buildPlan(gatherPlanInput(options, io, pkgRaw)));
761
+ const planInput = gatherPlanInput(options, io, pkgRaw);
762
+ const plan = spanSync('init.plan', {}, () => buildPlan(planInput));
807
763
  const effects = options.dryRun
808
764
  ? { failed: new Set(), skipped: new Set(), degraded: new Map() }
809
765
  : spanSync('init.apply', { steps: plan.steps.length }, () => applyEffects(plan, io));
810
766
  const { failed, skipped, degraded } = effects;
811
- const result = report(plan, options.dryRun, failed, skipped, degraded, io, options.cwd, agentRootOf(options));
767
+ // The project's own dev command, so the closing line names what a human would actually type.
768
+ const devCommand = devCommandFrom(pkgRaw, planInput.detection.packageManager);
769
+ const result = report(plan, options.dryRun, failed, skipped, degraded, io, options.cwd, agentRootOf(options), devCommand);
812
770
  // A dry run is a preview, not an outcome — reporting it would inflate both success and failure.
813
771
  if (options.dryRun)
814
772
  return result;