@reticlehq/init 3.7.0 → 3.8.1

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 (45) hide show
  1. package/dist/detect/detect.d.ts +9 -0
  2. package/dist/detect/detect.js +5 -1
  3. package/dist/detect/dev-server-ports.d.ts +2 -0
  4. package/dist/detect/dev-server-ports.js +20 -0
  5. package/dist/detect/prettier-config.d.ts +21 -0
  6. package/dist/detect/prettier-config.js +38 -0
  7. package/dist/detect/workspace-redirect.js +23 -8
  8. package/dist/diagnose/closing-hint.d.ts +5 -0
  9. package/dist/diagnose/closing-hint.js +18 -11
  10. package/dist/gather-plan-input.js +70 -18
  11. package/dist/host.d.ts +6 -0
  12. package/dist/index.d.ts +8 -2
  13. package/dist/index.js +8 -2
  14. package/dist/no-package-json.js +62 -14
  15. package/dist/patch/next-patch.d.ts +19 -11
  16. package/dist/patch/next-patch.js +33 -6
  17. package/dist/patch/prettier-ignore.d.ts +34 -0
  18. package/dist/patch/prettier-ignore.js +49 -0
  19. package/dist/patch/python-snippets.d.ts +22 -0
  20. package/dist/patch/python-snippets.js +113 -0
  21. package/dist/patch/snippets.d.ts +9 -30
  22. package/dist/patch/snippets.js +40 -103
  23. package/dist/patch/static-page.js +3 -5
  24. package/dist/patch/vite-config.js +11 -0
  25. package/dist/plan/connect-steps.d.ts +1 -0
  26. package/dist/plan/connect-steps.js +1 -0
  27. package/dist/plan/framework-adapter.d.ts +10 -0
  28. package/dist/plan/framework-adapter.js +120 -1
  29. package/dist/plan/notices.js +1 -1
  30. package/dist/plan/plan-framework.js +101 -32
  31. package/dist/plan/plan-types.d.ts +27 -0
  32. package/dist/plan/plan.d.ts +1 -10
  33. package/dist/plan/plan.js +128 -131
  34. package/dist/plan/stop-hook-step.d.ts +11 -0
  35. package/dist/plan/stop-hook-step.js +82 -6
  36. package/dist/project/agent-rules.d.ts +1 -1
  37. package/dist/project/agent-rules.js +8 -8
  38. package/dist/project/refresh-rules.js +2 -1
  39. package/dist/register/prompt-hooks.d.ts +123 -0
  40. package/dist/register/prompt-hooks.js +449 -0
  41. package/dist/register/slash-command.d.ts +1 -1
  42. package/dist/register/slash-command.js +5 -4
  43. package/dist/run-types.d.ts +4 -0
  44. package/dist/run.js +4 -1
  45. package/package.json +2 -2
@@ -0,0 +1,449 @@
1
+ /**
2
+ * The prompt-capture hook, in the config of each agent that runs a command when a prompt is sent.
3
+ *
4
+ * The user's request is what a verdict is checked against. An agent relays it with
5
+ * `reticle_intent { declare, request }` only when it remembers to, so the agents that have a
6
+ * prompt-submit hook hand it over themselves: the hook runs `reticle hook prompt`, which writes the
7
+ * prompt into the project's `.reticle/`.
8
+ *
9
+ * Every event name, path and shape here comes from the vendor's documentation, read on 2026-10-14
10
+ * (the `docs` field names the page). The configs are PROJECT-level, so the capture follows the
11
+ * repository. An agent with no prompt hook, or one whose hook format is not documented well enough
12
+ * to write, is not here: it relies on the MCP instructions, which tell the agent to declare the
13
+ * request. A config at a guessed path is one nobody reads, and that looks the same as success.
14
+ *
15
+ * The merge is JSON only, and it never rewrites a file that is not a plain JSON object (comments,
16
+ * trailing commas): the caller prints the entry instead.
17
+ */
18
+ import { RETICLE_NPM_PACKAGE, RETICLE_VERSION } from '../version.js';
19
+ export const PromptHookAgent = {
20
+ CLAUDE_CODE: 'claude-code',
21
+ CURSOR: 'cursor',
22
+ CODEX: 'codex',
23
+ GEMINI: 'gemini',
24
+ WINDSURF: 'windsurf',
25
+ /** VS Code agent mode and Copilot CLI: both read `.github/hooks/*.json`. */
26
+ COPILOT: 'copilot',
27
+ FACTORY: 'factory-droid',
28
+ };
29
+ export const PromptHookStatus = {
30
+ APPLY: 'apply',
31
+ ALREADY: 'already',
32
+ MANUAL: 'manual',
33
+ };
34
+ /** The words every version of our command holds, and no other command does. */
35
+ export const PROMPT_HOOK_MARKER = ' hook prompt --agent ';
36
+ export const AGENT_FLAG = '--agent';
37
+ /**
38
+ * Seconds. The hook itself finishes in well under one. The rest is for `npx`, which downloads the
39
+ * pinned version once when its cache does not hold it.
40
+ */
41
+ const HOOK_TIMEOUT_S = 30;
42
+ /**
43
+ * The command, PINNED to the version that installed it. Unpinned, `npx` runs whatever version its
44
+ * cache holds: on a real machine that was 3.4.0, which has no `hook` command, so every prompt was
45
+ * lost while the hook still exited quietly. A re-run of `init` moves the pin forward.
46
+ */
47
+ export const promptHookCommand = (agent) => `npx -y ${RETICLE_NPM_PACKAGE}@${RETICLE_VERSION}${PROMPT_HOOK_MARKER}${agent}`;
48
+ /** The words every version of the post-tool command holds. */
49
+ export const TOOL_HOOK_MARKER = ' hook tool --agent ';
50
+ /**
51
+ * The post-tool command: keeps one of the agent's own tool calls in `.reticle/episodes/`. Pinned
52
+ * for the same reason as the prompt command.
53
+ */
54
+ export const toolHookCommand = (agent) => `npx -y ${RETICLE_NPM_PACKAGE}@${RETICLE_VERSION}${TOOL_HOOK_MARKER}${agent}`;
55
+ /**
56
+ * The agent's own tools worth keeping: shell, edits, and the web. Reads and searches of the local
57
+ * tree are left out: they change nothing, and they are most of the calls.
58
+ */
59
+ const RECORDED_TOOLS = 'Bash|Edit|Write|MultiEdit|NotebookEdit|WebFetch|WebSearch';
60
+ /**
61
+ * Claude Code's post-tool entry. `async`, documented on 2026-10-18, runs it in the background, so
62
+ * the `npx` start never slows the agent's next step.
63
+ */
64
+ const postToolGroup = (command) => ({
65
+ matcher: RECORDED_TOOLS,
66
+ hooks: [{ type: 'command', command, async: true }],
67
+ });
68
+ /** Claude Code names the model only at SessionStart (`model`) and on PostModelSwitch (`to_model`). */
69
+ const modelGroup = (command) => ({
70
+ hooks: [{ type: 'command', command, async: true }],
71
+ });
72
+ /**
73
+ * Codex's post-tool entry: the tool names its reference gives (`Bash`, `apply_patch`), and `async`,
74
+ * which it documents for command hooks (2026-10-18).
75
+ */
76
+ const codexPostTool = (command) => ({
77
+ matcher: 'Bash|apply_patch',
78
+ hooks: [{ type: 'command', command, timeout: HOOK_TIMEOUT_S, async: true }],
79
+ });
80
+ /**
81
+ * Cursor's post-tool entry, for the tool names its reference lists (`Shell`, `Write`, `Delete`).
82
+ * Cursor documents no background option, so the agent may wait for each run.
83
+ */
84
+ const cursorPostTool = (command) => ({
85
+ command,
86
+ matcher: 'Shell|Write|Delete',
87
+ });
88
+ /**
89
+ * Gemini CLI's AfterTool entry, for the one built-in tool name its reference gives
90
+ * (`run_shell_command`). No background option is documented, and no timeout: the unit is unsettled.
91
+ */
92
+ /** An entry that is only the command: Windsurf and Cursor events with no matcher (2026-10-18). */
93
+ const commandOnly = (command) => ({ command });
94
+ /**
95
+ * Copilot CLI's `postToolUse` entry. `matcher` is a full-match regex on `toolName`; `bash` and
96
+ * `edit` are the names its reference shows (2026-10-18). VS Code reads `PostToolUse` from the same
97
+ * file, but its reference gives no input fields, so nothing is written for it.
98
+ */
99
+ const copilotPostTool = (command) => ({
100
+ type: 'command',
101
+ bash: command,
102
+ powershell: npxPart(command),
103
+ timeoutSec: HOOK_TIMEOUT_S,
104
+ matcher: 'bash|edit',
105
+ });
106
+ /** Factory Droid's `PostToolUse` entry, for its documented shell, edit and web tool names. */
107
+ const factoryPostTool = (command) => ({
108
+ matcher: 'Execute|Edit|ApplyPatch|Create|FetchUrl|WebSearch',
109
+ hooks: [{ type: 'command', command, timeout: HOOK_TIMEOUT_S }],
110
+ });
111
+ /** Codex's Stop entry, in the background like its post-tool one. */
112
+ const codexStop = (command) => ({
113
+ hooks: [{ type: 'command', command, timeout: HOOK_TIMEOUT_S, async: true }],
114
+ });
115
+ /** Gemini CLI's AfterAgent entry: not a tool event, so no matcher. */
116
+ const geminiAfterAgent = (command) => ({
117
+ hooks: [{ type: 'command', command }],
118
+ });
119
+ const geminiAfterTool = (command) => ({
120
+ matcher: 'run_shell_command',
121
+ hooks: [{ type: 'command', command }],
122
+ });
123
+ /**
124
+ * A bin path a shell reads as one word: absolute (or under `$HOME`), with no quote, backtick,
125
+ * backslash or other `$`. Anything else keeps npx alone: a hook that runs the wrong thing is worse
126
+ * than a slow one.
127
+ */
128
+ const SAFE_BIN = /^(?:\$HOME)?\/[^"`\\$]*$/;
129
+ /**
130
+ * Run the installed CLI with node, and fall back to the pinned npx command when that file is gone.
131
+ * npx's own start (~350 ms on a laptop) came before every prompt and tool call. The path is this
132
+ * machine's; the npx half is what every other machine runs, so the entry stays correct for a
133
+ * teammate. `node` exits at once on a missing file, before it reads stdin, so the fallback still
134
+ * gets the payload.
135
+ */
136
+ export function directHookCommand(bin, npx) {
137
+ if (!SAFE_BIN.test(bin)) {
138
+ return npx;
139
+ }
140
+ const args = npx.slice(npx.indexOf(' hook ') + 1);
141
+ return `node "${bin}" ${args}${OR_ELSE}${npx}`;
142
+ }
143
+ /**
144
+ * The npx half of a command. Windows PowerShell 5 has no `||`, and a committed file reaches
145
+ * teammates on every shell, so a `powershell` field always runs npx alone.
146
+ */
147
+ function npxPart(command) {
148
+ const at = command.indexOf(OR_ELSE);
149
+ return -1 === at ? command : command.slice(at + OR_ELSE.length);
150
+ }
151
+ const OR_ELSE = ' || ';
152
+ const slotCommand = (spec, slot) => (slot.command ?? promptHookCommand)(spec.id);
153
+ /** `{ hooks: [{ type, command, timeout }] }`: Claude Code's group shape, which Codex and Factory copy. */
154
+ const group = (command) => ({
155
+ hooks: [{ type: 'command', command, timeout: HOOK_TIMEOUT_S }],
156
+ });
157
+ export const PROMPT_HOOK_AGENTS = [
158
+ {
159
+ id: PromptHookAgent.CLAUDE_CODE,
160
+ label: 'Claude Code',
161
+ paths: ['.claude/settings.json'],
162
+ defaultPath: '.claude/settings.json',
163
+ slots: [
164
+ { path: ['hooks', 'UserPromptSubmit'], entry: group },
165
+ // The agent's own tool calls, after they ran or failed (2026-10-18).
166
+ { path: ['hooks', 'PostToolUse'], entry: postToolGroup, command: toolHookCommand },
167
+ { path: ['hooks', 'PostToolUseFailure'], entry: postToolGroup, command: toolHookCommand },
168
+ // The model, which no other Claude Code event carries.
169
+ { path: ['hooks', 'SessionStart'], entry: modelGroup, command: toolHookCommand },
170
+ { path: ['hooks', 'PostModelSwitch'], entry: modelGroup, command: toolHookCommand },
171
+ // The closing message: Stop carries `last_assistant_message` (2026-10-18).
172
+ { path: ['hooks', 'Stop'], entry: modelGroup, command: toolHookCommand },
173
+ ],
174
+ projectMarkers: ['.claude'],
175
+ homeMarkers: ['.claude'],
176
+ docs: 'https://code.claude.com/docs/en/hooks',
177
+ },
178
+ {
179
+ id: PromptHookAgent.CURSOR,
180
+ label: 'Cursor',
181
+ paths: ['.cursor/hooks.json'],
182
+ defaultPath: '.cursor/hooks.json',
183
+ header: { version: 1 },
184
+ slots: [
185
+ { path: ['hooks', 'beforeSubmitPrompt'], entry: (command) => ({ command }) },
186
+ { path: ['hooks', 'postToolUse'], entry: cursorPostTool, command: toolHookCommand },
187
+ { path: ['hooks', 'postToolUseFailure'], entry: cursorPostTool, command: toolHookCommand },
188
+ // Cursor's `stop` carries no text; `afterAgentResponse` carries the final message.
189
+ { path: ['hooks', 'afterAgentResponse'], entry: commandOnly, command: toolHookCommand },
190
+ ],
191
+ projectMarkers: ['.cursor'],
192
+ homeMarkers: ['.cursor'],
193
+ docs: 'https://cursor.com/docs/agent/hooks',
194
+ },
195
+ {
196
+ id: PromptHookAgent.CODEX,
197
+ label: 'Codex CLI',
198
+ paths: ['.codex/hooks.json'],
199
+ defaultPath: '.codex/hooks.json',
200
+ slots: [
201
+ { path: ['hooks', 'UserPromptSubmit'], entry: group },
202
+ { path: ['hooks', 'PostToolUse'], entry: codexPostTool, command: toolHookCommand },
203
+ { path: ['hooks', 'Stop'], entry: codexStop, command: toolHookCommand },
204
+ ],
205
+ projectMarkers: ['.codex'],
206
+ homeMarkers: ['.codex'],
207
+ docs: 'https://learn.chatgpt.com/docs/hooks',
208
+ // Codex runs a project hook only once its hash is trusted, and skips it silently until then:
209
+ // a headless `codex exec` ran a whole task here on 2026-10-19 and no hook fired.
210
+ activation: 'Codex runs these hooks only after you trust them: open `codex` here, type /hooks and trust the Reticle entries (again after an upgrade changes them). Its web search reaches no hook, so each Reticle hook imports it from the Codex session transcript.',
211
+ },
212
+ {
213
+ id: PromptHookAgent.GEMINI,
214
+ label: 'Gemini CLI',
215
+ paths: ['.gemini/settings.json'],
216
+ defaultPath: '.gemini/settings.json',
217
+ // No timeout: the docs do not settle its unit.
218
+ slots: [
219
+ {
220
+ path: ['hooks', 'BeforeAgent'],
221
+ entry: (command) => ({ matcher: '*', hooks: [{ type: 'command', command }] }),
222
+ },
223
+ { path: ['hooks', 'AfterTool'], entry: geminiAfterTool, command: toolHookCommand },
224
+ { path: ['hooks', 'AfterAgent'], entry: geminiAfterAgent, command: toolHookCommand },
225
+ ],
226
+ projectMarkers: ['.gemini'],
227
+ // The settings FILE: Antigravity shares `~/.gemini` and has no prompt hook.
228
+ homeMarkers: ['.gemini/settings.json'],
229
+ docs: 'https://geminicli.com/docs/hooks/reference/',
230
+ },
231
+ {
232
+ id: PromptHookAgent.WINDSURF,
233
+ label: 'Windsurf',
234
+ // The current layout first; the legacy file is read when the current one is absent.
235
+ paths: ['.devin/hooks.json', '.windsurf/hooks.json'],
236
+ defaultPath: '.windsurf/hooks.json',
237
+ slots: [
238
+ { path: ['hooks', 'pre_user_prompt'], entry: (command) => ({ command }) },
239
+ { path: ['hooks', 'post_run_command'], entry: commandOnly, command: toolHookCommand },
240
+ { path: ['hooks', 'post_write_code'], entry: commandOnly, command: toolHookCommand },
241
+ { path: ['hooks', 'post_cascade_response'], entry: commandOnly, command: toolHookCommand },
242
+ ],
243
+ projectMarkers: ['.windsurf', '.devin'],
244
+ homeMarkers: ['.codeium/windsurf'],
245
+ docs: 'https://docs.devin.ai/desktop/cascade/hooks.md',
246
+ },
247
+ {
248
+ id: PromptHookAgent.COPILOT,
249
+ label: 'VS Code Copilot and Copilot CLI',
250
+ // A file of our own, so uninstall can remove it whole.
251
+ paths: ['.github/hooks/reticle-prompt-capture.json'],
252
+ defaultPath: '.github/hooks/reticle-prompt-capture.json',
253
+ header: { version: 1 },
254
+ slots: [
255
+ {
256
+ path: ['hooks', 'UserPromptSubmit'],
257
+ entry: (command) => ({ type: 'command', command, timeout: HOOK_TIMEOUT_S }),
258
+ },
259
+ {
260
+ path: ['hooks', 'userPromptSubmitted'],
261
+ entry: (command) => ({
262
+ type: 'command',
263
+ bash: command,
264
+ powershell: npxPart(command),
265
+ timeoutSec: HOOK_TIMEOUT_S,
266
+ }),
267
+ },
268
+ { path: ['hooks', 'postToolUse'], entry: copilotPostTool, command: toolHookCommand },
269
+ ],
270
+ projectMarkers: ['.github/hooks', '.vscode'],
271
+ homeMarkers: ['.copilot', '.vscode'],
272
+ docs: 'https://code.visualstudio.com/docs/copilot/customization/hooks and https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-hooks-reference',
273
+ },
274
+ {
275
+ id: PromptHookAgent.FACTORY,
276
+ label: 'Factory Droid',
277
+ paths: ['.factory/hooks.json'],
278
+ defaultPath: '.factory/hooks.json',
279
+ // The events sit at the top of this file, with no `hooks` key around them.
280
+ slots: [
281
+ { path: ['UserPromptSubmit'], entry: group },
282
+ { path: ['PostToolUse'], entry: factoryPostTool, command: toolHookCommand },
283
+ ],
284
+ projectMarkers: ['.factory'],
285
+ homeMarkers: ['.factory'],
286
+ docs: 'https://docs.factory.com/reference/hooks-reference.md',
287
+ },
288
+ ];
289
+ /**
290
+ * The agents with no prompt hook that can be written, which therefore rely on the MCP instructions
291
+ * telling the agent to declare the request. Each one was checked on 2026-10-14: no prompt-submit
292
+ * event, or one whose config format the vendor does not document well enough to write.
293
+ */
294
+ export const MCP_FALLBACK_AGENTS = [
295
+ 'Claude Desktop (chat)',
296
+ 'Cursor CLI in -p mode',
297
+ 'Codex IDE extension',
298
+ 'OpenCode',
299
+ 'Antigravity',
300
+ 'Zed',
301
+ 'Warp',
302
+ 'Kiro',
303
+ 'Amazon Q Developer CLI',
304
+ 'Cline',
305
+ 'Roo Code',
306
+ 'Continue',
307
+ 'Amp',
308
+ ];
309
+ /**
310
+ * The agents used in `projectDir` or installed under `home`, each with the config to write.
311
+ *
312
+ * Only those: a `.gemini/settings.json` in the repository of somebody who has never run Gemini is
313
+ * litter, and it makes the next tool's detection wrong too. `claudeCli` counts Claude Code as
314
+ * present, because it keeps its registration behind its CLI rather than in a folder.
315
+ *
316
+ * `home` undefined reads the project only. `reticle hook install` passes that: it adds the hook to
317
+ * a project already set up, and an agent installed on this machine is not one the PROJECT uses.
318
+ * Reading the home folder there wrote five committed configs into a repository that had `.claude/`.
319
+ */
320
+ export function promptHookTargets(io, projectDir, home, claudeCli, join) {
321
+ const inProject = (rel) => join(projectDir, rel);
322
+ return PROMPT_HOOK_AGENTS.flatMap((spec) => {
323
+ const used = (PromptHookAgent.CLAUDE_CODE === spec.id && claudeCli) ||
324
+ spec.projectMarkers.some((marker) => io.exists(inProject(marker))) ||
325
+ (home !== undefined && spec.homeMarkers.some((marker) => io.exists(join(home, marker))));
326
+ if (!used) {
327
+ return [];
328
+ }
329
+ const path = inProject(promptHookPath(spec, (rel) => io.exists(inProject(rel))));
330
+ return [{ spec, path, existing: io.readFile(path) }];
331
+ });
332
+ }
333
+ const isObject = (value) => 'object' === typeof value && null !== value && !Array.isArray(value);
334
+ /** The config as an object, or undefined when the text is not one. Empty text is an empty config. */
335
+ function parseConfig(text) {
336
+ if (null === text || 0 === text.trim().length) {
337
+ return {};
338
+ }
339
+ try {
340
+ const parsed = JSON.parse(text);
341
+ return isObject(parsed) ? parsed : undefined;
342
+ }
343
+ catch {
344
+ return undefined;
345
+ }
346
+ }
347
+ const serialize = (config) => `${JSON.stringify(config, null, 2)}\n`;
348
+ /** Any version of our entry. */
349
+ const ours = (item) => {
350
+ const text = JSON.stringify(item);
351
+ return (text.includes(RETICLE_NPM_PACKAGE) &&
352
+ (text.includes(PROMPT_HOOK_MARKER) || text.includes(TOOL_HOOK_MARKER)));
353
+ };
354
+ /** The list at `path`, or an empty one. */
355
+ function listAt(config, path) {
356
+ let node = config;
357
+ for (const key of path) {
358
+ node = isObject(node) ? node[key] : undefined;
359
+ }
360
+ return Array.isArray(node) ? node : [];
361
+ }
362
+ /** A copy of `config` with the list at `path` replaced, or its key removed when the list is empty. */
363
+ function withList(config, path, list) {
364
+ const [key, ...rest] = path;
365
+ if (key === undefined) {
366
+ return config;
367
+ }
368
+ if (0 === rest.length) {
369
+ const { [key]: _dropped, ...others } = config;
370
+ return 0 === list.length ? others : { ...config, [key]: list };
371
+ }
372
+ const child = isObject(config[key]) ? config[key] : {};
373
+ const next = withList(child, rest, list);
374
+ if (0 < Object.keys(next).length) {
375
+ return { ...config, [key]: next };
376
+ }
377
+ const { [key]: _gone, ...others } = config;
378
+ return others;
379
+ }
380
+ /** The project-relative config to write, given which files exist. */
381
+ export function promptHookPath(spec, exists) {
382
+ return spec.paths.find((path) => exists(path)) ?? spec.defaultPath;
383
+ }
384
+ /** THIS version's entry is in every slot. An older version's entry is replaced by install. */
385
+ export function promptHookInstalled(spec, text) {
386
+ const config = parseConfig(text);
387
+ return (config !== undefined &&
388
+ spec.slots.every((slot) => listAt(config, slot.path).some((item) => JSON.stringify(item).includes(slotCommand(spec, slot)))));
389
+ }
390
+ /**
391
+ * THIS version is installed, but some slot runs it through npx alone (~426 ms of npx start per
392
+ * call): an install from before the direct form. Any machine's bin counts as direct, so a
393
+ * teammate's path is never rewritten with this one's.
394
+ */
395
+ export function promptHookNpxOnly(spec, text) {
396
+ const config = parseConfig(text);
397
+ return (config !== undefined &&
398
+ promptHookInstalled(spec, text) &&
399
+ spec.slots.some((slot) => !listAt(config, slot.path).some((item) => JSON.stringify(item).includes(`${OR_ELSE}${slotCommand(spec, slot)}`))));
400
+ }
401
+ /** Our entry merged into the config. `content` is the text to write; MANUAL keeps the text as it is. */
402
+ export function installPromptHook(spec, text,
403
+ /** The installed CLI's bin, when known: the hook runs it directly. See `directHookCommand`. */
404
+ bin) {
405
+ const config = parseConfig(text);
406
+ if (config === undefined) {
407
+ return { status: PromptHookStatus.MANUAL, content: text ?? '' };
408
+ }
409
+ // An npx-only install is rewritten when this machine can run the bin directly.
410
+ const direct = bin !== undefined && SAFE_BIN.test(bin);
411
+ if (promptHookInstalled(spec, text) && !(direct && promptHookNpxOnly(spec, text))) {
412
+ return { status: PromptHookStatus.ALREADY, content: text ?? '' };
413
+ }
414
+ let next = { ...(spec.header ?? {}), ...config };
415
+ for (const slot of spec.slots) {
416
+ const list = listAt(next, slot.path).filter((item) => !ours(item));
417
+ const npx = slotCommand(spec, slot);
418
+ const command = bin === undefined ? npx : directHookCommand(bin, npx);
419
+ next = withList(next, slot.path, [...list, slot.entry(command)]);
420
+ }
421
+ return { status: PromptHookStatus.APPLY, content: serialize(next) };
422
+ }
423
+ /**
424
+ * The config with our entries removed. `content` null means delete the file: it holds nothing but
425
+ * what install added. A config that is not ours to parse is never changed.
426
+ */
427
+ export function uninstallPromptHook(spec, text) {
428
+ const config = parseConfig(text);
429
+ if (null === text || config === undefined) {
430
+ return { changed: false, content: text };
431
+ }
432
+ let next = config;
433
+ let changed = false;
434
+ for (const slot of spec.slots) {
435
+ const list = listAt(next, slot.path);
436
+ const kept = list.filter((item) => !ours(item));
437
+ if (kept.length === list.length) {
438
+ continue;
439
+ }
440
+ changed = true;
441
+ next = withList(next, slot.path, kept);
442
+ }
443
+ if (!changed) {
444
+ return { changed: false, content: text };
445
+ }
446
+ const header = spec.header ?? {};
447
+ const onlyHeader = Object.keys(next).every((key) => JSON.stringify(next[key]) === JSON.stringify(header[key]));
448
+ return { changed: true, content: onlyHeader ? null : serialize(next) };
449
+ }
@@ -24,4 +24,4 @@ export declare const CURSOR_COMMAND_PATH = ".cursor/commands/reticle.md";
24
24
  * are told apart.
25
25
  */
26
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";
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. Tell the user in one line that `init` will change package.json,\n the lockfile and the build config, then run `npx @reticlehq/server@latest init`, fix every\n `\u26A0` and `\u2139` it prints, and carry on. Do not ask which framework or port \u2014 both come out\n 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`, `reticle_assert`, `reticle_act { steps }` with an `expect`, and `reticle_verify` 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";
@@ -42,9 +42,10 @@ answer. Every step below is yours to do without them. Work these in order, and s
42
42
  on, one line each:
43
43
 
44
44
  0. **Is Reticle even wired into this project?** \`cat .reticle.json\`. If it is not there, this
45
- project was never set up: run \`npx @reticlehq/server@latest init\`,
46
- fix every \`⚠\` and \`ℹ\` it prints, and carry on. Do not ask first, and do not ask which
47
- framework or port — both come out of the repo you are sitting in.
45
+ project was never set up. Tell the user in one line that \`init\` will change package.json,
46
+ the lockfile and the build config, then run \`npx @reticlehq/server@latest init\`, fix every
47
+ \`⚠\` and \`ℹ\` it prints, and carry on. Do not ask which framework or port — both come out
48
+ of the repo you are sitting in.
48
49
 
49
50
  1. **Is anything serving the app at all?** If a dev server is already listening, use it and go to
50
51
  step 2 — the cause is then almost always that the SDK never loaded in the page, not a missing
@@ -102,7 +103,7 @@ State which flow you picked in one line before you start.
102
103
  that the thing it was supposed to do actually happened.
103
104
  4. Finish with \`reticle_observe { action: "console" }\` and \`reticle_observe { action: "network" }\` to catch errors the DOM does not show.
104
105
 
105
- **Only \`reticle_act_and_wait\` and \`reticle_assert\` produce a verdict.** A run that ends on a
106
+ **Only \`reticle_act_and_wait\`, \`reticle_assert\`, \`reticle_act { steps }\` with an \`expect\`, and \`reticle_verify\` produce a verdict.** A run that ends on a
106
107
  snapshot has proved nothing, however many tools it used.
107
108
 
108
109
  ## Report
@@ -12,6 +12,10 @@ export interface InitOptions {
12
12
  captureBodies?: boolean | undefined;
13
13
  /** `--hooks`: also install the print-only Claude Code Stop hook. Off by default. */
14
14
  hooks?: boolean | undefined;
15
+ /** `--new-id`: replace the project id a copied `.reticle.json` carries with one for this checkout. */
16
+ newId?: boolean | undefined;
17
+ /** `--no-prompt-capture`: install no prompt-capture hook. On by default, with the agent wiring. */
18
+ promptCapture?: boolean | undefined;
15
19
  cwd: string;
16
20
  port: number | undefined;
17
21
  mcp: boolean;
package/dist/run.js CHANGED
@@ -7,7 +7,7 @@ import { OnboardingPhase, OnboardingStepStatus } from '@reticlehq/core/telemetry
7
7
  import { explainInstallFailure } from './diagnose/install-retries.js';
8
8
  import { preflight } from './plan/preflight.js';
9
9
  import { devCommandFrom } from './detect/dev-script.js';
10
- import { restartHint, FEEDBACK_HINT } from './diagnose/closing-hint.js';
10
+ import { restartHint, COMMUNITY_HINT, FEEDBACK_HINT } from './diagnose/closing-hint.js';
11
11
  import { projectIdOf, rememberProjectOnDisk } from './project/remember-project.js';
12
12
  import { initWithoutPackageJson } from './no-package-json.js';
13
13
  import { sdkPackagesDeclared, sdkPackagesPresent } from './sdk-packages.js';
@@ -317,6 +317,9 @@ export function runInit(options, io) {
317
317
  if (true !== options.redirected && true !== options.continuesToRuntime) {
318
318
  io.print('');
319
319
  io.print(FEEDBACK_HINT);
320
+ if (result.ok && true !== options.dryRun) {
321
+ io.print(COMMUNITY_HINT);
322
+ }
320
323
  }
321
324
  return result;
322
325
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/init",
3
- "version": "3.7.0",
3
+ "version": "3.8.1",
4
4
  "description": "Reticle project scaffolder — detects the framework, writes the config, patches the build and registers the MCP server. The build-time half of `reticle init`.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -13,7 +13,7 @@
13
13
  "./package.json": "./package.json"
14
14
  },
15
15
  "dependencies": {
16
- "@reticlehq/core": "3.7.0"
16
+ "@reticlehq/core": "3.8.1"
17
17
  },
18
18
  "license": "FSL-1.1-ALv2",
19
19
  "author": "Reticle contributors",