@reticlehq/server 2.7.0 → 2.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/README.md +14 -5
- package/SKILL.md +64 -29
- package/dist/capsule/causal-summary.d.ts +20 -0
- package/dist/capsule/causal-summary.js +31 -6
- package/dist/capsule/divergence.js +7 -1
- package/dist/cli/chromium-hint.d.ts +33 -5
- package/dist/cli/chromium-hint.js +82 -12
- package/dist/cli/cli-doctor.js +28 -25
- package/dist/cli/cli-kill.d.ts +58 -0
- package/dist/cli/cli-kill.js +162 -0
- package/dist/cli/cli-launch.d.ts +17 -0
- package/dist/cli/cli-launch.js +28 -0
- package/dist/cli/cli-parse.d.ts +16 -1
- package/dist/cli/cli-parse.js +32 -9
- package/dist/cli/cli-port.d.ts +8 -0
- package/dist/cli/cli-port.js +16 -0
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/doctor-sessions-line.d.ts +12 -0
- package/dist/cli/doctor-sessions-line.js +66 -0
- package/dist/cli/port-holder.d.ts +10 -0
- package/dist/cli/port-holder.js +26 -2
- package/dist/cli.js +99 -14
- package/dist/crawl/crawl-tools.js +12 -2
- package/dist/crawl/crawl.d.ts +13 -0
- package/dist/crawl/crawl.js +50 -2
- package/dist/events/contradictions.d.ts +14 -0
- package/dist/events/contradictions.js +26 -17
- package/dist/events/predicate-eval.d.ts +20 -1
- package/dist/events/predicate-eval.js +125 -3
- package/dist/events/predicate.js +170 -18
- package/dist/flows/flow-classify.js +1 -1
- package/dist/flows/flow-tools.js +12 -4
- package/dist/flows/heal-run.js +1 -1
- package/dist/honesty/blind-spots.js +6 -2
- package/dist/honesty/unsettled.d.ts +40 -0
- package/dist/honesty/unsettled.js +78 -0
- package/dist/honesty/verified.d.ts +7 -0
- package/dist/honesty/verified.js +3 -3
- package/dist/index.js +3 -0
- package/dist/init/agent-rules.d.ts +16 -0
- package/dist/init/agent-rules.js +96 -22
- package/dist/init/astro-patch.d.ts +2 -1
- package/dist/init/astro-patch.js +11 -5
- package/dist/init/capabilities.d.ts +22 -0
- package/dist/init/capabilities.js +76 -0
- package/dist/init/confirm.d.ts +96 -0
- package/dist/init/confirm.js +138 -0
- package/dist/init/cra.js +3 -2
- package/dist/init/non-js-project.js +29 -5
- package/dist/init/plan-framework.js +53 -15
- package/dist/init/plan.d.ts +40 -4
- package/dist/init/plan.js +163 -40
- package/dist/init/run.d.ts +24 -2
- package/dist/init/run.js +148 -42
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +46 -6
- package/dist/init/snippets.d.ts +44 -4
- package/dist/init/snippets.js +161 -37
- package/dist/init/vite-config.js +29 -3
- package/dist/input/network-mock-tools.d.ts +19 -0
- package/dist/input/network-mock-tools.js +29 -4
- package/dist/license/license.js +1 -1
- package/dist/mcp/mcp-proxy.d.ts +69 -3
- package/dist/mcp/mcp-proxy.js +205 -20
- package/dist/mcp/mcp.js +23 -7
- package/dist/mcp/proxy-lifecycle.d.ts +1 -1
- package/dist/mcp/proxy-lifecycle.js +1 -1
- package/dist/pool/browser-pool.d.ts +22 -2
- package/dist/pool/browser-pool.js +44 -4
- package/dist/project/learned-routes.d.ts +24 -0
- package/dist/project/learned-routes.js +63 -0
- package/dist/project/project-store.d.ts +6 -0
- package/dist/project/project-store.js +25 -1
- package/dist/session/no-session-diagnosis.d.ts +19 -0
- package/dist/session/no-session-diagnosis.js +71 -18
- package/dist/session/no-session-watch.js +12 -0
- package/dist/session/ref-provenance.d.ts +54 -0
- package/dist/session/ref-provenance.js +61 -0
- package/dist/session/session-lease.js +1 -1
- package/dist/telemetry/automation-hint.d.ts +17 -0
- package/dist/telemetry/automation-hint.js +71 -0
- package/dist/telemetry/bug-found.d.ts +0 -20
- package/dist/telemetry/bug-found.js +38 -21
- package/dist/telemetry/cli-telemetry.js +7 -1
- package/dist/telemetry/feedback.js +1 -1
- package/dist/telemetry/init-telemetry.js +9 -1
- package/dist/telemetry/install-source.d.ts +36 -0
- package/dist/telemetry/install-source.js +40 -0
- package/dist/telemetry/run-telemetry.js +4 -1
- package/dist/telemetry/session-metrics.js +5 -0
- package/dist/telemetry/telemetry.d.ts +5 -1
- package/dist/telemetry/telemetry.js +15 -2
- package/dist/telemetry/tool-refused.d.ts +19 -0
- package/dist/telemetry/tool-refused.js +55 -0
- package/dist/telemetry/verification-of.js +46 -7
- package/dist/temp-dir.d.ts +27 -0
- package/dist/temp-dir.js +46 -0
- package/dist/tools/act-tools.js +45 -1
- package/dist/tools/assert-grade.d.ts +1 -1
- package/dist/tools/assert-grade.js +27 -5
- package/dist/tools/assert-verdict.js +18 -1
- package/dist/tools/contract-tools.js +4 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.d.ts +10 -0
- package/dist/tools/error-recovery.js +64 -4
- package/dist/tools/feedback-invite.js +1 -1
- package/dist/tools/invoke-tool.js +139 -23
- package/dist/tools/lease-tools.d.ts +29 -1
- package/dist/tools/lease-tools.js +58 -5
- package/dist/tools/navigate-result.js +1 -1
- package/dist/tools/observe-tools.js +1 -1
- package/dist/tools/observed-nothing.js +1 -1
- package/dist/tools/react-grace.d.ts +48 -0
- package/dist/tools/react-grace.js +87 -0
- package/dist/tools/read-tools.js +20 -4
- package/dist/tools/settle-in-flight.d.ts +79 -0
- package/dist/tools/settle-in-flight.js +106 -0
- package/dist/tools/snapshot-delta.d.ts +7 -0
- package/dist/tools/snapshot-delta.js +130 -6
- package/dist/tools/tool-surface.js +19 -0
- package/dist/tools/tools.js +16 -4
- package/docs/README.md +56 -11
- package/docs/actions.mdx +87 -0
- package/docs/agent-cheatsheet.md +93 -29
- package/docs/architecture.md +46 -35
- package/docs/benchmarks.md +76 -72
- package/docs/best-practices.mdx +211 -0
- package/docs/capabilities.mdx +387 -0
- package/docs/cli/affected.mdx +51 -0
- package/docs/cli/capsules.mdx +69 -0
- package/docs/cli/cloud.mdx +127 -0
- package/docs/cli/doctor.mdx +76 -0
- package/docs/cli/drive.mdx +55 -0
- package/docs/cli/feedback.mdx +67 -0
- package/docs/cli/gate.mdx +64 -0
- package/docs/cli/help.mdx +92 -0
- package/docs/cli/hunt.mdx +57 -0
- package/docs/cli/identify.mdx +62 -0
- package/docs/cli/init.mdx +94 -0
- package/docs/cli/kill.mdx +91 -0
- package/docs/cli/license.mdx +48 -0
- package/docs/cli/mcp.mdx +61 -0
- package/docs/cli/open.mdx +68 -0
- package/docs/cli/restart.mdx +60 -0
- package/docs/cli/rollback.mdx +35 -0
- package/docs/cli/serve.mdx +62 -0
- package/docs/cli/status.mdx +64 -0
- package/docs/cli/stop.mdx +56 -0
- package/docs/cli/telemetry.mdx +57 -0
- package/docs/cli/update.mdx +52 -0
- package/docs/cli/verify.mdx +57 -0
- package/docs/cli/version.mdx +39 -0
- package/docs/cli/watch.mdx +55 -0
- package/docs/cli.mdx +219 -0
- package/docs/debugging.md +13 -9
- package/docs/deploy-checks.md +15 -11
- package/docs/desktop-apps.md +49 -40
- package/docs/desktop.mdx +188 -0
- package/docs/docs.json +363 -0
- package/docs/enterprise.md +20 -16
- package/docs/faq.mdx +88 -0
- package/docs/fixtures.md +11 -7
- package/docs/flows.md +26 -22
- package/docs/for-agents.md +71 -0
- package/docs/frameworks.mdx +199 -0
- package/docs/gate-plan.md +45 -41
- package/docs/gates.md +22 -18
- package/docs/getting-started.md +76 -65
- package/docs/human-control.md +22 -18
- package/docs/index.mdx +101 -0
- package/docs/install-agentic.mdx +215 -0
- package/docs/install-manual.mdx +194 -0
- package/docs/instrumentation.mdx +232 -0
- package/docs/integration-patterns.md +38 -34
- package/docs/local-registry.md +16 -12
- package/docs/multi-agent-testing.md +16 -12
- package/docs/packages/babel-plugin.mdx +70 -0
- package/docs/packages/browser.mdx +111 -0
- package/docs/packages/core.mdx +106 -0
- package/docs/packages/electron.mdx +87 -0
- package/docs/packages/eslint-plugin.mdx +84 -0
- package/docs/packages/next.mdx +91 -0
- package/docs/packages/react.mdx +91 -0
- package/docs/packages/server.mdx +98 -0
- package/docs/packages/tauri.mdx +63 -0
- package/docs/packages/test.mdx +108 -0
- package/docs/packages/vite-plugin.mdx +96 -0
- package/docs/packages.mdx +217 -0
- package/docs/platform-integration.md +33 -26
- package/docs/predicates.mdx +179 -0
- package/docs/quickstart.mdx +211 -0
- package/docs/recipes.mdx +242 -0
- package/docs/skill-file.mdx +105 -0
- package/docs/state-management.mdx +197 -0
- package/docs/style.css +14 -0
- package/docs/system-map.md +47 -32
- package/docs/telemetry-contract.md +93 -50
- package/docs/telemetry-events.mdx +430 -0
- package/docs/telemetry.md +57 -52
- package/docs/testing.md +10 -6
- package/docs/token-efficiency.md +30 -26
- package/docs/tools-act-and-wait.mdx +123 -0
- package/docs/tools-act-sequence.mdx +120 -0
- package/docs/tools-act.mdx +82 -0
- package/docs/tools-assert.mdx +70 -0
- package/docs/tools-console.mdx +59 -0
- package/docs/tools-inspect.mdx +85 -0
- package/docs/tools-navigate.mdx +63 -0
- package/docs/tools-network.mdx +86 -0
- package/docs/tools-observe.mdx +89 -0
- package/docs/tools-overview.mdx +157 -0
- package/docs/tools-query.mdx +82 -0
- package/docs/tools-session-and-feedback.mdx +91 -0
- package/docs/tools-sessions.mdx +65 -0
- package/docs/tools-snapshot.mdx +75 -0
- package/docs/tools-state.mdx +81 -0
- package/docs/tools-tools-and-run.mdx +125 -0
- package/docs/tools-wait-for.mdx +66 -0
- package/docs/troubleshooting.mdx +193 -0
- package/docs/usage.md +175 -153
- package/docs/vs-chrome-devtools-mcp.mdx +82 -0
- package/docs/vs-playwright-mcp.mdx +85 -0
- package/docs/vs-screenshots.mdx +96 -0
- package/docs/what-is-reticle.mdx +63 -0
- package/docs/why-reticle.mdx +107 -0
- package/package.json +2 -2
- package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
- package/docs/matrix/2.5.0/codex-machine.json +0 -20
- package/docs/matrix/2.5.0/cursor-machine.json +0 -22
- package/docs/matrix/2.5.0/gemini-machine.json +0 -22
- package/docs/matrix/2.5.0/opencode-machine.json +0 -22
- package/docs/matrix/2.5.0/vscode-machine.json +0 -22
- package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
- package/docs/matrix/MATRIX.md +0 -26
- package/docs/matrix/README.md +0 -59
- package/docs/mint.json +0 -61
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ import { RecordingStore } from './flows/recordings.js';
|
|
|
14
14
|
import { FlowStore } from './flows/flows.js';
|
|
15
15
|
import { buildFlowChips } from './flows/flow-scope.js';
|
|
16
16
|
import { ProjectStore } from './project/project-store.js';
|
|
17
|
+
import { attachRouteLearning } from './project/learned-routes.js';
|
|
17
18
|
import { AnnotationStore } from './flows/annotation-store.js';
|
|
18
19
|
import { createNodeFileSystem } from './project/fs-port.js';
|
|
19
20
|
import { cleanupCaptureDirectories } from './visual/capture-cleanup.js';
|
|
@@ -253,6 +254,7 @@ export async function start(options = {}) {
|
|
|
253
254
|
attachJournal(bridge, { fs, reticleRoot, enabled: journalEnabled });
|
|
254
255
|
const flows = new FlowStore(fs, reticleRoot, { now });
|
|
255
256
|
const project = new ProjectStore(fs, reticleRoot, { now });
|
|
257
|
+
attachRouteLearning(bridge, project);
|
|
256
258
|
const annotations = new AnnotationStore();
|
|
257
259
|
pool = createBrowserPool(options.headless ?? true);
|
|
258
260
|
leaseReaper = new LeaseReaper(pool);
|
|
@@ -345,6 +347,7 @@ export async function startDaemon(options = {}) {
|
|
|
345
347
|
attachJournal(bridge, { fs, reticleRoot, enabled: journalEnabled });
|
|
346
348
|
const flows = new FlowStore(fs, reticleRoot, { now });
|
|
347
349
|
const project = new ProjectStore(fs, reticleRoot, { now });
|
|
350
|
+
attachRouteLearning(bridge, project);
|
|
348
351
|
const annotations = new AnnotationStore();
|
|
349
352
|
const pool = createBrowserPool(options.headless ?? true);
|
|
350
353
|
const leaseReaper = new LeaseReaper(pool);
|
|
@@ -20,6 +20,20 @@
|
|
|
20
20
|
/** Project-root instruction files the agent re-reads every session. */
|
|
21
21
|
export declare const CLAUDE_MD_PATH = "CLAUDE.md";
|
|
22
22
|
export declare const AGENTS_MD_PATH = "AGENTS.md";
|
|
23
|
+
/**
|
|
24
|
+
* The full rules, written once and pointed at from every always-loaded file.
|
|
25
|
+
*
|
|
26
|
+
* Two problems it solves at the same time. The rule text is dense and most of it is REFERENCE (what
|
|
27
|
+
* to do when the tools are missing, what a `version_skew` field means, how to file feedback), which
|
|
28
|
+
* an agent needs on the turn it hits that situation and never before — so carrying all of it in
|
|
29
|
+
* `CLAUDE.md` taxed every session for text almost every session did not read. And `AGENTS.md` was
|
|
30
|
+
* only written when NO agent was detected, so a repo set up on a Claude Code machine told Codex,
|
|
31
|
+
* Copilot, Amp and every other agent nothing at all.
|
|
32
|
+
*
|
|
33
|
+
* So: the operative rules (when to verify, WHEN NOT TO, how, and the honesty guards) go in the files
|
|
34
|
+
* that load every turn, and this file holds the rest, one hop away and named in that block.
|
|
35
|
+
*/
|
|
36
|
+
export declare const RETICLE_MD_PATH = "RETICLE.md";
|
|
23
37
|
/** Cursor project rule; the frontmatter `alwaysApply` keeps it in context on every turn. */
|
|
24
38
|
export declare const CURSOR_RULE_PATH = ".cursor/rules/reticle.mdc";
|
|
25
39
|
/**
|
|
@@ -33,6 +47,8 @@ export declare const CURSOR_RULE_PATH = ".cursor/rules/reticle.mdc";
|
|
|
33
47
|
* the package's own name so the two can never drift apart.
|
|
34
48
|
*/
|
|
35
49
|
export declare const CLI: string;
|
|
50
|
+
/** The full-rules file, written verbatim (no markers: `init` owns this file entirely). */
|
|
51
|
+
export declare function reticleMdFile(): string;
|
|
36
52
|
export declare const AgentRuleStatus: {
|
|
37
53
|
readonly APPLY: "apply";
|
|
38
54
|
readonly ALREADY: "already";
|
package/dist/init/agent-rules.js
CHANGED
|
@@ -22,6 +22,20 @@ import { RETICLE_NPM_PACKAGE } from '../version/server-version.js';
|
|
|
22
22
|
/** Project-root instruction files the agent re-reads every session. */
|
|
23
23
|
export const CLAUDE_MD_PATH = 'CLAUDE.md';
|
|
24
24
|
export const AGENTS_MD_PATH = 'AGENTS.md';
|
|
25
|
+
/**
|
|
26
|
+
* The full rules, written once and pointed at from every always-loaded file.
|
|
27
|
+
*
|
|
28
|
+
* Two problems it solves at the same time. The rule text is dense and most of it is REFERENCE (what
|
|
29
|
+
* to do when the tools are missing, what a `version_skew` field means, how to file feedback), which
|
|
30
|
+
* an agent needs on the turn it hits that situation and never before — so carrying all of it in
|
|
31
|
+
* `CLAUDE.md` taxed every session for text almost every session did not read. And `AGENTS.md` was
|
|
32
|
+
* only written when NO agent was detected, so a repo set up on a Claude Code machine told Codex,
|
|
33
|
+
* Copilot, Amp and every other agent nothing at all.
|
|
34
|
+
*
|
|
35
|
+
* So: the operative rules (when to verify, WHEN NOT TO, how, and the honesty guards) go in the files
|
|
36
|
+
* that load every turn, and this file holds the rest, one hop away and named in that block.
|
|
37
|
+
*/
|
|
38
|
+
export const RETICLE_MD_PATH = 'RETICLE.md';
|
|
25
39
|
/** Cursor project rule; the frontmatter `alwaysApply` keeps it in context on every turn. */
|
|
26
40
|
export const CURSOR_RULE_PATH = '.cursor/rules/reticle.mdc';
|
|
27
41
|
/** Markers delimiting the managed block so a re-run is idempotent and the block stays updatable. */
|
|
@@ -38,36 +52,96 @@ const RULE_END = '<!-- reticle:end -->';
|
|
|
38
52
|
* the package's own name so the two can never drift apart.
|
|
39
53
|
*/
|
|
40
54
|
export const CLI = `${NPX} ${RETICLE_NPM_PACKAGE}`;
|
|
41
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* The operative rules, carried by every file that loads on every turn.
|
|
57
|
+
*
|
|
58
|
+
* Short on purpose. This is the text that has to CHANGE BEHAVIOUR, so it states when to verify, when
|
|
59
|
+
* NOT to, how, and the guards against calling something proven when it is not. Everything an agent
|
|
60
|
+
* needs only at the moment it hits a specific situation lives in RETICLE.md, named at the end.
|
|
61
|
+
*/
|
|
42
62
|
const RULE_BODY = `## Verifying with Reticle
|
|
43
63
|
|
|
44
|
-
This app is instrumented by **Reticle
|
|
64
|
+
This app is instrumented by **Reticle**, an in-app verification layer exposed as \`reticle_*\` MCP tools and the \`${CLI}\` CLI (always through npx: Reticle's server is not installed into this project). Verifying is part of "done", not an optional extra.
|
|
65
|
+
|
|
66
|
+
**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.
|
|
67
|
+
|
|
68
|
+
**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.
|
|
69
|
+
|
|
70
|
+
**How to verify:**
|
|
71
|
+
|
|
72
|
+
- 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.
|
|
73
|
+
- Batch a multi-step journey (a login, a form) into one \`reticle_act_sequence\` rather than one round trip per field.
|
|
74
|
+
- Read the surrounding evidence with \`reticle_snapshot\`, \`reticle_state\`, \`reticle_network\`, \`reticle_console\`.
|
|
75
|
+
- **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.
|
|
76
|
+
- Covered flows: \`${CLI} gate\` reports which recorded flows the changed files affect and whether they still pass.
|
|
77
|
+
|
|
78
|
+
**Honesty, which is the whole point:**
|
|
79
|
+
|
|
80
|
+
- **\`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.
|
|
81
|
+
- **Never weaken a check to make it green.** Downgrading, skipping or deleting an assertion is a finding, not a fix.
|
|
82
|
+
- **If Reticle cannot run** (no daemon, or this is not a running web app), say so. Do not skip verification silently.
|
|
83
|
+
- **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.
|
|
84
|
+
|
|
85
|
+
**Report Reticle's own defects with \`reticle_feedback\` the moment you notice**, then carry on with your task. You are the user Reticle is built for and the only one who can say what it cost you, and that knowledge is gone when your context is.
|
|
86
|
+
|
|
87
|
+
📄 **The rest is in [RETICLE.md](./${RETICLE_MD_PATH}): what to do when the tools are missing, when a result carries \`version_skew\` or \`update_available\`, when \`reticle_state\` comes back empty, and how to write a feedback report that can be acted on. Read it when you hit one of those, not before.**`;
|
|
88
|
+
/**
|
|
89
|
+
* The reference half. Fetched when a situation calls for it rather than carried every turn.
|
|
90
|
+
*
|
|
91
|
+
* Every section here answers a question an agent only has once it is already stuck, and each one was
|
|
92
|
+
* written because the absence of it cost somebody a session: tools that were never loaded read as a
|
|
93
|
+
* broken install, a `version_skew` field went unread while its green verdicts were trusted, an empty
|
|
94
|
+
* `reticle_state` was reported as a clean setup, and a defect in Reticle was worked around in silence.
|
|
95
|
+
*/
|
|
96
|
+
const RETICLE_MD_BODY = `# Reticle: the full rules
|
|
97
|
+
|
|
98
|
+
The short version lives in your agent instruction file (\`CLAUDE.md\` / \`AGENTS.md\` / \`.cursor/rules/reticle.mdc\`) and loads every turn. This file is the reference: read the section that matches the situation you are in.
|
|
99
|
+
|
|
100
|
+
## The tools are missing entirely
|
|
101
|
+
|
|
102
|
+
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).
|
|
103
|
+
|
|
104
|
+
Ask the human to RESTART their client: restart Claude Code, reload the window in Cursor, or hit Start in \`.vscode/mcp.json\` in VS Code. Then wait and carry on. This is once per machine, because Reticle is registered globally and later projects already have the tools. \`${CLI} status\` works meanwhile and says whether the daemon is up and whether the app has connected.
|
|
105
|
+
|
|
106
|
+
## A result carries \`verify_next\`
|
|
107
|
+
|
|
108
|
+
You have driven this page several times without asking for a verdict, so nothing you have done so far is proven. Do what the field says next.
|
|
109
|
+
|
|
110
|
+
## A result carries \`version_skew\`
|
|
111
|
+
|
|
112
|
+
Act on it before trusting anything else. Two pieces of Reticle are on different wire contracts, so tools can misbehave in ways neither side reports, and a green is not evidence. The field names the pair that disagrees and the exact fix: run \`${CLI} stop\` and retry when the DAEMON is stale, or tell the human to install the matching SDK and restart their dev server when the PAGE is. Then redo whatever you were verifying.
|
|
45
113
|
|
|
46
|
-
|
|
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.
|
|
51
|
-
- **Covered flows:** run \`${CLI} gate\` — it reports which recorded flows the changed files affect and whether they still pass.
|
|
52
|
-
- **Never weaken a check to make it green.** Downgrading, skipping, or deleting an assertion is a finding, not a fix.
|
|
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.
|
|
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.
|
|
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.
|
|
114
|
+
## A result carries \`update_available\`
|
|
58
115
|
|
|
59
|
-
|
|
116
|
+
Reticle checked npm and a newer version exists. Tell the human the version numbers and offer to run \`${CLI} update\`, between tasks and never mid-verification, because it restarts the daemon and drops the session. Finish what you are doing first.
|
|
60
117
|
|
|
61
|
-
|
|
118
|
+
## \`reticle_state\` comes back empty
|
|
62
119
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
120
|
+
The generated \`reticle-dev\` file registers nothing. Say so. \`init\` registers every store it could prove from your source; the ones needing an argument only reading the code supplies (Jotai atoms, an XState actor, a TanStack \`queryClient\`) are left commented, and a file that registers nothing is indistinguishable from a working one until you read the state back.
|
|
121
|
+
|
|
122
|
+
Finish that file when you see it. It is what lets you check what the app BELIEVES, not only what it rendered, which is the class of bug a screenshot cannot show. Never report a clean setup over an empty state read. It blocks nothing else: DOM, network, console and source pointers all work without it.
|
|
123
|
+
|
|
124
|
+
## Sending feedback to the Reticle team
|
|
125
|
+
|
|
126
|
+
**Reporting your experience is part of using Reticle, from the moment you start wiring it in.** Reticle is built FOR agents, which makes you its actual user and the only one who can see what it costs you.
|
|
127
|
+
|
|
128
|
+
- **Send it with \`reticle_feedback\` the moment you notice.** One call, then carry straight on. Do not batch reports, do not wait until the task is done, and do not decide something is too minor. The small awkward things are what nobody ever reports, so they never get fixed.
|
|
129
|
+
- **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\`).
|
|
130
|
+
- **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.
|
|
66
131
|
- **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
|
|
132
|
+
- **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
133
|
- **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
|
|
134
|
+
- **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.
|
|
135
|
+
- **Never include app source, secrets, user data, or customer records** in a report.
|
|
136
|
+
|
|
137
|
+
## More
|
|
138
|
+
|
|
139
|
+
The docs are built to be fetched rather than browsed: \`https://docs.reticle.sh/llms.txt\` lists every page, and appending \`.md\` to any page URL returns its source with no site chrome.
|
|
140
|
+
`;
|
|
141
|
+
/** The full-rules file, written verbatim (no markers: `init` owns this file entirely). */
|
|
142
|
+
export function reticleMdFile() {
|
|
143
|
+
return RETICLE_MD_BODY;
|
|
144
|
+
}
|
|
71
145
|
export const AgentRuleStatus = {
|
|
72
146
|
APPLY: 'apply',
|
|
73
147
|
ALREADY: 'already',
|
|
@@ -12,5 +12,6 @@
|
|
|
12
12
|
* Half-editing a build config is worse than a documented manual step.
|
|
13
13
|
*/
|
|
14
14
|
import { type SourcePatch } from './patch-kind.js';
|
|
15
|
+
import { UiLibrary } from './detect.js';
|
|
15
16
|
export declare function patchAstroConfig(source: string): SourcePatch;
|
|
16
|
-
export declare function patchAstroLayout(source: string, port: number | undefined, projectId: string | undefined): SourcePatch;
|
|
17
|
+
export declare function patchAstroLayout(source: string, port: number | undefined, projectId: string | undefined, uiLibrary?: UiLibrary): SourcePatch;
|
package/dist/init/astro-patch.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { RETICLE_DEFAULT_PORT, bridgeWsUrl } from '@reticlehq/core';
|
|
15
15
|
import { PatchKind } from './patch-kind.js';
|
|
16
|
+
import { UiLibrary } from './detect.js';
|
|
17
|
+
import { sdkImport } from './snippets.js';
|
|
16
18
|
/** Present in a patched config AND in a hand-followed recipe — so both count as already wired. */
|
|
17
19
|
const CONFIG_MARKER = '__RETICLE_TOKEN__';
|
|
18
20
|
/** Present in a patched layout and in the printed recipe's script. */
|
|
@@ -204,7 +206,11 @@ export function patchAstroConfig(source) {
|
|
|
204
206
|
};
|
|
205
207
|
}
|
|
206
208
|
/** The dev-only connect that goes inside the layout's `<body>`. */
|
|
207
|
-
function astroConnectScript(port, projectId) {
|
|
209
|
+
function astroConnectScript(port, projectId, uiLibrary) {
|
|
210
|
+
// Must match what `frameworkPackages` installed. Astro hosts islands from any framework, so an
|
|
211
|
+
// Astro app whose islands are Vue or Svelte gets the sensor, and a script importing the React
|
|
212
|
+
// adapter would load a package that is not in node_modules. See sdkImport.
|
|
213
|
+
const sdk = sdkImport(uiLibrary);
|
|
208
214
|
const url = port !== undefined && port !== RETICLE_DEFAULT_PORT
|
|
209
215
|
? `\n url: '${bridgeWsUrl(port)}',`
|
|
210
216
|
: '';
|
|
@@ -213,8 +219,8 @@ function astroConnectScript(port, projectId) {
|
|
|
213
219
|
if (import.meta.env.DEV) {
|
|
214
220
|
const token = typeof __RETICLE_TOKEN__ !== 'undefined' ? __RETICLE_TOKEN__ : '';
|
|
215
221
|
const root = typeof __RETICLE_ROOT__ !== 'undefined' ? __RETICLE_ROOT__ : '';
|
|
216
|
-
const { reticle, install } = await import('
|
|
217
|
-
install();
|
|
222
|
+
const { reticle${sdk.usesInstall ? ', install' : ''} } = await import('${sdk.specifier}');
|
|
223
|
+
${sdk.usesInstall ? 'install();' : '// The sensor has no install(); that is the React adapter.'}
|
|
218
224
|
reticle.connect({${id}${url}
|
|
219
225
|
...(token.length > 0 ? { token } : {}),
|
|
220
226
|
...(root.length > 0 ? { root } : {}),
|
|
@@ -223,7 +229,7 @@ function astroConnectScript(port, projectId) {
|
|
|
223
229
|
</script>
|
|
224
230
|
`;
|
|
225
231
|
}
|
|
226
|
-
export function patchAstroLayout(source, port, projectId) {
|
|
232
|
+
export function patchAstroLayout(source, port, projectId, uiLibrary = UiLibrary.REACT) {
|
|
227
233
|
if (source.includes(LAYOUT_MARKER))
|
|
228
234
|
return { kind: PatchKind.ALREADY };
|
|
229
235
|
const at = source.lastIndexOf(BODY_CLOSE);
|
|
@@ -235,6 +241,6 @@ export function patchAstroLayout(source, port, projectId) {
|
|
|
235
241
|
}
|
|
236
242
|
return {
|
|
237
243
|
kind: PatchKind.APPLY,
|
|
238
|
-
code: `${source.slice(0, at)}${astroConnectScript(port, projectId)}${source.slice(at)}`,
|
|
244
|
+
code: `${source.slice(0, at)}${astroConnectScript(port, projectId, uiLibrary)}${source.slice(at)}`,
|
|
239
245
|
};
|
|
240
246
|
}
|
|
@@ -15,3 +15,25 @@ export declare const MAX_TESTIDS = 60;
|
|
|
15
15
|
export declare function scanTestids(sources: readonly string[]): string[];
|
|
16
16
|
/** The store libraries this app depends on, as ready-to-uncomment registration lines. */
|
|
17
17
|
export declare function storeHints(deps: ReadonlySet<string>): string[];
|
|
18
|
+
/** A store instance we found in the app's own source, with everything needed to import and register it. */
|
|
19
|
+
export interface FoundStore {
|
|
20
|
+
/** The name it registers under — what `reticle_state` keys its output by. */
|
|
21
|
+
key: string;
|
|
22
|
+
/** The exported identifier. */
|
|
23
|
+
ident: string;
|
|
24
|
+
/** Module specifier relative to `src/reticle-dev.ts`. */
|
|
25
|
+
importPath: string;
|
|
26
|
+
}
|
|
27
|
+
/** Where the generated dev module lives, for Vite. Next passes its own. */
|
|
28
|
+
export declare const DEFAULT_DEV_MODULE_DIR = "src";
|
|
29
|
+
/**
|
|
30
|
+
* Store instances declared in the app's own source, ready to import and register.
|
|
31
|
+
*
|
|
32
|
+
* Only exported declarations of a factory the app actually depends on: anything less certain would
|
|
33
|
+
* generate an import that breaks the dev module everything else hangs off, and a broken module is
|
|
34
|
+
* worse than the commented hint it replaces.
|
|
35
|
+
*/
|
|
36
|
+
export declare function scanStores(files: readonly {
|
|
37
|
+
path: string;
|
|
38
|
+
source: string;
|
|
39
|
+
}[], deps: ReadonlySet<string>, fromDir?: string): FoundStore[];
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* depends on zustand is easy, knowing which module exports the store instance is not, and generating
|
|
10
10
|
* a wrong import would break the dev module that everything else now hangs off.
|
|
11
11
|
*/
|
|
12
|
+
import { posix } from 'node:path';
|
|
12
13
|
/** `data-testid="foo"` / `data-testid='foo'` / `data-testid={"foo"}` — the forms people actually write. */
|
|
13
14
|
const TESTID_ATTR = /data-testid\s*=\s*[{]?\s*["'`]([^"'`]+)["'`]/g;
|
|
14
15
|
/** Cap the generated list. A capabilities block is a hint for an agent, not an inventory. */
|
|
@@ -52,3 +53,78 @@ const STORE_LIBRARIES = [
|
|
|
52
53
|
export function storeHints(deps) {
|
|
53
54
|
return STORE_LIBRARIES.filter(([dep]) => deps.has(dep)).map(([, hint]) => hint);
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Store factories whose EXPORTED result is the store instance itself — the form `registerStore`
|
|
58
|
+
* takes directly. Each is gated on the app actually depending on that library, so a local helper
|
|
59
|
+
* called `create` in an app with no store library is never mistaken for one.
|
|
60
|
+
*
|
|
61
|
+
* Deliberately short. The adapter-wrapped libraries (Jotai, XState, Valtio, MobX, TanStack Query)
|
|
62
|
+
* need an argument we cannot infer — which atoms, which actor, which queryClient — so they keep the
|
|
63
|
+
* commented hint. Guessing there produces a module that throws on import, which is strictly worse
|
|
64
|
+
* than a comment.
|
|
65
|
+
*/
|
|
66
|
+
const STORE_FACTORIES = [
|
|
67
|
+
['zustand', 'create'],
|
|
68
|
+
['@reduxjs/toolkit', 'configureStore'],
|
|
69
|
+
['redux', 'createStore'],
|
|
70
|
+
];
|
|
71
|
+
/** `export const useCart = create<S>()(...)` / `export const store = configureStore({...})`. */
|
|
72
|
+
function storeDeclaration(factory) {
|
|
73
|
+
return new RegExp(`export\\s+const\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*${factory}\\s*[<(]`, 'g');
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `src/cart-store.ts` → `cart`; `src/store.ts` → `app`. The key is what the agent reads state under,
|
|
77
|
+
* so it has to be stable and distinct — two stores sharing one key would silently overwrite.
|
|
78
|
+
*/
|
|
79
|
+
function storeKey(path, taken) {
|
|
80
|
+
const file = (path.split('/').pop() ?? '').replace(/\.[^.]+$/, '');
|
|
81
|
+
const base = file.replace(/[-_.]?stores?$/i, '').replace(/[^a-z0-9]+/gi, '-') || 'app';
|
|
82
|
+
const key = base.toLowerCase();
|
|
83
|
+
if (!taken.has(key))
|
|
84
|
+
return key;
|
|
85
|
+
for (let n = 2;; n += 1) {
|
|
86
|
+
const candidate = `${key}-${String(n)}`;
|
|
87
|
+
if (!taken.has(candidate))
|
|
88
|
+
return candidate;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Module specifier for `store.ts` as seen FROM the generated dev module.
|
|
93
|
+
*
|
|
94
|
+
* The dev module is not always a sibling: Vite puts it at `src/reticle-dev.ts`, Next at
|
|
95
|
+
* `app/reticle-dev.tsx` or `src/app/reticle-dev.tsx`. A specifier computed for one is dead in the
|
|
96
|
+
* other, and a dead import in this file breaks the module every other capability hangs off — so the
|
|
97
|
+
* relative path is computed, not assumed.
|
|
98
|
+
*/
|
|
99
|
+
function importPathFor(storePath, fromDir) {
|
|
100
|
+
const noExt = storePath.replace(/\.[^./]+$/, '');
|
|
101
|
+
const rel = posix.relative(fromDir, noExt);
|
|
102
|
+
return rel.startsWith('.') ? rel : `./${rel}`;
|
|
103
|
+
}
|
|
104
|
+
/** Where the generated dev module lives, for Vite. Next passes its own. */
|
|
105
|
+
export const DEFAULT_DEV_MODULE_DIR = 'src';
|
|
106
|
+
/**
|
|
107
|
+
* Store instances declared in the app's own source, ready to import and register.
|
|
108
|
+
*
|
|
109
|
+
* Only exported declarations of a factory the app actually depends on: anything less certain would
|
|
110
|
+
* generate an import that breaks the dev module everything else hangs off, and a broken module is
|
|
111
|
+
* worse than the commented hint it replaces.
|
|
112
|
+
*/
|
|
113
|
+
export function scanStores(files, deps, fromDir = DEFAULT_DEV_MODULE_DIR) {
|
|
114
|
+
const factories = STORE_FACTORIES.filter(([dep]) => deps.has(dep)).map(([, f]) => f);
|
|
115
|
+
const found = [];
|
|
116
|
+
const keys = new Set();
|
|
117
|
+
for (const { path, source } of files) {
|
|
118
|
+
for (const factory of factories) {
|
|
119
|
+
for (const m of source.matchAll(storeDeclaration(factory))) {
|
|
120
|
+
const ident = m[1];
|
|
121
|
+
if (ident === undefined)
|
|
122
|
+
continue;
|
|
123
|
+
const key = storeKey(path, keys);
|
|
124
|
+
keys.add(key);
|
|
125
|
+
found.push({ key, ident, importPath: importPathFor(path, fromDir) });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return found;
|
|
130
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The step `init` never took: stay and watch for an app to connect.
|
|
3
|
+
*
|
|
4
|
+
* The install has two halves. Registering the MCP server gives the agent the tools; getting the SDK
|
|
5
|
+
* into a running page gives it something to look at. Almost everyone finishes the first, and nothing
|
|
6
|
+
* joined "init finished" to "an app connected" — so `init` exited 0 having written files, and the
|
|
7
|
+
* person left with a config file and no instrumented page was never told (#269).
|
|
8
|
+
*
|
|
9
|
+
* The capability existed in three places and was used together by none of them: the poll behind
|
|
10
|
+
* `reticle open`, the install gate (which calls this exact check "the only step that proves the
|
|
11
|
+
* install works"), and the event that measures whether it ever happened.
|
|
12
|
+
*
|
|
13
|
+
* WHY NO FLAG, and no dev-server management:
|
|
14
|
+
*
|
|
15
|
+
* - No flag, because the people this is for are the ones who do not know to ask. `--verify` would
|
|
16
|
+
* be typed by the users who already run `status` afterwards, which is exactly the cohort that is
|
|
17
|
+
* not being lost. The gate instead is whether a HUMAN IS AT THE TERMINAL: `init` waits when
|
|
18
|
+
* stdout is a TTY and skips the wait entirely otherwise, so a scripted or agent-driven `init`
|
|
19
|
+
* keeps today's cost and today's exit behaviour, and blocking never happens where blocking is
|
|
20
|
+
* wrong. The signal is the standard "am I being piped" one and needs no argument parsing, which
|
|
21
|
+
* also keeps `init`'s documented flag surface where it is.
|
|
22
|
+
* - It never starts anything. Reticle attaches to what is already running and does not manage a
|
|
23
|
+
* dev server; that rule holds here too. So the honest shape is: confirm the app if it connects,
|
|
24
|
+
* otherwise name the ONE command that is outstanding and the one that proves it worked.
|
|
25
|
+
*/
|
|
26
|
+
import { InitConfirmation } from '@reticlehq/core';
|
|
27
|
+
import type { InitOutcome } from '@reticlehq/core';
|
|
28
|
+
/**
|
|
29
|
+
* How long a human is asked to wait.
|
|
30
|
+
*
|
|
31
|
+
* Long enough for a page that is already loading to finish dialling, short enough that somebody who
|
|
32
|
+
* has not restarted their dev server yet is not held hostage by a wait that cannot succeed — they
|
|
33
|
+
* have not typed the command that would make it succeed, and the message tells them which one.
|
|
34
|
+
*/
|
|
35
|
+
export declare const CONFIRM_WINDOW_MS = 12000;
|
|
36
|
+
export declare const CONFIRM_POLL_MS = 500;
|
|
37
|
+
export interface ConfirmDeps {
|
|
38
|
+
/** Session ids connected right now, or null when nothing is listening on the bridge port. */
|
|
39
|
+
listSessionIds: () => Promise<readonly string[] | null>;
|
|
40
|
+
now: () => number;
|
|
41
|
+
sleep: (ms: number) => Promise<void>;
|
|
42
|
+
windowMs?: number;
|
|
43
|
+
pollMs?: number;
|
|
44
|
+
}
|
|
45
|
+
/** The narrow slice of the init IO this needs — it prints, and nothing else. */
|
|
46
|
+
interface PrintIo {
|
|
47
|
+
print: (line: string) => void;
|
|
48
|
+
}
|
|
49
|
+
/** What `runInit` hands back; `outcome` is present only on a real (non-dry) run. */
|
|
50
|
+
interface ConfirmableResult {
|
|
51
|
+
ok: boolean;
|
|
52
|
+
applied: number;
|
|
53
|
+
manual: number;
|
|
54
|
+
outcome?: InitOutcome;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Poll until an app connects or the window runs out.
|
|
58
|
+
*
|
|
59
|
+
* A session that was ALREADY there when the wait began does not count, and this is the whole reason
|
|
60
|
+
* the check reads ids rather than a count. A daemon is shared across every project on the machine,
|
|
61
|
+
* so "a session exists" is satisfied by another app, by a stray tab, by yesterday's work — and
|
|
62
|
+
* confirming this install on somebody else's session is a false green in the one message a user
|
|
63
|
+
* reads to decide whether they are done. `reticle open` refuses the same way, for the same reason.
|
|
64
|
+
*
|
|
65
|
+
* The conservative direction is deliberate: an app that reconnects under an id it already held is
|
|
66
|
+
* reported as not-yet-connected, and the message it gets sends them to `status`, which shows it. A
|
|
67
|
+
* missed confirmation costs one command; a false one costs the whole point of confirming.
|
|
68
|
+
*/
|
|
69
|
+
export declare function confirmAppConnected(deps: ConfirmDeps): Promise<InitConfirmation>;
|
|
70
|
+
/**
|
|
71
|
+
* What the user reads, in the marks the rest of the report already uses: `⚠` means work left to do
|
|
72
|
+
* and nothing else, `ℹ` is worth reading but is not work, `✓` happened.
|
|
73
|
+
*
|
|
74
|
+
* Only NO_SESSION earns a `⚠`, and it earns it honestly: the app is wired and has not connected, so
|
|
75
|
+
* there is a command outstanding. A missing daemon is not a defect in the install — the daemon comes
|
|
76
|
+
* up when the agent loads the tools — so it is a notice.
|
|
77
|
+
*/
|
|
78
|
+
export declare function confirmationMessage(confirmation: InitConfirmation, port: number): string;
|
|
79
|
+
/**
|
|
80
|
+
* Finish `init` on evidence rather than on a write.
|
|
81
|
+
*
|
|
82
|
+
* The telemetry report happens HERE rather than inside `runInit`, so the one `init_completed` event
|
|
83
|
+
* carries what was seen. Emitting inside `runInit` and again after the wait would double-count the
|
|
84
|
+
* funnel it exists to measure, and a second event kind for the same command would need joining back
|
|
85
|
+
* to the first before anybody could read either.
|
|
86
|
+
*/
|
|
87
|
+
export declare function confirmInstall(result: ConfirmableResult, io: PrintIo, deps: ConfirmDeps & {
|
|
88
|
+
interactive: boolean;
|
|
89
|
+
port: number;
|
|
90
|
+
}, report?: (outcome: InitOutcome) => void): Promise<void>;
|
|
91
|
+
/** The real deps: the daemon's own status endpoint, and whether a human is watching. */
|
|
92
|
+
export declare function nodeConfirmDeps(port?: number): ConfirmDeps & {
|
|
93
|
+
interactive: boolean;
|
|
94
|
+
port: number;
|
|
95
|
+
};
|
|
96
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The step `init` never took: stay and watch for an app to connect.
|
|
3
|
+
*
|
|
4
|
+
* The install has two halves. Registering the MCP server gives the agent the tools; getting the SDK
|
|
5
|
+
* into a running page gives it something to look at. Almost everyone finishes the first, and nothing
|
|
6
|
+
* joined "init finished" to "an app connected" — so `init` exited 0 having written files, and the
|
|
7
|
+
* person left with a config file and no instrumented page was never told (#269).
|
|
8
|
+
*
|
|
9
|
+
* The capability existed in three places and was used together by none of them: the poll behind
|
|
10
|
+
* `reticle open`, the install gate (which calls this exact check "the only step that proves the
|
|
11
|
+
* install works"), and the event that measures whether it ever happened.
|
|
12
|
+
*
|
|
13
|
+
* WHY NO FLAG, and no dev-server management:
|
|
14
|
+
*
|
|
15
|
+
* - No flag, because the people this is for are the ones who do not know to ask. `--verify` would
|
|
16
|
+
* be typed by the users who already run `status` afterwards, which is exactly the cohort that is
|
|
17
|
+
* not being lost. The gate instead is whether a HUMAN IS AT THE TERMINAL: `init` waits when
|
|
18
|
+
* stdout is a TTY and skips the wait entirely otherwise, so a scripted or agent-driven `init`
|
|
19
|
+
* keeps today's cost and today's exit behaviour, and blocking never happens where blocking is
|
|
20
|
+
* wrong. The signal is the standard "am I being piped" one and needs no argument parsing, which
|
|
21
|
+
* also keeps `init`'s documented flag surface where it is.
|
|
22
|
+
* - It never starts anything. Reticle attaches to what is already running and does not manage a
|
|
23
|
+
* dev server; that rule holds here too. So the honest shape is: confirm the app if it connects,
|
|
24
|
+
* otherwise name the ONE command that is outstanding and the one that proves it worked.
|
|
25
|
+
*/
|
|
26
|
+
import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
27
|
+
import { fetchStatus, summarizeStatus } from '../cli/cli-launch.js';
|
|
28
|
+
import { reportInitOutcome } from '../telemetry/init-telemetry.js';
|
|
29
|
+
/**
|
|
30
|
+
* How long a human is asked to wait.
|
|
31
|
+
*
|
|
32
|
+
* Long enough for a page that is already loading to finish dialling, short enough that somebody who
|
|
33
|
+
* has not restarted their dev server yet is not held hostage by a wait that cannot succeed — they
|
|
34
|
+
* have not typed the command that would make it succeed, and the message tells them which one.
|
|
35
|
+
*/
|
|
36
|
+
export const CONFIRM_WINDOW_MS = 12_000;
|
|
37
|
+
export const CONFIRM_POLL_MS = 500;
|
|
38
|
+
/**
|
|
39
|
+
* Poll until an app connects or the window runs out.
|
|
40
|
+
*
|
|
41
|
+
* A session that was ALREADY there when the wait began does not count, and this is the whole reason
|
|
42
|
+
* the check reads ids rather than a count. A daemon is shared across every project on the machine,
|
|
43
|
+
* so "a session exists" is satisfied by another app, by a stray tab, by yesterday's work — and
|
|
44
|
+
* confirming this install on somebody else's session is a false green in the one message a user
|
|
45
|
+
* reads to decide whether they are done. `reticle open` refuses the same way, for the same reason.
|
|
46
|
+
*
|
|
47
|
+
* The conservative direction is deliberate: an app that reconnects under an id it already held is
|
|
48
|
+
* reported as not-yet-connected, and the message it gets sends them to `status`, which shows it. A
|
|
49
|
+
* missed confirmation costs one command; a false one costs the whole point of confirming.
|
|
50
|
+
*/
|
|
51
|
+
export async function confirmAppConnected(deps) {
|
|
52
|
+
const deadline = deps.now() + (deps.windowMs ?? CONFIRM_WINDOW_MS);
|
|
53
|
+
let before;
|
|
54
|
+
for (;;) {
|
|
55
|
+
const ids = await deps.listSessionIds();
|
|
56
|
+
// A port nobody is listening on cannot receive a session, and no amount of waiting changes that.
|
|
57
|
+
// One look settles it; polling on would spend the whole window to re-learn the first answer.
|
|
58
|
+
if (null === ids)
|
|
59
|
+
return InitConfirmation.NO_DAEMON;
|
|
60
|
+
const baseline = before;
|
|
61
|
+
if (baseline === undefined)
|
|
62
|
+
before = new Set(ids);
|
|
63
|
+
else if (ids.some((id) => !baseline.has(id)))
|
|
64
|
+
return InitConfirmation.CONNECTED;
|
|
65
|
+
if (deadline <= deps.now())
|
|
66
|
+
return InitConfirmation.NO_SESSION;
|
|
67
|
+
await deps.sleep(deps.pollMs ?? CONFIRM_POLL_MS);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const PROVE_COMMAND = '`npx @reticlehq/server status`';
|
|
71
|
+
/**
|
|
72
|
+
* What the user reads, in the marks the rest of the report already uses: `⚠` means work left to do
|
|
73
|
+
* and nothing else, `ℹ` is worth reading but is not work, `✓` happened.
|
|
74
|
+
*
|
|
75
|
+
* Only NO_SESSION earns a `⚠`, and it earns it honestly: the app is wired and has not connected, so
|
|
76
|
+
* there is a command outstanding. A missing daemon is not a defect in the install — the daemon comes
|
|
77
|
+
* up when the agent loads the tools — so it is a notice.
|
|
78
|
+
*/
|
|
79
|
+
export function confirmationMessage(confirmation, port) {
|
|
80
|
+
if (confirmation === InitConfirmation.CONNECTED) {
|
|
81
|
+
return (' [✓] an app is connected — that is the install finished, not just written.\n' +
|
|
82
|
+
' Ask your agent to drive a flow.');
|
|
83
|
+
}
|
|
84
|
+
if (confirmation === InitConfirmation.NO_DAEMON) {
|
|
85
|
+
return (` [ℹ] nothing is listening on port ${String(port)} yet, so no app could have connected and ` +
|
|
86
|
+
'this run did not wait for one.\n' +
|
|
87
|
+
' The daemon starts on its own when your agent loads the Reticle tools. After that, ' +
|
|
88
|
+
`${PROVE_COMMAND} confirms the app.`);
|
|
89
|
+
}
|
|
90
|
+
return (' [⚠] no app has connected yet — the files are written, the page half of the install is not ' +
|
|
91
|
+
'done.\n' +
|
|
92
|
+
' Restart your dev server (Reticle attaches to it, it never starts it for you) and load ' +
|
|
93
|
+
'the app in a browser.\n' +
|
|
94
|
+
` ${PROVE_COMMAND} then confirms it, or says exactly why it has not connected.`);
|
|
95
|
+
}
|
|
96
|
+
const WATCHING = (windowMs) => ` watching for an app to connect (up to ${String(Math.round(windowMs / 1000))}s) — Ctrl-C is safe, ` +
|
|
97
|
+
'everything above is already written.';
|
|
98
|
+
/**
|
|
99
|
+
* Finish `init` on evidence rather than on a write.
|
|
100
|
+
*
|
|
101
|
+
* The telemetry report happens HERE rather than inside `runInit`, so the one `init_completed` event
|
|
102
|
+
* carries what was seen. Emitting inside `runInit` and again after the wait would double-count the
|
|
103
|
+
* funnel it exists to measure, and a second event kind for the same command would need joining back
|
|
104
|
+
* to the first before anybody could read either.
|
|
105
|
+
*/
|
|
106
|
+
export async function confirmInstall(result, io, deps, report = reportInitOutcome) {
|
|
107
|
+
const outcome = result.outcome;
|
|
108
|
+
// No outcome means a dry run or an exit that already reported for itself (no package.json). A
|
|
109
|
+
// preview is not an install and must not land in the funnel as either a success or a failure.
|
|
110
|
+
if (outcome === undefined)
|
|
111
|
+
return;
|
|
112
|
+
if (!deps.interactive) {
|
|
113
|
+
report(outcome);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
io.print('');
|
|
117
|
+
io.print(WATCHING(deps.windowMs ?? CONFIRM_WINDOW_MS));
|
|
118
|
+
const confirmation = await confirmAppConnected(deps);
|
|
119
|
+
io.print(confirmationMessage(confirmation, deps.port));
|
|
120
|
+
report({ ...outcome, confirmation });
|
|
121
|
+
}
|
|
122
|
+
/** The real deps: the daemon's own status endpoint, and whether a human is watching. */
|
|
123
|
+
export function nodeConfirmDeps(port = RETICLE_DEFAULT_PORT) {
|
|
124
|
+
return {
|
|
125
|
+
listSessionIds: async () => {
|
|
126
|
+
const payload = await fetchStatus(port);
|
|
127
|
+
// `fetchStatus` resolves undefined when nothing answered, which is precisely the "no daemon"
|
|
128
|
+
// case — a zero-session daemon and an absent one need opposite messages.
|
|
129
|
+
if (payload === undefined)
|
|
130
|
+
return null;
|
|
131
|
+
return summarizeStatus(payload).sessions.map((s) => s.sessionId);
|
|
132
|
+
},
|
|
133
|
+
now: () => Date.now(),
|
|
134
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
135
|
+
interactive: true === process.stdout.isTTY,
|
|
136
|
+
port,
|
|
137
|
+
};
|
|
138
|
+
}
|
package/dist/init/cra.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* `REACT_APP_`. So it travels through `.env.development.local`, which is CRA's own documented
|
|
13
13
|
* mechanism and is gitignored by CRA's own template.
|
|
14
14
|
*/
|
|
15
|
+
import { CLI } from './agent-rules.js';
|
|
15
16
|
/** The line added to `src/index.tsx`. Side-effect import: the module guards itself on NODE_ENV. */
|
|
16
17
|
export const CRA_DEV_MODULE_IMPORT = "import './reticle-dev';";
|
|
17
18
|
export const CRA_DEV_MODULE_PATH = 'src/reticle-dev.ts';
|
|
@@ -30,7 +31,7 @@ export const TOKEN_VAR = 'REACT_APP_RETICLE_TOKEN';
|
|
|
30
31
|
*/
|
|
31
32
|
export const CRA_TOKEN_MISSING_NOTE = `${TOKEN_VAR} is not set, so Reticle cannot pair with the daemon. ` +
|
|
32
33
|
`The pairing token is per-machine and ${CRA_ENV_PATH} is gitignored by CRA's template, ` +
|
|
33
|
-
|
|
34
|
+
`so it does not survive a clone. Run \`${CLI} init\` in this project to write it for this machine.`;
|
|
34
35
|
/**
|
|
35
36
|
* Said as its own NOTICE line, beside the write — not inside it.
|
|
36
37
|
*
|
|
@@ -46,7 +47,7 @@ export const CRA_TOKEN_MISSING_NOTE = `${TOKEN_VAR} is not set, so Reticle canno
|
|
|
46
47
|
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
48
|
'It works on THIS machine and nowhere else: a teammate cloning the repo, CI, or a container ' +
|
|
48
49
|
'gets an app that boots, never pairs, and reports it only in the browser console. ' +
|
|
49
|
-
|
|
50
|
+
`Each developer runs \`${CLI} init\` once locally.`;
|
|
50
51
|
/** Add the import after the last existing one, or null when it is already present. */
|
|
51
52
|
export function craImportPatch(source) {
|
|
52
53
|
if (source.includes(CRA_DEV_MODULE_IMPORT))
|