@reticlehq/server 2.5.0 → 2.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.
- package/SKILL.md +226 -26
- package/dist/bridge/bridge.d.ts +18 -1
- package/dist/bridge/bridge.js +59 -6
- package/dist/bridge/bridge.test-harness.d.ts +8 -0
- package/dist/bridge/bridge.test-harness.js +9 -1
- package/dist/cli/chromium-hint.d.ts +40 -0
- package/dist/cli/chromium-hint.js +75 -0
- package/dist/cli/cli-doctor.d.ts +0 -5
- package/dist/cli/cli-doctor.js +74 -14
- package/dist/cli/cli-launch.d.ts +1 -0
- package/dist/cli/cli-launch.js +4 -1
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/cli/cli-parse.d.ts +5 -9
- package/dist/cli/cli-parse.js +7 -23
- package/dist/cli/cli-port.js +20 -1
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +44 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.js +170 -19
- package/dist/daemon/daemon-resilience.d.ts +24 -0
- package/dist/daemon/daemon-resilience.js +105 -17
- package/dist/daemon/daemon.js +26 -1
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-shutdown.d.ts +13 -5
- package/dist/daemon/idle-shutdown.js +14 -6
- package/dist/daemon/loopback-alias.d.ts +32 -0
- package/dist/daemon/loopback-alias.js +80 -0
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/events/predicate-eval.d.ts +17 -3
- package/dist/events/predicate-eval.js +183 -96
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +68 -0
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +17 -3
- package/dist/events/ring-buffer.d.ts +8 -0
- package/dist/events/ring-buffer.js +35 -17
- package/dist/flows/flow-replay.d.ts +1 -1
- package/dist/flows/flow-replay.js +19 -2
- package/dist/flows/flow-step-runners.js +2 -3
- package/dist/flows/flow-tools.js +10 -14
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/honesty/blind-spots.d.ts +19 -0
- package/dist/honesty/blind-spots.js +33 -0
- package/dist/honesty/honesty.d.ts +11 -0
- package/dist/honesty/honesty.js +9 -1
- package/dist/honesty/verified.d.ts +10 -1
- package/dist/honesty/verified.js +42 -3
- package/dist/http-server.js +20 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +36 -3
- package/dist/init/agent-rules.d.ts +18 -0
- package/dist/init/agent-rules.js +28 -5
- package/dist/init/cra.d.ts +13 -0
- package/dist/init/cra.js +16 -0
- package/dist/init/detect.d.ts +14 -0
- package/dist/init/detect.js +23 -0
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/node-io.js +8 -2
- package/dist/init/non-js-project.d.ts +27 -0
- package/dist/init/non-js-project.js +60 -0
- package/dist/init/plan-framework.d.ts +9 -0
- package/dist/init/plan-framework.js +55 -3
- package/dist/init/plan.d.ts +14 -0
- package/dist/init/plan.js +93 -12
- package/dist/init/run.d.ts +18 -1
- package/dist/init/run.js +169 -23
- package/dist/init/snippets.d.ts +20 -1
- package/dist/init/snippets.js +71 -6
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/mcp/mcp-outage.d.ts +1 -0
- package/dist/mcp/mcp-outage.js +8 -1
- package/dist/mcp/mcp-proxy.d.ts +11 -1
- package/dist/mcp/mcp-proxy.js +39 -9
- package/dist/mcp/mcp.js +64 -2
- package/dist/mcp/proxy-lifecycle.d.ts +19 -0
- package/dist/mcp/proxy-lifecycle.js +21 -0
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/pool/playwright-launcher.js +10 -2
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- package/dist/session/live-control-tools.js +39 -2
- package/dist/session/live-control.d.ts +7 -9
- package/dist/session/live-control.js +34 -1
- package/dist/session/no-session-diagnosis.d.ts +11 -4
- package/dist/session/no-session-diagnosis.js +95 -14
- package/dist/session/no-session-watch.d.ts +3 -1
- package/dist/session/no-session-watch.js +10 -2
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/review-store.d.ts +19 -0
- package/dist/session/review-store.js +34 -5
- package/dist/session/session-info.d.ts +17 -1
- package/dist/session/session-info.js +5 -1
- package/dist/session/session-manager.d.ts +12 -0
- package/dist/session/session-manager.js +83 -3
- package/dist/session/session.d.ts +9 -0
- package/dist/session/session.js +20 -0
- package/dist/status-payload.d.ts +13 -1
- package/dist/status-payload.js +6 -1
- package/dist/telemetry/app-instrumented.d.ts +37 -0
- package/dist/telemetry/app-instrumented.js +54 -0
- package/dist/telemetry/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +25 -7
- package/dist/telemetry/daemon-telemetry.js +32 -13
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/feedback-cli.d.ts +7 -2
- package/dist/telemetry/feedback-cli.js +49 -5
- package/dist/telemetry/feedback-context.d.ts +1 -6
- package/dist/telemetry/feedback-context.js +64 -4
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +4 -4
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +45 -3
- package/dist/telemetry/session-metrics.js +247 -9
- package/dist/telemetry/telemetry.d.ts +21 -1
- package/dist/telemetry/telemetry.js +34 -9
- package/dist/telemetry/verification-of.js +26 -1
- package/dist/tools/act-tools.js +63 -14
- package/dist/tools/assert-verdict.d.ts +7 -1
- package/dist/tools/assert-verdict.js +22 -3
- package/dist/tools/browser-tools.js +8 -2
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +26 -0
- package/dist/tools/dynamic-tools.js +6 -1
- package/dist/tools/error-recovery.d.ts +14 -0
- package/dist/tools/error-recovery.js +89 -8
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +76 -7
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +15 -1
- package/dist/tools/navigate-result.js +17 -10
- package/dist/tools/observe-tools.js +22 -4
- package/dist/tools/real-input-attempt.js +10 -5
- package/dist/tools/tool-kit.d.ts +60 -5
- package/dist/tools/tool-kit.js +55 -24
- package/dist/tools/tool-surface.js +13 -0
- package/dist/tools/tools.js +22 -0
- package/dist/visual/capture-cleanup.d.ts +16 -0
- package/dist/visual/capture-cleanup.js +39 -0
- package/dist/visual/visual-tools.d.ts +18 -0
- package/dist/visual/visual-tools.js +24 -5
- package/docs/README.md +55 -0
- package/docs/agent-cheatsheet.md +15 -3
- package/docs/benchmarks.md +1 -1
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +17 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/local-registry.md +1 -1
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +1 -1
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +15 -3
- package/docs/telemetry.md +16 -1
- package/docs/token-efficiency.md +2 -2
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { originOf } from './session/session-manager.js';
|
|
|
5
5
|
import { setBrowserMode, BrowserMode } from './telemetry/browser-mode.js';
|
|
6
6
|
import { replayNamedFlow } from './flows/flow-tools.js';
|
|
7
7
|
import { createSharedServer } from './http-server.js';
|
|
8
|
+
import { openLoopbackAlias } from './daemon/loopback-alias.js';
|
|
9
|
+
import { reportAppInstrumented } from './telemetry/app-instrumented.js';
|
|
8
10
|
import { resolveBridgeSecurityWithAutoToken } from './bridge/bridge-security.js';
|
|
9
11
|
import { Bridge } from './bridge/bridge.js';
|
|
10
12
|
import { BaselineStore } from './project/baselines.js';
|
|
@@ -14,6 +16,7 @@ import { buildFlowChips } from './flows/flow-scope.js';
|
|
|
14
16
|
import { ProjectStore } from './project/project-store.js';
|
|
15
17
|
import { AnnotationStore } from './flows/annotation-store.js';
|
|
16
18
|
import { createNodeFileSystem } from './project/fs-port.js';
|
|
19
|
+
import { cleanupCaptureDirectories } from './visual/capture-cleanup.js';
|
|
17
20
|
import { ReticleRunner } from './runs/reticle-runner.js';
|
|
18
21
|
import { createRunnerPort } from './runs/runner-port.js';
|
|
19
22
|
import { RunStore } from './runs/run-store.js';
|
|
@@ -66,6 +69,10 @@ export { BrowserPool, DEFAULT_LEASE_TTL_MS } from './pool/browser-pool.js';
|
|
|
66
69
|
export { playwrightLauncher, resolveMaxContexts } from './pool/playwright-launcher.js';
|
|
67
70
|
export { appendReticleParams } from './tools/lease-tools.js';
|
|
68
71
|
export { writePid, removePid, isRunning, logPath, readPid, isAlive } from './daemon/daemon.js';
|
|
72
|
+
// The daemon's own liveness vocabulary, exported so a GATE can read a daemon log back and say how
|
|
73
|
+
// that daemon ended. Without this the battery would re-implement the rule, and a guard that
|
|
74
|
+
// re-implements what it guards is insensitive to it.
|
|
75
|
+
export { classifyDaemonLife, DaemonEnd, DAEMON_HEARTBEAT_EVENT, DAEMON_HEARTBEAT_MS, } from './daemon/heartbeat.js';
|
|
69
76
|
export { scrollToFind } from './input/scroll-find.js';
|
|
70
77
|
export { CORE_TOOL_NAMES, TOOL_SURFACE, TOOL_PROFILE_ENV, filterTools, resolveToolSurface, } from './tools/tool-surface.js';
|
|
71
78
|
export { AnnotationStore } from './flows/annotation-store.js';
|
|
@@ -262,6 +269,9 @@ export async function start(options = {}) {
|
|
|
262
269
|
reticleRoot,
|
|
263
270
|
now,
|
|
264
271
|
bridgePort: port,
|
|
272
|
+
// The daemon's OWN project, so a tool can tell "this session is mine" from "this session
|
|
273
|
+
// belongs to a sibling app under the same daemon". contract_save refuses on the second.
|
|
274
|
+
...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
|
|
265
275
|
};
|
|
266
276
|
const profile = resolveToolSurface(options.toolProfile);
|
|
267
277
|
const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile);
|
|
@@ -280,6 +290,7 @@ export async function start(options = {}) {
|
|
|
280
290
|
...(security.token !== undefined ? { token: security.token } : {}),
|
|
281
291
|
close: async () => {
|
|
282
292
|
reaper.stop();
|
|
293
|
+
await cleanupCaptureDirectories();
|
|
283
294
|
leaseReaper?.stop();
|
|
284
295
|
await pool?.shutdown();
|
|
285
296
|
await owned?.dispose();
|
|
@@ -302,7 +313,9 @@ export async function startDaemon(options = {}) {
|
|
|
302
313
|
// mirror rejection so a port collision can't surface as an unhandled promise rejection.
|
|
303
314
|
void bridge.ready.catch(() => undefined);
|
|
304
315
|
// `reticle status` GETs this for a live, at-a-glance view of connected tabs + their health.
|
|
305
|
-
|
|
316
|
+
// The same diagnosis agents get on an empty `reticle_sessions`, so `reticle status` — the
|
|
317
|
+
// most-run command in the field — stops answering "sessionCount: 0" and nothing else.
|
|
318
|
+
shared.attachStatus(() => statusPayload(bridge.sessions.count(), bridge.sessions.list(), bridge.sessions.noSessionHint()));
|
|
306
319
|
// Agent-independent presence: the daemon outlives any single agent, so when the LAST agent's MCP
|
|
307
320
|
// connection drops (it stopped, or is waiting on the human), end every session and push a clear
|
|
308
321
|
// "go to your terminal" notice to the panel — the human is on the browser and must not lose a typed
|
|
@@ -321,7 +334,9 @@ export async function startDaemon(options = {}) {
|
|
|
321
334
|
// never picked when the agent omits a sessionId. Explicit per-call scope/sessionId still overrides.
|
|
322
335
|
// Scope + the no-session diagnosis: "no browser session connected" is the error that ends most
|
|
323
336
|
// sessions, and the agent is told to check two things it cannot see. See no-session-diagnosis.ts.
|
|
324
|
-
|
|
337
|
+
// The pool reader is passed lazily: `pool` is assigned above but may be undefined when this
|
|
338
|
+
// daemon runs without one, and the diagnosis must degrade to the tab message rather than lie.
|
|
339
|
+
wireSessionScope(bridge.sessions, readProjectId(process.cwd()), port, () => pool?.reapedLeaseCount() ?? 0);
|
|
325
340
|
const { realInput, owned } = await resolveRealInput(options, () => shared.close(), makeNetworkDetailRouter(bridge, options.driveUrl));
|
|
326
341
|
const fs = createNodeFileSystem();
|
|
327
342
|
const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
|
|
@@ -380,6 +395,16 @@ export async function startDaemon(options = {}) {
|
|
|
380
395
|
// connecting session's project (a shared daemon serves many apps; each panel shows only its own
|
|
381
396
|
// flows + legacy untagged ones). Each chip carries a `start` hint (the first step's testid anchor)
|
|
382
397
|
// so the HUD shows a flow only on the page it can begin from — the panel re-scopes per route.
|
|
398
|
+
// The first instrumented app of this daemon run — the funnel step nothing could measure before.
|
|
399
|
+
// Inside the existing session-ready hook rather than beside it: this fires exactly when a page
|
|
400
|
+
// carrying the SDK has completed its handshake, which is the definition of "instrumented", and
|
|
401
|
+
// `reportAppInstrumented` is idempotent so later sessions cost a boolean check.
|
|
402
|
+
bridge.attachSessionReady(() => {
|
|
403
|
+
reportAppInstrumented({
|
|
404
|
+
initialized: readProjectId(process.cwd()) !== undefined,
|
|
405
|
+
agentAttached: agentConnected,
|
|
406
|
+
});
|
|
407
|
+
});
|
|
383
408
|
bridge.attachSessionReady((session) => {
|
|
384
409
|
flows
|
|
385
410
|
.list(session.projectId)
|
|
@@ -408,7 +433,13 @@ export async function startDaemon(options = {}) {
|
|
|
408
433
|
shared.httpServer.once('listening', onListening);
|
|
409
434
|
shared.httpServer.listen(port, security.host ?? LOOPBACK_HOST);
|
|
410
435
|
});
|
|
411
|
-
|
|
436
|
+
// `localhost` is a name with two answers, and Windows/Chrome tries the IPv6 one first. Only alias
|
|
437
|
+
// when we are on the default loopback bind: an explicit RETICLE_HOST is a deliberate choice about
|
|
438
|
+
// reachability, and quietly adding a second listener to it would be the opposite of deliberate.
|
|
439
|
+
const loopbackAlias = security.host === undefined
|
|
440
|
+
? await openLoopbackAlias(port)
|
|
441
|
+
: { opened: false, close: undefined };
|
|
442
|
+
log('mcp_daemon_started', { port, loopbackAlias: loopbackAlias.opened });
|
|
412
443
|
return {
|
|
413
444
|
bridge,
|
|
414
445
|
...(realInput !== undefined ? { realInput } : {}),
|
|
@@ -422,10 +453,12 @@ export async function startDaemon(options = {}) {
|
|
|
422
453
|
agentAttached: () => agentConnected,
|
|
423
454
|
close: async () => {
|
|
424
455
|
reaper.stop();
|
|
456
|
+
await cleanupCaptureDirectories();
|
|
425
457
|
const vh = verifyHttp;
|
|
426
458
|
if (vh !== undefined)
|
|
427
459
|
await new Promise((resolve) => vh.server.close(() => resolve()));
|
|
428
460
|
leaseReaper.stop();
|
|
461
|
+
await loopbackAlias.close?.();
|
|
429
462
|
await pool.shutdown();
|
|
430
463
|
await owned?.dispose();
|
|
431
464
|
await bridge.close();
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* it with reticle". Registering the MCP server (mcp.ts / cursor.ts) only makes the tools AVAILABLE;
|
|
5
5
|
* this is the piece that makes the agent reach for them, every session, unprompted.
|
|
6
6
|
*
|
|
7
|
+
* It carries a SECOND standing instruction, and that one is not about this project at all: send
|
|
8
|
+
* feedback to the Reticle maintainers, from the moment instrumentation starts. The agent is the party
|
|
9
|
+
* that experiences Reticle, and the only one who can say what it cost — and by the time anyone would
|
|
10
|
+
* think to ask, the context that knew is gone. Written into the same block, because the moment the
|
|
11
|
+
* rule lands (init, before any framework wiring) is the moment the setup itself is being lived
|
|
12
|
+
* through, which is when the reports we hear least about are actually generated.
|
|
13
|
+
*
|
|
7
14
|
* The rule is framed as a definition-of-done (WHEN to verify, HOW, and the anti-reward-hacking guard),
|
|
8
15
|
* written into whichever instruction file the detected agent reads: `CLAUDE.md` (Claude Code),
|
|
9
16
|
* `.cursor/rules/reticle.mdc` (Cursor, `alwaysApply` so it stays in context), or `AGENTS.md` as the
|
|
@@ -15,6 +22,17 @@ export declare const CLAUDE_MD_PATH = "CLAUDE.md";
|
|
|
15
22
|
export declare const AGENTS_MD_PATH = "AGENTS.md";
|
|
16
23
|
/** Cursor project rule; the frontmatter `alwaysApply` keeps it in context on every turn. */
|
|
17
24
|
export declare const CURSOR_RULE_PATH = ".cursor/rules/reticle.mdc";
|
|
25
|
+
/**
|
|
26
|
+
* How the agent must INVOKE the CLI.
|
|
27
|
+
*
|
|
28
|
+
* `reticle init` installs the SDK packages, never the server — so there is no `reticle` on the
|
|
29
|
+
* agent's PATH, and every bare `reticle <cmd>` this rule named was an instruction that could not be
|
|
30
|
+
* followed. Reported from a field sweep: three of them, in the file the agent re-reads every session.
|
|
31
|
+
*
|
|
32
|
+
* `npx @reticlehq/server` is the same invocation the MCP registration uses, and it is derived from
|
|
33
|
+
* the package's own name so the two can never drift apart.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CLI: string;
|
|
18
36
|
export declare const AgentRuleStatus: {
|
|
19
37
|
readonly APPLY: "apply";
|
|
20
38
|
readonly ALREADY: "already";
|
package/dist/init/agent-rules.js
CHANGED
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
* it with reticle". Registering the MCP server (mcp.ts / cursor.ts) only makes the tools AVAILABLE;
|
|
5
5
|
* this is the piece that makes the agent reach for them, every session, unprompted.
|
|
6
6
|
*
|
|
7
|
+
* It carries a SECOND standing instruction, and that one is not about this project at all: send
|
|
8
|
+
* feedback to the Reticle maintainers, from the moment instrumentation starts. The agent is the party
|
|
9
|
+
* that experiences Reticle, and the only one who can say what it cost — and by the time anyone would
|
|
10
|
+
* think to ask, the context that knew is gone. Written into the same block, because the moment the
|
|
11
|
+
* rule lands (init, before any framework wiring) is the moment the setup itself is being lived
|
|
12
|
+
* through, which is when the reports we hear least about are actually generated.
|
|
13
|
+
*
|
|
7
14
|
* The rule is framed as a definition-of-done (WHEN to verify, HOW, and the anti-reward-hacking guard),
|
|
8
15
|
* written into whichever instruction file the detected agent reads: `CLAUDE.md` (Claude Code),
|
|
9
16
|
* `.cursor/rules/reticle.mdc` (Cursor, `alwaysApply` so it stays in context), or `AGENTS.md` as the
|
|
@@ -30,21 +37,37 @@ const RULE_END = '<!-- reticle:end -->';
|
|
|
30
37
|
* `npx @reticlehq/server` is the same invocation the MCP registration uses, and it is derived from
|
|
31
38
|
* the package's own name so the two can never drift apart.
|
|
32
39
|
*/
|
|
33
|
-
const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
|
|
40
|
+
export const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
|
|
34
41
|
/** The rule body (markdown). States WHEN to verify, HOW, and never-weaken-a-check. */
|
|
35
42
|
const RULE_BODY = `## Verifying with Reticle
|
|
36
43
|
|
|
37
44
|
This app is instrumented by **Reticle** — an in-app verification layer exposed as \`reticle_*\` MCP tools and the \`${CLI}\` CLI (run it through npx — Reticle's server is not installed into this project). Verifying is part of "done", not an optional extra.
|
|
38
45
|
|
|
39
46
|
- **When:** after you build or change any user-facing feature, flow, or UI, verify it with Reticle **before** telling the user it is complete. Reading the code or passing unit tests is not enough — drive the real running app.
|
|
40
|
-
- **How:** drive the
|
|
47
|
+
- **How:** 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. Batch a multi-step flow (a login, a form) into one \`reticle_act_sequence\` rather than one round trip per field. Read the surrounding evidence with \`reticle_snapshot\`, \`reticle_state\`, \`reticle_network\`, \`reticle_console\`.
|
|
48
|
+
- **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 that ends without one of those two has no result however many tools it used. In practice \`reticle_act\` is reached for far more often and proves nothing; \`act_and_wait\` is where the defects actually get caught.
|
|
49
|
+
- **\`verified: "unknown"\` is not a pass.** It means Reticle drove the app and could not tell what happened; \`verifiedReason\` says which of eleven clauses decided that and what to do about it. Report it as unknown — never as working.
|
|
50
|
+
- **If a tool result carries \`verify_next\`, do what it says next.** It means you have driven this page several times without asking for a verdict, so nothing you have done so far is proven.
|
|
41
51
|
- **Covered flows:** run \`${CLI} gate\` — it reports which recorded flows the changed files affect and whether they still pass.
|
|
42
52
|
- **Never weaken a check to make it green.** Downgrading, skipping, or deleting an assertion is a finding, not a fix.
|
|
43
53
|
- **If Reticle can't run** (no daemon, or this is not a running web app), say so — do not skip verification silently.
|
|
54
|
+
- **If the \`reticle_*\` tools are missing entirely, they have not been loaded yet — not broken.** \`init\` registers the MCP server, but a client reads its tool list at startup and does not re-read it. Ask the human to reload their MCP tools (\`/mcp\` in Claude Code, or reload the window in Cursor/VS Code), wait, then carry on. \`npx ${RETICLE_NPM_PACKAGE} status\` works meanwhile and says whether the daemon is up and whether the app has connected.
|
|
55
|
+
- **Setup is not finished until one real flow has been driven and produced a verdict.** \`init\` exiting 0 means files were written; the tools appearing means your client can reach a daemon; a listed session means the app dialled in. None of those is evidence that anything works. If you are blocked before a verdict, say which step and what you tried — never report a clean install you did not verify.
|
|
44
56
|
- **If a tool result carries \`version_skew\`, act on it before trusting anything else.** Two pieces of Reticle are on different wire contracts, so tools can misbehave in ways neither side reports — a green here is not evidence. The field says which pair disagrees and names the exact fix: run \`${CLI} stop\` and retry when the DAEMON is stale, or tell the human to install the matching SDK and restart their dev server when the PAGE is. Do that first, then redo whatever you were verifying.
|
|
45
57
|
- **If a tool result carries \`update_available\`, say so.** Reticle checked npm and a newer version exists. Tell the human the version numbers and offer to run \`${CLI} update\` — between tasks, never mid-verification, because it restarts the daemon and drops the session. Finish what you are doing first.
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
|
|
59
|
+
## Sending feedback to the Reticle team (first-class)
|
|
60
|
+
|
|
61
|
+
**Reporting your experience with Reticle is part of using Reticle, from the moment you start wiring it in — not something to do at the end if anything went wrong.** Reticle is built FOR agents, which makes you its actual user and the only one who can see what it costs you. That knowledge is gone when your context is, so it has to leave the turn it happened in.
|
|
62
|
+
|
|
63
|
+
- **Send it with \`reticle_feedback\`, the moment you notice.** One call, then carry straight on with your task. 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.
|
|
64
|
+
- **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 or has an awkward shape (\`improvement\`).
|
|
65
|
+
- **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 usually go unreported.
|
|
66
|
+
- **When the tools are not reachable yet**, file the same report from the shell: \`${CLI} feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "what happened"\`. It needs no daemon and no working install, so a Reticle that broke before it started can still be reported.
|
|
67
|
+
- **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.
|
|
68
|
+
- **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.
|
|
69
|
+
- **Report defects in *Reticle*, not in the app under test.** A bug you find in this app is Reticle working as intended; that belongs in your report to the user.
|
|
70
|
+
- **Never include app source, secrets, user data, or customer records** in a report.`;
|
|
48
71
|
export const AgentRuleStatus = {
|
|
49
72
|
APPLY: 'apply',
|
|
50
73
|
ALREADY: 'already',
|
|
@@ -96,7 +119,7 @@ function refreshMarkedBlock(existing) {
|
|
|
96
119
|
/** The Cursor rule file (.mdc): `alwaysApply` keeps the rule in every turn's context. */
|
|
97
120
|
export function cursorRuleFile() {
|
|
98
121
|
return `---
|
|
99
|
-
description: Verify web features with Reticle after building them
|
|
122
|
+
description: Verify web features with Reticle after building them, and report Reticle problems back to its maintainers
|
|
100
123
|
alwaysApply: true
|
|
101
124
|
---
|
|
102
125
|
|
package/dist/init/cra.d.ts
CHANGED
|
@@ -29,6 +29,19 @@ export declare const TOKEN_VAR = "REACT_APP_RETICLE_TOKEN";
|
|
|
29
29
|
* withReticle); CRA has no such hook without ejecting, so this message is the fix.
|
|
30
30
|
*/
|
|
31
31
|
export declare const CRA_TOKEN_MISSING_NOTE: string;
|
|
32
|
+
/**
|
|
33
|
+
* Said as its own NOTICE line, beside the write — not inside it.
|
|
34
|
+
*
|
|
35
|
+
* The caveat has always been in the write step's `detail`, and the write step renders `[✓]`. But
|
|
36
|
+
* `SKILL.md` tells whoever reads the report: *"If every line is `✓`, `·` or `–`, skip to Step 4 and
|
|
37
|
+
* validate."* So the one fact that makes this install conditional sat in the one place the reading
|
|
38
|
+
* protocol says to ignore — which is exactly how it was reported to us as "4 OK marks and no
|
|
39
|
+
* warning". The words were on screen; the reader was following instructions.
|
|
40
|
+
*
|
|
41
|
+
* It cannot simply be promoted: `run.ts` only writes steps whose status is APPLY, so demoting the
|
|
42
|
+
* token step to NOTICE would stop it writing the token and break the install outright.
|
|
43
|
+
*/
|
|
44
|
+
export declare const CRA_TOKEN_PER_MACHINE_NOTICE: string;
|
|
32
45
|
/** Add the import after the last existing one, or null when it is already present. */
|
|
33
46
|
export declare function craImportPatch(source: string): string | null;
|
|
34
47
|
/** Set the token variable, or null when nothing needs to change. */
|
package/dist/init/cra.js
CHANGED
|
@@ -31,6 +31,22 @@ export const TOKEN_VAR = 'REACT_APP_RETICLE_TOKEN';
|
|
|
31
31
|
export const CRA_TOKEN_MISSING_NOTE = `${TOKEN_VAR} is not set, so Reticle cannot pair with the daemon. ` +
|
|
32
32
|
`The pairing token is per-machine and ${CRA_ENV_PATH} is gitignored by CRA's template, ` +
|
|
33
33
|
'so it does not survive a clone. Run `npx reticle init` in this project to write it for this machine.';
|
|
34
|
+
/**
|
|
35
|
+
* Said as its own NOTICE line, beside the write — not inside it.
|
|
36
|
+
*
|
|
37
|
+
* The caveat has always been in the write step's `detail`, and the write step renders `[✓]`. But
|
|
38
|
+
* `SKILL.md` tells whoever reads the report: *"If every line is `✓`, `·` or `–`, skip to Step 4 and
|
|
39
|
+
* validate."* So the one fact that makes this install conditional sat in the one place the reading
|
|
40
|
+
* protocol says to ignore — which is exactly how it was reported to us as "4 OK marks and no
|
|
41
|
+
* warning". The words were on screen; the reader was following instructions.
|
|
42
|
+
*
|
|
43
|
+
* It cannot simply be promoted: `run.ts` only writes steps whose status is APPLY, so demoting the
|
|
44
|
+
* token step to NOTICE would stop it writing the token and break the install outright.
|
|
45
|
+
*/
|
|
46
|
+
export const CRA_TOKEN_PER_MACHINE_NOTICE = `${CRA_ENV_PATH} is in CRA's own .gitignore, so the token just written does not travel. ` +
|
|
47
|
+
'It works on THIS machine and nowhere else: a teammate cloning the repo, CI, or a container ' +
|
|
48
|
+
'gets an app that boots, never pairs, and reports it only in the browser console. ' +
|
|
49
|
+
'Each developer runs `npx reticle init` once locally.';
|
|
34
50
|
/** Add the import after the last existing one, or null when it is already present. */
|
|
35
51
|
export function craImportPatch(source) {
|
|
36
52
|
if (source.includes(CRA_DEV_MODULE_IMPORT))
|
package/dist/init/detect.d.ts
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const Framework: {
|
|
6
6
|
readonly NEXT: "next";
|
|
7
|
+
/**
|
|
8
|
+
* Nuxt owns its own Vite instance and has no `vite.config`, so it used to fall all the way through
|
|
9
|
+
* to HTML — and then be handed a React kit and a connect snippet guarded on
|
|
10
|
+
* `window.location.hostname === 'localhost'`, which cannot work in a Vue app (the guard throws
|
|
11
|
+
* during SSR, and never runs at all on a non-localhost dev host). Detected in its own right so it
|
|
12
|
+
* gets the package and the recipe that actually fit it.
|
|
13
|
+
*/
|
|
14
|
+
readonly NUXT: "nuxt";
|
|
7
15
|
readonly VITE: "vite";
|
|
8
16
|
readonly SVELTEKIT: "sveltekit";
|
|
9
17
|
readonly ASTRO: "astro";
|
|
@@ -62,6 +70,12 @@ export interface Detection {
|
|
|
62
70
|
}
|
|
63
71
|
/** Extract the leading major version from a semver range like "^19.0.0" or "19.1.1". */
|
|
64
72
|
export declare function parseMajor(range: string | undefined): number | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Whether an installed tree identifies the manager that built it. Callers use this to decide whether
|
|
75
|
+
* they still need weaker evidence — see `resolveLockfiles`, which stops inheriting an ancestor
|
|
76
|
+
* lockfile once the project's own tree can answer the question.
|
|
77
|
+
*/
|
|
78
|
+
export declare function namesAPackageManager(markers: ReadonlySet<string>): boolean;
|
|
65
79
|
export declare function detectPackageManager(lockfiles: ReadonlySet<string>, nodeModulesMarkers: ReadonlySet<string>): PackageManager;
|
|
66
80
|
export declare function detect(input: DetectInput): Detection;
|
|
67
81
|
interface InstallCommand {
|
package/dist/init/detect.js
CHANGED
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const Framework = {
|
|
6
6
|
NEXT: 'next',
|
|
7
|
+
/**
|
|
8
|
+
* Nuxt owns its own Vite instance and has no `vite.config`, so it used to fall all the way through
|
|
9
|
+
* to HTML — and then be handed a React kit and a connect snippet guarded on
|
|
10
|
+
* `window.location.hostname === 'localhost'`, which cannot work in a Vue app (the guard throws
|
|
11
|
+
* during SSR, and never runs at all on a non-localhost dev host). Detected in its own right so it
|
|
12
|
+
* gets the package and the recipe that actually fit it.
|
|
13
|
+
*/
|
|
14
|
+
NUXT: 'nuxt',
|
|
7
15
|
VITE: 'vite',
|
|
8
16
|
SVELTEKIT: 'sveltekit',
|
|
9
17
|
ASTRO: 'astro',
|
|
@@ -32,6 +40,7 @@ export const UiLibrary = {
|
|
|
32
40
|
const NEXT_CONFIGS = ['next.config.js', 'next.config.mjs', 'next.config.ts', 'next.config.cjs'];
|
|
33
41
|
const VITE_CONFIGS = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs', 'vite.config.mts'];
|
|
34
42
|
const SVELTE_CONFIGS = ['svelte.config.js', 'svelte.config.ts', 'svelte.config.mjs'];
|
|
43
|
+
const NUXT_CONFIGS = ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs'];
|
|
35
44
|
const ASTRO_CONFIGS = [
|
|
36
45
|
'astro.config.mjs',
|
|
37
46
|
'astro.config.js',
|
|
@@ -75,6 +84,14 @@ function packageManagerFromNodeModules(markers) {
|
|
|
75
84
|
return pm;
|
|
76
85
|
return undefined;
|
|
77
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Whether an installed tree identifies the manager that built it. Callers use this to decide whether
|
|
89
|
+
* they still need weaker evidence — see `resolveLockfiles`, which stops inheriting an ancestor
|
|
90
|
+
* lockfile once the project's own tree can answer the question.
|
|
91
|
+
*/
|
|
92
|
+
export function namesAPackageManager(markers) {
|
|
93
|
+
return packageManagerFromNodeModules(markers) !== undefined;
|
|
94
|
+
}
|
|
78
95
|
export function detectPackageManager(lockfiles, nodeModulesMarkers) {
|
|
79
96
|
if (lockfiles.has('pnpm-lock.yaml'))
|
|
80
97
|
return PackageManager.PNPM;
|
|
@@ -90,6 +107,12 @@ function detectFramework(input) {
|
|
|
90
107
|
if (depVersion(pkg, 'next') !== undefined || hasAnyConfig(configFiles, NEXT_CONFIGS)) {
|
|
91
108
|
return Framework.NEXT;
|
|
92
109
|
}
|
|
110
|
+
// Nuxt before Vite: Nuxt apps can carry a `vite` dependency and even a vite.config, but the
|
|
111
|
+
// generic Vite path would wire a plugin into a config Nuxt does not read, and inject connect into
|
|
112
|
+
// an index.html Nuxt does not serve.
|
|
113
|
+
if (depVersion(pkg, 'nuxt') !== undefined || hasAnyConfig(configFiles, NUXT_CONFIGS)) {
|
|
114
|
+
return Framework.NUXT;
|
|
115
|
+
}
|
|
93
116
|
// SvelteKit is Vite-based but renders through app.html, so the Vite plugin's index.html injection
|
|
94
117
|
// never fires (verified) — it needs a manual client connect. Check BEFORE the generic Vite branch.
|
|
95
118
|
if (depVersion(pkg, '@sveltejs/kit') !== undefined || hasAnyConfig(configFiles, SVELTE_CONFIGS)) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
export declare const McpClient: {
|
|
2
|
+
readonly CLAUDE_CODE: "claude-code";
|
|
3
|
+
readonly CURSOR: "cursor";
|
|
4
|
+
readonly WINDSURF: "windsurf";
|
|
5
|
+
readonly OPENCODE: "opencode";
|
|
6
|
+
readonly CODEX: "codex";
|
|
7
|
+
readonly GEMINI: "gemini";
|
|
8
|
+
readonly VSCODE: "vscode";
|
|
9
|
+
};
|
|
10
|
+
export type McpClient = (typeof McpClient)[keyof typeof McpClient];
|
|
11
|
+
/** Where the config lives. `cli` means the client owns registration and we shell out to it. */
|
|
12
|
+
export declare const ConfigScope: {
|
|
13
|
+
readonly HOME: "home";
|
|
14
|
+
readonly PROJECT: "project";
|
|
15
|
+
readonly CLI: "cli";
|
|
16
|
+
};
|
|
17
|
+
export type ConfigScope = (typeof ConfigScope)[keyof typeof ConfigScope];
|
|
18
|
+
/** How the file is written. `toml` is deliberately never auto-merged — see mergeClientConfig. */
|
|
19
|
+
export declare const ConfigFormat: {
|
|
20
|
+
readonly JSON: "json";
|
|
21
|
+
readonly TOML: "toml";
|
|
22
|
+
readonly CLI: "cli";
|
|
23
|
+
};
|
|
24
|
+
export type ConfigFormat = (typeof ConfigFormat)[keyof typeof ConfigFormat];
|
|
25
|
+
export interface ClientSpec {
|
|
26
|
+
id: McpClient;
|
|
27
|
+
label: string;
|
|
28
|
+
scope: ConfigScope;
|
|
29
|
+
/** Relative to `$HOME` for `home`, to the project root for `project`. Empty for `cli`. */
|
|
30
|
+
relPath: string;
|
|
31
|
+
format: ConfigFormat;
|
|
32
|
+
/** The object key holding the server map — `mcpServers`, `servers`, or `mcp`. */
|
|
33
|
+
serversKey: string;
|
|
34
|
+
/** Our entry, in THIS client's shape. */
|
|
35
|
+
entry: () => unknown;
|
|
36
|
+
/** Where the shape came from, so the next person can re-check it rather than trust this file. */
|
|
37
|
+
docs: string;
|
|
38
|
+
}
|
|
39
|
+
export declare const MCP_CLIENTS: readonly ClientSpec[];
|
|
40
|
+
export declare function clientSpec(id: McpClient): ClientSpec;
|
|
41
|
+
export declare const ClientMergeStatus: {
|
|
42
|
+
readonly APPLY: "apply";
|
|
43
|
+
readonly ALREADY: "already";
|
|
44
|
+
readonly MANUAL: "manual";
|
|
45
|
+
};
|
|
46
|
+
export type ClientMergeStatus = (typeof ClientMergeStatus)[keyof typeof ClientMergeStatus];
|
|
47
|
+
export interface ClientMergeResult {
|
|
48
|
+
status: ClientMergeStatus;
|
|
49
|
+
/** Full file content to write. Byte-identical to `existing` when not `apply`. */
|
|
50
|
+
content: string;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Merge our server into this client's config.
|
|
54
|
+
*
|
|
55
|
+
* TOML is never auto-merged. Editing TOML without a parser is how a config file gets corrupted, and
|
|
56
|
+
* a corrupted `~/.codex/config.toml` costs the user every server they had, not just ours. Printing
|
|
57
|
+
* the exact block is honest; writing a guess is not — so Codex returns MANUAL with a snippet.
|
|
58
|
+
*/
|
|
59
|
+
export declare function mergeClientConfig(spec: ClientSpec, existing: string | null): ClientMergeResult;
|
|
60
|
+
/**
|
|
61
|
+
* A block the user can paste, for a client we could not write.
|
|
62
|
+
*
|
|
63
|
+
* Every client gets one, including the ones we CAN write: when a merge comes back MANUAL because the
|
|
64
|
+
* file could not be parsed, the snippet is the only actionable thing left to give.
|
|
65
|
+
*/
|
|
66
|
+
export declare function clientSnippet(spec: ClientSpec): string;
|
|
67
|
+
/**
|
|
68
|
+
* The path whose existence means "this user has this client".
|
|
69
|
+
*
|
|
70
|
+
* Detection is deliberately conservative and one-directional: we write into a config a client
|
|
71
|
+
* ALREADY has, and never create `~/.gemini/` or `~/.codeium/` for somebody who does not use them.
|
|
72
|
+
* Littering a home directory to register a server nobody asked for is worse than printing a snippet.
|
|
73
|
+
*
|
|
74
|
+
* For a `home` client this is the directory above its config; for a `project` client it is the
|
|
75
|
+
* config file (or its directory) inside the repo, which is unambiguous evidence somebody uses it
|
|
76
|
+
* here. Same reasoning as Cursor's existing rule, which also accepts a project-level `.cursor/`
|
|
77
|
+
* because a fresh Cursor profile has not written `~/.cursor` yet.
|
|
78
|
+
*/
|
|
79
|
+
export declare function clientMarkerRelPath(spec: ClientSpec): string;
|
|
80
|
+
/** Clients `init` can wire by writing a file — everything but the CLI-registered one. */
|
|
81
|
+
export declare function fileBackedClients(): readonly ClientSpec[];
|