@reticlehq/init 3.5.0 → 3.7.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 (52) hide show
  1. package/dist/detect/detect.d.ts +8 -0
  2. package/dist/detect/detect.js +6 -3
  3. package/dist/detect/dev-script.d.ts +17 -0
  4. package/dist/detect/dev-script.js +128 -0
  5. package/dist/detect/non-js-project.d.ts +23 -0
  6. package/dist/detect/non-js-project.js +55 -23
  7. package/dist/detect/workspace-apps.d.ts +16 -0
  8. package/dist/detect/workspace-apps.js +70 -0
  9. package/dist/detect/workspace-redirect.d.ts +12 -0
  10. package/dist/detect/workspace-redirect.js +84 -9
  11. package/dist/diagnose/closing-hint.d.ts +7 -1
  12. package/dist/diagnose/closing-hint.js +39 -12
  13. package/dist/diagnose/csp-check.js +65 -20
  14. package/dist/diagnose/init-failure.d.ts +2 -0
  15. package/dist/diagnose/init-failure.js +2 -0
  16. package/dist/gather-plan-input.d.ts +11 -0
  17. package/dist/gather-plan-input.js +416 -0
  18. package/dist/index.d.ts +4 -2
  19. package/dist/index.js +7 -2
  20. package/dist/no-package-json.js +17 -8
  21. package/dist/node-io.d.ts +1 -1
  22. package/dist/node-io.js +11 -2
  23. package/dist/patch/next-patch.d.ts +28 -0
  24. package/dist/patch/next-patch.js +66 -0
  25. package/dist/patch/nuxt-snippets.js +1 -1
  26. package/dist/patch/snippets.d.ts +1 -1
  27. package/dist/patch/snippets.js +2 -2
  28. package/dist/patch/static-page.d.ts +1 -0
  29. package/dist/patch/static-page.js +3 -3
  30. package/dist/plan/connect-steps.d.ts +1 -0
  31. package/dist/plan/connect-steps.js +5 -0
  32. package/dist/plan/framework-adapter.js +2 -2
  33. package/dist/plan/plan-angular.js +1 -1
  34. package/dist/plan/plan-framework.d.ts +1 -1
  35. package/dist/plan/plan-framework.js +47 -13
  36. package/dist/plan/plan-types.d.ts +23 -2
  37. package/dist/plan/plan-vite.js +15 -0
  38. package/dist/plan/plan.js +68 -10
  39. package/dist/plan/stop-hook-step.d.ts +19 -0
  40. package/dist/plan/stop-hook-step.js +74 -1
  41. package/dist/project/agent-rules.d.ts +1 -1
  42. package/dist/project/agent-rules.js +16 -11
  43. package/dist/register/codex-toml.d.ts +12 -0
  44. package/dist/register/codex-toml.js +44 -0
  45. package/dist/register/mcp.d.ts +15 -2
  46. package/dist/register/mcp.js +16 -0
  47. package/dist/run-types.d.ts +6 -0
  48. package/dist/run.d.ts +1 -1
  49. package/dist/run.js +27 -414
  50. package/dist/sdk-packages.d.ts +19 -0
  51. package/dist/sdk-packages.js +34 -0
  52. package/package.json +2 -2
package/dist/plan/plan.js CHANGED
@@ -6,11 +6,13 @@
6
6
  import { Framework, UiLibrary, installCommand, installCommandParts } from '../detect/detect.js';
7
7
  import { installFailureHint } from '../diagnose/install-hint.js';
8
8
  import { installRetries } from '../diagnose/install-retries.js';
9
- import { claudeAddCommand, claudeProjectMcpJson, mcpManual } from '../register/mcp.js';
9
+ import { claudeAddCommand, claudeProjectMcpJson, codexAddCommand, mcpManual, } from '../register/mcp.js';
10
+ import { codexNamesOurServer } from '../register/codex-toml.js';
10
11
  import { mergeClientConfig, ClientMergeStatus, clientSnippet, clientSpec, CLAUDE_PROJECT_CONFIG, CLAUDE_PROJECT_SPEC, McpClient, } from '../register/mcp-clients.js';
11
12
  import { CLAUDE_COMMAND_PATH, CURSOR_COMMAND_PATH, SLASH_COMMAND_BODY, SLASH_COMMAND_SIGNATURE, } from '../register/slash-command.js';
12
13
  import { mergeMarkedInstruction, reticleMdFile, cursorRuleFile, AgentRuleStatus, CLAUDE_MD_PATH, markedBlock, AGENTS_MD_PATH, RETICLE_MD_PATH, CURSOR_RULE_PATH, } from '../project/agent-rules.js';
13
14
  import { cspStep } from '../diagnose/csp-step.js';
15
+ import { HTML_INDEX_PATH } from '../patch/static-page.js';
14
16
  import { frameworkSteps } from './framework-adapter.js';
15
17
  import { FRAMEWORK_ADAPTERS, RETICLE_BROWSER_SDK, RETICLE_REACT_KIT } from './framework-adapter.js';
16
18
  import { MCP_TARGET, StepStatus } from './plan-types.js';
@@ -21,7 +23,7 @@ import { configWithInstallSource } from '../project/install-source-config.js';
21
23
  import { portMoveDetail, retargetConfigPort } from './port-steps.js';
22
24
  import { containerisedStep, uiLibraryStep, webGlCanvasStep, windowsMcpNoteStep, } from './notices.js';
23
25
  import { existingConfigProblem, projectIdOf, RETICLE_CONFIG_FILE, } from '../detect/existing-config.js';
24
- import { CLAUDE_SETTINGS_PATH, stopHookStep } from './stop-hook-step.js';
26
+ import { CLAUDE_SETTINGS_PATH, claudeSettingsSteps } from './stop-hook-step.js';
25
27
  // Re-exported: it moved to the module that reads it, and every existing importer says `plan.js`.
26
28
  export { RETICLE_CONFIG_FILE };
27
29
  /**
@@ -92,7 +94,34 @@ function claudeMcpStep(input) {
92
94
  * parse is reported with a paste-able block rather than overwritten, and everything else is written.
93
95
  */
94
96
  function otherClientSteps(input) {
95
- return (input.detectedClients ?? []).map((detected) => clientStep(clientSpec(detected.id), detected.configPath, detected.existing));
97
+ return (input.detectedClients ?? []).map((detected) => {
98
+ const spec = clientSpec(detected.id);
99
+ return (codexCliStep(spec, detected.configPath, detected.existing, input) ??
100
+ clientStep(spec, detected.configPath, detected.existing));
101
+ });
102
+ }
103
+ /**
104
+ * Codex, registered through its own CLI instead of a printed block (#1238).
105
+ *
106
+ * `mergeClientConfig` answers MANUAL for TOML, since we never edit it blind. `codex mcp add` does
107
+ * the editing, so with the binary on PATH nothing is left for a human. It REPLACES an entry of the
108
+ * same name and exits 0, so this answers only when the config names no `reticle` server at all, in
109
+ * any shape. An entry somebody wrote — a local build, a `url` — falls through to `clientStep`, which
110
+ * leaves it alone (ALREADY) or hands over the block (MANUAL), exactly as before.
111
+ */
112
+ function codexCliStep(spec, configPath, existing, input) {
113
+ if (McpClient.CODEX !== spec.id || true !== input.codexCli)
114
+ return null;
115
+ if (codexNamesOurServer(existing, spec.serversKey))
116
+ return null;
117
+ const cmd = codexAddCommand();
118
+ return {
119
+ title: `MCP server (${spec.label})`,
120
+ target: configPath,
121
+ status: StepStatus.APPLY,
122
+ detail: `register reticle with ${spec.label} (${cmd.display})`,
123
+ exec: { command: cmd.command, args: cmd.args, fallback: cmd.display },
124
+ };
96
125
  }
97
126
  /** One client's step: already-correct is left alone, an unparseable file gets a paste-able block. */
98
127
  function clientStep(spec, configPath, existing) {
@@ -488,12 +517,22 @@ function installStep(input) {
488
517
  detail: `${names.join(', ')} already declared${version === undefined || 0 === version.length ? '' : ` at ${version}`}`,
489
518
  };
490
519
  }
520
+ // Never install into a linked tree: the package manager replaces the link (a workspace's tree).
521
+ if (true === input.nodeModulesLinked) {
522
+ return {
523
+ title: 'Install dependencies',
524
+ target: 'package.json',
525
+ status: StepStatus.SKIP,
526
+ detail: `skipped: node_modules is a link to another tree, which an install here would replace; install them where it points: ${command}`,
527
+ };
528
+ }
529
+ // The person said `--no-install`: a choice, not a step left undone, so not a ⚠.
491
530
  if (!input.options.install) {
492
531
  return {
493
532
  title: 'Install dependencies',
494
533
  target: 'package.json',
495
- status: StepStatus.MANUAL,
496
- detail: command,
534
+ status: StepStatus.SKIP,
535
+ detail: `skipped (--no-install); install them yourself: ${command}`,
497
536
  };
498
537
  }
499
538
  const parts = installCommandParts(pm, packages, [], pmCommand);
@@ -561,6 +600,19 @@ function agentRootConfigStep(input, content) {
561
600
  },
562
601
  ];
563
602
  }
603
+ // Several apps: a root config names one and is wrong for the rest. Not written; an agent at the
604
+ // root is served whichever app's daemon is live, and one started in an app reads that app's.
605
+ if (true === input.agentRootSeveralApps && existingProject === undefined) {
606
+ return [
607
+ {
608
+ title: AGENT_ROOT_CONFIG_TITLE,
609
+ target: path,
610
+ status: StepStatus.NOTICE,
611
+ detail: 'not written: this workspace has several apps and the root can name only one. Run the ' +
612
+ "agent from an app's directory to pin it to that app.",
613
+ },
614
+ ];
615
+ }
564
616
  if (input.agentRootConfigSource === content) {
565
617
  return [
566
618
  {
@@ -652,20 +704,26 @@ function reticleConfigSteps(input) {
652
704
  ];
653
705
  }
654
706
  export function buildPlan(input) {
707
+ const cspSteps = cspStep(input);
655
708
  const steps = [
656
- ...cspStep(input),
709
+ ...cspSteps,
657
710
  ...mcpSteps(input),
658
711
  ...agentRuleSteps(input),
659
712
  ...slashCommandSteps(input),
660
- ...(true === input.hooks
661
- ? [stopHookStep(agentFile(input, CLAUDE_SETTINGS_PATH), input.claudeSettingsContent)]
662
- : []),
713
+ ...claudeSettingsSteps(agentFile(input, CLAUDE_SETTINGS_PATH), input.claudeSettingsContent, {
714
+ stop: true === input.hooks,
715
+ // Beside the CLAUDE.md rule: the same agent, the same consent to write its instruction files.
716
+ compact: input.options.mcp && input.claudeCli,
717
+ }),
663
718
  ...uiLibraryStep(input),
664
719
  ...webGlCanvasStep(input),
665
720
  installStep(input),
666
721
  ...reticleConfigSteps(input),
667
722
  ];
668
- steps.push(...frameworkSteps(input));
723
+ // Both steps can patch index.html: the connect write must keep the CSP edit that runs first.
724
+ const patchedIndex = cspSteps.find((step) => step.write?.path === HTML_INDEX_PATH)?.write
725
+ ?.content;
726
+ steps.push(...frameworkSteps(patchedIndex === undefined ? input : { ...input, htmlIndexSource: patchedIndex }));
669
727
  // LAST, and a notice rather than an action: it is a statement about this machine's shape, and it
670
728
  // only matters once every file above has been written. See containerised-dev-server.ts.
671
729
  const container = containerisedStep(input);
@@ -2,3 +2,22 @@ import { type Step } from './plan-types.js';
2
2
  export declare const CLAUDE_SETTINGS_PATH = ".claude/settings.json";
3
3
  export declare const STOP_HOOK_COMMAND = "npx @reticlehq/server report --hook";
4
4
  export declare function stopHookStep(path: string, existing: string | null | undefined): Step;
5
+ /**
6
+ * The Claude Code `SessionStart` hook with matcher `compact`: right after a compaction it prints
7
+ * `reticle context --compact` — what this session proved, what failed, what is still owed — back
8
+ * into the agent's fresh context. Reticle holds the memory; this is how it is handed back without
9
+ * the agent having to remember to ask.
10
+ *
11
+ * Written by default wherever the CLAUDE.md rule is (it only reads the project's own `.reticle/`, and
12
+ * prints nothing where there is none). Removable like any hook: delete the entry.
13
+ */
14
+ export declare const COMPACT_HOOK_COMMAND = "npx @reticlehq/server context --compact";
15
+ export declare function compactHookStep(path: string, existing: string | null | undefined): Step;
16
+ /**
17
+ * Every hook this run writes into `.claude/settings.json`, each merged on top of the one before, so
18
+ * two steps writing the same file cannot undo each other.
19
+ */
20
+ export declare function claudeSettingsSteps(path: string, existing: string | null | undefined, options: {
21
+ stop: boolean;
22
+ compact: boolean;
23
+ }): Step[];
@@ -36,7 +36,12 @@ export function stopHookStep(path, existing) {
36
36
  const hooks = isObject(settings['hooks']) ? settings['hooks'] : {};
37
37
  const stop = Array.isArray(hooks[STOP_EVENT]) ? hooks[STOP_EVENT] : [];
38
38
  if (JSON.stringify(stop).includes(STOP_HOOK_COMMAND)) {
39
- return { title: TITLE, target: path, status: StepStatus.ALREADY, detail: 'hook already installed' };
39
+ return {
40
+ title: TITLE,
41
+ target: path,
42
+ status: StepStatus.ALREADY,
43
+ detail: 'hook already installed',
44
+ };
40
45
  }
41
46
  const merged = { ...settings, hooks: { ...hooks, [STOP_EVENT]: [...stop, HOOK_ENTRY] } };
42
47
  return {
@@ -47,3 +52,71 @@ export function stopHookStep(path, existing) {
47
52
  write: { path, content: `${JSON.stringify(merged, null, 2)}\n`, expect: [STOP_HOOK_COMMAND] },
48
53
  };
49
54
  }
55
+ /**
56
+ * The Claude Code `SessionStart` hook with matcher `compact`: right after a compaction it prints
57
+ * `reticle context --compact` — what this session proved, what failed, what is still owed — back
58
+ * into the agent's fresh context. Reticle holds the memory; this is how it is handed back without
59
+ * the agent having to remember to ask.
60
+ *
61
+ * Written by default wherever the CLAUDE.md rule is (it only reads the project's own `.reticle/`, and
62
+ * prints nothing where there is none). Removable like any hook: delete the entry.
63
+ */
64
+ export const COMPACT_HOOK_COMMAND = `npx ${RETICLE_NPM_PACKAGE} context --compact`;
65
+ const COMPACT_TITLE = 'Claude Code resume-after-compaction hook';
66
+ const SESSION_START_EVENT = 'SessionStart';
67
+ const COMPACT_MATCHER = 'compact';
68
+ export function compactHookStep(path, existing) {
69
+ const settings = null === existing || undefined === existing ? {} : parseSettings(existing);
70
+ if (settings === undefined) {
71
+ return {
72
+ title: COMPACT_TITLE,
73
+ target: path,
74
+ status: StepStatus.MANUAL,
75
+ detail: `${CLAUDE_SETTINGS_PATH} is not a JSON object, so it was left alone. Add a SessionStart hook (matcher "${COMPACT_MATCHER}") running \`${COMPACT_HOOK_COMMAND}\``,
76
+ };
77
+ }
78
+ const hooks = isObject(settings['hooks']) ? settings['hooks'] : {};
79
+ const start = Array.isArray(hooks[SESSION_START_EVENT])
80
+ ? hooks[SESSION_START_EVENT]
81
+ : [];
82
+ if (JSON.stringify(start).includes(COMPACT_HOOK_COMMAND)) {
83
+ return {
84
+ title: COMPACT_TITLE,
85
+ target: path,
86
+ status: StepStatus.ALREADY,
87
+ detail: 'hook already installed',
88
+ };
89
+ }
90
+ const entry = {
91
+ matcher: COMPACT_MATCHER,
92
+ hooks: [{ type: 'command', command: COMPACT_HOOK_COMMAND }],
93
+ };
94
+ const merged = { ...settings, hooks: { ...hooks, [SESSION_START_EVENT]: [...start, entry] } };
95
+ return {
96
+ title: COMPACT_TITLE,
97
+ target: path,
98
+ status: StepStatus.APPLY,
99
+ detail: 'after a context compaction, hand the agent back what this session proved and still owes',
100
+ write: {
101
+ path,
102
+ content: `${JSON.stringify(merged, null, 2)}\n`,
103
+ expect: [COMPACT_HOOK_COMMAND],
104
+ },
105
+ };
106
+ }
107
+ /**
108
+ * Every hook this run writes into `.claude/settings.json`, each merged on top of the one before, so
109
+ * two steps writing the same file cannot undo each other.
110
+ */
111
+ export function claudeSettingsSteps(path, existing, options) {
112
+ const steps = [];
113
+ let current = existing;
114
+ if (options.stop) {
115
+ const stop = stopHookStep(path, current);
116
+ steps.push(stop);
117
+ current = stop.write?.content ?? current;
118
+ }
119
+ if (options.compact)
120
+ steps.push(compactHookStep(path, current));
121
+ return steps;
122
+ }
@@ -90,7 +90,7 @@ export declare const DEV_SERVER_POLICY = "**A dev server already running when `r
90
90
  * NOT to, how, and the guards against calling something proven when it is not. Everything an agent
91
91
  * needs only at the moment it hits a specific situation lives in RETICLE.md, named at the end.
92
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 { steps: [...] }` rather than one round trip per field.\n- Read the surrounding evidence with `reticle_look { action: \"page\" | \"state\" }` and `reticle_observe { action: \"network\" | \"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? You are mid-sequence \u2014 do not stop until a verdict exists.** The whole of it\nis: instrument the app \u2192 get a dev server running \u2192 open the app in a browser \u2192 drive one flow \u2192\nreport the verdict. Every step is yours to do, and none of them needs the user. Stopping short leaves an app that\nlooks installed and can verify nothing, which is the commonest way this goes wrong. `/reticle`\ncarries the recovery ladder; never report the install as finished without a verdict 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_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\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_look { action: \"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.** The business outcome a change is meant to produce is known only while the change is being made. Pass `intent` when a flow is saved, so the saved flow carries the reason it exists. A flow without one replays for months and then reports \"step 3 failed\" instead of what stopped being true for a user.\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?** Call `reticle_tools` before assuming it: this surface merges several families behind an `action`, so what looks absent is usually one argument away. It is the verify loop and nothing else on purpose, and a daemon started with `RETICLE_ADVERTISE_ALL_TOOLS=1` advertises the wider set \u2014 it reads that at startup, so it takes effect on the next one.\n\n**Report Reticle's own defects with `reticle_session { action: \"feedback\" }` the moment you notice**, then carry on with your task. You are the user Reticle is built for and the only one who can say what it cost you, and that knowledge is gone when your context is.\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_look { action: \"state\" }` comes back empty, and how to write a feedback report that can be acted on. Read it when you hit one of those, not before.**";
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 \u2014 the rules to keep in mind on every call:**\n\n1. **Only `verified: \"yes\"` is a pass.** `\"no-fault\"` (nothing was declared \u2014 you omitted `until`) and `\"unknown\"` (Reticle could not tell; `verifiedReason` says why) are not a pass: report them as not proved, never as working.\n2. **Name the consequence BEFORE acting:** `reticle_act_and_wait({ target, action, until })` (add `durable: true` if it must survive a reload). Only it, `reticle_assert` and `reticle_act { steps }` produce a verdict; everything else proves nothing.\n3. **A journey is one call:** `reticle_act { steps: [{ target, action, expect }] }` runs every step and stops at the first `expect` that fails. The last step's `expect` is the end state you were asked to deliver.\n4. **Text checks are exact-case.** Add `ignoreCase: true` only when case truly does not matter.\n5. **Target by `{ testid }` or `{ role, name }`.** An ambiguous target is refused, never guessed.\n6. **On a failure, read `expected` / `observed` / the nearest candidates before retrying.** Never weaken a check to make it green.\n7. **Open `screenshot.path` only when the look matters;** `reticle_observe { since }` has the rest.\n8. **Predicates:** `{ kind: \"signal\", name }` \u00B7 `{ kind: \"net\", method, urlContains, status }` \u00B7 `{ kind: \"route\", pathname }` \u00B7 `{ kind: \"state\", path, equals }` \u00B7 `{ kind: \"element\", query: { role, name } }` \u00B7 `{ kind: \"text\", contains }` \u00B7 `allOf` / `anyOf` / `not`.\n9. **No session?** `reticle_session { action: \"list\" }` and do its `next_action`. **No app connected at all?** Run `npx @reticlehq/server init` in the app's own folder yourself. **A version-skew warning?** `npx @reticlehq/server stop`, then retry.\n10. **End with the Reticle result block the server gives you**, verbatim.\n11. **If your context was compacted or you lost track, run `npx @reticlehq/server context --compact` first.** It is the daemon's own record of what was proved, what failed and what is still owed.\n\nCovered flows: `npx @reticlehq/server gate` reports which recorded flows the changed files affect and whether they still pass.\n\n**Setting Reticle up? You are mid-sequence \u2014 do not stop until a verdict exists.** The whole of it\nis: instrument the app \u2192 get a dev server running \u2192 open the app in a browser \u2192 drive one flow \u2192\nreport the verdict. Every step is yours to do, and none of them needs the user. Stopping short leaves an app that\nlooks installed and can verify nothing, which is the commonest way this goes wrong. `/reticle`\ncarries the recovery ladder; never report the install as finished without a verdict 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_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\nIf 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_look { action: \"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.** The business outcome a change is meant to produce is known only while the change is being made. Pass `intent` when a flow is saved, so the saved flow carries the reason it exists. A flow without one replays for months and then reports \"step 3 failed\" instead of what stopped being true for a user.\n\n**Honesty, which is the whole point:**\n\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?** Call `reticle_tools` before assuming it: this surface merges several families behind an `action`, so what looks absent is usually one argument away. It is the verify loop and nothing else on purpose, and a daemon started with `RETICLE_ADVERTISE_ALL_TOOLS=1` advertises the wider set \u2014 it reads that at startup, so it takes effect on the next one.\n\n**Report Reticle's own defects with `reticle_session { action: \"feedback\" }` the moment you notice**, then carry on with your task. You are the user Reticle is built for and the only one who can say what it cost you, and that knowledge is gone when your context is.\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_look { action: \"state\" }` comes back empty, and how to write a feedback report that can be acted on. Read it when you hit one of those, not before.**";
94
94
  /** The full-rules file, written verbatim (no markers: `init` owns this file entirely). */
95
95
  export declare function reticleMdFile(): string;
96
96
  export declare const AgentRuleStatus: {
@@ -118,13 +118,21 @@ This app is instrumented by **Reticle**, an in-app verification layer exposed as
118
118
 
119
119
  **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.
120
120
 
121
- **How to verify:**
122
-
123
- - 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.
124
- - Batch a multi-step journey (a login, a form) into one \`reticle_act { steps: [...] }\` rather than one round trip per field.
125
- - Read the surrounding evidence with \`reticle_look { action: "page" | "state" }\` and \`reticle_observe { action: "network" | "console" }\`.
126
- - **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.
127
- - Covered flows: \`${CLI} gate\` reports which recorded flows the changed files affect and whether they still pass.
121
+ **How to verify — the rules to keep in mind on every call:**
122
+
123
+ 1. **Only \`verified: "yes"\` is a pass.** \`"no-fault"\` (nothing was declared — you omitted \`until\`) and \`"unknown"\` (Reticle could not tell; \`verifiedReason\` says why) are not a pass: report them as not proved, never as working.
124
+ 2. **Name the consequence BEFORE acting:** \`reticle_act_and_wait({ target, action, until })\` (add \`durable: true\` if it must survive a reload). Only it, \`reticle_assert\` and \`reticle_act { steps }\` produce a verdict; everything else proves nothing.
125
+ 3. **A journey is one call:** \`reticle_act { steps: [{ target, action, expect }] }\` runs every step and stops at the first \`expect\` that fails. The last step's \`expect\` is the end state you were asked to deliver.
126
+ 4. **Text checks are exact-case.** Add \`ignoreCase: true\` only when case truly does not matter.
127
+ 5. **Target by \`{ testid }\` or \`{ role, name }\`.** An ambiguous target is refused, never guessed.
128
+ 6. **On a failure, read \`expected\` / \`observed\` / the nearest candidates before retrying.** Never weaken a check to make it green.
129
+ 7. **Open \`screenshot.path\` only when the look matters;** \`reticle_observe { since }\` has the rest.
130
+ 8. **Predicates:** \`{ kind: "signal", name }\` · \`{ kind: "net", method, urlContains, status }\` · \`{ kind: "route", pathname }\` · \`{ kind: "state", path, equals }\` · \`{ kind: "element", query: { role, name } }\` · \`{ kind: "text", contains }\` · \`allOf\` / \`anyOf\` / \`not\`.
131
+ 9. **No session?** \`reticle_session { action: "list" }\` and do its \`next_action\`. **No app connected at all?** Run \`${CLI} init\` in the app's own folder yourself. **A version-skew warning?** \`${CLI} stop\`, then retry.
132
+ 10. **End with the Reticle result block the server gives you**, verbatim.
133
+ 11. **If your context was compacted or you lost track, run \`${CLI} context --compact\` first.** It is the daemon's own record of what was proved, what failed and what is still owed.
134
+
135
+ Covered flows: \`${CLI} gate\` reports which recorded flows the changed files affect and whether they still pass.
128
136
 
129
137
  **Setting Reticle up? You are mid-sequence — do not stop until a verdict exists.** The whole of it
130
138
  is: instrument the app → get a dev server running → open the app in a browser → drive one flow →
@@ -136,7 +144,7 @@ carries the recovery ladder; never report the install as finished without a verd
136
144
 
137
145
  ${DEV_SERVER_POLICY}
138
146
 
139
- 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.
147
+ 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.
140
148
 
141
149
  **Finish \`src/reticle-dev.ts\` before you claim setup is done.** \`init\` writes it and cannot always fill it in: a store that needs an argument only reading the code supplies (Jotai atoms, an XState actor, a TanStack \`queryClient\`) is left as a commented \`registerStore\` line. A file that registers nothing looks exactly like a finished one, and \`reticle_look { action: "state" }\` then returns empty forever — which is indistinguishable from an app that has nothing to report, so it reads as success. Uncomment the line, complete it, and prove it by driving one flow and seeing your keys come back. If \`init\` told you to restart your client, this is the job waiting for you on the other side of that restart.
142
150
 
@@ -146,9 +154,6 @@ A dev server that is already running does not pick up an edited build config or
146
154
 
147
155
  **Honesty, which is the whole point:**
148
156
 
149
- - **\`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.
150
- - **\`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.
151
- - **Never weaken a check to make it green.** Downgrading, skipping or deleting an assertion is a finding, not a fix.
152
157
  - **If Reticle cannot run** (no daemon, or this is not a running web app), say so. Do not skip verification silently.
153
158
  - **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.
154
159
 
@@ -8,3 +8,15 @@
8
8
  export declare function codexServerTokens(existing: string | null, serversKey: string): string[];
9
9
  /** Does this config already declare a Reticle server under our name? */
10
10
  export declare function codexDeclaresOurServer(existing: string | null, serversKey: string): boolean;
11
+ /**
12
+ * Does this config NAME a Reticle server at all, in any shape?
13
+ *
14
+ * Wider than {@link codexDeclaresOurServer}, which asks whether the entry holds a command we can
15
+ * read. A `url` entry, one that only has an `env` table, or an inline `mcp_servers = { … }` has no
16
+ * command and args, so it reads as absent there. `codex mcp add reticle` REPLACES such an entry
17
+ * without a word and exits 0, so anything that registers through that command has to ask this.
18
+ *
19
+ * Structural rather than a substring match: a project called `reticle`, under
20
+ * `[projects."/home/u/reticle"]`, does not name a server.
21
+ */
22
+ export declare function codexNamesOurServer(existing: string | null, serversKey: string): boolean;
@@ -142,3 +142,47 @@ export function codexServerTokens(existing, serversKey) {
142
142
  export function codexDeclaresOurServer(existing, serversKey) {
143
143
  return 0 < codexServerTokens(existing, serversKey).length;
144
144
  }
145
+ /**
146
+ * Our key, as a KEY, inside an inline table's text: `reticle = …`, `"reticle" = …`, or the dotted
147
+ * `reticle.command = …`. A key follows the `{` or a `,`, and its quotes match, so a value that merely
148
+ * starts with the name (`command = "reticle.js"`) is not one.
149
+ */
150
+ const OUR_KEY_INLINE = new RegExp(`(?:^|[{,])\\s*(?:${MCP_SERVER_NAME}|"${MCP_SERVER_NAME}"|'${MCP_SERVER_NAME}')\\s*[.=]`);
151
+ /**
152
+ * Does this config NAME a Reticle server at all, in any shape?
153
+ *
154
+ * Wider than {@link codexDeclaresOurServer}, which asks whether the entry holds a command we can
155
+ * read. A `url` entry, one that only has an `env` table, or an inline `mcp_servers = { … }` has no
156
+ * command and args, so it reads as absent there. `codex mcp add reticle` REPLACES such an entry
157
+ * without a word and exits 0, so anything that registers through that command has to ask this.
158
+ *
159
+ * Structural rather than a substring match: a project called `reticle`, under
160
+ * `[projects."/home/u/reticle"]`, does not name a server.
161
+ */
162
+ export function codexNamesOurServer(existing, serversKey) {
163
+ if (null === existing)
164
+ return false;
165
+ const want = ourTablePath(serversKey);
166
+ const isOurs = (path) => want.every((segment, i) => path[i] === segment);
167
+ let table = [];
168
+ for (const rawLine of existing.split(/\r?\n/)) {
169
+ const line = withoutComment(rawLine).trim();
170
+ const header = /^\[{1,2}([^\]]+)\]{1,2}$/.exec(line);
171
+ if (null !== header) {
172
+ table = keyPath(header[1] ?? '');
173
+ if (isOurs(table))
174
+ return true;
175
+ continue;
176
+ }
177
+ const assignment = /^([^=]+)=(.*)$/.exec(line);
178
+ if (null === assignment)
179
+ continue;
180
+ const path = [...table, ...keyPath(assignment[1] ?? '')];
181
+ if (isOurs(path))
182
+ return true;
183
+ // `mcp_servers = { reticle = { … } }`: the whole table is one inline value, our key inside it.
184
+ if (samePath(path, [serversKey]) && OUR_KEY_INLINE.test(assignment[2] ?? ''))
185
+ return true;
186
+ }
187
+ return false;
188
+ }
@@ -58,14 +58,14 @@ export declare function npxServerArgs(): string[];
58
58
  * @param tokens every string in the entry's command + args, in any of the client shapes.
59
59
  */
60
60
  export declare function isReticleRegistration(tokens: readonly string[]): boolean;
61
- interface ClaudeAddCommand {
61
+ interface CliAddCommand {
62
62
  command: string;
63
63
  args: string[];
64
64
  /** Human-readable form of the same command, for reports and manual fallback. */
65
65
  display: string;
66
66
  }
67
67
  /** `claude mcp add reticle -s user -- npx @reticlehq/server mcp` — registers globally for all projects (portless). */
68
- export declare function claudeAddCommand(): ClaudeAddCommand;
68
+ export declare function claudeAddCommand(): CliAddCommand;
69
69
  /**
70
70
  * Is a `reticle` server already registered with Claude Code, in any scope this project sees?
71
71
  *
@@ -89,6 +89,19 @@ export declare function claudeAvailableProbe(): {
89
89
  command: string;
90
90
  args: string[];
91
91
  };
92
+ /**
93
+ * `codex mcp add reticle -- npx @reticlehq/server mcp` — Codex keeps the entry in its own
94
+ * `~/.codex/config.toml`, shared by the desktop app and the CLI, so one registration covers both.
95
+ *
96
+ * It REPLACES an existing entry of the same name and still exits 0, so callers run it only when the
97
+ * config names no `reticle` server at all (see `codexNamesOurServer`).
98
+ */
99
+ export declare function codexAddCommand(): CliAddCommand;
100
+ /** Probe args for whether the `codex` CLI is installed at all. */
101
+ export declare function codexAvailableProbe(): {
102
+ command: string;
103
+ args: string[];
104
+ };
92
105
  /**
93
106
  * The Windows `cmd /c npx` registration. Printed on every Windows init, not only when the `claude`
94
107
  * CLI is missing — a successful `claude mcp add` of bare `npx` is exactly the failure that reported
@@ -36,6 +36,7 @@ export const NPX = 'npx';
36
36
  const RETICLE_PACKAGE = RETICLE_NPM_PACKAGE;
37
37
  const MCP_SUBCOMMAND = 'mcp';
38
38
  const CLAUDE_CLI = 'claude';
39
+ const CODEX_CLI = 'codex';
39
40
  /** Windows `cmd /c` — the spawn that PowerShell's execution policy does not gate. */
40
41
  const WINDOWS_SHELL = 'cmd';
41
42
  const WINDOWS_SHELL_C = '/c';
@@ -140,6 +141,21 @@ export function claudeHasReticle(io, projectDir, configDir = process.env['CLAUDE
140
141
  export function claudeAvailableProbe() {
141
142
  return { command: CLAUDE_CLI, args: ['--version'] };
142
143
  }
144
+ /**
145
+ * `codex mcp add reticle -- npx @reticlehq/server mcp` — Codex keeps the entry in its own
146
+ * `~/.codex/config.toml`, shared by the desktop app and the CLI, so one registration covers both.
147
+ *
148
+ * It REPLACES an existing entry of the same name and still exits 0, so callers run it only when the
149
+ * config names no `reticle` server at all (see `codexNamesOurServer`).
150
+ */
151
+ export function codexAddCommand() {
152
+ const args = [MCP_SUBCOMMAND, 'add', MCP_SERVER_NAME, '--', ...serverInvocation()];
153
+ return { command: CODEX_CLI, args, display: `${CODEX_CLI} ${args.join(' ')}` };
154
+ }
155
+ /** Probe args for whether the `codex` CLI is installed at all. */
156
+ export function codexAvailableProbe() {
157
+ return { command: CODEX_CLI, args: ['--version'] };
158
+ }
143
159
  /**
144
160
  * The Windows `cmd /c npx` registration. Printed on every Windows init, not only when the `claude`
145
161
  * CLI is missing — a successful `claude mcp add` of bare `npx` is exactly the failure that reported
@@ -41,6 +41,8 @@ export interface InitOptions {
41
41
  * at `src/admin` ended up with no `/reticle` at all (#318).
42
42
  */
43
43
  agentRoot?: string;
44
+ /** The agent root is a workspace with several apps: it is pinned to none of them. */
45
+ severalApps?: boolean;
44
46
  /**
45
47
  * Hand the telemetry outcome back rather than emitting it here.
46
48
  *
@@ -74,6 +76,8 @@ export interface InitIo {
74
76
  listDirs(relPath: string): readonly string[];
75
77
  /** File (non-directory) basenames inside a project-relative directory, including dotfiles. */
76
78
  listFiles(relPath: string): readonly string[];
79
+ /** Whether a project-relative path is a symbolic link. Optional: an IO without it reads none. */
80
+ isSymlink?(relPath: string): boolean;
77
81
  /** The same IO re-rooted at a project-relative subdirectory (used for the workspace redirect). */
78
82
  scoped(relPath: string): InitIo;
79
83
  /** Runs a subprocess to completion (inherits stdio); returns true on exit code 0. */
@@ -138,4 +142,6 @@ export interface InitResult {
138
142
  * `deferOutcome` is set. Absent on a dry run and on the exits that report for themselves.
139
143
  */
140
144
  outcome?: InitOutcome;
145
+ /** No app where init ran; it listed the apps below instead. Reported as `no_package_json`. */
146
+ listedAppsBelow?: true;
141
147
  }
package/dist/run.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { InitOptions, InitIo, InitResult } from './run-types.js';
2
- export type { InitOptions, InitIo, InitResult } from './run-types.js';
3
2
  import { resolveLockfiles } from './detect/lockfiles.js';
3
+ export type { InitOptions, InitIo, InitResult } from './run-types.js';
4
4
  export { resolveLockfiles };
5
5
  export declare function runInit(options: InitOptions, io: InitIo): InitResult;