@reticlehq/server 2.11.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 (252) hide show
  1. package/SKILL.md +26 -2
  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 +0 -2
  45. package/dist/dev/stale-issue-guard.js +1 -1
  46. package/dist/events/contradictions.d.ts +41 -0
  47. package/dist/events/contradictions.js +125 -12
  48. package/dist/events/declared.d.ts +14 -0
  49. package/dist/events/declared.js +33 -0
  50. package/dist/events/echo-mismatch.js +14 -0
  51. package/dist/events/observed-in-window.d.ts +12 -0
  52. package/dist/events/observed-in-window.js +17 -0
  53. package/dist/events/predicate-asks.d.ts +13 -0
  54. package/dist/events/predicate-asks.js +22 -0
  55. package/dist/events/predicate-eval.js +64 -22
  56. package/dist/events/predicate-parse.d.ts +0 -6
  57. package/dist/events/predicate-parse.js +43 -3
  58. package/dist/events/predicate-schema.d.ts +44 -0
  59. package/dist/events/predicate-schema.js +126 -23
  60. package/dist/events/predicate.d.ts +15 -0
  61. package/dist/events/predicate.js +41 -2
  62. package/dist/flows/coverage.d.ts +0 -8
  63. package/dist/flows/coverage.js +1 -1
  64. package/dist/flows/decision.d.ts +5 -1
  65. package/dist/flows/decision.js +33 -7
  66. package/dist/flows/flake.d.ts +0 -2
  67. package/dist/flows/flake.js +1 -1
  68. package/dist/flows/flow-intent.d.ts +52 -0
  69. package/dist/flows/flow-intent.js +123 -0
  70. package/dist/flows/flow-replay-run.js +27 -4
  71. package/dist/flows/flow-success.js +15 -3
  72. package/dist/flows/flow-tools.js +32 -4
  73. package/dist/flows/flows.d.ts +6 -1
  74. package/dist/flows/flows.js +34 -22
  75. package/dist/flows/predicate-to-expect.js +6 -3
  76. package/dist/flows/server-verify.js +16 -2
  77. package/dist/flows/verify-change-tools.js +4 -1
  78. package/dist/honesty/blind-spots.d.ts +0 -17
  79. package/dist/honesty/blind-spots.js +1 -1
  80. package/dist/honesty/feature-capture.d.ts +150 -0
  81. package/dist/honesty/feature-capture.js +233 -0
  82. package/dist/honesty/instrumentation-gaps.d.ts +30 -6
  83. package/dist/honesty/instrumentation-gaps.js +61 -5
  84. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  85. package/dist/honesty/tool-hit-rate.js +90 -0
  86. package/dist/honesty/undeclared-change.d.ts +51 -0
  87. package/dist/honesty/undeclared-change.js +56 -0
  88. package/dist/honesty/verified.d.ts +15 -0
  89. package/dist/honesty/verified.js +68 -4
  90. package/dist/http-server.d.ts +0 -1
  91. package/dist/http-server.js +1 -1
  92. package/dist/hunt/hunt-report.d.ts +0 -2
  93. package/dist/hunt/hunt-report.js +1 -1
  94. package/dist/impact/impact-store.d.ts +0 -5
  95. package/dist/impact/impact-store.js +1 -1
  96. package/dist/index.d.ts +4 -1
  97. package/dist/init/agent-rules.d.ts +17 -0
  98. package/dist/init/agent-rules.js +31 -1
  99. package/dist/init/capabilities.d.ts +0 -2
  100. package/dist/init/capabilities.js +1 -1
  101. package/dist/init/closing-hint.d.ts +36 -0
  102. package/dist/init/closing-hint.js +120 -0
  103. package/dist/init/confirm.d.ts +0 -25
  104. package/dist/init/confirm.js +3 -3
  105. package/dist/init/cra.d.ts +14 -1
  106. package/dist/init/cra.js +43 -12
  107. package/dist/init/dev-script.d.ts +48 -0
  108. package/dist/init/dev-script.js +63 -0
  109. package/dist/init/mcp.d.ts +16 -5
  110. package/dist/init/mcp.js +26 -10
  111. package/dist/init/plan-framework.d.ts +0 -5
  112. package/dist/init/plan-framework.js +83 -9
  113. package/dist/init/plan.d.ts +10 -30
  114. package/dist/init/plan.js +30 -15
  115. package/dist/init/refresh-rules.d.ts +39 -0
  116. package/dist/init/refresh-rules.js +51 -0
  117. package/dist/init/run.d.ts +0 -26
  118. package/dist/init/run.js +14 -87
  119. package/dist/init/snippets.js +53 -10
  120. package/dist/input/real-input.d.ts +2 -0
  121. package/dist/input/real-input.js +3 -2
  122. package/dist/input/scroll-find.d.ts +7 -4
  123. package/dist/input/scroll-find.js +63 -25
  124. package/dist/intent/inline-intent.d.ts +46 -0
  125. package/dist/intent/inline-intent.js +103 -0
  126. package/dist/intent/intent-store.d.ts +37 -0
  127. package/dist/intent/intent-store.js +112 -0
  128. package/dist/intent/intent-tools.d.ts +2 -0
  129. package/dist/intent/intent-tools.js +80 -0
  130. package/dist/intent/open-intents.d.ts +21 -0
  131. package/dist/intent/open-intents.js +25 -0
  132. package/dist/journal/envelope.d.ts +26 -26
  133. package/dist/journal/journal-recorder.d.ts +23 -0
  134. package/dist/journal/journal-recorder.js +35 -0
  135. package/dist/mcp/attach-memory.d.ts +67 -0
  136. package/dist/mcp/attach-memory.js +142 -0
  137. package/dist/mcp/mcp-proxy.d.ts +0 -7
  138. package/dist/mcp/mcp-proxy.js +14 -2
  139. package/dist/mcp/mcp.d.ts +2 -0
  140. package/dist/mcp/mcp.js +45 -23
  141. package/dist/mcp/server-instructions.js +45 -3
  142. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  143. package/dist/mcp/tool-catalog-cache.js +10 -0
  144. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  145. package/dist/oracles/flow-instrument-gaps.js +4 -5
  146. package/dist/oracles/self-instrument.d.ts +9 -9
  147. package/dist/oracles/self-instrument.js +19 -13
  148. package/dist/pool/browser-pool.d.ts +12 -1
  149. package/dist/pool/browser-pool.js +31 -1
  150. package/dist/pool/playwright-launcher.js +3 -1
  151. package/dist/project/fs-port.d.ts +3 -0
  152. package/dist/project/fs-port.js +3 -2
  153. package/dist/project/memory-fs.js +6 -1
  154. package/dist/project/project-tools.js +3 -32
  155. package/dist/project/reticle-dir.d.ts +2 -0
  156. package/dist/project/reticle-dir.js +1 -0
  157. package/dist/runs/context-tools.d.ts +2 -0
  158. package/dist/runs/context-tools.js +89 -0
  159. package/dist/runs/run-context.d.ts +40 -0
  160. package/dist/runs/run-context.js +158 -0
  161. package/dist/runs/run-diff.d.ts +2 -1
  162. package/dist/runs/verification-sync.js +2 -2
  163. package/dist/session/dev-server-probe.d.ts +0 -8
  164. package/dist/session/dev-server-probe.js +1 -1
  165. package/dist/session/last-act.d.ts +7 -1
  166. package/dist/session/last-act.js +2 -2
  167. package/dist/session/live-control-tools.js +1 -42
  168. package/dist/session/no-session-diagnosis.d.ts +10 -0
  169. package/dist/session/no-session-diagnosis.js +24 -7
  170. package/dist/session/no-session-next-action.d.ts +5 -0
  171. package/dist/session/no-session-next-action.js +35 -3
  172. package/dist/session/no-session-watch.d.ts +8 -0
  173. package/dist/session/no-session-watch.js +82 -64
  174. package/dist/session/output-budget.js +4 -1
  175. package/dist/session/session-health.d.ts +9 -0
  176. package/dist/session/session-health.js +23 -0
  177. package/dist/session/session-manager.js +50 -1
  178. package/dist/session/session-successor.d.ts +37 -0
  179. package/dist/session/session-successor.js +78 -0
  180. package/dist/session/session.d.ts +31 -1
  181. package/dist/session/session.js +75 -2
  182. package/dist/telemetry/automation-hint.d.ts +2 -1
  183. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  184. package/dist/telemetry/project-profile.d.ts +0 -8
  185. package/dist/telemetry/project-profile.js +1 -1
  186. package/dist/telemetry/run-telemetry.d.ts +0 -2
  187. package/dist/telemetry/run-telemetry.js +1 -1
  188. package/dist/telemetry/verification-of.js +2 -2
  189. package/dist/tools/act-tools.js +126 -21
  190. package/dist/tools/assert-source.d.ts +22 -0
  191. package/dist/tools/assert-source.js +53 -0
  192. package/dist/tools/assert-verdict.d.ts +20 -3
  193. package/dist/tools/assert-verdict.js +36 -4
  194. package/dist/tools/browser-tools.js +0 -20
  195. package/dist/tools/complete-snapshot.d.ts +48 -0
  196. package/dist/tools/complete-snapshot.js +75 -0
  197. package/dist/tools/coverage-tools.d.ts +10 -1
  198. package/dist/tools/coverage-tools.js +123 -69
  199. package/dist/tools/dynamic-tools.js +29 -8
  200. package/dist/tools/error-recovery.d.ts +24 -14
  201. package/dist/tools/error-recovery.js +70 -9
  202. package/dist/tools/feedback-tools.d.ts +1 -1
  203. package/dist/tools/feedback-tools.js +2 -2
  204. package/dist/tools/invoke-tool.js +62 -2
  205. package/dist/tools/lease-hint.d.ts +10 -0
  206. package/dist/tools/lease-hint.js +53 -4
  207. package/dist/tools/lease-tools.d.ts +5 -1
  208. package/dist/tools/lease-tools.js +22 -8
  209. package/dist/tools/navigate-arrival.d.ts +0 -2
  210. package/dist/tools/navigate-arrival.js +1 -1
  211. package/dist/tools/observe-tools.js +63 -26
  212. package/dist/tools/read-tools.js +5 -3
  213. package/dist/tools/real-input-attempt.d.ts +22 -0
  214. package/dist/tools/real-input-attempt.js +153 -1
  215. package/dist/tools/reconcile-tools.js +18 -4
  216. package/dist/tools/resolve-target.d.ts +0 -7
  217. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  218. package/dist/tools/sdk-marker-probe.js +57 -0
  219. package/dist/tools/snapshot-delta.d.ts +1 -1
  220. package/dist/tools/tool-kit.d.ts +12 -0
  221. package/dist/tools/tool-kit.js +16 -0
  222. package/dist/tools/tool-names.d.ts +3 -0
  223. package/dist/tools/tool-names.js +3 -0
  224. package/dist/tools/tool-surface.d.ts +85 -6
  225. package/dist/tools/tool-surface.js +122 -2
  226. package/dist/tools/tools.js +22 -4
  227. package/dist/tools/unadvertised-help.js +4 -4
  228. package/dist/tools/verify-next-baton.d.ts +53 -0
  229. package/dist/tools/verify-next-baton.js +52 -0
  230. package/dist/update/update-nudge.d.ts +7 -10
  231. package/dist/update/update-nudge.js +12 -3
  232. package/dist/update/updater.d.ts +1 -1
  233. package/docs/README.md +2 -1
  234. package/docs/cli/cloud.mdx +1 -1
  235. package/docs/cli/doctor.mdx +1 -0
  236. package/docs/cli/verify.mdx +4 -5
  237. package/docs/desktop.mdx +1 -1
  238. package/docs/docs.json +2 -2
  239. package/docs/gates.md +1 -1
  240. package/docs/install-manual.mdx +1 -1
  241. package/docs/packages/browser.mdx +2 -2
  242. package/docs/packages/server.mdx +1 -1
  243. package/docs/packages.mdx +8 -6
  244. package/docs/platform-integration.md +17 -0
  245. package/docs/predicates.mdx +8 -0
  246. package/docs/reticle-manual.md +106 -0
  247. package/docs/tools-tools-and-run.mdx +2 -4
  248. package/docs/troubleshooting.mdx +9 -0
  249. package/docs/usage.md +3 -2
  250. package/package.json +2 -2
  251. package/dist/session/session-readiness.d.ts +0 -34
  252. package/dist/session/session-readiness.js +0 -36
package/dist/init/cra.js CHANGED
@@ -12,12 +12,26 @@
12
12
  * `REACT_APP_`. So it travels through `.env.development.local`, which is CRA's own documented
13
13
  * mechanism and is gitignored by CRA's own template.
14
14
  */
15
+ import { bridgeWsUrl } from '@reticlehq/core';
15
16
  import { CLI } from './agent-rules.js';
16
17
  /** The line added to `src/index.tsx`. Side-effect import: the module guards itself on NODE_ENV. */
17
18
  export const CRA_DEV_MODULE_IMPORT = "import './reticle-dev';";
18
19
  export const CRA_DEV_MODULE_PATH = 'src/reticle-dev.ts';
19
20
  export const CRA_ENV_PATH = '.env.development.local';
20
21
  export const TOKEN_VAR = 'REACT_APP_RETICLE_TOKEN';
22
+ /**
23
+ * The bridge URL, carried through the same channel as the token.
24
+ *
25
+ * CRA is the one supported stack with no build-time hook of ours: no plugin, no config wrapper, no
26
+ * `define`. Vite and Astro re-resolve the daemon every dev-server start and Next does it through
27
+ * `withReticle`; CRA has nowhere to run that code. So the URL goes where the token already goes, and
28
+ * is refreshed by the same `init` step that refreshes the token.
29
+ *
30
+ * That makes `reticle init` the heal rather than the dev server, which is weaker and is stated
31
+ * plainly in the step's own detail. It is still strictly better than a URL frozen into a source file
32
+ * the owner has to hand-edit, and `.env.development.local` is already the per-machine file.
33
+ */
34
+ export const URL_VAR = 'REACT_APP_RETICLE_URL';
21
35
  /**
22
36
  * What the app itself says when the token is not there.
23
37
  *
@@ -64,25 +78,38 @@ export function craImportPatch(source) {
64
78
  return lines.join('\n');
65
79
  }
66
80
  /** Set the token variable, or null when nothing needs to change. */
67
- export function craEnvPatch(existing, token) {
81
+ export function craEnvPatch(existing, token, url) {
68
82
  if ('' === token)
69
83
  return null;
70
- const line = `${TOKEN_VAR}=${token}`;
71
- if (null === existing || '' === existing.trim())
72
- return `${line}\n`;
73
- if (existing.includes(line))
74
- return null;
75
- // Replace rather than append: two assignments of one variable is a silent coin flip on which wins.
76
- if (new RegExp(`^${TOKEN_VAR}=`, 'm').test(existing)) {
77
- return existing.replace(new RegExp(`^${TOKEN_VAR}=.*$`, 'm'), line);
84
+ const pairs = [
85
+ [TOKEN_VAR, token],
86
+ ...(url !== undefined && url.length > 0 ? [[URL_VAR, url]] : []),
87
+ ];
88
+ let next = null === existing || '' === existing.trim() ? '' : existing;
89
+ for (const [name, value] of pairs) {
90
+ const line = `${name}=${value}`;
91
+ if (next.includes(line))
92
+ continue;
93
+ // Replace rather than append: two assignments of one variable is a silent coin flip on which
94
+ // wins, and for the URL that coin decides which daemon the app talks to.
95
+ if (new RegExp(`^${name}=`, 'm').test(next)) {
96
+ next = next.replace(new RegExp(`^${name}=.*$`, 'm'), line);
97
+ }
98
+ else {
99
+ next = `${'' === next || next.endsWith('\n') ? next : `${next}\n`}${line}\n`;
100
+ }
78
101
  }
79
- return `${existing.endsWith('\n') ? existing : `${existing}\n`}${line}\n`;
102
+ return next === (existing ?? '') ? null : next;
80
103
  }
81
104
  /** The dev-only connect module imported from `src/index.tsx`. */
82
105
  export function craDevModuleFile(port, projectId) {
83
106
  const fields = [];
107
+ // `bridgeWsUrl`, not a hand-written string. This was the only client URL in the product spelling
108
+ // the host as `127.0.0.1` while every other generator said `localhost` — same endpoint, but a
109
+ // difference with no reason behind it is the kind that becomes a real one after somebody edits half
110
+ // of it.
84
111
  if (port !== undefined)
85
- fields.push(`url: 'ws://127.0.0.1:${String(port)}/reticle'`);
112
+ fields.push(`url: '${bridgeWsUrl(port)}'`);
86
113
  if (projectId !== undefined && projectId.length > 0)
87
114
  fields.push(`projectId: '${projectId}'`);
88
115
  const inline = fields.length > 0 ? `${fields.join(', ')}, ` : '';
@@ -95,10 +122,14 @@ if (process.env.NODE_ENV === 'development') {
95
122
  void import('@reticlehq/react').then(({ reticle, install }) => {
96
123
  install();
97
124
  const token = process.env.${TOKEN_VAR} ?? '';
125
+ // Written by \`reticle init\` from the daemon that was live when it ran, and refreshed by
126
+ // re-running it. CRA gives us no hook to resolve this at dev-server start, so if the daemon
127
+ // moves, re-run \`reticle init\` rather than editing the url below.
128
+ const url = process.env.${URL_VAR} ?? '';
98
129
  // Loud on purpose: without this the only symptom is the bridge's generic auth failure.
99
130
  if (token.length === 0) console.error(${JSON.stringify(`[reticle] ${CRA_TOKEN_MISSING_NOTE}`)});
100
131
  // Still attempt it — a bridge running without a token pairs fine.
101
- reticle.connect({ ${inline}...(token.length > 0 ? { token } : {}) });
132
+ reticle.connect({ ${inline}...(url.length > 0 ? { url } : {}), ...(token.length > 0 ? { token } : {}) });
102
133
  });
103
134
  }
104
135
 
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Which command starts this project, and should we start it ourselves?
3
+ *
4
+ * `init` finishes by telling the human to start or restart their dev server, in a closing
5
+ * paragraph, in a terminal that is about to be cleared by the client restart it also asks for.
6
+ * That is the second unmarked hand-off in the install, after the capabilities file, and it lands
7
+ * on the same surface: prose nobody re-reads.
8
+ *
9
+ * Every fact needed to remove it is already in the project. `package.json` names the script; the
10
+ * framework tells us the conventional port; a probe tells us whether something already answers
11
+ * there. Nothing here needs a human.
12
+ *
13
+ * Kept as a PURE decision, separate from spawning, because the interesting part is the judgement —
14
+ * which script, and whether to touch anything at all — and that is what wants testing. Spawning a
15
+ * process is not.
16
+ */
17
+ export declare const DevScriptChoice: {
18
+ /** Something already answers on the port. Use it; never replace a server the user is running. */
19
+ readonly ALREADY_SERVING: "already-serving";
20
+ /** We know the command and nothing is listening. Start it. */
21
+ readonly START: "start";
22
+ /** No script we recognise. Say so rather than guess a command. */
23
+ readonly NO_SCRIPT: "no-script";
24
+ };
25
+ export type DevScriptChoice = (typeof DevScriptChoice)[keyof typeof DevScriptChoice];
26
+ export interface DevScriptPlan {
27
+ choice: DevScriptChoice;
28
+ /** The npm script name, when there is one. */
29
+ script?: string;
30
+ /** What a human would type, for the line we print. */
31
+ command?: string;
32
+ }
33
+ /**
34
+ * Pick the script and decide whether to run it.
35
+ *
36
+ * `serving` is the probe result, passed in rather than measured here so the decision stays pure.
37
+ * When something is already answering we do not care which script exists: the rule is never to
38
+ * start a second server, because the one that is running may be the user's, with their state in it.
39
+ */
40
+ export declare function planDevScript(scripts: Readonly<Record<string, string>>, packageManager: string, serving: boolean): DevScriptPlan;
41
+ /**
42
+ * The command to print for this project, straight from its `package.json`.
43
+ *
44
+ * Lives here rather than in `run.ts` because it is the same judgement as `planDevScript` with the
45
+ * parsing attached, and `run.ts` is at its cohesion limit. Returns undefined for anything it cannot
46
+ * read: this phrasing decides nothing, so it must never be able to break an install.
47
+ */
48
+ export declare function devCommandFrom(pkgRaw: string, packageManager: string): string | undefined;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Which command starts this project, and should we start it ourselves?
3
+ *
4
+ * `init` finishes by telling the human to start or restart their dev server, in a closing
5
+ * paragraph, in a terminal that is about to be cleared by the client restart it also asks for.
6
+ * That is the second unmarked hand-off in the install, after the capabilities file, and it lands
7
+ * on the same surface: prose nobody re-reads.
8
+ *
9
+ * Every fact needed to remove it is already in the project. `package.json` names the script; the
10
+ * framework tells us the conventional port; a probe tells us whether something already answers
11
+ * there. Nothing here needs a human.
12
+ *
13
+ * Kept as a PURE decision, separate from spawning, because the interesting part is the judgement —
14
+ * which script, and whether to touch anything at all — and that is what wants testing. Spawning a
15
+ * process is not.
16
+ */
17
+ /** Script names a JS project uses to run itself, in the order we would pick them. */
18
+ const CANDIDATES = ['dev', 'start', 'serve'];
19
+ export const DevScriptChoice = {
20
+ /** Something already answers on the port. Use it; never replace a server the user is running. */
21
+ ALREADY_SERVING: 'already-serving',
22
+ /** We know the command and nothing is listening. Start it. */
23
+ START: 'start',
24
+ /** No script we recognise. Say so rather than guess a command. */
25
+ NO_SCRIPT: 'no-script',
26
+ };
27
+ /**
28
+ * Pick the script and decide whether to run it.
29
+ *
30
+ * `serving` is the probe result, passed in rather than measured here so the decision stays pure.
31
+ * When something is already answering we do not care which script exists: the rule is never to
32
+ * start a second server, because the one that is running may be the user's, with their state in it.
33
+ */
34
+ export function planDevScript(scripts, packageManager, serving) {
35
+ if (serving)
36
+ return { choice: DevScriptChoice.ALREADY_SERVING };
37
+ const name = CANDIDATES.find((c) => 'string' === typeof scripts[c] && scripts[c] !== '');
38
+ if (name === undefined)
39
+ return { choice: DevScriptChoice.NO_SCRIPT };
40
+ // `npm` needs `run`; the others take the script name directly. Getting this wrong prints a
41
+ // command that fails, which is worse than printing nothing.
42
+ const command = 'npm' === packageManager ? `npm run ${name}` : `${packageManager} ${name}`;
43
+ return { choice: DevScriptChoice.START, script: name, command };
44
+ }
45
+ /**
46
+ * The command to print for this project, straight from its `package.json`.
47
+ *
48
+ * Lives here rather than in `run.ts` because it is the same judgement as `planDevScript` with the
49
+ * parsing attached, and `run.ts` is at its cohesion limit. Returns undefined for anything it cannot
50
+ * read: this phrasing decides nothing, so it must never be able to break an install.
51
+ */
52
+ export function devCommandFrom(pkgRaw, packageManager) {
53
+ try {
54
+ const parsed = JSON.parse(pkgRaw);
55
+ const scripts = 'object' === typeof parsed && null !== parsed
56
+ ? (parsed.scripts ?? {})
57
+ : {};
58
+ return planDevScript(scripts, packageManager, false).command;
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
@@ -15,11 +15,16 @@ export { MCP_SERVER_NAME } from '@reticlehq/core';
15
15
  /**
16
16
  * The registered command is STILL bare `npx` on every platform, deliberately.
17
17
  *
18
- * Windows is 66% of Reticle's users (65 of 99 in a day of telemetry) and has NO CI, no fixture, and
19
- * nothing in this repo has ever run there. Switching the majority platform to a launch command
20
- * nobody can test — where `.cmd` also has its own spawn caveats in some hosts — risks breaking the
21
- * users it is meant to help. So the fallback is DOCUMENTED (see mcpManual) rather than defaulted,
22
- * until somebody can run it on Windows.
18
+ * Windows is most of Reticle's users and, when this was written, had no CI. There is Windows CI
19
+ * now, so the `cmd /c` fallback is testable. Defaulting the majority platform to that launch
20
+ * command is still the riskier change: `.cmd` has spawn caveats in some hosts, and a registration
21
+ * that works on the machines we can test can still fail in the hosts we cannot. So the fallback
22
+ * stays documented rather than defaulted.
23
+ *
24
+ * What DID change is when it is printed. `mcpManual` used to run only when the `claude` CLI was
25
+ * absent. The reported Windows install had `claude mcp add` succeed, so the one paragraph that
26
+ * would have unblocked zero tools never appeared. `mcpWindowsNote` is that paragraph, and init
27
+ * prints it on every Windows run — including the ones that look like a clean registration.
23
28
  */
24
29
  export declare const NPX = "npx";
25
30
  /**
@@ -74,5 +79,11 @@ export declare function claudeAvailableProbe(): {
74
79
  command: string;
75
80
  args: string[];
76
81
  };
82
+ /**
83
+ * The Windows `cmd /c npx` registration. Printed on every Windows init, not only when the `claude`
84
+ * CLI is missing — a successful `claude mcp add` of bare `npx` is exactly the failure that reported
85
+ * as a clean install with zero tools.
86
+ */
87
+ export declare function mcpWindowsNote(): string;
77
88
  /** Printed when the `claude` CLI isn't available — register Reticle globally once, by hand. */
78
89
  export declare function mcpManual(): string;
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
  }
@@ -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;