@reticlehq/init 3.6.0 → 3.8.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.
- package/dist/detect/detect.d.ts +9 -0
- package/dist/detect/detect.js +5 -1
- package/dist/detect/dev-script.d.ts +17 -0
- package/dist/detect/dev-script.js +128 -0
- package/dist/detect/dev-server-ports.d.ts +2 -0
- package/dist/detect/dev-server-ports.js +20 -0
- package/dist/detect/prettier-config.d.ts +21 -0
- package/dist/detect/prettier-config.js +38 -0
- package/dist/detect/workspace-apps.d.ts +11 -0
- package/dist/detect/workspace-apps.js +63 -0
- package/dist/detect/workspace-redirect.js +69 -10
- package/dist/diagnose/closing-hint.d.ts +5 -0
- package/dist/diagnose/closing-hint.js +18 -11
- package/dist/diagnose/init-failure.d.ts +2 -0
- package/dist/diagnose/init-failure.js +2 -0
- package/dist/gather-plan-input.d.ts +11 -0
- package/dist/gather-plan-input.js +468 -0
- package/dist/host.d.ts +6 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +14 -4
- package/dist/no-package-json.js +62 -14
- package/dist/node-io.d.ts +1 -1
- package/dist/node-io.js +11 -2
- package/dist/patch/next-patch.d.ts +45 -9
- package/dist/patch/next-patch.js +93 -0
- package/dist/patch/nuxt-snippets.js +1 -1
- package/dist/patch/prettier-ignore.d.ts +34 -0
- package/dist/patch/prettier-ignore.js +49 -0
- package/dist/patch/python-snippets.d.ts +22 -0
- package/dist/patch/python-snippets.js +113 -0
- package/dist/patch/snippets.d.ts +10 -31
- package/dist/patch/snippets.js +42 -105
- package/dist/patch/static-page.d.ts +1 -0
- package/dist/patch/static-page.js +6 -8
- package/dist/patch/vite-config.js +11 -0
- package/dist/plan/connect-steps.d.ts +2 -0
- package/dist/plan/connect-steps.js +6 -0
- package/dist/plan/framework-adapter.d.ts +10 -0
- package/dist/plan/framework-adapter.js +122 -3
- package/dist/plan/notices.js +1 -1
- package/dist/plan/plan-angular.js +1 -1
- package/dist/plan/plan-framework.d.ts +1 -1
- package/dist/plan/plan-framework.js +140 -37
- package/dist/plan/plan-types.d.ts +50 -2
- package/dist/plan/plan-vite.js +15 -0
- package/dist/plan/plan.d.ts +1 -10
- package/dist/plan/plan.js +181 -126
- package/dist/plan/stop-hook-step.d.ts +30 -0
- package/dist/plan/stop-hook-step.js +154 -5
- package/dist/project/agent-rules.d.ts +1 -1
- package/dist/project/agent-rules.js +22 -17
- package/dist/project/refresh-rules.js +2 -1
- package/dist/register/codex-toml.d.ts +12 -0
- package/dist/register/codex-toml.js +44 -0
- package/dist/register/mcp.d.ts +15 -2
- package/dist/register/mcp.js +16 -0
- package/dist/register/prompt-hooks.d.ts +123 -0
- package/dist/register/prompt-hooks.js +449 -0
- package/dist/register/slash-command.d.ts +1 -1
- package/dist/register/slash-command.js +5 -4
- package/dist/run-types.d.ts +10 -0
- package/dist/run.d.ts +1 -1
- package/dist/run.js +15 -390
- package/package.json +2 -2
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `init --hooks`: the
|
|
2
|
+
* `init --hooks`: the Claude Code Stop hook, merged into `.claude/settings.json`.
|
|
3
3
|
*
|
|
4
4
|
* Opt-in, because it writes into a file the user and other tools own. When the agent stops without a
|
|
5
|
-
* single claim having held, the hook prints one line saying so
|
|
6
|
-
* itself is `reticle report --hook`, which reads the session journal on disk.
|
|
5
|
+
* single claim having held, the hook prints one line saying so and does not block. The line
|
|
6
|
+
* itself is `reticle report --hook`, which reads the session journal on disk. It blocks the stop in one
|
|
7
|
+
* case: a Harness drive the agent started still runs, so the agent stays to read its result.
|
|
7
8
|
*/
|
|
8
9
|
import { RETICLE_NPM_PACKAGE } from '../version.js';
|
|
9
10
|
import { StepStatus } from './plan-types.js';
|
|
11
|
+
import { PROMPT_HOOK_AGENTS, PromptHookAgent, PromptHookStatus, installPromptHook, promptHookCommand, } from '../register/prompt-hooks.js';
|
|
10
12
|
export const CLAUDE_SETTINGS_PATH = '.claude/settings.json';
|
|
11
13
|
export const STOP_HOOK_COMMAND = `npx ${RETICLE_NPM_PACKAGE} report --hook`;
|
|
12
14
|
const TITLE = 'Claude Code Stop hook';
|
|
@@ -36,14 +38,161 @@ export function stopHookStep(path, existing) {
|
|
|
36
38
|
const hooks = isObject(settings['hooks']) ? settings['hooks'] : {};
|
|
37
39
|
const stop = Array.isArray(hooks[STOP_EVENT]) ? hooks[STOP_EVENT] : [];
|
|
38
40
|
if (JSON.stringify(stop).includes(STOP_HOOK_COMMAND)) {
|
|
39
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
title: TITLE,
|
|
43
|
+
target: path,
|
|
44
|
+
status: StepStatus.ALREADY,
|
|
45
|
+
detail: 'hook already installed',
|
|
46
|
+
};
|
|
40
47
|
}
|
|
41
48
|
const merged = { ...settings, hooks: { ...hooks, [STOP_EVENT]: [...stop, HOOK_ENTRY] } };
|
|
42
49
|
return {
|
|
43
50
|
title: TITLE,
|
|
44
51
|
target: path,
|
|
45
52
|
status: StepStatus.APPLY,
|
|
46
|
-
detail: 'print one line when the agent stops without a claim that held;
|
|
53
|
+
detail: 'print one line when the agent stops without a claim that held; blocks only while its Harness drive runs',
|
|
47
54
|
write: { path, content: `${JSON.stringify(merged, null, 2)}\n`, expect: [STOP_HOOK_COMMAND] },
|
|
48
55
|
};
|
|
49
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* The Claude Code `SessionStart` hook with matcher `compact`: right after a compaction it prints
|
|
59
|
+
* `reticle context --compact` — what this session proved, what failed, what is still owed — back
|
|
60
|
+
* into the agent's fresh context. Reticle holds the memory; this is how it is handed back without
|
|
61
|
+
* the agent having to remember to ask.
|
|
62
|
+
*
|
|
63
|
+
* Written by default wherever the CLAUDE.md rule is (it only reads the project's own `.reticle/`, and
|
|
64
|
+
* prints nothing where there is none). Removable like any hook: delete the entry.
|
|
65
|
+
*/
|
|
66
|
+
export const COMPACT_HOOK_COMMAND = `npx ${RETICLE_NPM_PACKAGE} context --compact`;
|
|
67
|
+
const COMPACT_TITLE = 'Claude Code resume-after-compaction hook';
|
|
68
|
+
const SESSION_START_EVENT = 'SessionStart';
|
|
69
|
+
const COMPACT_MATCHER = 'compact';
|
|
70
|
+
export function compactHookStep(path, existing) {
|
|
71
|
+
const settings = null === existing || undefined === existing ? {} : parseSettings(existing);
|
|
72
|
+
if (settings === undefined) {
|
|
73
|
+
return {
|
|
74
|
+
title: COMPACT_TITLE,
|
|
75
|
+
target: path,
|
|
76
|
+
status: StepStatus.MANUAL,
|
|
77
|
+
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}\``,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const hooks = isObject(settings['hooks']) ? settings['hooks'] : {};
|
|
81
|
+
const start = Array.isArray(hooks[SESSION_START_EVENT])
|
|
82
|
+
? hooks[SESSION_START_EVENT]
|
|
83
|
+
: [];
|
|
84
|
+
if (JSON.stringify(start).includes(COMPACT_HOOK_COMMAND)) {
|
|
85
|
+
return {
|
|
86
|
+
title: COMPACT_TITLE,
|
|
87
|
+
target: path,
|
|
88
|
+
status: StepStatus.ALREADY,
|
|
89
|
+
detail: 'hook already installed',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const entry = {
|
|
93
|
+
matcher: COMPACT_MATCHER,
|
|
94
|
+
hooks: [{ type: 'command', command: COMPACT_HOOK_COMMAND }],
|
|
95
|
+
};
|
|
96
|
+
const merged = { ...settings, hooks: { ...hooks, [SESSION_START_EVENT]: [...start, entry] } };
|
|
97
|
+
return {
|
|
98
|
+
title: COMPACT_TITLE,
|
|
99
|
+
target: path,
|
|
100
|
+
status: StepStatus.APPLY,
|
|
101
|
+
detail: 'after a context compaction, hand the agent back what this session proved and still owes',
|
|
102
|
+
write: {
|
|
103
|
+
path,
|
|
104
|
+
content: `${JSON.stringify(merged, null, 2)}\n`,
|
|
105
|
+
expect: [COMPACT_HOOK_COMMAND],
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Every hook this run writes into `.claude/settings.json`, each merged on top of the one before, so
|
|
111
|
+
* two steps writing the same file cannot undo each other.
|
|
112
|
+
*/
|
|
113
|
+
export function claudeSettingsSteps(path, existing, options) {
|
|
114
|
+
const steps = [];
|
|
115
|
+
let current = existing;
|
|
116
|
+
if (options.stop) {
|
|
117
|
+
const stop = stopHookStep(path, current);
|
|
118
|
+
steps.push(stop);
|
|
119
|
+
current = stop.write?.content ?? current;
|
|
120
|
+
}
|
|
121
|
+
if (options.compact) {
|
|
122
|
+
const compact = compactHookStep(path, current);
|
|
123
|
+
steps.push(compact);
|
|
124
|
+
current = compact.write?.content ?? current;
|
|
125
|
+
}
|
|
126
|
+
const claude = PROMPT_HOOK_AGENTS.find((agent) => PromptHookAgent.CLAUDE_CODE === agent.id);
|
|
127
|
+
if (true === options.prompt && claude !== undefined) {
|
|
128
|
+
steps.push(promptHookStep({
|
|
129
|
+
spec: claude,
|
|
130
|
+
path,
|
|
131
|
+
existing: current ?? null,
|
|
132
|
+
...(options.bin === undefined ? {} : { bin: options.bin }),
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
return steps;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The prompt-capture hook, for each agent found. On by default, because the request is what a
|
|
139
|
+
* verdict is checked against and an agent forgets to relay it. `--no-prompt-capture` skips it;
|
|
140
|
+
* `"promptCapture": false` in `.reticle.json` keeps every agent hook from writing; `reticle hook uninstall`
|
|
141
|
+
* removes exactly what this added.
|
|
142
|
+
*/
|
|
143
|
+
const PROMPT_TITLE = 'Prompt capture';
|
|
144
|
+
const NOTICE_TARGET = '.reticle/request.json';
|
|
145
|
+
/**
|
|
146
|
+
* The consent line. Names all that the agent hooks keep, what leaves with a linked run and under
|
|
147
|
+
* which setting, and the one switch that stops all of it.
|
|
148
|
+
*/
|
|
149
|
+
export const PROMPT_CAPTURE_NOTICE = 'Reticle keeps these in .reticle/ in this project, with secrets removed: each prompt you send, the tool calls your agent makes (commands, file edits, web calls), its model, and its closing message for each turn. ' +
|
|
150
|
+
'Tool calls stay on this machine. ' +
|
|
151
|
+
'When the project is linked (reticle connect, or RETICLE_API_KEY in the environment), each synced run sends the model to your own workspace. ' +
|
|
152
|
+
'Your prompts and the closing messages also go there with the runs, and to the Harness, unless "shareRequests": false is in .reticle.json. ' +
|
|
153
|
+
'Stop all of it: "promptCapture": false in .reticle.json, or RETICLE_PROMPT_CAPTURE=0 on this machine.';
|
|
154
|
+
export function promptHookStep(target) {
|
|
155
|
+
const { spec, path, existing, bin } = target;
|
|
156
|
+
const title = `${PROMPT_TITLE}: ${spec.label}`;
|
|
157
|
+
const merged = installPromptHook(spec, existing, bin);
|
|
158
|
+
const then = spec.activation === undefined ? '' : `. ${spec.activation}`;
|
|
159
|
+
if (PromptHookStatus.ALREADY === merged.status) {
|
|
160
|
+
return {
|
|
161
|
+
title,
|
|
162
|
+
target: path,
|
|
163
|
+
status: StepStatus.ALREADY,
|
|
164
|
+
detail: `hook already installed${then}`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (PromptHookStatus.MANUAL === merged.status) {
|
|
168
|
+
return {
|
|
169
|
+
title,
|
|
170
|
+
target: path,
|
|
171
|
+
status: StepStatus.MANUAL,
|
|
172
|
+
detail: `${path} is not plain JSON, so it was left alone. Add a prompt-submit hook that runs \`${promptHookCommand(spec.id)}\` (${spec.docs})`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
title,
|
|
177
|
+
target: path,
|
|
178
|
+
status: StepStatus.APPLY,
|
|
179
|
+
detail: `save each prompt, tool call, model and closing message to .reticle/; never blocks the agent${then}`,
|
|
180
|
+
write: { path, content: merged.content, expect: [promptHookCommand(spec.id)] },
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
/** The steps for every detected agent, then the one consent line when any hook is in place. */
|
|
184
|
+
export function promptHookSteps(targets, claude, bin) {
|
|
185
|
+
const steps = targets.map((target) => promptHookStep(bin === undefined ? target : { ...target, bin }));
|
|
186
|
+
if (0 === steps.length && !claude) {
|
|
187
|
+
return steps;
|
|
188
|
+
}
|
|
189
|
+
return [
|
|
190
|
+
...steps,
|
|
191
|
+
{
|
|
192
|
+
title: PROMPT_TITLE,
|
|
193
|
+
target: NOTICE_TARGET,
|
|
194
|
+
status: StepStatus.NOTICE,
|
|
195
|
+
detail: PROMPT_CAPTURE_NOTICE,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
}
|
|
@@ -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\
|
|
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?** Ask the user, then run `npx @reticlehq/server init` in the app's own folder. **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 --skill 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: every other tool is one `reticle_run` call away. Do not restart the daemon with `RETICLE_ADVERTISE_ALL_TOOLS=1`: it uses older tool names that the rules above do not.\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: {
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { NPX } from '../register/mcp.js';
|
|
21
21
|
import { RETICLE_NPM_PACKAGE } from '../version.js';
|
|
22
|
-
import { DISCOVERY_CALL_URL, DISCOVERY_EMAIL_COMMAND } from '@reticlehq/core';
|
|
22
|
+
import { AgentRules, DISCOVERY_CALL_URL, DISCOVERY_EMAIL_COMMAND, FEEDBACK_KIND_CHOICES, } from '@reticlehq/core';
|
|
23
23
|
/** Project-root instruction files the agent re-reads every session. */
|
|
24
24
|
export const CLAUDE_MD_PATH = 'CLAUDE.md';
|
|
25
25
|
export const AGENTS_MD_PATH = 'AGENTS.md';
|
|
@@ -40,7 +40,7 @@ export const RETICLE_MD_PATH = 'RETICLE.md';
|
|
|
40
40
|
/** Cursor project rule; the frontmatter `alwaysApply` keeps it in context on every turn. */
|
|
41
41
|
export const CURSOR_RULE_PATH = '.cursor/rules/reticle.mdc';
|
|
42
42
|
/** Markers delimiting the managed block so a re-run is idempotent and the block stays updatable. */
|
|
43
|
-
const RULE_BEGIN =
|
|
43
|
+
const RULE_BEGIN = AgentRules.BEGIN;
|
|
44
44
|
const RULE_END = '<!-- reticle:end -->';
|
|
45
45
|
/**
|
|
46
46
|
* Does this file already carry the managed block?
|
|
@@ -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:**
|
|
121
|
+
**How to verify — the rules to keep in mind on every call:**
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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?** Ask the user, then run \`${CLI} init\` in the app's own folder. **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
|
-
|
|
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,15 +154,12 @@ 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
|
|
|
155
|
-
**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.
|
|
160
|
+
**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 --skill reticle\` anywhere the skills CLI works.
|
|
156
161
|
|
|
157
|
-
**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
|
|
162
|
+
**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: every other tool is one \`reticle_run\` call away. Do not restart the daemon with \`RETICLE_ADVERTISE_ALL_TOOLS=1\`: it uses older tool names that the rules above do not.
|
|
158
163
|
|
|
159
164
|
**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.
|
|
160
165
|
|
|
@@ -175,7 +180,7 @@ The short version lives in your agent instruction file (\`CLAUDE.md\` / \`AGENTS
|
|
|
175
180
|
|
|
176
181
|
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).
|
|
177
182
|
|
|
178
|
-
|
|
183
|
+
On Claude Code and Codex, run \`${CLI} init --relaunch\` and run the command it prints: it restarts this conversation with the tools. Elsewhere, ask the human to restart their client: reload the window in Cursor, or press 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.
|
|
179
184
|
|
|
180
185
|
## A result carries \`verify_next\`
|
|
181
186
|
|
|
@@ -202,12 +207,12 @@ Finish that file when you see it. It is what lets you check what the app BELIEVE
|
|
|
202
207
|
- **Send it with \`reticle_session { action: "feedback" }\` the moment you notice.** One call, then carry straight on. Do not batch reports, do not wait until the task is done, and do not decide something is too minor. The small awkward things are what nobody ever reports, so they never get fixed.
|
|
203
208
|
- **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\`).
|
|
204
209
|
- **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.
|
|
205
|
-
- **When the tools are not reachable yet**, file the same report from the shell: \`${CLI} feedback --agent --kind
|
|
210
|
+
- **When the tools are not reachable yet**, file the same report from the shell: \`${CLI} feedback --agent --kind ${FEEDBACK_KIND_CHOICES} "what happened"\`. It needs no daemon and no working install, so a Reticle that broke before it started can still be reported.
|
|
206
211
|
- **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.
|
|
207
212
|
- **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.
|
|
208
213
|
- **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.
|
|
209
214
|
- **Never include app source, secrets, user data, or customer records** in a report.
|
|
210
|
-
- **When the person you work with is stuck, or Reticle got in their way, offer them a call with the founder** (${DISCOVERY_CALL_URL}) or
|
|
215
|
+
- **When the person you work with is stuck, or Reticle got in their way, offer them a call with the founder** (${DISCOVERY_CALL_URL}) or a note with their email: \`${DISCOVERY_EMAIL_COMMAND}\`. That conversation is where Reticle's direction comes from. Offer it; never book a call or submit an email for them.
|
|
211
216
|
|
|
212
217
|
## More
|
|
213
218
|
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
* install.
|
|
18
18
|
*/
|
|
19
19
|
import { join } from 'node:path';
|
|
20
|
+
import { AgentRules } from '@reticlehq/core';
|
|
20
21
|
import { mergeMarkedInstruction, AgentRuleStatus, hasManagedBlock } from './agent-rules.js';
|
|
21
22
|
/** The instruction files the managed block lives in. Cursor's rule file is regenerated whole. */
|
|
22
|
-
export const RULE_FILES =
|
|
23
|
+
export const RULE_FILES = AgentRules.FILES;
|
|
23
24
|
/**
|
|
24
25
|
* Refresh every instruction file that already carries a Reticle block.
|
|
25
26
|
*
|
|
@@ -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
|
+
}
|
package/dist/register/mcp.d.ts
CHANGED
|
@@ -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
|
|
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():
|
|
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
|
package/dist/register/mcp.js
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export declare const PromptHookAgent: {
|
|
2
|
+
readonly CLAUDE_CODE: "claude-code";
|
|
3
|
+
readonly CURSOR: "cursor";
|
|
4
|
+
readonly CODEX: "codex";
|
|
5
|
+
readonly GEMINI: "gemini";
|
|
6
|
+
readonly WINDSURF: "windsurf";
|
|
7
|
+
/** VS Code agent mode and Copilot CLI: both read `.github/hooks/*.json`. */
|
|
8
|
+
readonly COPILOT: "copilot";
|
|
9
|
+
readonly FACTORY: "factory-droid";
|
|
10
|
+
};
|
|
11
|
+
export type PromptHookAgent = (typeof PromptHookAgent)[keyof typeof PromptHookAgent];
|
|
12
|
+
export declare const PromptHookStatus: {
|
|
13
|
+
readonly APPLY: "apply";
|
|
14
|
+
readonly ALREADY: "already";
|
|
15
|
+
readonly MANUAL: "manual";
|
|
16
|
+
};
|
|
17
|
+
export type PromptHookStatus = (typeof PromptHookStatus)[keyof typeof PromptHookStatus];
|
|
18
|
+
/** The words every version of our command holds, and no other command does. */
|
|
19
|
+
export declare const PROMPT_HOOK_MARKER = " hook prompt --agent ";
|
|
20
|
+
export declare const AGENT_FLAG = "--agent";
|
|
21
|
+
/**
|
|
22
|
+
* The command, PINNED to the version that installed it. Unpinned, `npx` runs whatever version its
|
|
23
|
+
* cache holds: on a real machine that was 3.4.0, which has no `hook` command, so every prompt was
|
|
24
|
+
* lost while the hook still exited quietly. A re-run of `init` moves the pin forward.
|
|
25
|
+
*/
|
|
26
|
+
export declare const promptHookCommand: (agent: PromptHookAgent) => string;
|
|
27
|
+
/** The words every version of the post-tool command holds. */
|
|
28
|
+
export declare const TOOL_HOOK_MARKER = " hook tool --agent ";
|
|
29
|
+
/**
|
|
30
|
+
* The post-tool command: keeps one of the agent's own tool calls in `.reticle/episodes/`. Pinned
|
|
31
|
+
* for the same reason as the prompt command.
|
|
32
|
+
*/
|
|
33
|
+
export declare const toolHookCommand: (agent: PromptHookAgent) => string;
|
|
34
|
+
/** One place in the config that holds a list of hook entries, and the entry we add to it. */
|
|
35
|
+
interface HookSlot {
|
|
36
|
+
/** Keys from the top of the file down to the list. */
|
|
37
|
+
readonly path: readonly string[];
|
|
38
|
+
readonly entry: (command: string) => Record<string, unknown>;
|
|
39
|
+
/** The command this slot runs, when it is not the prompt command. */
|
|
40
|
+
readonly command?: (agent: PromptHookAgent) => string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Run the installed CLI with node, and fall back to the pinned npx command when that file is gone.
|
|
44
|
+
* npx's own start (~350 ms on a laptop) came before every prompt and tool call. The path is this
|
|
45
|
+
* machine's; the npx half is what every other machine runs, so the entry stays correct for a
|
|
46
|
+
* teammate. `node` exits at once on a missing file, before it reads stdin, so the fallback still
|
|
47
|
+
* gets the payload.
|
|
48
|
+
*/
|
|
49
|
+
export declare function directHookCommand(bin: string, npx: string): string;
|
|
50
|
+
export interface PromptHookSpec {
|
|
51
|
+
readonly id: PromptHookAgent;
|
|
52
|
+
readonly label: string;
|
|
53
|
+
/** Project-relative configs this agent reads, first wins. We write to the first that exists. */
|
|
54
|
+
readonly paths: readonly string[];
|
|
55
|
+
/** Written when none of `paths` exists. */
|
|
56
|
+
readonly defaultPath: string;
|
|
57
|
+
/** Keys we add at the top of a NEW file, and remove again when the file holds nothing else. */
|
|
58
|
+
readonly header?: Readonly<Record<string, unknown>>;
|
|
59
|
+
readonly slots: readonly HookSlot[];
|
|
60
|
+
/** Project-relative directories that show the agent is used here. */
|
|
61
|
+
readonly projectMarkers: readonly string[];
|
|
62
|
+
/** Home-relative directories that show the agent is on this machine. */
|
|
63
|
+
readonly homeMarkers: readonly string[];
|
|
64
|
+
readonly docs: string;
|
|
65
|
+
/** What the user must do before the agent runs a hook it was given, when writing it is not enough. */
|
|
66
|
+
readonly activation?: string;
|
|
67
|
+
}
|
|
68
|
+
export declare const PROMPT_HOOK_AGENTS: readonly PromptHookSpec[];
|
|
69
|
+
/**
|
|
70
|
+
* The agents with no prompt hook that can be written, which therefore rely on the MCP instructions
|
|
71
|
+
* telling the agent to declare the request. Each one was checked on 2026-10-14: no prompt-submit
|
|
72
|
+
* event, or one whose config format the vendor does not document well enough to write.
|
|
73
|
+
*/
|
|
74
|
+
export declare const MCP_FALLBACK_AGENTS: readonly string[];
|
|
75
|
+
/** One agent found here: its spec, its config's absolute path, and what that file holds now. */
|
|
76
|
+
export interface PromptHookTarget {
|
|
77
|
+
readonly spec: PromptHookSpec;
|
|
78
|
+
readonly path: string;
|
|
79
|
+
readonly existing: string | null;
|
|
80
|
+
/** The installed CLI's bin, when known. See `directHookCommand`. */
|
|
81
|
+
readonly bin?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The agents used in `projectDir` or installed under `home`, each with the config to write.
|
|
85
|
+
*
|
|
86
|
+
* Only those: a `.gemini/settings.json` in the repository of somebody who has never run Gemini is
|
|
87
|
+
* litter, and it makes the next tool's detection wrong too. `claudeCli` counts Claude Code as
|
|
88
|
+
* present, because it keeps its registration behind its CLI rather than in a folder.
|
|
89
|
+
*
|
|
90
|
+
* `home` undefined reads the project only. `reticle hook install` passes that: it adds the hook to
|
|
91
|
+
* a project already set up, and an agent installed on this machine is not one the PROJECT uses.
|
|
92
|
+
* Reading the home folder there wrote five committed configs into a repository that had `.claude/`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function promptHookTargets(io: {
|
|
95
|
+
exists(path: string): boolean;
|
|
96
|
+
readFile(path: string): string | null;
|
|
97
|
+
}, projectDir: string, home: string | undefined, claudeCli: boolean, join: (...parts: string[]) => string): PromptHookTarget[];
|
|
98
|
+
/** The project-relative config to write, given which files exist. */
|
|
99
|
+
export declare function promptHookPath(spec: PromptHookSpec, exists: (path: string) => boolean): string;
|
|
100
|
+
/** THIS version's entry is in every slot. An older version's entry is replaced by install. */
|
|
101
|
+
export declare function promptHookInstalled(spec: PromptHookSpec, text: string | null): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* THIS version is installed, but some slot runs it through npx alone (~426 ms of npx start per
|
|
104
|
+
* call): an install from before the direct form. Any machine's bin counts as direct, so a
|
|
105
|
+
* teammate's path is never rewritten with this one's.
|
|
106
|
+
*/
|
|
107
|
+
export declare function promptHookNpxOnly(spec: PromptHookSpec, text: string | null): boolean;
|
|
108
|
+
/** Our entry merged into the config. `content` is the text to write; MANUAL keeps the text as it is. */
|
|
109
|
+
export declare function installPromptHook(spec: PromptHookSpec, text: string | null,
|
|
110
|
+
/** The installed CLI's bin, when known: the hook runs it directly. See `directHookCommand`. */
|
|
111
|
+
bin?: string): {
|
|
112
|
+
status: PromptHookStatus;
|
|
113
|
+
content: string;
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* The config with our entries removed. `content` null means delete the file: it holds nothing but
|
|
117
|
+
* what install added. A config that is not ours to parse is never changed.
|
|
118
|
+
*/
|
|
119
|
+
export declare function uninstallPromptHook(spec: PromptHookSpec, text: string | null): {
|
|
120
|
+
changed: boolean;
|
|
121
|
+
content: string | null;
|
|
122
|
+
};
|
|
123
|
+
export {};
|