@reticlehq/init 2.14.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 (114) hide show
  1. package/LICENSE +105 -0
  2. package/NOTICE +5 -0
  3. package/README.md +29 -0
  4. package/dist/agent-rules.d.ts +116 -0
  5. package/dist/agent-rules.js +279 -0
  6. package/dist/app-choice.d.ts +29 -0
  7. package/dist/app-choice.js +39 -0
  8. package/dist/astro-host.d.ts +21 -0
  9. package/dist/astro-host.js +27 -0
  10. package/dist/astro-patch.d.ts +33 -0
  11. package/dist/astro-patch.js +103 -0
  12. package/dist/brace-scan.d.ts +24 -0
  13. package/dist/brace-scan.js +46 -0
  14. package/dist/capabilities.d.ts +40 -0
  15. package/dist/capabilities.js +167 -0
  16. package/dist/closing-hint.d.ts +35 -0
  17. package/dist/closing-hint.js +122 -0
  18. package/dist/codex-toml.d.ts +10 -0
  19. package/dist/codex-toml.js +144 -0
  20. package/dist/connect-steps.d.ts +55 -0
  21. package/dist/connect-steps.js +92 -0
  22. package/dist/cra.d.ts +82 -0
  23. package/dist/cra.js +183 -0
  24. package/dist/csp-check.d.ts +49 -0
  25. package/dist/csp-check.js +168 -0
  26. package/dist/csp-doctor.d.ts +32 -0
  27. package/dist/csp-doctor.js +62 -0
  28. package/dist/desktop-doctor.d.ts +46 -0
  29. package/dist/desktop-doctor.js +201 -0
  30. package/dist/detect.d.ts +225 -0
  31. package/dist/detect.js +334 -0
  32. package/dist/dev-script.d.ts +51 -0
  33. package/dist/dev-script.js +66 -0
  34. package/dist/dev-server-ports.d.ts +20 -0
  35. package/dist/dev-server-ports.js +45 -0
  36. package/dist/electron-patch.d.ts +11 -0
  37. package/dist/electron-patch.js +70 -0
  38. package/dist/electron-vite-patch.d.ts +12 -0
  39. package/dist/electron-vite-patch.js +100 -0
  40. package/dist/existing-config.d.ts +31 -0
  41. package/dist/existing-config.js +63 -0
  42. package/dist/format-generated.d.ts +18 -0
  43. package/dist/format-generated.js +48 -0
  44. package/dist/framework-adapter.d.ts +60 -0
  45. package/dist/framework-adapter.js +138 -0
  46. package/dist/host.d.ts +41 -0
  47. package/dist/host.js +21 -0
  48. package/dist/index.d.ts +25 -0
  49. package/dist/index.js +25 -0
  50. package/dist/init-failure.d.ts +19 -0
  51. package/dist/init-failure.js +18 -0
  52. package/dist/init-opt-out.d.ts +17 -0
  53. package/dist/init-opt-out.js +30 -0
  54. package/dist/install-hint.d.ts +9 -0
  55. package/dist/install-hint.js +41 -0
  56. package/dist/install-retries.d.ts +22 -0
  57. package/dist/install-retries.js +65 -0
  58. package/dist/install-source-config.d.ts +18 -0
  59. package/dist/install-source-config.js +34 -0
  60. package/dist/lockfiles.d.ts +14 -0
  61. package/dist/lockfiles.js +49 -0
  62. package/dist/mcp-clients.d.ts +94 -0
  63. package/dist/mcp-clients.js +327 -0
  64. package/dist/mcp-registered.d.ts +12 -0
  65. package/dist/mcp-registered.js +14 -0
  66. package/dist/mcp.d.ts +89 -0
  67. package/dist/mcp.js +129 -0
  68. package/dist/next-patch.d.ts +49 -0
  69. package/dist/next-patch.js +237 -0
  70. package/dist/node-io.d.ts +22 -0
  71. package/dist/node-io.js +150 -0
  72. package/dist/non-js-project.d.ts +44 -0
  73. package/dist/non-js-project.js +137 -0
  74. package/dist/nuxt-patch.d.ts +18 -0
  75. package/dist/nuxt-patch.js +33 -0
  76. package/dist/patch-kind.d.ts +20 -0
  77. package/dist/patch-kind.js +10 -0
  78. package/dist/plan-electron-vite.d.ts +16 -0
  79. package/dist/plan-electron-vite.js +72 -0
  80. package/dist/plan-framework.d.ts +122 -0
  81. package/dist/plan-framework.js +773 -0
  82. package/dist/plan.d.ts +312 -0
  83. package/dist/plan.js +650 -0
  84. package/dist/platform.d.ts +16 -0
  85. package/dist/platform.js +16 -0
  86. package/dist/preflight.d.ts +30 -0
  87. package/dist/preflight.js +52 -0
  88. package/dist/project-id.d.ts +12 -0
  89. package/dist/project-id.js +25 -0
  90. package/dist/refresh-rules.d.ts +40 -0
  91. package/dist/refresh-rules.js +51 -0
  92. package/dist/remember-project.d.ts +24 -0
  93. package/dist/remember-project.js +40 -0
  94. package/dist/run.d.ts +122 -0
  95. package/dist/run.js +762 -0
  96. package/dist/slash-command.d.ts +27 -0
  97. package/dist/slash-command.js +132 -0
  98. package/dist/snippets.d.ts +267 -0
  99. package/dist/snippets.js +898 -0
  100. package/dist/tanstack-start.d.ts +18 -0
  101. package/dist/tanstack-start.js +57 -0
  102. package/dist/version.d.ts +18 -0
  103. package/dist/version.js +23 -0
  104. package/dist/vite-config.d.ts +18 -0
  105. package/dist/vite-config.js +111 -0
  106. package/dist/vite-owning-config.d.ts +25 -0
  107. package/dist/vite-owning-config.js +219 -0
  108. package/dist/windows-quote.d.ts +38 -0
  109. package/dist/windows-quote.js +70 -0
  110. package/dist/workspace-apps.d.ts +40 -0
  111. package/dist/workspace-apps.js +188 -0
  112. package/dist/workspace-redirect.d.ts +22 -0
  113. package/dist/workspace-redirect.js +97 -0
  114. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Reticle Labs
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
package/NOTICE ADDED
@@ -0,0 +1,5 @@
1
+ Reticle
2
+ Copyright 2026 Reticle Labs
3
+
4
+ This product includes software developed at Reticle Labs.
5
+ Licensed under the Apache License, Version 2.0.
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @reticlehq/init
2
+
3
+ The project scaffolder behind `reticle init`. Detects the framework and package manager, writes `.reticle.json`, patches the build config, generates the connect snippet and the agent rules, and registers the Reticle MCP server with whichever clients are on the machine.
4
+
5
+ Split out of [`@reticlehq/server`](https://www.npmjs.com/package/@reticlehq/server) because nothing in here observes a running app: it is a build-time codemod that runs once. Keeping it inside the daemon meant a typo in a generated snippet forced a republish of the MCP server, and every MCP user downloaded a scaffolder they run once or never.
6
+
7
+ You do not install this directly — `@reticlehq/server` depends on it and the `reticle` CLI drives it:
8
+
9
+ ```bash
10
+ npx @reticlehq/server init
11
+ ```
12
+
13
+ ## Using it as a library
14
+
15
+ ```ts
16
+ import { buildNodeIo, runInit, type InitHost } from '@reticlehq/init';
17
+
18
+ const io = buildNodeIo(process.cwd(), host); // host: version, tracing, telemetry, pairing token
19
+ const result = runInit(
20
+ { cwd: process.cwd(), port: undefined, mcp: true, dryRun: false, install: true },
21
+ io,
22
+ );
23
+ ```
24
+
25
+ `InitHost` is the seam: everything the scaffolder cannot know for itself — the release version, a tracing hook, where to report the outcome, the bridge pairing token, the declared install channel — is supplied by the caller. That is what keeps this package free of any dependency on the daemon.
26
+
27
+ ## License
28
+
29
+ FSL-1.1-ALv2 — free for any Permitted Purpose, converts to Apache-2.0 two years after release. See [LICENSE](./LICENSE).
@@ -0,0 +1,116 @@
1
+ /**
2
+ * `reticle init` writes a behavioral rule into the coding agent's instruction file so the agent
3
+ * VERIFIES a feature with Reticle after building it — not only when the user remembers to say "test
4
+ * it with reticle". Registering the MCP server (mcp.ts / mcp-clients.ts) only makes the tools AVAILABLE;
5
+ * this is the piece that makes the agent reach for them, every session, unprompted.
6
+ *
7
+ * It carries a SECOND standing instruction, and that one is not about this project at all: send
8
+ * feedback to the Reticle maintainers, from the moment instrumentation starts. The agent is the party
9
+ * that experiences Reticle, and the only one who can say what it cost — and by the time anyone would
10
+ * think to ask, the context that knew is gone. Written into the same block, because the moment the
11
+ * rule lands (init, before any framework wiring) is the moment the setup itself is being lived
12
+ * through, which is when the reports we hear least about are actually generated.
13
+ *
14
+ * The rule is framed as a definition-of-done (WHEN to verify, HOW, and the anti-reward-hacking guard),
15
+ * written into whichever instruction file the detected agent reads: `CLAUDE.md` (Claude Code),
16
+ * `.cursor/rules/reticle.mdc` (Cursor, `alwaysApply` so it stays in context), or `AGENTS.md` as the
17
+ * cross-agent fallback. Idempotent via a marker block — safe to append to an existing file and a
18
+ * re-run is a no-op. Mirrors the apply/already result shape used across init (see mcp-clients.ts).
19
+ */
20
+ /** Project-root instruction files the agent re-reads every session. */
21
+ export declare const CLAUDE_MD_PATH = "CLAUDE.md";
22
+ export declare const AGENTS_MD_PATH = "AGENTS.md";
23
+ /**
24
+ * The full rules, written once and pointed at from every always-loaded file.
25
+ *
26
+ * Two problems it solves at the same time. The rule text is dense and most of it is REFERENCE (what
27
+ * to do when the tools are missing, what a `version_skew` field means, how to file feedback), which
28
+ * an agent needs on the turn it hits that situation and never before — so carrying all of it in
29
+ * `CLAUDE.md` taxed every session for text almost every session did not read. And `AGENTS.md` was
30
+ * only written when NO agent was detected, so a repo set up on a Claude Code machine told Codex,
31
+ * Copilot, Amp and every other agent nothing at all.
32
+ *
33
+ * So: the operative rules (when to verify, WHEN NOT TO, how, and the honesty guards) go in the files
34
+ * that load every turn, and this file holds the rest, one hop away and named in that block.
35
+ */
36
+ export declare const RETICLE_MD_PATH = "RETICLE.md";
37
+ /** Cursor project rule; the frontmatter `alwaysApply` keeps it in context on every turn. */
38
+ export declare const CURSOR_RULE_PATH = ".cursor/rules/reticle.mdc";
39
+ /**
40
+ * Does this file already carry the managed block?
41
+ *
42
+ * Exported so callers cannot invent their own looser test. The begin marker carries explanatory
43
+ * text inside the comment, so a substring check for `reticle:begin` matches files that
44
+ * `mergeMarkedInstruction` would treat as having NO block — and the two disagreeing means a refresh
45
+ * silently appends a second copy instead of updating the first.
46
+ */
47
+ export declare function hasManagedBlock(text: string): boolean;
48
+ /**
49
+ * How the agent must INVOKE the CLI.
50
+ *
51
+ * `reticle init` installs the SDK packages, never the server — so there is no `reticle` on the
52
+ * agent's PATH, and every bare `reticle <cmd>` this rule named was an instruction that could not be
53
+ * followed. Reported from a field sweep: three of them, in the file the agent re-reads every session.
54
+ *
55
+ * `npx @reticlehq/server` is the same invocation the MCP registration uses, and it is derived from
56
+ * the package's own name so the two can never drift apart.
57
+ */
58
+ export declare const CLI = "npx @reticlehq/server";
59
+ /**
60
+ * Who starts the dev server, stated once and rendered everywhere it is needed.
61
+ *
62
+ * The old rule was "Reticle never starts the dev server for you — that's your job", and agents obeyed
63
+ * it exactly: they read it, declined to start anything, and ended the turn with nothing verified. An
64
+ * instruction that reliably produces no verification is a defect in the instruction.
65
+ *
66
+ * The AGENT starts it, not the daemon. A daemon that spawns a build process is invisible to the
67
+ * person whose machine it is running on, was never consented to, and orphans when the daemon exits.
68
+ * The agent already has shell access in this repo, already runs install and build commands, and runs
69
+ * inside a host that asks the human for permission — so a dev server it starts is visible in the
70
+ * transcript, attributable, and stoppable.
71
+ *
72
+ * The guards are the whole reason this is safe, so no renderer may drop them: one server at a time,
73
+ * read the command rather than invent one, never kill anything you did not start, and leave the
74
+ * permission prompt to the host. "Background it and say so" is no longer among them because the
75
+ * branch above now states it at the point of action, and this text is read every turn — a rule
76
+ * stated twice costs every agent tokens forever and is the first thing to drift.
77
+ *
78
+ * The RESTART branch outranks the rest and is stated first. It used to be an aside, while "if a dev
79
+ * server is already listening, use it" was a numbered guard — so an agent resolving the two
80
+ * reasonably reused a server started before `init` edited the build config, served a bundle with no
81
+ * SDK in it, and landed on an empty session list with every other step done correctly.
82
+ *
83
+ * Exported because slash-command.ts renders the same rule; there must not be two versions of it.
84
+ */
85
+ export declare 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:\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_sessions` 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.";
86
+ /**
87
+ * The operative rules, carried by every file that loads on every turn.
88
+ *
89
+ * Short on purpose. This is the text that has to CHANGE BEHAVIOUR, so it states when to verify, when
90
+ * NOT to, how, and the guards against calling something proven when it is not. Everything an agent
91
+ * needs only at the moment it hits a specific situation lives in RETICLE.md, named at the end.
92
+ */
93
+ export declare const RULE_BODY = "## Verifying with Reticle\n\nThis app is instrumented by **Reticle**, an in-app verification layer exposed as `reticle_*` MCP tools and the `npx @reticlehq/server` CLI (always through npx: Reticle's server is not installed into this project). Verifying is part of \"done\", not an optional extra.\n\n**Verify when you have changed something a user can see or do.** A component, a form, a route, a request, a piece of state that reaches the screen. Do it BEFORE telling the user it is complete. Reading the diff proves nothing and unit tests do not run the app.\n\n**Do not reach for Reticle when the change cannot show up in the running app.** It costs tool calls and the user's patience, and a verdict over an unrelated flow proves nothing about what you changed. Skip it for: documentation, comments, tests, build config, CI, dependency bumps with no user-facing effect, backend or CLI work with no UI surface, and any change to a project that is not a running web app. Say in one line that you skipped verification and why, rather than silently not doing it.\n\n**How to verify:**\n\n- 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.\n- Batch a multi-step journey (a login, a form) into one `reticle_act_sequence` rather than one round trip per field.\n- Read the surrounding evidence with `reticle_snapshot`, `reticle_state`, `reticle_network`, `reticle_console`.\n- **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.\n- Covered flows: `npx @reticlehq/server gate` reports which recorded flows the changed files affect and whether they still pass.\n\n**Setting Reticle up, or just restarted? You are mid-sequence \u2014 do not stop until a verdict exists.**\nThe whole of it is: instrument the app \u2192 get a dev server running \u2192 open the app in a browser \u2192\ndrive one flow \u2192 report the verdict. Every step is yours to do, and none of them needs the user.\nThe ONE legitimate pause is a client restart, because a client reads its server list at startup;\nwhen you come back, you are not finished, you are on the next step \u2014 resume it without being asked.\nStopping anywhere else leaves an app that looks installed and can verify nothing, which is the\nsingle commonest way this goes wrong. `/reticle` carries the full recovery ladder when a step\ndoes not work; use it rather than guessing, and never report the install as finished without a\nverdict to point at.\n\n**Nothing connected? Get the app running.**\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_sessions` 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\nA dev server that is already running does not pick up an edited build config or a newly created plugin file \u2014 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.\n\n**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 \u2014 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.\n\n**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.\n\n**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.\n\n**Honesty, which is the whole point:**\n\n- **`verified: \"unknown\"` is not a pass.** It means Reticle drove the app and could not tell what happened; `verifiedReason` says which clause decided that. Report it as unknown, never as working.\n- **`verified: \"no-fault\"` is not a pass either.** It means nothing was DECLARED to prove: the page settled and no channel complained, but you asserted nothing, so there is no verification. You get it whenever `until` is omitted. Name a consequence the action changes \u2014 a signal, a request, a route, or store state \u2014 and call again.\n- **Never weaken a check to make it green.** Downgrading, skipping or deleting an assertion is a finding, not a fix.\n- **If Reticle cannot run** (no daemon, or this is not a running web app), say so. Do not skip verification silently.\n- **Setup is not finished until one real flow has been driven and produced a verdict.** `init` exiting 0, the tools appearing, and a session being listed are all things that happen before anything has been verified.\n\n**The `/reticle` skill runs this whole loop for you** \u2014 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.\n\n**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 \u2014 what is established, what is proven, what is still unverified \u2014 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.\n\n**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.\n\n\uD83D\uDCC4 **The rest is in [RETICLE.md](./RETICLE.md): 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.**";
94
+ /** The full-rules file, written verbatim (no markers: `init` owns this file entirely). */
95
+ export declare function reticleMdFile(): string;
96
+ export declare const AgentRuleStatus: {
97
+ readonly APPLY: "apply";
98
+ readonly ALREADY: "already";
99
+ };
100
+ export type AgentRuleStatus = (typeof AgentRuleStatus)[keyof typeof AgentRuleStatus];
101
+ interface AgentRuleResult {
102
+ status: AgentRuleStatus;
103
+ /** Full file content to write when status is APPLY; the unchanged input otherwise. */
104
+ content: string;
105
+ }
106
+ /** The marker-wrapped block appended to a CLAUDE.md / AGENTS.md instruction file. */
107
+ export declare function markedBlock(): string;
108
+ /**
109
+ * Merge the managed block into a marker-delimited instruction file. Already present ⇒ no change
110
+ * (idempotent re-run). Absent/empty ⇒ the block alone. Otherwise append the block, preserving the
111
+ * existing content and a clean separating blank line.
112
+ */
113
+ export declare function mergeMarkedInstruction(existing: string | null | undefined): AgentRuleResult;
114
+ /** The Cursor rule file (.mdc): `alwaysApply` keeps the rule in every turn's context. */
115
+ export declare function cursorRuleFile(): string;
116
+ export {};
@@ -0,0 +1,279 @@
1
+ /**
2
+ * `reticle init` writes a behavioral rule into the coding agent's instruction file so the agent
3
+ * VERIFIES a feature with Reticle after building it — not only when the user remembers to say "test
4
+ * it with reticle". Registering the MCP server (mcp.ts / mcp-clients.ts) only makes the tools AVAILABLE;
5
+ * this is the piece that makes the agent reach for them, every session, unprompted.
6
+ *
7
+ * It carries a SECOND standing instruction, and that one is not about this project at all: send
8
+ * feedback to the Reticle maintainers, from the moment instrumentation starts. The agent is the party
9
+ * that experiences Reticle, and the only one who can say what it cost — and by the time anyone would
10
+ * think to ask, the context that knew is gone. Written into the same block, because the moment the
11
+ * rule lands (init, before any framework wiring) is the moment the setup itself is being lived
12
+ * through, which is when the reports we hear least about are actually generated.
13
+ *
14
+ * The rule is framed as a definition-of-done (WHEN to verify, HOW, and the anti-reward-hacking guard),
15
+ * written into whichever instruction file the detected agent reads: `CLAUDE.md` (Claude Code),
16
+ * `.cursor/rules/reticle.mdc` (Cursor, `alwaysApply` so it stays in context), or `AGENTS.md` as the
17
+ * cross-agent fallback. Idempotent via a marker block — safe to append to an existing file and a
18
+ * re-run is a no-op. Mirrors the apply/already result shape used across init (see mcp-clients.ts).
19
+ */
20
+ import { NPX } from './mcp.js';
21
+ import { RETICLE_NPM_PACKAGE } from './version.js';
22
+ /** Project-root instruction files the agent re-reads every session. */
23
+ export const CLAUDE_MD_PATH = 'CLAUDE.md';
24
+ export const AGENTS_MD_PATH = 'AGENTS.md';
25
+ /**
26
+ * The full rules, written once and pointed at from every always-loaded file.
27
+ *
28
+ * Two problems it solves at the same time. The rule text is dense and most of it is REFERENCE (what
29
+ * to do when the tools are missing, what a `version_skew` field means, how to file feedback), which
30
+ * an agent needs on the turn it hits that situation and never before — so carrying all of it in
31
+ * `CLAUDE.md` taxed every session for text almost every session did not read. And `AGENTS.md` was
32
+ * only written when NO agent was detected, so a repo set up on a Claude Code machine told Codex,
33
+ * Copilot, Amp and every other agent nothing at all.
34
+ *
35
+ * So: the operative rules (when to verify, WHEN NOT TO, how, and the honesty guards) go in the files
36
+ * that load every turn, and this file holds the rest, one hop away and named in that block.
37
+ */
38
+ export const RETICLE_MD_PATH = 'RETICLE.md';
39
+ /** Cursor project rule; the frontmatter `alwaysApply` keeps it in context on every turn. */
40
+ export const CURSOR_RULE_PATH = '.cursor/rules/reticle.mdc';
41
+ /** Markers delimiting the managed block so a re-run is idempotent and the block stays updatable. */
42
+ const RULE_BEGIN = '<!-- reticle:begin (managed by `reticle init` — edit outside these markers) -->';
43
+ const RULE_END = '<!-- reticle:end -->';
44
+ /**
45
+ * Does this file already carry the managed block?
46
+ *
47
+ * Exported so callers cannot invent their own looser test. The begin marker carries explanatory
48
+ * text inside the comment, so a substring check for `reticle:begin` matches files that
49
+ * `mergeMarkedInstruction` would treat as having NO block — and the two disagreeing means a refresh
50
+ * silently appends a second copy instead of updating the first.
51
+ */
52
+ export function hasManagedBlock(text) {
53
+ return text.includes(RULE_BEGIN);
54
+ }
55
+ /**
56
+ * How the agent must INVOKE the CLI.
57
+ *
58
+ * `reticle init` installs the SDK packages, never the server — so there is no `reticle` on the
59
+ * agent's PATH, and every bare `reticle <cmd>` this rule named was an instruction that could not be
60
+ * followed. Reported from a field sweep: three of them, in the file the agent re-reads every session.
61
+ *
62
+ * `npx @reticlehq/server` is the same invocation the MCP registration uses, and it is derived from
63
+ * the package's own name so the two can never drift apart.
64
+ */
65
+ export const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
66
+ /**
67
+ * Who starts the dev server, stated once and rendered everywhere it is needed.
68
+ *
69
+ * The old rule was "Reticle never starts the dev server for you — that's your job", and agents obeyed
70
+ * it exactly: they read it, declined to start anything, and ended the turn with nothing verified. An
71
+ * instruction that reliably produces no verification is a defect in the instruction.
72
+ *
73
+ * The AGENT starts it, not the daemon. A daemon that spawns a build process is invisible to the
74
+ * person whose machine it is running on, was never consented to, and orphans when the daemon exits.
75
+ * The agent already has shell access in this repo, already runs install and build commands, and runs
76
+ * inside a host that asks the human for permission — so a dev server it starts is visible in the
77
+ * transcript, attributable, and stoppable.
78
+ *
79
+ * The guards are the whole reason this is safe, so no renderer may drop them: one server at a time,
80
+ * read the command rather than invent one, never kill anything you did not start, and leave the
81
+ * permission prompt to the host. "Background it and say so" is no longer among them because the
82
+ * branch above now states it at the point of action, and this text is read every turn — a rule
83
+ * stated twice costs every agent tokens forever and is the first thing to drift.
84
+ *
85
+ * The RESTART branch outranks the rest and is stated first. It used to be an aside, while "if a dev
86
+ * server is already listening, use it" was a numbered guard — so an agent resolving the two
87
+ * reasonably reused a server started before `init` edited the build config, served a bundle with no
88
+ * SDK in it, and landed on an empty session list with every other step done correctly.
89
+ *
90
+ * Exported because slash-command.ts renders the same rule; there must not be two versions of it.
91
+ */
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
+
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.
96
+
97
+ Stopping to ask is how a verification turn ends with nothing verified.
98
+
99
+ Four guards, none optional:
100
+
101
+ 1. **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.
102
+ 2. **Never guess the command.** It comes from \`package.json\` scripts. No recognisable dev script means say so and stop, not invent one.
103
+ 3. **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.
104
+ 4. **The permission prompt belongs to your host.** Never bypass, suppress or auto-approve it, and take a refusal as the answer.`;
105
+ /**
106
+ * The operative rules, carried by every file that loads on every turn.
107
+ *
108
+ * Short on purpose. This is the text that has to CHANGE BEHAVIOUR, so it states when to verify, when
109
+ * NOT to, how, and the guards against calling something proven when it is not. Everything an agent
110
+ * needs only at the moment it hits a specific situation lives in RETICLE.md, named at the end.
111
+ */
112
+ export const RULE_BODY = `## Verifying with Reticle
113
+
114
+ This app is instrumented by **Reticle**, an in-app verification layer exposed as \`reticle_*\` MCP tools and the \`${CLI}\` CLI (always through npx: Reticle's server is not installed into this project). Verifying is part of "done", not an optional extra.
115
+
116
+ **Verify when you have changed something a user can see or do.** A component, a form, a route, a request, a piece of state that reaches the screen. Do it BEFORE telling the user it is complete. Reading the diff proves nothing and unit tests do not run the app.
117
+
118
+ **Do not reach for Reticle when the change cannot show up in the running app.** It costs tool calls and the user's patience, and a verdict over an unrelated flow proves nothing about what you changed. Skip it for: documentation, comments, tests, build config, CI, dependency bumps with no user-facing effect, backend or CLI work with no UI surface, and any change to a project that is not a running web app. Say in one line that you skipped verification and why, rather than silently not doing it.
119
+
120
+ **How to verify:**
121
+
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\`.
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
+ - Covered flows: \`${CLI} gate\` reports which recorded flows the changed files affect and whether they still pass.
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.
137
+
138
+ **Nothing connected? Get the app running.**
139
+
140
+ ${DEV_SERVER_POLICY}
141
+
142
+ 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
+
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.
145
+
146
+ **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
+
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.
149
+
150
+ **Honesty, which is the whole point:**
151
+
152
+ - **\`verified: "unknown"\` is not a pass.** It means Reticle drove the app and could not tell what happened; \`verifiedReason\` says which clause decided that. Report it as unknown, never as working.
153
+ - **\`verified: "no-fault"\` is not a pass either.** It means nothing was DECLARED to prove: the page settled and no channel complained, but you asserted nothing, so there is no verification. You get it whenever \`until\` is omitted. Name a consequence the action changes — a signal, a request, a route, or store state — and call again.
154
+ - **Never weaken a check to make it green.** Downgrading, skipping or deleting an assertion is a finding, not a fix.
155
+ - **If Reticle cannot run** (no daemon, or this is not a running web app), say so. Do not skip verification silently.
156
+ - **Setup is not finished until one real flow has been driven and produced a verdict.** \`init\` exiting 0, the tools appearing, and a session being listed are all things that happen before anything has been verified.
157
+
158
+ **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
+
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.
161
+
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.
163
+
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.**`;
165
+ /**
166
+ * The reference half. Fetched when a situation calls for it rather than carried every turn.
167
+ *
168
+ * Every section here answers a question an agent only has once it is already stuck, and each one was
169
+ * written because the absence of it cost somebody a session: tools that were never loaded read as a
170
+ * broken install, a `version_skew` field went unread while its green verdicts were trusted, an empty
171
+ * `reticle_state` was reported as a clean setup, and a defect in Reticle was worked around in silence.
172
+ */
173
+ const RETICLE_MD_BODY = `# Reticle: the full rules
174
+
175
+ The short version lives in your agent instruction file (\`CLAUDE.md\` / \`AGENTS.md\` / \`.cursor/rules/reticle.mdc\`) and loads every turn. This file is the reference: read the section that matches the situation you are in.
176
+
177
+ ## The tools are missing entirely
178
+
179
+ They have not been loaded yet, which is not the same as broken. \`init\` registers the MCP server, but a client reads its server list at startup and does not re-read it, and no slash command reloads it (\`/mcp\` only manages servers already loaded).
180
+
181
+ Ask the human to RESTART their client: restart Claude Code, reload the window in Cursor, or hit Start in \`.vscode/mcp.json\` in VS Code. Then wait and carry on. This is once per machine, because Reticle is registered globally and later projects already have the tools. \`${CLI} status\` works meanwhile and says whether the daemon is up and whether the app has connected.
182
+
183
+ ## A result carries \`verify_next\`
184
+
185
+ You have driven this page several times without asking for a verdict, so nothing you have done so far is proven. Do what the field says next.
186
+
187
+ ## A result carries \`version_skew\`
188
+
189
+ Act on it before trusting anything else. Two pieces of Reticle are on different wire contracts, so tools can misbehave in ways neither side reports, and a green is not evidence. The field names the pair that disagrees and the exact fix: run \`${CLI} stop\` and retry when the DAEMON is stale, or tell the human to install the matching SDK and restart their dev server when the PAGE is. Then redo whatever you were verifying.
190
+
191
+ ## A result carries \`update_available\`
192
+
193
+ 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
+
195
+ ## \`reticle_state\` comes back empty
196
+
197
+ 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
+
199
+ Finish that file when you see it. It is what lets you check what the app BELIEVES, not only what it rendered, which is the class of bug a screenshot cannot show. Never report a clean setup over an empty state read. It blocks nothing else: DOM, network, console and source pointers all work without it.
200
+
201
+ ## Sending feedback to the Reticle team
202
+
203
+ **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
+
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.
206
+ - **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
+ - **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
+ - **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.
209
+ - **Write it so it can be acted on.** For a failure: what you called, what you expected, what you got, and the call trail in \`trace\`. For a request: the GOAL in \`need\` (not your guess at the solution), what improves in \`impact\`, and how you work around it today in \`currentApproach\`. That workaround is usually the most useful line in the report. Set \`model\` to the model you are running.
210
+ - **Working around a Reticle defect in silence is the one thing not to do.** A silent workaround is how the same defect survives to the next user.
211
+ - **Report defects in *Reticle*, not in the app under test.** A bug you find in this app is Reticle working as intended; that one belongs in your report to the user.
212
+ - **Never include app source, secrets, user data, or customer records** in a report.
213
+
214
+ ## More
215
+
216
+ The docs are built to be fetched rather than browsed: \`https://docs.reticle.sh/llms.txt\` lists every page, and appending \`.md\` to any page URL returns its source with no site chrome.
217
+ `;
218
+ /** The full-rules file, written verbatim (no markers: `init` owns this file entirely). */
219
+ export function reticleMdFile() {
220
+ return RETICLE_MD_BODY;
221
+ }
222
+ export const AgentRuleStatus = {
223
+ APPLY: 'apply',
224
+ ALREADY: 'already',
225
+ };
226
+ /** The marker-wrapped block appended to a CLAUDE.md / AGENTS.md instruction file. */
227
+ export function markedBlock() {
228
+ return `${RULE_BEGIN}\n${RULE_BODY}\n${RULE_END}\n`;
229
+ }
230
+ /**
231
+ * Merge the managed block into a marker-delimited instruction file. Already present ⇒ no change
232
+ * (idempotent re-run). Absent/empty ⇒ the block alone. Otherwise append the block, preserving the
233
+ * existing content and a clean separating blank line.
234
+ */
235
+ export function mergeMarkedInstruction(existing) {
236
+ if (existing !== null && existing !== undefined && existing.includes(RULE_BEGIN)) {
237
+ // A block that can never be updated is not managed. Returning ALREADY on sight of the marker
238
+ // meant a project that ran init once kept that release's rule text forever, so every improvement
239
+ // reached only new projects — and a rule about a field introduced later (`version_skew`) could
240
+ // never arrive at all. The markers promise the inside is ours; idempotence comes from comparing
241
+ // CONTENT instead of presence.
242
+ return refreshMarkedBlock(existing);
243
+ }
244
+ const block = markedBlock();
245
+ if (null === existing || existing === undefined || 0 === existing.trim().length) {
246
+ return { status: AgentRuleStatus.APPLY, content: block };
247
+ }
248
+ const separator = existing.endsWith('\n') ? '\n' : '\n\n';
249
+ return { status: AgentRuleStatus.APPLY, content: `${existing}${separator}${block}` };
250
+ }
251
+ /**
252
+ * Swap the managed block for the current one, leaving every character outside the markers alone.
253
+ *
254
+ * A malformed file — a begin marker with no end — is left untouched and reported ALREADY. Rewriting
255
+ * from a marker to end-of-file would eat whatever the human wrote after it, and silently destroying
256
+ * someone's instruction file is far worse than a stale rule.
257
+ */
258
+ function refreshMarkedBlock(existing) {
259
+ const start = existing.indexOf(RULE_BEGIN);
260
+ const endAt = existing.indexOf(RULE_END, start);
261
+ if (-1 === endAt)
262
+ return { status: AgentRuleStatus.ALREADY, content: existing };
263
+ const current = existing.slice(start, endAt + RULE_END.length);
264
+ const wanted = markedBlock().trimEnd();
265
+ if (current === wanted)
266
+ return { status: AgentRuleStatus.ALREADY, content: existing };
267
+ const content = `${existing.slice(0, start)}${wanted}${existing.slice(endAt + RULE_END.length)}`;
268
+ return { status: AgentRuleStatus.APPLY, content };
269
+ }
270
+ /** The Cursor rule file (.mdc): `alwaysApply` keeps the rule in every turn's context. */
271
+ export function cursorRuleFile() {
272
+ return `---
273
+ description: Verify web features with Reticle after building them, and report Reticle problems back to its maintainers
274
+ alwaysApply: true
275
+ ---
276
+
277
+ ${RULE_BODY}
278
+ `;
279
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Which app in a workspace `reticle init` should wire, when the caller says.
3
+ *
4
+ * Refusing to guess between several apps is right — instrumenting one silently would leave the rest
5
+ * unverified while reporting success. But "re-run inside the one you want" is not an answer a
6
+ * script, a CI step, or an agent that cannot change directory can act on, so the refusal was a dead
7
+ * end for exactly the callers most likely to hit it.
8
+ *
9
+ * A name that is not one of the discovered apps is refused rather than trusted: it is almost always
10
+ * a typo or a stale path, and wiring a directory that is not an app writes files nothing compiles.
11
+ */
12
+ type AppChoice = {
13
+ ok: true;
14
+ app: string | undefined;
15
+ } | {
16
+ ok: false;
17
+ message: string;
18
+ };
19
+ /**
20
+ * Trim the trailing slashes tab-completion adds, without a regex.
21
+ *
22
+ * `/\/+$/` is quadratic on a long run of slashes: the engine retries the match from every start
23
+ * position. Nobody is attacking their own `--app` flag, so the risk here is theoretical — but the
24
+ * loop is the same two lines, is linear, and keeps a real ReDoS finding out of the scanner's
25
+ * results, where a theoretical one costs the attention that a genuine one needs.
26
+ */
27
+ export declare function withoutTrailingSlashes(path: string): string;
28
+ export declare function chooseWorkspaceApp(requested: string | undefined, apps: readonly string[]): AppChoice;
29
+ export {};
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Which app in a workspace `reticle init` should wire, when the caller says.
3
+ *
4
+ * Refusing to guess between several apps is right — instrumenting one silently would leave the rest
5
+ * unverified while reporting success. But "re-run inside the one you want" is not an answer a
6
+ * script, a CI step, or an agent that cannot change directory can act on, so the refusal was a dead
7
+ * end for exactly the callers most likely to hit it.
8
+ *
9
+ * A name that is not one of the discovered apps is refused rather than trusted: it is almost always
10
+ * a typo or a stale path, and wiring a directory that is not an app writes files nothing compiles.
11
+ */
12
+ /**
13
+ * Trim the trailing slashes tab-completion adds, without a regex.
14
+ *
15
+ * `/\/+$/` is quadratic on a long run of slashes: the engine retries the match from every start
16
+ * position. Nobody is attacking their own `--app` flag, so the risk here is theoretical — but the
17
+ * loop is the same two lines, is linear, and keeps a real ReDoS finding out of the scanner's
18
+ * results, where a theoretical one costs the attention that a genuine one needs.
19
+ */
20
+ export function withoutTrailingSlashes(path) {
21
+ let end = path.length;
22
+ while (0 < end && '/' === path[end - 1])
23
+ end -= 1;
24
+ return path.slice(0, end);
25
+ }
26
+ export function chooseWorkspaceApp(requested, apps) {
27
+ if (requested === undefined || '' === requested)
28
+ return { ok: true, app: undefined };
29
+ // Tab-completion adds a trailing slash; the discovered names never carry one.
30
+ const wanted = withoutTrailingSlashes(requested);
31
+ if (apps.includes(wanted))
32
+ return { ok: true, app: wanted };
33
+ return {
34
+ ok: false,
35
+ message: 0 === apps.length
36
+ ? `--app ${requested} was given, but no app was found in this workspace`
37
+ : `--app ${requested} is not one of the apps found here: ${apps.join(', ')}`,
38
+ };
39
+ }