@reticlehq/init 2.14.0 → 3.2.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 (125) hide show
  1. package/dist/{detect.d.ts → detect/detect.d.ts} +1 -1
  2. package/dist/{detect.js → detect/detect.js} +6 -2
  3. package/dist/{dev-server-ports.js → detect/dev-server-ports.js} +8 -0
  4. package/dist/{existing-config.d.ts → detect/existing-config.d.ts} +2 -4
  5. package/dist/{existing-config.js → detect/existing-config.js} +2 -4
  6. package/dist/{lockfiles.d.ts → detect/lockfiles.d.ts} +1 -1
  7. package/dist/{lockfiles.js → detect/lockfiles.js} +1 -2
  8. package/dist/{workspace-apps.d.ts → detect/workspace-apps.d.ts} +3 -5
  9. package/dist/{workspace-apps.js → detect/workspace-apps.js} +2 -4
  10. package/dist/{workspace-redirect.d.ts → detect/workspace-redirect.d.ts} +3 -5
  11. package/dist/{workspace-redirect.js → detect/workspace-redirect.js} +8 -12
  12. package/dist/{closing-hint.d.ts → diagnose/closing-hint.d.ts} +3 -4
  13. package/dist/{closing-hint.js → diagnose/closing-hint.js} +14 -19
  14. package/dist/diagnose/containerised-dev-server.d.ts +32 -0
  15. package/dist/diagnose/containerised-dev-server.js +56 -0
  16. package/dist/{csp-check.d.ts → diagnose/csp-check.d.ts} +2 -2
  17. package/dist/{csp-check.js → diagnose/csp-check.js} +2 -2
  18. package/dist/{desktop-doctor.js → diagnose/desktop-doctor.js} +12 -6
  19. package/dist/{init-failure.d.ts → diagnose/init-failure.d.ts} +1 -2
  20. package/dist/{init-failure.js → diagnose/init-failure.js} +1 -2
  21. package/dist/diagnose/install-hint.d.ts +8 -0
  22. package/dist/{install-hint.js → diagnose/install-hint.js} +3 -4
  23. package/dist/{install-retries.d.ts → diagnose/install-retries.d.ts} +3 -4
  24. package/dist/{install-retries.js → diagnose/install-retries.js} +6 -8
  25. package/dist/host.d.ts +14 -1
  26. package/dist/host.js +3 -0
  27. package/dist/index.d.ts +32 -12
  28. package/dist/index.js +36 -12
  29. package/dist/node-io.d.ts +12 -1
  30. package/dist/node-io.js +53 -25
  31. package/dist/{astro-patch.d.ts → patch/astro-patch.d.ts} +1 -1
  32. package/dist/{astro-patch.js → patch/astro-patch.js} +1 -1
  33. package/dist/{cra.js → patch/cra.js} +1 -1
  34. package/dist/{electron-patch.js → patch/electron-patch.js} +1 -1
  35. package/dist/{next-patch.d.ts → patch/next-patch.d.ts} +2 -2
  36. package/dist/{next-patch.js → patch/next-patch.js} +2 -2
  37. package/dist/{nuxt-patch.js → patch/nuxt-patch.js} +1 -1
  38. package/dist/{snippets.d.ts → patch/snippets.d.ts} +11 -21
  39. package/dist/{snippets.js → patch/snippets.js} +20 -29
  40. package/dist/{tanstack-start.d.ts → patch/tanstack-start.d.ts} +2 -3
  41. package/dist/{tanstack-start.js → patch/tanstack-start.js} +3 -4
  42. package/dist/{vite-config.js → patch/vite-config.js} +15 -13
  43. package/dist/{connect-steps.d.ts → plan/connect-steps.d.ts} +3 -3
  44. package/dist/{connect-steps.js → plan/connect-steps.js} +4 -4
  45. package/dist/{framework-adapter.d.ts → plan/framework-adapter.d.ts} +11 -2
  46. package/dist/{framework-adapter.js → plan/framework-adapter.js} +12 -1
  47. package/dist/plan/notices.d.ts +23 -0
  48. package/dist/plan/notices.js +94 -0
  49. package/dist/{plan-electron-vite.d.ts → plan/plan-electron-vite.d.ts} +3 -4
  50. package/dist/{plan-electron-vite.js → plan/plan-electron-vite.js} +7 -8
  51. package/dist/{plan-framework.d.ts → plan/plan-framework.d.ts} +4 -13
  52. package/dist/{plan-framework.js → plan/plan-framework.js} +14 -34
  53. package/dist/{plan.d.ts → plan/plan-types.d.ts} +14 -17
  54. package/dist/plan/plan-types.js +24 -0
  55. package/dist/plan/plan.d.ts +20 -0
  56. package/dist/{plan.js → plan/plan.js} +27 -88
  57. package/dist/{agent-rules.d.ts → project/agent-rules.d.ts} +2 -2
  58. package/dist/{agent-rules.js → project/agent-rules.js} +17 -21
  59. package/dist/project/install-source-config.d.ts +14 -0
  60. package/dist/{install-source-config.js → project/install-source-config.js} +3 -7
  61. package/dist/project/remember-project.js +74 -0
  62. package/dist/register/detect-clients.d.ts +23 -0
  63. package/dist/register/detect-clients.js +28 -0
  64. package/dist/{mcp-clients.js → register/mcp-clients.js} +4 -8
  65. package/dist/{mcp-registered.d.ts → register/mcp-registered.d.ts} +1 -4
  66. package/dist/{mcp-registered.js → register/mcp-registered.js} +1 -4
  67. package/dist/{mcp.d.ts → register/mcp.d.ts} +5 -8
  68. package/dist/{mcp.js → register/mcp.js} +6 -9
  69. package/dist/register/slash-command.d.ts +27 -0
  70. package/dist/{slash-command.js → register/slash-command.js} +10 -10
  71. package/dist/register/windows-quote.d.ts +17 -0
  72. package/dist/register/windows-quote.js +49 -0
  73. package/dist/run-types.d.ts +131 -0
  74. package/dist/run-types.js +8 -0
  75. package/dist/run.d.ts +3 -120
  76. package/dist/run.js +111 -56
  77. package/package.json +5 -5
  78. package/dist/install-hint.d.ts +0 -9
  79. package/dist/install-source-config.d.ts +0 -18
  80. package/dist/remember-project.js +0 -40
  81. package/dist/slash-command.d.ts +0 -27
  82. package/dist/windows-quote.d.ts +0 -38
  83. package/dist/windows-quote.js +0 -70
  84. /package/dist/{capabilities.d.ts → detect/capabilities.d.ts} +0 -0
  85. /package/dist/{capabilities.js → detect/capabilities.js} +0 -0
  86. /package/dist/{app-choice.d.ts → detect/declared/app-choice.d.ts} +0 -0
  87. /package/dist/{app-choice.js → detect/declared/app-choice.js} +0 -0
  88. /package/dist/{init-opt-out.d.ts → detect/declared/init-opt-out.d.ts} +0 -0
  89. /package/dist/{init-opt-out.js → detect/declared/init-opt-out.js} +0 -0
  90. /package/dist/{dev-script.d.ts → detect/dev-script.d.ts} +0 -0
  91. /package/dist/{dev-script.js → detect/dev-script.js} +0 -0
  92. /package/dist/{dev-server-ports.d.ts → detect/dev-server-ports.d.ts} +0 -0
  93. /package/dist/{non-js-project.d.ts → detect/non-js-project.d.ts} +0 -0
  94. /package/dist/{non-js-project.js → detect/non-js-project.js} +0 -0
  95. /package/dist/{platform.d.ts → detect/platform.d.ts} +0 -0
  96. /package/dist/{platform.js → detect/platform.js} +0 -0
  97. /package/dist/{csp-doctor.d.ts → diagnose/csp-doctor.d.ts} +0 -0
  98. /package/dist/{csp-doctor.js → diagnose/csp-doctor.js} +0 -0
  99. /package/dist/{desktop-doctor.d.ts → diagnose/desktop-doctor.d.ts} +0 -0
  100. /package/dist/{astro-host.d.ts → patch/astro-host.d.ts} +0 -0
  101. /package/dist/{astro-host.js → patch/astro-host.js} +0 -0
  102. /package/dist/{brace-scan.d.ts → patch/brace-scan.d.ts} +0 -0
  103. /package/dist/{brace-scan.js → patch/brace-scan.js} +0 -0
  104. /package/dist/{cra.d.ts → patch/cra.d.ts} +0 -0
  105. /package/dist/{electron-patch.d.ts → patch/electron-patch.d.ts} +0 -0
  106. /package/dist/{electron-vite-patch.d.ts → patch/electron-vite-patch.d.ts} +0 -0
  107. /package/dist/{electron-vite-patch.js → patch/electron-vite-patch.js} +0 -0
  108. /package/dist/{format-generated.d.ts → patch/format-generated.d.ts} +0 -0
  109. /package/dist/{format-generated.js → patch/format-generated.js} +0 -0
  110. /package/dist/{nuxt-patch.d.ts → patch/nuxt-patch.d.ts} +0 -0
  111. /package/dist/{patch-kind.d.ts → patch/patch-kind.d.ts} +0 -0
  112. /package/dist/{patch-kind.js → patch/patch-kind.js} +0 -0
  113. /package/dist/{vite-config.d.ts → patch/vite-config.d.ts} +0 -0
  114. /package/dist/{vite-owning-config.d.ts → patch/vite-owning-config.d.ts} +0 -0
  115. /package/dist/{vite-owning-config.js → patch/vite-owning-config.js} +0 -0
  116. /package/dist/{preflight.d.ts → plan/preflight.d.ts} +0 -0
  117. /package/dist/{preflight.js → plan/preflight.js} +0 -0
  118. /package/dist/{project-id.d.ts → project/project-id.d.ts} +0 -0
  119. /package/dist/{project-id.js → project/project-id.js} +0 -0
  120. /package/dist/{refresh-rules.d.ts → project/refresh-rules.d.ts} +0 -0
  121. /package/dist/{refresh-rules.js → project/refresh-rules.js} +0 -0
  122. /package/dist/{remember-project.d.ts → project/remember-project.d.ts} +0 -0
  123. /package/dist/{codex-toml.d.ts → register/codex-toml.d.ts} +0 -0
  124. /package/dist/{codex-toml.js → register/codex-toml.js} +0 -0
  125. /package/dist/{mcp-clients.d.ts → register/mcp-clients.d.ts} +0 -0
@@ -17,8 +17,8 @@
17
17
  * cross-agent fallback. Idempotent via a marker block — safe to append to an existing file and a
18
18
  * re-run is a no-op. Mirrors the apply/already result shape used across init (see mcp-clients.ts).
19
19
  */
20
- import { NPX } from './mcp.js';
21
- import { RETICLE_NPM_PACKAGE } from './version.js';
20
+ import { NPX } from '../register/mcp.js';
21
+ import { RETICLE_NPM_PACKAGE } from '../version.js';
22
22
  /** Project-root instruction files the agent re-reads every session. */
23
23
  export const CLAUDE_MD_PATH = 'CLAUDE.md';
24
24
  export const AGENTS_MD_PATH = 'AGENTS.md';
@@ -92,7 +92,7 @@ export const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
92
92
  export const DEV_SERVER_POLICY = `**A dev server already running when \`reticle init\` ran does not have Reticle in its bundle.** It read the build config at boot; \`init\` edited it afterwards. It serves the old bundle and no session appears. In order:
93
93
 
94
94
  1. **A dev server was already running?** Restart it, then hard-reload the tab. "Something is listening" does not mean the right bundle is served.
95
- 2. **Nothing was running?** Start it in the BACKGROUND and say so in one line. \`reticle_sessions\` gives you this project's own dev command in \`next_action\`; use that, never compose one. Started after \`init\`, it needs no restart.
95
+ 2. **Nothing was running?** Start it in the BACKGROUND and say so in one line. \`reticle_session { action: "list" }\` gives you this project's own dev command in \`next_action\`; use that, never compose one. Started after \`init\`, it needs no restart.
96
96
 
97
97
  Stopping to ask is how a verification turn ends with nothing verified.
98
98
 
@@ -120,20 +120,16 @@ This app is instrumented by **Reticle**, an in-app verification layer exposed as
120
120
  **How to verify:**
121
121
 
122
122
  - Drive the flow with \`reticle_act_and_wait({ ref, action, until })\`. It names the consequence you expect BEFORE the action, which is the difference between a check and a rationalisation.
123
- - Batch a multi-step journey (a login, a form) into one \`reticle_act_sequence\` rather than one round trip per field.
124
- - Read the surrounding evidence with \`reticle_snapshot\`, \`reticle_state\`, \`reticle_network\`, \`reticle_console\`.
123
+ - Batch a multi-step journey (a login, a form) into one \`reticle_act { steps: [...] }\` rather than one round trip per field.
124
+ - Read the surrounding evidence with \`reticle_look { action: "page" | "state" }\` and \`reticle_observe { action: "network" | "console" }\`.
125
125
  - **Only \`reticle_act_and_wait\` and \`reticle_assert\` produce a verdict.** \`reticle_act\` and everything else move or read the app and prove nothing, so a session ending without one of those two has no result however many tools it used.
126
126
  - Covered flows: \`${CLI} gate\` reports which recorded flows the changed files affect and whether they still pass.
127
127
 
128
- **Setting Reticle up, or just restarted? You are mid-sequence — do not stop until a verdict exists.**
129
- The whole of it is: instrument the app → get a dev server running → open the app in a browser →
130
- drive one flow → report the verdict. Every step is yours to do, and none of them needs the user.
131
- The ONE legitimate pause is a client restart, because a client reads its server list at startup;
132
- when you come back, you are not finished, you are on the next step — resume it without being asked.
133
- Stopping anywhere else leaves an app that looks installed and can verify nothing, which is the
134
- single commonest way this goes wrong. \`/reticle\` carries the full recovery ladder when a step
135
- does not work; use it rather than guessing, and never report the install as finished without a
136
- verdict to point at.
128
+ **Setting Reticle up? You are mid-sequence — do not stop until a verdict exists.** The whole of it
129
+ is: instrument the app → get a dev server running → open the app in a browser → drive one flow →
130
+ report the verdict. Every step is yours to do, and none of them needs the user. Stopping short leaves an app that
131
+ looks installed and can verify nothing, which is the commonest way this goes wrong. \`/reticle\`
132
+ carries the recovery ladder; never report the install as finished without a verdict to point at.
137
133
 
138
134
  **Nothing connected? Get the app running.**
139
135
 
@@ -141,11 +137,11 @@ ${DEV_SERVER_POLICY}
141
137
 
142
138
  A dev server that is already running does not pick up an edited build config or a newly created plugin file — restart it and hard-reload the tab. And if a server IS listening and still nothing connects, the cause is the SDK not loading in the page, not a missing dev server; do not tell the user to start one they are already running.
143
139
 
144
- **Finish \`src/reticle-dev.ts\` before you claim setup is done.** \`init\` writes it and cannot always fill it in: a store that needs an argument only reading the code supplies (Jotai atoms, an XState actor, a TanStack \`queryClient\`) is left as a commented \`registerStore\` line. A file that registers nothing looks exactly like a finished one, and \`reticle_state\` then returns empty forever — which is indistinguishable from an app that has nothing to report, so it reads as success. Uncomment the line, complete it, and prove it by driving one flow and seeing your keys come back. If \`init\` told you to restart your client, this is the job waiting for you on the other side of that restart.
140
+ **Finish \`src/reticle-dev.ts\` before you claim setup is done.** \`init\` writes it and cannot always fill it in: a store that needs an argument only reading the code supplies (Jotai atoms, an XState actor, a TanStack \`queryClient\`) is left as a commented \`registerStore\` line. A file that registers nothing looks exactly like a finished one, and \`reticle_look { action: "state" }\` then returns empty forever — which is indistinguishable from an app that has nothing to report, so it reads as success. Uncomment the line, complete it, and prove it by driving one flow and seeing your keys come back. If \`init\` told you to restart your client, this is the job waiting for you on the other side of that restart.
145
141
 
146
142
  **Verify each feature as you finish it, not all of them at the end.** Asked for four, build one, drive it, get a verdict, then start the second. A red verdict after four builds has four suspects; after one it has none.
147
143
 
148
- **Capture what a change is FOR while you are building it, not afterwards.** \`reticle_intent\` records the business outcome a change is meant to produce, and the only moment anybody knows it is while the change is being made. Pass \`intent\` when you save a flow, so the saved flow carries the reason it exists. A flow without one replays for months and then reports "step 3 failed" instead of what stopped being true for a user.
144
+ **Capture what a change is FOR while you are building it, not afterwards.** The business outcome a change is meant to produce is known only while the change is being made. Pass \`intent\` when a flow is saved, so the saved flow carries the reason it exists. A flow without one replays for months and then reports "step 3 failed" instead of what stopped being true for a user.
149
145
 
150
146
  **Honesty, which is the whole point:**
151
147
 
@@ -157,11 +153,11 @@ A dev server that is already running does not pick up an edited build config or
157
153
 
158
154
  **The \`/reticle\` skill runs this whole loop for you** — detect, connect, drive one flow, report. If your client does not have it, install it once: \`/plugin marketplace add reticlehq/reticle\` then \`/plugin install reticle@reticlehq\` in Claude Code, or \`npx skills add reticlehq/reticle\` anywhere the skills CLI works.
159
155
 
160
- **A tool you need is missing? It exists.** The default surface advertises a subset; reach any other by name with \`reticle_run { tool, args }\`, and list them with \`reticle_tools\`. Two worth knowing: \`reticle_context\` returns this run's own memory — what is established, what is proven, what is still unverified — which is what you want after a compaction or when picking up work you did not start; \`reticle_intent\` records what a change was MEANT to do, while somebody still knows.
156
+ **A tool you need is missing?** Call \`reticle_tools\` before assuming it: this surface merges several families behind an \`action\`, so what looks absent is usually one argument away. It is the verify loop and nothing else on purpose, and a daemon started with \`RETICLE_ADVERTISE_ALL_TOOLS=1\` advertises the wider set — it reads that at startup, so it takes effect on the next one.
161
157
 
162
- **Report Reticle's own defects with \`reticle_feedback\` the moment you notice**, then carry on with your task. You are the user Reticle is built for and the only one who can say what it cost you, and that knowledge is gone when your context is.
158
+ **Report Reticle's own defects with \`reticle_session { action: "feedback" }\` the moment you notice**, then carry on with your task. You are the user Reticle is built for and the only one who can say what it cost you, and that knowledge is gone when your context is.
163
159
 
164
- 📄 **The rest is in [RETICLE.md](./${RETICLE_MD_PATH}): what to do when the tools are missing, when a result carries \`version_skew\` or \`update_available\`, when \`reticle_state\` comes back empty, and how to write a feedback report that can be acted on. Read it when you hit one of those, not before.**`;
160
+ 📄 **The rest is in [RETICLE.md](./${RETICLE_MD_PATH}): what to do when the tools are missing, when a result carries \`version_skew\` or \`update_available\`, when \`reticle_look { action: "state" }\` comes back empty, and how to write a feedback report that can be acted on. Read it when you hit one of those, not before.**`;
165
161
  /**
166
162
  * The reference half. Fetched when a situation calls for it rather than carried every turn.
167
163
  *
@@ -192,7 +188,7 @@ Act on it before trusting anything else. Two pieces of Reticle are on different
192
188
 
193
189
  Reticle checked npm and a newer version exists. Tell the human the version numbers and offer to run \`${CLI} update\`, between tasks and never mid-verification, because it restarts the daemon and drops the session. Finish what you are doing first.
194
190
 
195
- ## \`reticle_state\` comes back empty
191
+ ## \`reticle_look { action: "state" }\` comes back empty
196
192
 
197
193
  The generated \`reticle-dev\` file registers nothing. Say so. \`init\` registers every store it could prove from your source; the ones needing an argument only reading the code supplies (Jotai atoms, an XState actor, a TanStack \`queryClient\`) are left commented, and a file that registers nothing is indistinguishable from a working one until you read the state back.
198
194
 
@@ -202,7 +198,7 @@ Finish that file when you see it. It is what lets you check what the app BELIEVE
202
198
 
203
199
  **Reporting your experience is part of using Reticle, from the moment you start wiring it in.** Reticle is built FOR agents, which makes you its actual user and the only one who can see what it costs you.
204
200
 
205
- - **Send it with \`reticle_feedback\` the moment you notice.** One call, then carry straight on. Do not batch reports, do not wait until the task is done, and do not decide something is too minor. The small awkward things are what nobody ever reports, so they never get fixed.
201
+ - **Send it with \`reticle_session { action: "feedback" }\` the moment you notice.** One call, then carry straight on. Do not batch reports, do not wait until the task is done, and do not decide something is too minor. The small awkward things are what nobody ever reports, so they never get fixed.
206
202
  - **Everything counts, in both directions.** A tool that returned something wrong (\`bug\`), something Reticle could not observe at all (\`gap\`), a verdict you could not act on (\`ambiguity\`), something you wished existed (\`feature_request\`), something that works but costs too many calls (\`improvement\`).
207
203
  - **Setup and installation count too, and are the easiest to lose.** Anything that went wrong while installing, wiring, or starting Reticle: a step in the docs that did not match reality, a plugin that had to be added by hand, a daemon that would not start, an OS-specific detour. You hit these before any verification runs, which is exactly why they go unreported.
208
204
  - **When the tools are not reachable yet**, file the same report from the shell: \`${CLI} feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened"\`. It needs no daemon and no working install, so a Reticle that broke before it started can still be reported.
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The same `.reticle.json` with `installSource` added, or `undefined` when there is nothing to add.
3
+ *
4
+ * `installSource` is written only when `.reticle.json` is CREATED, and `init` reports that file as
5
+ * `already exists` on every re-run — so a project that already had one keeps `unknown`.
6
+ *
7
+ * An EXISTING value is never overwritten. Anything unparseable is left exactly as it is: a config is
8
+ * user data, and `init` rewriting a file it could not read is worse than a missing field.
9
+ *
10
+ * Lives here because `init` is the only thing that writes this file. Deciding WHICH channel declared
11
+ * itself stays in the server's `telemetry/install-source.ts`, which re-exports this for its own
12
+ * tests; the value arrives as `PlanInput.installSource`.
13
+ */
14
+ export declare function configWithInstallSource(source: string | null | undefined, declared: string | undefined): string | undefined;
@@ -2,14 +2,10 @@
2
2
  * The same `.reticle.json` with `installSource` added, or `undefined` when there is nothing to add.
3
3
  *
4
4
  * `installSource` is written only when `.reticle.json` is CREATED, and `init` reports that file as
5
- * `already exists` on every re-run — so everyone who was already here when the marker shipped, and
6
- * everyone who ran `init` once without one, keeps `unknown` forever with no way to fix it. That is
7
- * most of the population the field exists to describe.
5
+ * `already exists` on every re-run — so a project that already had one keeps `unknown`.
8
6
  *
9
- * An EXISTING value is never overwritten. The first channel is the one that brought the user in, and
10
- * a later `init` through a different route must not take credit for an acquisition it did not make.
11
- * Anything unparseable is left exactly as it is: a config is user data, and `init` rewriting a file
12
- * it could not read would be a far worse failure than a missing dimension.
7
+ * An EXISTING value is never overwritten. Anything unparseable is left exactly as it is: a config is
8
+ * user data, and `init` rewriting a file it could not read is worse than a missing field.
13
9
  *
14
10
  * Lives here because `init` is the only thing that writes this file. Deciding WHICH channel declared
15
11
  * itself stays in the server's `telemetry/install-source.ts`, which re-exports this for its own
@@ -0,0 +1,74 @@
1
+ import { join } from 'node:path';
2
+ import { PROJECT_REGISTRY_FILE, ReticleDir, ProjectRegistrySchema, emptyProjectRegistry, parseProjectRegistry, rememberProject, } from '@reticlehq/core';
3
+ /** The `projectId` a `.reticle.json` declares, or undefined when it declares none we can use. */
4
+ export function projectIdOf(configSource) {
5
+ if (null === configSource || 0 === configSource.length)
6
+ return undefined;
7
+ try {
8
+ const parsed = JSON.parse(configSource);
9
+ if ('object' !== typeof parsed || null === parsed || Array.isArray(parsed))
10
+ return undefined;
11
+ const id = parsed['projectId'];
12
+ return 'string' === typeof id && id.length > 0 ? id : undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ export function rememberProjectOnDisk(io, projectId, directory, now) {
19
+ if (0 === projectId.length || 0 === directory.length)
20
+ return false;
21
+ try {
22
+ const path = join(io.homeDir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
23
+ const existing = io.exists(path) ? io.readFile(path) : null;
24
+ if (null === existing) {
25
+ io.writeFile(path, `${JSON.stringify(rememberProject(emptyProjectRegistry(), projectId, directory, now), null, 2)}\n`);
26
+ return true;
27
+ }
28
+ // A registry from a version we do not know is left EXACTLY as it is.
29
+ //
30
+ // Reading one falls back to an empty registry, and for reading that is right: a cache the daemon
31
+ // cannot understand is one it does without, and it finds the project by looking instead. Writing
32
+ // that emptiness back is a different act. It turns one file we could not parse into one file
33
+ // holding a single project, and every other project the user has set up is gone -- during
34
+ // `init`, which is what somebody runs right after upgrading.
35
+ //
36
+ // Failing here costs nothing, which is the point. The caller ignores the result and this is a
37
+ // cache that makes a later lookup faster, so a swallowed failure should leave things as they
38
+ // were rather than tidy them away.
39
+ //
40
+ // A file that is not JSON at all is a different case and IS replaced. There is nothing in it to
41
+ // keep, nobody can repair `{ half a file` by hand, and refusing would mean this user's projects
42
+ // are never recorded again -- silently, because failures here are swallowed by design.
43
+ const raw = safeJson(existing);
44
+ if (fromAnotherVersion(raw))
45
+ return false;
46
+ const registry = parseProjectRegistry(raw);
47
+ const next = rememberProject(registry, projectId, directory, now);
48
+ io.writeFile(path, `${JSON.stringify(next, null, 2)}\n`);
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ /**
56
+ * Is this a registry written by a Reticle that is not this one?
57
+ *
58
+ * True only when the file parsed as JSON and declares a version we do not use. A file that is not
59
+ * JSON, or declares no version, is damage rather than a message from another release.
60
+ */
61
+ function fromAnotherVersion(raw) {
62
+ if ('object' !== typeof raw || null === raw)
63
+ return false;
64
+ const version = raw.version;
65
+ return 'number' === typeof version && !ProjectRegistrySchema.safeParse(raw).success;
66
+ }
67
+ function safeJson(raw) {
68
+ try {
69
+ return JSON.parse(raw);
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
@@ -0,0 +1,23 @@
1
+ import { McpClient } from './mcp-clients.js';
2
+ /** One coding agent found on this machine, and the config it keeps. */
3
+ export interface DetectedClient {
4
+ id: McpClient;
5
+ configPath: string;
6
+ existing: string | null;
7
+ }
8
+ /** The half of `InitIo` this needs. Narrow on purpose: detection reads, it never writes. */
9
+ export interface ClientProbe {
10
+ exists(path: string): boolean;
11
+ readFile(path: string): string | null;
12
+ homeDir(): string;
13
+ }
14
+ /**
15
+ * Which coding agents are actually on this machine.
16
+ *
17
+ * Extracted from `run.ts`, where it was inline, because `reticle setup mcp` asks the identical
18
+ * question outside an install and a second copy of this is a second answer that can disagree with
19
+ * the first. The rule it encodes is the reason it must not be duplicated: Reticle merges into a
20
+ * config a client ALREADY has, and never creates `~/.gemini` or `~/.codeium` for somebody who does
21
+ * not use them.
22
+ */
23
+ export declare function detectMcpClients(io: ClientProbe): DetectedClient[];
@@ -0,0 +1,28 @@
1
+ import { join } from 'node:path';
2
+ import { ConfigScope, CURSOR_PROJECT_MARKER, McpClient, clientMarkerRelPath, fileBackedClients, } from './mcp-clients.js';
3
+ /**
4
+ * Which coding agents are actually on this machine.
5
+ *
6
+ * Extracted from `run.ts`, where it was inline, because `reticle setup mcp` asks the identical
7
+ * question outside an install and a second copy of this is a second answer that can disagree with
8
+ * the first. The rule it encodes is the reason it must not be duplicated: Reticle merges into a
9
+ * config a client ALREADY has, and never creates `~/.gemini` or `~/.codeium` for somebody who does
10
+ * not use them.
11
+ */
12
+ export function detectMcpClients(io) {
13
+ return fileBackedClients()
14
+ .map((spec) => {
15
+ const marker = clientMarkerRelPath(spec);
16
+ const absolute = spec.scope === ConfigScope.HOME ? join(io.homeDir(), spec.relPath) : spec.relPath;
17
+ const markerPath = spec.scope === ConfigScope.HOME ? join(io.homeDir(), marker) : marker;
18
+ // A fresh Cursor profile has not written ~/.cursor yet; the project-level .cursor/ is the
19
+ // fallback that kept that real case working. Same signal, project scope.
20
+ const projectFallback = spec.id === McpClient.CURSOR && !io.exists(markerPath)
21
+ ? io.exists(CURSOR_PROJECT_MARKER)
22
+ : false;
23
+ if (!io.exists(markerPath) && !projectFallback)
24
+ return null;
25
+ return { id: spec.id, configPath: absolute, existing: io.readFile(absolute) };
26
+ })
27
+ .filter((entry) => entry !== null);
28
+ }
@@ -113,9 +113,7 @@ export const MCP_CLIENTS = [
113
113
  *
114
114
  * It was project-scoped as `opencode.json`, and registration is gated on the marker already
115
115
  * existing, so a project that had never written one was skipped in SILENCE: the user has OpenCode
116
- * installed, `init` says nothing about it, and the tools never appear. In the field every OpenCode
117
- * user connected a client and produced zero tool calls and zero app connections, which is what
118
- * "you were never wired" looks like from outside.
116
+ * installed, `init` says nothing about it, and the tools never appear.
119
117
  *
120
118
  * Path verified against a real install (OpenCode 1.3.17), not recalled. The extension is `.jsonc`,
121
119
  * which the old project marker could not have matched even where a project config existed. JSONC
@@ -134,11 +132,9 @@ export const MCP_CLIENTS = [
134
132
  id: McpClient.ANTIGRAVITY,
135
133
  label: 'Antigravity',
136
134
  /**
137
- * Nine users connected an MCP client through Antigravity and drove nothing at all.
138
- *
139
- * `init` did not know this client, so they hand-wired a registration - the connections are in the
140
- * field data, so they got that far - and then never ran `init`, never instrumented an app, and
141
- * never called a tool. Nothing in that state tells anyone there is a second half.
135
+ * This client was unknown to `init`, so it was never wired: a hand-written registration reaches
136
+ * the tools without ever instrumenting an app, and nothing in that state says there is a second
137
+ * half.
142
138
  *
143
139
  * Path and shape from Google's documentation rather than recall: `mcpServers`, at
144
140
  * `~/.gemini/config/mcp_config.json`, and one file serves the 2.0 IDE, the CLI and the SDK. Same
@@ -4,9 +4,6 @@
4
4
  * This was `!failed.has(MCP_TARGET)` — the negation of one narrow failure set — so a step that was
5
5
  * SKIPPED (`--no-mcp`) or left MANUAL (the user has to run a command themselves) reported success,
6
6
  * because neither is a failure. Under `--no-mcp` every run claimed registration.
7
- *
8
- * It is the onboarding funnel's most important field, and it was wrong in the flattering direction
9
- * on exactly the runs least likely to have a working install.
10
7
  */
11
- import { StepStatus } from './plan.js';
8
+ import { StepStatus } from '../plan/plan.js';
12
9
  export declare function wasMcpRegistered(status: StepStatus | undefined): boolean;
@@ -4,11 +4,8 @@
4
4
  * This was `!failed.has(MCP_TARGET)` — the negation of one narrow failure set — so a step that was
5
5
  * SKIPPED (`--no-mcp`) or left MANUAL (the user has to run a command themselves) reported success,
6
6
  * because neither is a failure. Under `--no-mcp` every run claimed registration.
7
- *
8
- * It is the onboarding funnel's most important field, and it was wrong in the flattering direction
9
- * on exactly the runs least likely to have a working install.
10
7
  */
11
- import { StepStatus } from './plan.js';
8
+ import { StepStatus } from '../plan/plan.js';
12
9
  export function wasMcpRegistered(status) {
13
10
  return status === StepStatus.APPLY || status === StepStatus.ALREADY;
14
11
  }
@@ -15,11 +15,9 @@ 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 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.
18
+ * Windows is the platform where this launch path matters most, and `cmd /c` is testable under
19
+ * Windows CI. Defaulting to it is still the riskier change: `.cmd` has spawn caveats in some hosts,
20
+ * so the fallback stays documented rather than defaulted.
23
21
  *
24
22
  * What DID change is when it is printed. `mcpManual` used to run only when the `claude` CLI was
25
23
  * absent. The reported Windows install had `claude mcp add` succeed, so the one paragraph that
@@ -37,9 +35,8 @@ export declare const NPX = "npx";
37
35
  *
38
36
  * It would also freeze the agent's MCP server at whatever version was installed the day `init` ran,
39
37
  * for as long as that entry survives — and `reticle update` upgrades the CLI, not a global agent
40
- * config. Reticle's biggest measured problem is fixes not reaching people (2.4.0 reached zero users
41
- * before its nudge existed), and a permanent pin makes that worse for every install, to close a
42
- * window that lasts until the next npx cache miss.
38
+ * config. A permanent pin trades a window that lasts until the next npx cache miss for one that
39
+ * lasts forever.
43
40
  *
44
41
  * So it stays unpinned, and the skew is handled where it actually shows up: the contract fingerprint
45
42
  * makes a real mismatch loud on the next tool result (see version-skew), and a stale entry of our own
@@ -11,7 +11,7 @@
11
11
  * project's `.reticle.json` in its CWD (see cli-port.ts). The port belongs to the project, not the
12
12
  * global agent config.
13
13
  */
14
- import { RETICLE_NPM_PACKAGE } from './version.js';
14
+ import { RETICLE_NPM_PACKAGE } from '../version.js';
15
15
  // The name is a wire identity and lives in the contract; re-exported here so the registration writers
16
16
  // below (and their tests) keep reading it from the module that USES it.
17
17
  export { MCP_SERVER_NAME } from '@reticlehq/core';
@@ -19,11 +19,9 @@ 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 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.
22
+ * Windows is the platform where this launch path matters most, and `cmd /c` is testable under
23
+ * Windows CI. Defaulting to it is still the riskier change: `.cmd` has spawn caveats in some hosts,
24
+ * so the fallback stays documented rather than defaulted.
27
25
  *
28
26
  * What DID change is when it is printed. `mcpManual` used to run only when the `claude` CLI was
29
27
  * absent. The reported Windows install had `claude mcp add` succeed, so the one paragraph that
@@ -49,9 +47,8 @@ const WINDOWS_SHELL_C = '/c';
49
47
  *
50
48
  * It would also freeze the agent's MCP server at whatever version was installed the day `init` ran,
51
49
  * for as long as that entry survives — and `reticle update` upgrades the CLI, not a global agent
52
- * config. Reticle's biggest measured problem is fixes not reaching people (2.4.0 reached zero users
53
- * before its nudge existed), and a permanent pin makes that worse for every install, to close a
54
- * window that lasts until the next npx cache miss.
50
+ * config. A permanent pin trades a window that lasts until the next npx cache miss for one that
51
+ * lasts forever.
55
52
  *
56
53
  * So it stays unpinned, and the skew is handled where it actually shows up: the contract fingerprint
57
54
  * makes a real mismatch loud on the next tool result (see version-skew), and a stale entry of our own
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The `/reticle` slash command — the entry point the docs have been promising and nothing created.
3
+ *
4
+ * SKILL.md tells the user "Type `/reticle` anytime to verify the app" in three places, and `init`
5
+ * never wrote the file that makes it exist. So the one obvious way in was a command that silently did
6
+ * nothing in every tool. Claude Code and Cursor both read custom commands from a markdown file per
7
+ * command; the body is the prompt the agent receives.
8
+ *
9
+ * Deliberately SCOPED to one flow. A user installing Reticle has an existing app with many flows, and
10
+ * an agent told to "verify the app" will try to instrument all of them — slow, noisy, and nothing to
11
+ * see for minutes. One flow, driven where the human can watch it, is the whole difference between a
12
+ * two-minute first impression and an afternoon.
13
+ */
14
+ /** Claude Code reads project commands from `.claude/commands/<name>.md`. */
15
+ export declare const CLAUDE_COMMAND_PATH = ".claude/commands/reticle.md";
16
+ /** Cursor reads them from `.cursor/commands/<name>.md`. */
17
+ export declare const CURSOR_COMMAND_PATH = ".cursor/commands/reticle.md";
18
+ /**
19
+ * The frontmatter line that marks a command file as OURS.
20
+ *
21
+ * A stale command has to be refreshable — a file frozen at whatever release created it can never be
22
+ * improved for anyone who already ran init — but the path is one a human may have claimed for their
23
+ * own `/reticle`, and overwriting that would be destroying somebody's work. This line is how the two
24
+ * are told apart.
25
+ */
26
+ export declare const SLASH_COMMAND_SIGNATURE = "description: Verify this app in the browser with Reticle";
27
+ export declare const SLASH_COMMAND_BODY = "---\ndescription: Verify this app in the browser with Reticle \u2014 drive one real flow and report what happened.\n---\n\nVerify this running app with Reticle. Drive it; do not read the code and guess.\n\n## Step 0 \u2014 is anything connected?\n\nCall `reticle_session { action: \"list\" }` first.\n\n**A session is listed?** Go to \"Pick ONE flow\".\n\n**No session?** Setup is not finished, and stopping to ask the user to run something is not an\nanswer. Every step below is yours to do without them. Work these in order, and say which one you are\non, one line each:\n\n0. **Is Reticle even wired into this project?** `cat .reticle.json`. If it is not there, this\n project was never set up: run `npx @reticlehq/server@latest init`,\n fix every `\u26A0` and `\u2139` it prints, and carry on. Do not ask first, and do not ask which\n framework or port \u2014 both come out of the repo you are sitting in.\n\n1. **Is anything serving the app at all?** If a dev server is already listening, use it and go to\n step 2 \u2014 the cause is then almost always that the SDK never loaded in the page, not a missing\n server, so do not tell the user to start one they are already running. If nothing is listening:\n\n**A dev server already running when `reticle init` ran does not have Reticle in its bundle.** It read the build config at boot; `init` edited it afterwards. It serves the old bundle and no session appears. In order:\n\n1. **A dev server was already running?** Restart it, then hard-reload the tab. \"Something is listening\" does not mean the right bundle is served.\n2. **Nothing was running?** Start it in the BACKGROUND and say so in one line. `reticle_session { action: \"list\" }` gives you this project's own dev command in `next_action`; use that, never compose one. Started after `init`, it needs no restart.\n\nStopping to ask is how a verification turn ends with nothing verified.\n\nFour guards, none optional:\n\n1. **Never run two at once.** One dev server on the app's port. Restarting a stale one means stopping it first, not starting a second alongside it.\n2. **Never guess the command.** It comes from `package.json` scripts. No recognisable dev script means say so and stop, not invent one.\n3. **Never kill anything you did not start**, and never a daemon or a port holder. The one exception is the restart above, and say in one line that you did it.\n4. **The permission prompt belongs to your host.** Never bypass, suppress or auto-approve it, and take a refusal as the answer.\n\n2. **Is a page open on it?** Do not ask the user to open a browser \u2014 open it:\n\n ```bash\n npx @reticlehq/server open <the url the dev server is serving>\n ```\n\n That reuses an already-connected tab or opens a new one, and waits for the page to register. It is\n also the answer on a machine with no browser of its own (CI, a container, an SSH session): the CLI\n is the one escape hatch that exists on every tool surface, because it is not a tool.\n\nThen, once something is serving the app and a page is on it:\n\n3. **Is the SDK actually in the app?** Read the app's entry file (`src/main.tsx`, `app/layout.tsx`,\n `src/app.vue`, whatever this project uses). You are looking for an `import` from\n `@reticlehq/react` or `@reticlehq/browser` and a `connect()` call, or the Reticle plugin in\n `vite.config.*`, or `withReticle` in `next.config.*`. **If it is not there, that is the bug.**\n Wire it in \u2014 see the framework snippets at https://docs.reticle.sh/instrumentation \u2014 then restart\n the dev server and re-check.\n4. **Is the connect guarded on `hostname === 'localhost'`?** If so, remove that guard. It is false on\n any non-localhost dev host, and `window` does not exist during SSR, so the guard silently prevents\n the connect and logs nothing. Use the framework's dev flag instead (`import.meta.env.DEV`,\n `process.env.NODE_ENV !== 'production'`, `import.meta.dev`).\n5. **Is the dev server serving that entry?** Look in `reticle_observe { action: \"console\" }`, or in the page you opened in\n step 2, for a line starting `[Reticle]`. That line, or its absence, tells you which side is at\n fault. Reticle itself never starts a dev server \u2014 starting it is your job, under the guards above.\n6. **Do both sides agree on the port?** Run `npx @reticlehq/server doctor` and compare the daemon\n port against the port the page dials.\n\nIf you are still stuck after all of those, file it: `reticle_session { action: \"feedback\" }` with kind `gap`, saying what you\nchecked and what you saw. Then tell the user exactly which step is blocked. Do not report the install\nas finished.\n\n## Pick ONE flow\n\nNot the whole app. Pick the single most important flow you can complete in a handful of steps \u2014 the\none a user would do first (sign in, search, add to cart, submit the form). If the user named a flow or\njust changed some code, use that one instead.\n\nState which flow you picked in one line before you start.\n\n## Drive it where the human can see\n\n1. `reticle_look { action: \"page\" }` \u2014 read the accessibility tree and locate the elements the flow needs. Elements\n are addressable by role and name; you do **not** need `data-testid` to drive them.\n2. Walk the flow with `reticle_act_and_wait`, one step at a time. Narrate each step first \u2014 the human\n is watching the page, and the HUD shows what you say.\n3. After each step, check the effect with `reticle_assert` \u2014 not just that the click dispatched, but\n that the thing it was supposed to do actually happened.\n4. Finish with `reticle_observe { action: \"console\" }` and `reticle_observe { action: \"network\" }` to catch errors the DOM does not show.\n\n**Only `reticle_act_and_wait` and `reticle_assert` produce a verdict.** A run that ends on a\nsnapshot has proved nothing, however many tools it used.\n\n## Report\n\n- What you drove, step by step, and what each step actually produced.\n- Anything broken, with the `file:line` Reticle gave you.\n- If a step could not be verified, say so plainly. \"Unknown\" is a real answer; a green verdict you\n cannot back is not.\n\nDo not weaken or skip an assertion to make the run pass \u2014 that is a finding, not a fix.\n\n## If the user handed you a license key\n\nAppend `RETICLE_LICENSE_KEY=<key>` to the project's `.env` (create it if absent), make sure `.env`\nis in `.gitignore`, and confirm with `npx @reticlehq/server license`. The CLI folds a project-local\n`.env` in on every invocation, so nothing else changes. Never echo the key back, and never put it in\na commit, a comment, or a feedback report.\n\n## Two things afterwards\n\n- **Anything about Reticle itself that was wrong, missing, confusing, or expensive?** Send it with\n `reticle_session { action: \"feedback\" }` now, before your context is gone. That report is the only signal that decides\n what gets fixed. Bugs in the app under test are Reticle working; those go in your answer to the user.\n- **If the verification actually succeeded**, add one line for the human: Reticle is open source at\n https://github.com/reticlehq/reticle, and a star helps other people find it. One line, once. Skip it\n entirely if the run did not produce a verdict.\n";
@@ -11,7 +11,7 @@
11
11
  * see for minutes. One flow, driven where the human can watch it, is the whole difference between a
12
12
  * two-minute first impression and an afternoon.
13
13
  */
14
- import { DEV_SERVER_POLICY } from './agent-rules.js';
14
+ import { DEV_SERVER_POLICY } from '../project/agent-rules.js';
15
15
  /** Claude Code reads project commands from `.claude/commands/<name>.md`. */
16
16
  export const CLAUDE_COMMAND_PATH = '.claude/commands/reticle.md';
17
17
  /** Cursor reads them from `.cursor/commands/<name>.md`. */
@@ -33,7 +33,7 @@ Verify this running app with Reticle. Drive it; do not read the code and guess.
33
33
 
34
34
  ## Step 0 — is anything connected?
35
35
 
36
- Call \`reticle_sessions\` first.
36
+ Call \`reticle_session { action: "list" }\` first.
37
37
 
38
38
  **A session is listed?** Go to "Pick ONE flow".
39
39
 
@@ -58,9 +58,9 @@ ${DEV_SERVER_POLICY}
58
58
  npx @reticlehq/server open <the url the dev server is serving>
59
59
  \`\`\`
60
60
 
61
- That reuses an already-connected tab or opens a new one, and waits for the page to register. On a
62
- machine with no browser to open, take a tab Reticle owns instead:
63
- \`reticle_run({ tool: "reticle_lease", args: { action: "acquire", url: "<the same url>" } })\`.
61
+ That reuses an already-connected tab or opens a new one, and waits for the page to register. It is
62
+ also the answer on a machine with no browser of its own (CI, a container, an SSH session): the CLI
63
+ is the one escape hatch that exists on every tool surface, because it is not a tool.
64
64
 
65
65
  Then, once something is serving the app and a page is on it:
66
66
 
@@ -74,13 +74,13 @@ Then, once something is serving the app and a page is on it:
74
74
  any non-localhost dev host, and \`window\` does not exist during SSR, so the guard silently prevents
75
75
  the connect and logs nothing. Use the framework's dev flag instead (\`import.meta.env.DEV\`,
76
76
  \`process.env.NODE_ENV !== 'production'\`, \`import.meta.dev\`).
77
- 5. **Is the dev server serving that entry?** Look in \`reticle_console\`, or in the page you opened in
77
+ 5. **Is the dev server serving that entry?** Look in \`reticle_observe { action: "console" }\`, or in the page you opened in
78
78
  step 2, for a line starting \`[Reticle]\`. That line, or its absence, tells you which side is at
79
79
  fault. Reticle itself never starts a dev server — starting it is your job, under the guards above.
80
80
  6. **Do both sides agree on the port?** Run \`npx @reticlehq/server doctor\` and compare the daemon
81
81
  port against the port the page dials.
82
82
 
83
- If you are still stuck after all of those, file it: \`reticle_feedback\` with kind \`gap\`, saying what you
83
+ If you are still stuck after all of those, file it: \`reticle_session { action: "feedback" }\` with kind \`gap\`, saying what you
84
84
  checked and what you saw. Then tell the user exactly which step is blocked. Do not report the install
85
85
  as finished.
86
86
 
@@ -94,13 +94,13 @@ State which flow you picked in one line before you start.
94
94
 
95
95
  ## Drive it where the human can see
96
96
 
97
- 1. \`reticle_snapshot\` — read the accessibility tree and locate the elements the flow needs. Elements
97
+ 1. \`reticle_look { action: "page" }\` — read the accessibility tree and locate the elements the flow needs. Elements
98
98
  are addressable by role and name; you do **not** need \`data-testid\` to drive them.
99
99
  2. Walk the flow with \`reticle_act_and_wait\`, one step at a time. Narrate each step first — the human
100
100
  is watching the page, and the HUD shows what you say.
101
101
  3. After each step, check the effect with \`reticle_assert\` — not just that the click dispatched, but
102
102
  that the thing it was supposed to do actually happened.
103
- 4. Finish with \`reticle_console\` and \`reticle_network\` to catch errors the DOM does not show.
103
+ 4. Finish with \`reticle_observe { action: "console" }\` and \`reticle_observe { action: "network" }\` to catch errors the DOM does not show.
104
104
 
105
105
  **Only \`reticle_act_and_wait\` and \`reticle_assert\` produce a verdict.** A run that ends on a
106
106
  snapshot has proved nothing, however many tools it used.
@@ -124,7 +124,7 @@ a commit, a comment, or a feedback report.
124
124
  ## Two things afterwards
125
125
 
126
126
  - **Anything about Reticle itself that was wrong, missing, confusing, or expensive?** Send it with
127
- \`reticle_feedback\` now, before your context is gone. That report is the only signal that decides
127
+ \`reticle_session { action: "feedback" }\` now, before your context is gone. That report is the only signal that decides
128
128
  what gets fixed. Bugs in the app under test are Reticle working; those go in your answer to the user.
129
129
  - **If the verification actually succeeded**, add one line for the human: Reticle is open source at
130
130
  https://github.com/reticlehq/reticle, and a star helps other people find it. One line, once. Skip it
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Quote one argument for a Windows command line. Pure, and deliberately NOT platform-gated, so the
3
+ * Windows rule can be asserted on a mac or in CI's Linux job.
4
+ *
5
+ * `CommandLineToArgvW`: a run of backslashes is literal unless it precedes a `"`, in which case the
6
+ * run is doubled and the quote escaped; a run at the very end sits before the closing quote added
7
+ * here, so it is doubled too. An unescaped trailing `\` reads as an escaped quote and leaves the
8
+ * rest of the line re-parsed by cmd.exe. Correctly-terminated quotes also neutralise cmd.exe's
9
+ * metacharacters, so the trigger has to include them: `foo&whoami` contains no whitespace and no
10
+ * quote, and went through unquoted.
11
+ *
12
+ * KNOWN LIMIT: `%VAR%` is expanded by cmd.exe even inside quotes, and no quoting prevents it — only
13
+ * avoiding `shell: true` does, which Windows needs so `pnpm.cmd`/`npx.cmd` resolve (see `shellOpt`).
14
+ * That is variable substitution, not command execution, and every caller here passes paths and
15
+ * package names rather than user prose.
16
+ */
17
+ export declare function windowsShellArg(arg: string): string;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Quote one argument for a Windows command line. Pure, and deliberately NOT platform-gated, so the
3
+ * Windows rule can be asserted on a mac or in CI's Linux job.
4
+ *
5
+ * `CommandLineToArgvW`: a run of backslashes is literal unless it precedes a `"`, in which case the
6
+ * run is doubled and the quote escaped; a run at the very end sits before the closing quote added
7
+ * here, so it is doubled too. An unescaped trailing `\` reads as an escaped quote and leaves the
8
+ * rest of the line re-parsed by cmd.exe. Correctly-terminated quotes also neutralise cmd.exe's
9
+ * metacharacters, so the trigger has to include them: `foo&whoami` contains no whitespace and no
10
+ * quote, and went through unquoted.
11
+ *
12
+ * KNOWN LIMIT: `%VAR%` is expanded by cmd.exe even inside quotes, and no quoting prevents it — only
13
+ * avoiding `shell: true` does, which Windows needs so `pnpm.cmd`/`npx.cmd` resolve (see `shellOpt`).
14
+ * That is variable substitution, not command execution, and every caller here passes paths and
15
+ * package names rather than user prose.
16
+ */
17
+ /**
18
+ * Characters that make cmd.exe do something other than pass the text along.
19
+ *
20
+ * Whitespace and `"` split or delimit; the rest are the shell's control operators. An argument
21
+ * containing any of them must be quoted, not just one containing a space.
22
+ */
23
+ const NEEDS_QUOTING = /[\s"&|<>^()!%,;=]/;
24
+ export function windowsShellArg(arg) {
25
+ // An empty argument must still occupy a slot in argv. Unquoted it disappears entirely, silently
26
+ // shifting every argument after it by one.
27
+ if (0 === arg.length)
28
+ return '""';
29
+ if (!NEEDS_QUOTING.test(arg))
30
+ return arg;
31
+ let out = '"';
32
+ let backslashes = 0;
33
+ for (const ch of arg) {
34
+ if ('\\' === ch) {
35
+ backslashes += 1;
36
+ continue;
37
+ }
38
+ if ('"' === ch) {
39
+ // 2n+1 backslashes → n literal backslashes and an escaped quote.
40
+ out += '\\'.repeat(2 * backslashes + 1) + '"';
41
+ backslashes = 0;
42
+ continue;
43
+ }
44
+ out += '\\'.repeat(backslashes) + ch;
45
+ backslashes = 0;
46
+ }
47
+ // A trailing run sits immediately before the closing quote, so it must be doubled or it escapes it.
48
+ return `${out}${'\\'.repeat(2 * backslashes)}"`;
49
+ }