@reticlehq/server 2.10.0 → 2.12.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 +73 -11
- package/dist/bridge/bridge.d.ts +0 -13
- package/dist/bridge/bridge.js +1 -1
- package/dist/bridge/bridge.test-harness.d.ts +2 -0
- package/dist/bridge/bridge.test-harness.js +8 -1
- package/dist/capsule/capsule-store.d.ts +2 -1
- package/dist/capsule/capsule-store.js +1 -1
- package/dist/capsule/causal-summary.d.ts +2 -2
- package/dist/chromium-launch-options.d.ts +28 -0
- package/dist/chromium-launch-options.js +29 -0
- package/dist/cli/cli-doctor.js +8 -0
- package/dist/cli/cli-flow-commands.d.ts +3 -1
- package/dist/cli/cli-flow-commands.js +34 -4
- package/dist/cli/cli-kill.d.ts +2 -1
- package/dist/cli/cli-parse.d.ts +3 -1
- package/dist/cli/cli-parse.js +29 -7
- package/dist/cli/cli-update-commands.js +20 -1
- package/dist/cli/cli-verify.d.ts +1 -1
- package/dist/cli/cli-verify.js +2 -2
- package/dist/cli/cloud-cli.js +12 -6
- package/dist/cli/config-discovery.d.ts +2 -1
- package/dist/cli/doctor-rows.d.ts +3 -1
- package/dist/cli/doctor-rows.js +3 -1
- package/dist/cli/gate-exit.d.ts +21 -0
- package/dist/cli/gate-exit.js +21 -0
- package/dist/cli/gate-hook-message.d.ts +33 -0
- package/dist/cli/gate-hook-message.js +46 -0
- package/dist/cli/mcp-command.d.ts +25 -0
- package/dist/cli/mcp-command.js +143 -0
- package/dist/cli/sibling-ports.d.ts +37 -0
- package/dist/cli/sibling-ports.js +56 -0
- package/dist/cli.js +37 -110
- package/dist/cloud/cloud-config.d.ts +2 -1
- package/dist/cloud/cloud-sync.d.ts +11 -0
- package/dist/cloud/cloud-sync.js +32 -0
- package/dist/crawl/crawl.d.ts +9 -13
- package/dist/crawl/crawl.js +20 -3
- package/dist/daemon/daemon-resolve.d.ts +79 -0
- package/dist/daemon/daemon-resolve.js +118 -0
- package/dist/daemon/free-port.d.ts +29 -0
- package/dist/daemon/free-port.js +55 -0
- package/dist/daemon/heartbeat.d.ts +0 -4
- package/dist/daemon/heartbeat.js +2 -2
- package/dist/dev/stale-issue-guard.d.ts +41 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- package/dist/events/contradictions.d.ts +41 -0
- package/dist/events/contradictions.js +125 -12
- package/dist/events/declared.d.ts +14 -0
- package/dist/events/declared.js +33 -0
- package/dist/events/echo-mismatch.js +14 -0
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +17 -0
- package/dist/events/predicate-asks.d.ts +30 -0
- package/dist/events/predicate-asks.js +48 -0
- package/dist/events/predicate-eval.js +254 -25
- package/dist/events/predicate-parse.d.ts +0 -6
- package/dist/events/predicate-parse.js +51 -5
- package/dist/events/predicate-schema.d.ts +62 -0
- package/dist/events/predicate-schema.js +162 -4
- package/dist/events/predicate.d.ts +15 -0
- package/dist/events/predicate.js +41 -2
- package/dist/flows/coverage.d.ts +0 -8
- package/dist/flows/coverage.js +1 -1
- package/dist/flows/decision.d.ts +5 -1
- package/dist/flows/decision.js +33 -7
- package/dist/flows/flake.d.ts +0 -2
- package/dist/flows/flake.js +1 -1
- package/dist/flows/flow-intent.d.ts +52 -0
- package/dist/flows/flow-intent.js +123 -0
- package/dist/flows/flow-replay-run.js +27 -4
- package/dist/flows/flow-success.js +15 -3
- package/dist/flows/flow-tools.js +65 -8
- package/dist/flows/flows.d.ts +6 -1
- package/dist/flows/flows.js +34 -22
- package/dist/flows/predicate-to-expect.js +14 -3
- package/dist/flows/server-verify.js +16 -2
- package/dist/flows/verify-change-tools.js +9 -2
- package/dist/honesty/blind-spots.d.ts +8 -16
- package/dist/honesty/blind-spots.js +18 -5
- package/dist/honesty/feature-capture.d.ts +150 -0
- package/dist/honesty/feature-capture.js +233 -0
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +79 -0
- package/dist/honesty/instrumentation-gaps.js +77 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- package/dist/honesty/tool-hit-rate.d.ts +86 -0
- package/dist/honesty/tool-hit-rate.js +90 -0
- package/dist/honesty/undeclared-change.d.ts +51 -0
- package/dist/honesty/undeclared-change.js +56 -0
- package/dist/honesty/verified.d.ts +15 -0
- package/dist/honesty/verified.js +68 -4
- package/dist/http-server.d.ts +0 -1
- package/dist/http-server.js +1 -1
- package/dist/hunt/hunt-report.d.ts +0 -2
- package/dist/hunt/hunt-report.js +1 -1
- package/dist/impact/impact-store.d.ts +0 -5
- package/dist/impact/impact-store.js +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +41 -0
- package/dist/init/agent-rules.d.ts +17 -0
- package/dist/init/agent-rules.js +31 -1
- package/dist/init/capabilities.d.ts +0 -2
- package/dist/init/capabilities.js +1 -1
- package/dist/init/closing-hint.d.ts +36 -0
- package/dist/init/closing-hint.js +120 -0
- package/dist/init/confirm.d.ts +0 -25
- package/dist/init/confirm.js +3 -3
- package/dist/init/cra.d.ts +14 -1
- package/dist/init/cra.js +43 -12
- package/dist/init/dev-script.d.ts +48 -0
- package/dist/init/dev-script.js +63 -0
- package/dist/init/mcp.d.ts +16 -5
- package/dist/init/mcp.js +26 -10
- package/dist/init/node-io.js +3 -0
- package/dist/init/plan-framework.d.ts +0 -5
- package/dist/init/plan-framework.js +83 -9
- package/dist/init/plan.d.ts +10 -30
- package/dist/init/plan.js +30 -15
- package/dist/init/refresh-rules.d.ts +39 -0
- package/dist/init/refresh-rules.js +51 -0
- package/dist/init/run.d.ts +2 -10
- package/dist/init/run.js +26 -68
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- package/dist/init/snippets.js +53 -10
- package/dist/input/real-input.d.ts +2 -0
- package/dist/input/real-input.js +3 -2
- package/dist/input/scroll-find.d.ts +7 -4
- package/dist/input/scroll-find.js +63 -25
- package/dist/intent/inline-intent.d.ts +46 -0
- package/dist/intent/inline-intent.js +103 -0
- package/dist/intent/intent-store.d.ts +37 -0
- package/dist/intent/intent-store.js +112 -0
- package/dist/intent/intent-tools.d.ts +2 -0
- package/dist/intent/intent-tools.js +80 -0
- package/dist/intent/open-intents.d.ts +21 -0
- package/dist/intent/open-intents.js +25 -0
- package/dist/journal/envelope.d.ts +2 -2
- package/dist/journal/journal-recorder.d.ts +23 -0
- package/dist/journal/journal-recorder.js +35 -0
- package/dist/license/license.d.ts +8 -2
- package/dist/license/license.js +10 -15
- package/dist/mcp/attach-memory.d.ts +67 -0
- package/dist/mcp/attach-memory.js +142 -0
- package/dist/mcp/mcp-proxy.d.ts +0 -7
- package/dist/mcp/mcp-proxy.js +14 -2
- package/dist/mcp/mcp.d.ts +2 -0
- package/dist/mcp/mcp.js +45 -23
- package/dist/mcp/server-instructions.js +45 -3
- package/dist/mcp/tool-catalog-cache.d.ts +8 -0
- package/dist/mcp/tool-catalog-cache.js +10 -0
- package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
- package/dist/oracles/flow-instrument-gaps.js +4 -5
- package/dist/oracles/self-instrument.d.ts +9 -9
- package/dist/oracles/self-instrument.js +19 -13
- package/dist/pool/browser-pool.d.ts +12 -1
- package/dist/pool/browser-pool.js +31 -1
- package/dist/pool/playwright-launcher.js +3 -1
- package/dist/project/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/fs-port.d.ts +3 -0
- package/dist/project/fs-port.js +3 -2
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +93 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/project-tools.js +3 -32
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/reticle-dir.d.ts +2 -0
- package/dist/project/reticle-dir.js +1 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -0
- package/dist/runs/context-tools.d.ts +2 -0
- package/dist/runs/context-tools.js +89 -0
- package/dist/runs/run-context.d.ts +40 -0
- package/dist/runs/run-context.js +158 -0
- package/dist/runs/run-diff.d.ts +2 -1
- package/dist/runs/run-tools.js +2 -1
- package/dist/runs/verification-sync.js +2 -2
- package/dist/session/dev-server-probe.d.ts +0 -8
- package/dist/session/dev-server-probe.js +1 -1
- package/dist/session/last-act.d.ts +7 -1
- package/dist/session/last-act.js +2 -2
- package/dist/session/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/live-control-tools.js +1 -42
- package/dist/session/no-session-diagnosis.d.ts +20 -0
- package/dist/session/no-session-diagnosis.js +41 -12
- package/dist/session/no-session-next-action.d.ts +5 -0
- package/dist/session/no-session-next-action.js +35 -3
- package/dist/session/no-session-watch.d.ts +8 -0
- package/dist/session/no-session-watch.js +83 -65
- package/dist/session/output-budget.js +4 -1
- package/dist/session/session-health.d.ts +9 -0
- package/dist/session/session-health.js +23 -0
- package/dist/session/session-manager.js +50 -1
- package/dist/session/session-successor.d.ts +37 -0
- package/dist/session/session-successor.js +78 -0
- package/dist/session/session.d.ts +40 -1
- package/dist/session/session.js +84 -2
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/automation-hint.d.ts +2 -1
- package/dist/telemetry/daemon-telemetry.js +12 -23
- package/dist/telemetry/feedback-outbox.d.ts +2 -1
- package/dist/telemetry/project-profile.d.ts +0 -8
- package/dist/telemetry/project-profile.js +1 -1
- package/dist/telemetry/run-telemetry.d.ts +0 -2
- package/dist/telemetry/run-telemetry.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +1 -1
- package/dist/telemetry/session-metrics.js +1 -1
- package/dist/telemetry/telemetry.d.ts +12 -3
- package/dist/telemetry/telemetry.js +25 -5
- package/dist/telemetry/verification-of.js +2 -2
- package/dist/tools/act-tools.js +159 -24
- package/dist/tools/assert-source.d.ts +22 -0
- package/dist/tools/assert-source.js +53 -0
- package/dist/tools/assert-verdict.d.ts +21 -2
- package/dist/tools/assert-verdict.js +59 -4
- package/dist/tools/browser-tools.js +0 -20
- package/dist/tools/complete-snapshot.d.ts +48 -0
- package/dist/tools/complete-snapshot.js +75 -0
- package/dist/tools/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.d.ts +10 -1
- package/dist/tools/coverage-tools.js +124 -37
- package/dist/tools/dynamic-tools.js +29 -8
- package/dist/tools/error-recovery.d.ts +24 -14
- package/dist/tools/error-recovery.js +70 -9
- package/dist/tools/feedback-tools.d.ts +1 -1
- package/dist/tools/feedback-tools.js +2 -2
- package/dist/tools/invoke-tool.js +62 -2
- package/dist/tools/lease-hint.d.ts +10 -0
- package/dist/tools/lease-hint.js +53 -4
- package/dist/tools/lease-tools.d.ts +5 -1
- package/dist/tools/lease-tools.js +22 -8
- package/dist/tools/navigate-arrival.d.ts +0 -2
- package/dist/tools/navigate-arrival.js +1 -1
- package/dist/tools/observe-tools.js +70 -27
- package/dist/tools/read-tools.js +5 -3
- package/dist/tools/real-input-attempt.d.ts +22 -0
- package/dist/tools/real-input-attempt.js +153 -1
- package/dist/tools/reconcile-tools.js +18 -4
- package/dist/tools/resolve-target.d.ts +0 -7
- package/dist/tools/sdk-marker-probe.d.ts +29 -0
- package/dist/tools/sdk-marker-probe.js +57 -0
- package/dist/tools/snapshot-delta.d.ts +1 -1
- package/dist/tools/tool-kit.d.ts +28 -1
- package/dist/tools/tool-kit.js +16 -0
- package/dist/tools/tool-names.d.ts +3 -0
- package/dist/tools/tool-names.js +3 -0
- package/dist/tools/tool-surface.d.ts +85 -6
- package/dist/tools/tool-surface.js +122 -2
- package/dist/tools/tools.js +22 -4
- package/dist/tools/unadvertised-help.js +4 -4
- package/dist/tools/verify-next-baton.d.ts +53 -0
- package/dist/tools/verify-next-baton.js +52 -0
- package/dist/update/update-nudge.d.ts +7 -10
- package/dist/update/update-nudge.js +12 -3
- package/dist/update/updater.d.ts +1 -1
- package/docs/README.md +3 -1
- package/docs/cli/cloud.mdx +1 -1
- package/docs/cli/doctor.mdx +1 -0
- package/docs/cli/verify.mdx +4 -5
- package/docs/desktop.mdx +1 -1
- package/docs/docs.json +3 -3
- package/docs/enterprise.md +3 -1
- package/docs/gates.md +1 -1
- package/docs/install-manual.mdx +1 -1
- package/docs/license-activation.mdx +145 -0
- package/docs/packages/browser.mdx +2 -2
- package/docs/packages/server.mdx +1 -1
- package/docs/packages.mdx +8 -6
- package/docs/platform-integration.md +17 -0
- package/docs/predicates.mdx +8 -0
- package/docs/reticle-manual.md +106 -0
- package/docs/telemetry-contract.md +1 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- package/docs/tools-tools-and-run.mdx +2 -4
- package/docs/troubleshooting.mdx +9 -0
- package/docs/usage.md +3 -2
- package/package.json +2 -2
- package/dist/session/session-readiness.d.ts +0 -34
- package/dist/session/session-readiness.js +0 -36
- package/dist/telemetry/instrumentation-stall.d.ts +0 -57
- package/dist/telemetry/instrumentation-stall.js +0 -103
package/SKILL.md
CHANGED
|
@@ -14,6 +14,18 @@ This file is the whole critical path and nothing else. Everything it leaves out
|
|
|
14
14
|
|
|
15
15
|
**Do not tell the user Reticle is set up until step 5 has produced a verdict.** The most common failure in the field is an agent that finishes step 1, reports success, and leaves a user with config files and no instrumented page.
|
|
16
16
|
|
|
17
|
+
## Setup runs to the end without you
|
|
18
|
+
|
|
19
|
+
**Do not stop, and do not ask the user anything, until their app is running with Reticle inside it and a session is connected.** Everything between here and that point is yours to do: run `init`, fix the wiring it could not, start the dev server, open the browser. Not one of those needs a human, and every one of them has been the place an onboarding turn died with nothing to show.
|
|
20
|
+
|
|
21
|
+
Questions you must not ask, because the repo you are sitting in already answers them: which framework, which package manager, which port, which editor or MCP client, whether to start the dev server, whether to open the browser, whether to carry on. Decide, act, and say what you did in one line.
|
|
22
|
+
|
|
23
|
+
There are exactly three places you may stop, and none is a question about a preference:
|
|
24
|
+
|
|
25
|
+
1. **No recognisable dev script in `package.json`.** Say so; do not invent one.
|
|
26
|
+
2. **Your host asks the human to approve a command.** That prompt belongs to the host. Never bypass or suppress it, and take a refusal as the answer.
|
|
27
|
+
3. **On a first install only, the client restart that loads the MCP tools** (step 2). Even then, keep going until the app is running and connected, and stop for the restart last, with only the drive left.
|
|
28
|
+
|
|
17
29
|
Setup requires a client restart, which ends your turn. This file was fetched into the context that just ended, so nothing carries it across unless you make it. **Before you trigger the restart, write the remaining steps into `RETICLE-SETUP.md` at the repo root**, one line each, with the step you stopped at marked. That file is what you re-read afterwards. Delete it once step 5 has produced a verdict.
|
|
18
30
|
|
|
19
31
|
Better still, install the on-disk copy first. It survives the restart on its own, and it is the same content:
|
|
@@ -80,25 +92,29 @@ RETICLE_INSTALL_SOURCE=skill_file npx @reticlehq/server@latest init
|
|
|
80
92
|
|
|
81
93
|
It detects the framework, package manager and UI library, registers the MCP server with the agents on the machine, installs the SDK, writes `.reticle.json` and the `/reticle` command, wires the build config, and scaffolds a capabilities file from the `data-testid` values it found.
|
|
82
94
|
|
|
83
|
-
**Ask nothing:** not the framework, package manager, port, editor, or MCP client. Every one is answerable from the repo you are sitting in. In a monorepo run it at the root anyway
|
|
95
|
+
**Ask nothing:** not the framework, package manager, port, editor, or MCP client. Every one is answerable from the repo you are sitting in. In a monorepo run it at the root anyway. With one app under `apps/*` or `packages/*` it wires that app silently. With several it wires nothing, lists them, and names the flag: re-run with `--app <dir>` for the one the user is working in, the one their request named, or the one whose `package.json` has the dev script. **Pick it yourself and re-run.** Asking which app they meant is the most likely place this whole install stops, and the answer is almost always in the request you were already given.
|
|
84
96
|
|
|
85
97
|
**Never ask about the port.** There are two, and conflating them is a top setup failure. The dev-server port (3000, 5173) belongs to the project's own dev script and the daemon never binds it. The bridge port (**4400**) is the daemon-to-SDK channel and defaults correctly.
|
|
86
98
|
|
|
87
99
|
Read the report: `✓` applied, `·` already wired, `–` skipped, `ℹ` done but incomplete in a way that matters, `⚠` needs your edit. **`⚠` and `ℹ` both need you.** `ℹ` is the one people skim past: the step ran, and something about the result still stops a session appearing. Each line carries the exact snippet. A non-zero exit is a to-do list, not a failed install. Fix every `⚠` before moving on. Per-framework wiring is at `https://docs.reticle.sh/install-manual.md`.
|
|
88
100
|
|
|
89
|
-
|
|
101
|
+
**If the user gave you a license key**, along with the install request or at any later point, see [License key](#license-key) below and do it before moving on.
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
## 2. Check for the tools. Do not stop for them.
|
|
92
104
|
|
|
93
|
-
|
|
105
|
+
Call `reticle_sessions`. If the tool exists, go to step 3 and never mention a restart.
|
|
94
106
|
|
|
95
|
-
|
|
107
|
+
If it does not: your client read its server list at startup and has not re-read it. No retry, config edit or slash command loads it into the process you are already in. `init` registers globally once per machine, so this bites on the first install only.
|
|
96
108
|
|
|
97
|
-
|
|
109
|
+
**This is not a reason to stop.** Steps 3 and 4 need no MCP tools at all: they are a build config, a dev server and a browser tab, and every one of them is yours to do from the shell. An install that halts here leaves the user with config files, an uninstrumented page, and a question to answer; that is the single most common failure in the field, and it is this paragraph's fault when it happens.
|
|
110
|
+
|
|
111
|
+
So carry straight on through steps 3 and 4. Ask for the restart **once, at the end**, when a session is connected and the only thing left is the drive:
|
|
112
|
+
|
|
113
|
+
> "Reticle is installed and your app is connected. Restart your client so it picks up the MCP server, then say **'continue Reticle setup'**. One step is left: driving a flow."
|
|
98
114
|
|
|
99
115
|
Claude Code: restart (`/mcp` does not re-read the config). VS Code: press Start in `.vscode/mcp.json`. Cursor, Windsurf, Zed: reload the window.
|
|
100
116
|
|
|
101
|
-
**Do not report setup as finished
|
|
117
|
+
**Do not report setup as finished at that point either.** When the tools return, resume at step 5.
|
|
102
118
|
|
|
103
119
|
## 3. Wire the SDK into the app, and start the dev server.
|
|
104
120
|
|
|
@@ -132,7 +148,17 @@ Five guards, none optional:
|
|
|
132
148
|
4. **Background it, and say so.** A dev server the human does not know about is the same failure one step later.
|
|
133
149
|
5. **The permission prompt belongs to your host.** Never bypass, suppress or auto-approve it, and take a refusal as the answer.
|
|
134
150
|
|
|
135
|
-
Then ask the user to open
|
|
151
|
+
Then open the app yourself. Do not ask the user to do it. A setup turn that ends on "now open your browser" ends with nothing verified:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npx @reticlehq/server open <the url the dev server is serving>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
That reuses an already-connected tab or opens a new one, and waits for the page to register. On a headless machine with no browser to open, take a tab Reticle owns instead, once the tools are reachable:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
reticle_run({ tool: "reticle_lease", args: { action: "acquire", url: "<the same url>" } })
|
|
161
|
+
```
|
|
136
162
|
|
|
137
163
|
## 4. Prove a page is connected. This is a gate.
|
|
138
164
|
|
|
@@ -159,7 +185,7 @@ Drive it in as few calls as you can. Every call is a full model turn, and in a c
|
|
|
159
185
|
3. `reticle_act_and_wait({ ref, action, until })` for the final step only. This is the call that produces the verdict, and `until` names the expected consequence before the action fires.
|
|
160
186
|
4. `reticle_state()` once at the end.
|
|
161
187
|
|
|
162
|
-
Four calls for a login, not fourteen. If `reticle_state` comes back empty or `hasCapabilities` is false, the generated capabilities file
|
|
188
|
+
Four calls for a login, not fourteen. If `reticle_state` comes back empty or `hasCapabilities` is false, the capabilities file `init` generated registered nothing, which is what its `ℹ AGENT: finish the capabilities file` line was telling you. **Finish it yourself, before you drive**: open `src/reticle-dev.*`, register the app's store if it has one, and list the `testids` the flow you picked actually touches. Do not offer, and do not ask; it is a few lines in a file `init` already wrote for you. Never report a clean install over an empty state read.
|
|
163
189
|
|
|
164
190
|
**Then save what you just drove.** Two calls, and they are what make every future check a single call instead of a repeat of this one. Wrap the drive above in a recording:
|
|
165
191
|
|
|
@@ -190,6 +216,18 @@ Stop here. Do not continue into VERIFY.
|
|
|
190
216
|
|
|
191
217
|
---
|
|
192
218
|
|
|
219
|
+
# License key
|
|
220
|
+
|
|
221
|
+
Enterprise features are unlocked by a signed key, verified locally with Ed25519 and **no network call**. If the user hands you one, during setup or at any later point:
|
|
222
|
+
|
|
223
|
+
1. Append `RETICLE_LICENSE_KEY=<key>` to the project's `.env`, creating the file if it is not there. The CLI folds a project-local `.env` into the environment on every invocation, so nothing else has to change.
|
|
224
|
+
2. Make sure `.env` is in `.gitignore`. A license key in git is a leaked credential, so check before you write and add the line if it is missing.
|
|
225
|
+
3. Confirm with `npx @reticlehq/server license`, which prints `active`, `eval`, `missing` or `expired`.
|
|
226
|
+
|
|
227
|
+
Never echo the key back in your reply, and never put it in a commit, a code comment, or a feedback report. The rest: `curl https://docs.reticle.sh/enterprise.md`.
|
|
228
|
+
|
|
229
|
+
---
|
|
230
|
+
|
|
193
231
|
# VERIFY
|
|
194
232
|
|
|
195
233
|
**Only `reticle_act_and_wait` and `reticle_assert` produce a verdict.** Everything else (`act`, `snapshot`, `query`, `navigate`, `observe`, `network`, `console`) moves or reads the app and proves nothing. A drive that ends without one of those two has no result, however many tools it used.
|
|
@@ -211,6 +249,30 @@ Work down this list and stop at the first row that fits. Do not hand-drive a flo
|
|
|
211
249
|
|
|
212
250
|
`reticle_verify {action:"change"}` answers `unknown` when no saved flow covers the files you changed. That is the honest answer and not a failure. Nothing ran, so nothing was proved. It is also the signal to record one. Never read it as a pass.
|
|
213
251
|
|
|
252
|
+
## Two more you have to be told about
|
|
253
|
+
|
|
254
|
+
Same story as `reticle_verify`: extended surface, so they are not in the tool list you were handed, and reached through `reticle_run`.
|
|
255
|
+
|
|
256
|
+
**Context compacted, a turn starting, or a sub-agent taking over?** Ask what this run already established, instead of re-snapshotting to rediscover what you already knew:
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
reticle_run({ tool: "reticle_context", args: {} })
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**About to change something?** Declare what the change is SUPPOSED to make true, in prose, while you still know. A verdict with nothing declared can only be checked against itself:
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
reticle_run({ tool: "reticle_intent", args: { action: "declare", intents: [{ id: "checkin", statement: "clicking Send check-in makes the badge read 'checked in'" }] } })
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Or say it on the verdict itself and skip the round trip. `reticle_act_and_wait` and `reticle_assert` both take an optional `intent`, writing the same ledger:
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
reticle_act_and_wait({ ref: "e42", action: "click", until: { kind: "signal", name: "checkin:sent" }, intent: "clicking Send check-in makes the badge read 'checked in'" })
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The verdict that passes is the one that proves it. Already declared it? Pass the intent's **id** there instead of the prose, and several verdicts can answer to one statement.
|
|
275
|
+
|
|
214
276
|
## Record once, replay cheaply
|
|
215
277
|
|
|
216
278
|
The first drive of a journey is expensive. The rest should not be. After you drive something worth keeping:
|
|
@@ -232,7 +294,7 @@ Five calls, and the last one is the only one that counts:
|
|
|
232
294
|
|
|
233
295
|
```
|
|
234
296
|
reticle_sessions() // connected? if empty, read `why` — it names the fix
|
|
235
|
-
|
|
297
|
+
reticle_run({ tool: "reticle_capabilities", args: { sessionId } }) // the app's whole testable surface, ~1 KB
|
|
236
298
|
reticle_snapshot({ sessionId, mode: "interactive" }) // just the controls, with refs
|
|
237
299
|
reticle_act_and_wait({ sessionId, ref, action: "click", until: { kind: "allOf", predicates: [
|
|
238
300
|
{ kind: "net", urlContains: "/api/...", status: 200 },
|
|
@@ -247,7 +309,7 @@ A verdict of `verified: "unknown"` is not a pass. It means Reticle drove the app
|
|
|
247
309
|
|
|
248
310
|
Then report what you drove, what it produced, and the `file:line` for anything broken.
|
|
249
311
|
|
|
250
|
-
The surface is deliberately small: `default` 18, `all`
|
|
312
|
+
The surface is deliberately small: `default` 18, `all` 30. Editors budget tools across every MCP server you have connected (Cursor allows 40 in total), so the count is capped rather than allowed to grow. `reticle_tools` loads the argument grammar for the rest on demand, and `reticle_run` invokes any of them by name. Nothing is unreachable; the cold tail just costs one discovery hop.
|
|
251
313
|
|
|
252
314
|
- Batching, regression suites, reading a verdict: `https://docs.reticle.sh/agent-cheatsheet.md`
|
|
253
315
|
- Every predicate and action: `https://docs.reticle.sh/predicates.md`, `https://docs.reticle.sh/actions.md`
|
package/dist/bridge/bridge.d.ts
CHANGED
|
@@ -26,19 +26,6 @@ export declare const WS_CLOSE_REASON: {
|
|
|
26
26
|
*/
|
|
27
27
|
readonly HANDSHAKE_POOL_FULL: string;
|
|
28
28
|
};
|
|
29
|
-
/**
|
|
30
|
-
* A dial turned away at the ORIGIN gate, in words the reader can act on.
|
|
31
|
-
*
|
|
32
|
-
* This is the refusal that used to leave no trace anywhere. It happens before any HELLO, so it
|
|
33
|
-
* produced no `noteClosure`, no `session_disconnected`, nothing but an `origin_rejected` log line —
|
|
34
|
-
* and the daemon log is the file nobody opens until they have already lost the afternoon. Reported
|
|
35
|
-
* from a multi-tenant app resolving its tenant from the `Host` header: on a hosts-file alias every
|
|
36
|
-
* dial was refused here, `doctor` reported every check green, and the lease hint blamed the port.
|
|
37
|
-
*
|
|
38
|
-
* The origin is IN the message because it is the fix: it is either the alias to allow-list, or the
|
|
39
|
-
* proof that the page is not being served from where the developer thinks it is.
|
|
40
|
-
*/
|
|
41
|
-
export declare function originRejectedReason(origin: string | undefined): string;
|
|
42
29
|
interface BridgeOptions {
|
|
43
30
|
port: number;
|
|
44
31
|
host?: string;
|
package/dist/bridge/bridge.js
CHANGED
|
@@ -65,7 +65,7 @@ export const WS_CLOSE_REASON = {
|
|
|
65
65
|
* The origin is IN the message because it is the fix: it is either the alias to allow-list, or the
|
|
66
66
|
* proof that the page is not being served from where the developer thinks it is.
|
|
67
67
|
*/
|
|
68
|
-
|
|
68
|
+
function originRejectedReason(origin) {
|
|
69
69
|
const named = origin === undefined || 0 === origin.length ? 'a page sending no Origin' : origin;
|
|
70
70
|
return (`a browser dialled this daemon and was REFUSED at the origin gate: ${named} is not allowed. ` +
|
|
71
71
|
'Off localhost the SDK needs BOTH `allowNonLocalhost: true` AND a pairing token, and the ' +
|
|
@@ -16,6 +16,8 @@ export declare class FakeBrowser {
|
|
|
16
16
|
file: string;
|
|
17
17
|
line: number;
|
|
18
18
|
} | undefined;
|
|
19
|
+
/** DOM mutations the browser counted inside the acted element; undefined = it counted none. */
|
|
20
|
+
actMutatedWithin: number | undefined;
|
|
19
21
|
/** when false, ACT reports settled:false + settleReason:'timeout' (throttled-tab path). */
|
|
20
22
|
actSettled: boolean;
|
|
21
23
|
/** When false, QUERY by testid returns no match (testid not in current DOM at replay). */
|
|
@@ -26,6 +26,8 @@ export class FakeBrowser {
|
|
|
26
26
|
actHasTestid = true;
|
|
27
27
|
/** Provenance the real browser captures alongside the anchor; undefined = app built without it. */
|
|
28
28
|
actSource = undefined;
|
|
29
|
+
/** DOM mutations the browser counted inside the acted element; undefined = it counted none. */
|
|
30
|
+
actMutatedWithin = undefined;
|
|
29
31
|
/** when false, ACT reports settled:false + settleReason:'timeout' (throttled-tab path). */
|
|
30
32
|
actSettled = true;
|
|
31
33
|
/** When false, QUERY by testid returns no match (testid not in current DOM at replay). */
|
|
@@ -101,7 +103,12 @@ export class FakeBrowser {
|
|
|
101
103
|
dispatched: true,
|
|
102
104
|
settled: this.actSettled,
|
|
103
105
|
settleReason: this.actSettled ? null : 'timeout',
|
|
104
|
-
effect: {
|
|
106
|
+
effect: {
|
|
107
|
+
dispatched: true,
|
|
108
|
+
...(this.actMutatedWithin === undefined
|
|
109
|
+
? {}
|
|
110
|
+
: { domMutatedWithin: this.actMutatedWithin }),
|
|
111
|
+
},
|
|
105
112
|
...(this.actHasTestid ? { testid: 'pay-btn' } : {}),
|
|
106
113
|
...(this.actSource === undefined ? {} : { source: this.actSource }),
|
|
107
114
|
};
|
|
@@ -16,7 +16,7 @@ import type { FileSystemPort } from '../project/fs-port.js';
|
|
|
16
16
|
* in the schema literal, and in act-capsule's writer — with the named one used by nobody.
|
|
17
17
|
*/
|
|
18
18
|
export declare const CAPSULE_VERSION = 1;
|
|
19
|
-
|
|
19
|
+
declare const CapsuleSchema: z.ZodObject<{
|
|
20
20
|
version: z.ZodLiteral<1>;
|
|
21
21
|
id: z.ZodString;
|
|
22
22
|
/** The flow the failure happened in, when it came from one (a bare assert has no flow). */
|
|
@@ -68,3 +68,4 @@ export declare class CapsuleStore {
|
|
|
68
68
|
export declare function capsuleId(now: number, label: string): string;
|
|
69
69
|
/** The minimal reproduction: the steps up to and including the one that failed (prefix trim). */
|
|
70
70
|
export declare function minimalSteps(steps: readonly FlowStep[], failedIndex: number): FlowStep[];
|
|
71
|
+
export {};
|
|
@@ -17,7 +17,7 @@ import { reticleDirPaths } from '../project/reticle-dir.js';
|
|
|
17
17
|
* in the schema literal, and in act-capsule's writer — with the named one used by nobody.
|
|
18
18
|
*/
|
|
19
19
|
export const CAPSULE_VERSION = 1;
|
|
20
|
-
|
|
20
|
+
const CapsuleSchema = z.object({
|
|
21
21
|
version: z.literal(CAPSULE_VERSION),
|
|
22
22
|
id: z.string(),
|
|
23
23
|
/** The flow the failure happened in, when it came from one (a bare assert has no flow). */
|
|
@@ -4,7 +4,7 @@ import { type ReticleEvent } from '@reticlehq/core';
|
|
|
4
4
|
* app did in the act's window, as counts + a headline, not a raw event dump. Diffs come from the
|
|
5
5
|
* storage/state events; attribution from. Pure composition over the attributed event window.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
interface StateDiff {
|
|
8
8
|
path: string;
|
|
9
9
|
from: unknown;
|
|
10
10
|
to: unknown;
|
|
@@ -20,7 +20,7 @@ export interface StateDiff {
|
|
|
20
20
|
*/
|
|
21
21
|
atMs: number;
|
|
22
22
|
}
|
|
23
|
-
|
|
23
|
+
interface StorageDiff {
|
|
24
24
|
key: string;
|
|
25
25
|
from?: unknown;
|
|
26
26
|
to?: unknown;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launch options every Reticle-driven Chromium gets, in one place.
|
|
3
|
+
*
|
|
4
|
+
* A pooled browser serves several lease contexts while only one page is ever the visible one, and a
|
|
5
|
+
* driven page can sit in a window the host does not treat as visible. For everyone else Chromium
|
|
6
|
+
* throttles timers, suspends rAF, and deprioritizes the renderer, which is exactly the state a
|
|
7
|
+
* session reports as `throttled: true`: WebSocket- and timer-driven DOM updates land seconds late,
|
|
8
|
+
* asserts time out on content that appears right after the timeout, with no refetch, because the
|
|
9
|
+
* app was fine and the browser was parked. The three switches below are the documented opt-outs,
|
|
10
|
+
* shared by both launch sites so the pooled path (`playwright-launcher`) and the drive path
|
|
11
|
+
* (`real-input`) cannot drift apart.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Chromium switches that keep background pages running at full speed. Named individually rather
|
|
15
|
+
* than as one free string per call site, so a typo is a test failure instead of a silently ignored
|
|
16
|
+
* argument.
|
|
17
|
+
*/
|
|
18
|
+
export declare const CHROMIUM_ANTI_THROTTLING_ARGS: readonly ["--disable-background-timer-throttling", "--disable-backgrounding-occluded-windows", "--disable-renderer-backgrounding"];
|
|
19
|
+
/** The options object handed to `chromium.launch` at both launch sites. */
|
|
20
|
+
export interface ChromiumLaunchOptions {
|
|
21
|
+
headless: boolean;
|
|
22
|
+
args: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build the launch options for a headless or headed Chromium. Pure; returns a fresh mutable array
|
|
26
|
+
* each call so no caller can mutate the constant behind everyone else's back.
|
|
27
|
+
*/
|
|
28
|
+
export declare function chromiumLaunchOptions(headless: boolean): ChromiumLaunchOptions;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launch options every Reticle-driven Chromium gets, in one place.
|
|
3
|
+
*
|
|
4
|
+
* A pooled browser serves several lease contexts while only one page is ever the visible one, and a
|
|
5
|
+
* driven page can sit in a window the host does not treat as visible. For everyone else Chromium
|
|
6
|
+
* throttles timers, suspends rAF, and deprioritizes the renderer, which is exactly the state a
|
|
7
|
+
* session reports as `throttled: true`: WebSocket- and timer-driven DOM updates land seconds late,
|
|
8
|
+
* asserts time out on content that appears right after the timeout, with no refetch, because the
|
|
9
|
+
* app was fine and the browser was parked. The three switches below are the documented opt-outs,
|
|
10
|
+
* shared by both launch sites so the pooled path (`playwright-launcher`) and the drive path
|
|
11
|
+
* (`real-input`) cannot drift apart.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Chromium switches that keep background pages running at full speed. Named individually rather
|
|
15
|
+
* than as one free string per call site, so a typo is a test failure instead of a silently ignored
|
|
16
|
+
* argument.
|
|
17
|
+
*/
|
|
18
|
+
export const CHROMIUM_ANTI_THROTTLING_ARGS = [
|
|
19
|
+
'--disable-background-timer-throttling',
|
|
20
|
+
'--disable-backgrounding-occluded-windows',
|
|
21
|
+
'--disable-renderer-backgrounding',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Build the launch options for a headless or headed Chromium. Pure; returns a fresh mutable array
|
|
25
|
+
* each call so no caller can mutate the constant behind everyone else's back.
|
|
26
|
+
*/
|
|
27
|
+
export function chromiumLaunchOptions(headless) {
|
|
28
|
+
return { headless, args: [...CHROMIUM_ANTI_THROTTLING_ARGS] };
|
|
29
|
+
}
|
package/dist/cli/cli-doctor.js
CHANGED
|
@@ -17,6 +17,7 @@ import { diagnoseDesktop, isDesktopProject } from '../init/desktop-doctor.js';
|
|
|
17
17
|
import { diagnoseWebCsp } from '../init/csp-doctor.js';
|
|
18
18
|
import { RETICLE_CONFIG_BASENAME, diagnosePortMismatch, readProjectId, readProjectPort, } from './cli-port.js';
|
|
19
19
|
import { DoctorRow, doctorRow } from './doctor-rows.js';
|
|
20
|
+
import { findOccupiedSiblings, siblingListenerNote } from './sibling-ports.js';
|
|
20
21
|
/**
|
|
21
22
|
* `reticle doctor` — collapse the ~6 independent first-run failure modes into one command. Checks the
|
|
22
23
|
* Chromium install (the #1 silent failure), whether a daemon is up on the resolved bridge port, and
|
|
@@ -91,6 +92,13 @@ export async function handleDoctor(port) {
|
|
|
91
92
|
const mismatch = diagnosePortMismatch(port, projectPort);
|
|
92
93
|
if (mismatch !== undefined)
|
|
93
94
|
line(doctorRow(DoctorRow.PORT_CHECK, `✗ ${mismatch}`));
|
|
95
|
+
// Remaining half of #261. The SDK dialling a port we are not on is invisible as a refused inbound,
|
|
96
|
+
// but a listener on a well-known Reticle port is visible. Report the observation; do not conclude
|
|
97
|
+
// it is the daemon this app wants — somebody else's daemon looks the same from here.
|
|
98
|
+
const occupiedSiblings = await findOccupiedSiblings(port, probeDaemon);
|
|
99
|
+
const siblingNote = siblingListenerNote(port, occupiedSiblings);
|
|
100
|
+
if (siblingNote !== undefined)
|
|
101
|
+
line(doctorRow(DoctorRow.SIBLING, siblingNote));
|
|
94
102
|
// The check this checklist was missing: is the APP wired, not just the tools. Everything above can
|
|
95
103
|
// be green in a project that has never been through `init`, and that combination is precisely the
|
|
96
104
|
// one `doctor` gets run to explain.
|
|
@@ -27,4 +27,6 @@ export declare function handleCapsules(): Promise<void>;
|
|
|
27
27
|
* changed files. Flaky flows are quarantined (surfaced, not blocking). The environment-side enforcement
|
|
28
28
|
* that makes verification unavoidable. Never throws; a fault fails closed (exit 1).
|
|
29
29
|
*/
|
|
30
|
-
export declare function handleGate(files: string[], since: string | undefined
|
|
30
|
+
export declare function handleGate(files: string[], since: string | undefined,
|
|
31
|
+
/** Hook mode: prose for a human, and silence when there was simply nothing to check. */
|
|
32
|
+
hook?: boolean): Promise<void>;
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* opens first. These five functions form one cohesive group: they all read the flow store and report on
|
|
6
6
|
* verification state, and none of them touch daemon lifecycle, which is what the rest of cli.ts does.
|
|
7
7
|
*/
|
|
8
|
+
import { GateExit } from './gate-exit.js';
|
|
9
|
+
import { gateHookMessage, GATE_SKIP_ENV } from './gate-hook-message.js';
|
|
8
10
|
import { readProjectId } from './cli-port.js';
|
|
9
11
|
import { changedFilesSince } from '../flows/git-changed.js';
|
|
10
12
|
import { join } from 'node:path';
|
|
@@ -143,7 +145,9 @@ export async function handleCapsules() {
|
|
|
143
145
|
* changed files. Flaky flows are quarantined (surfaced, not blocking). The environment-side enforcement
|
|
144
146
|
* that makes verification unavoidable. Never throws; a fault fails closed (exit 1).
|
|
145
147
|
*/
|
|
146
|
-
export async function handleGate(files, since
|
|
148
|
+
export async function handleGate(files, since,
|
|
149
|
+
/** Hook mode: prose for a human, and silence when there was simply nothing to check. */
|
|
150
|
+
hook = false) {
|
|
147
151
|
try {
|
|
148
152
|
const fs = createNodeFileSystem();
|
|
149
153
|
const reticleRoot = join(process.cwd(), ReticleDir.ROOT);
|
|
@@ -196,11 +200,37 @@ export async function handleGate(files, since) {
|
|
|
196
200
|
...(result.deleted.length > 0 ? { deletedCoverage: result.deleted } : {}),
|
|
197
201
|
coverage: flowCoverage,
|
|
198
202
|
});
|
|
199
|
-
|
|
200
|
-
|
|
203
|
+
// Two non-zero codes, because two callers want opposite things from the same run. CI wants any
|
|
204
|
+
// problem to fail. A Stop hook wants to block a real regression and NOT block a project that
|
|
205
|
+
// has simply not recorded a flow yet — which is every project on its first day, and measured
|
|
206
|
+
// before this change as an exit 1 that would have blocked every stop. See GateExit.
|
|
207
|
+
if (!pass) {
|
|
208
|
+
const code = result.pass && flowCoverage.outcome !== undefined
|
|
209
|
+
? GateExit.NOTHING_TO_CHECK
|
|
210
|
+
: GateExit.FAIL;
|
|
211
|
+
process.exitCode = code;
|
|
212
|
+
if (hook) {
|
|
213
|
+
// An honest escape hatch, because the alternative to one is not compliance — it is somebody
|
|
214
|
+
// deleting the hook, and a deleted gate protects nothing. Recorded either way.
|
|
215
|
+
if (process.env[GATE_SKIP_ENV] !== undefined) {
|
|
216
|
+
process.stderr.write(`Reticle: gate skipped via ${GATE_SKIP_ENV}. This change is unverified.\n`);
|
|
217
|
+
process.exitCode = GateExit.PASS;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const message = gateHookMessage(code, {
|
|
221
|
+
uncovered: result.uncovered,
|
|
222
|
+
quarantined: result.quarantined,
|
|
223
|
+
// A downgrade is reported per flow with its step indices; the hook names the flow.
|
|
224
|
+
downgraded: result.downgraded.map((d) => d.flow),
|
|
225
|
+
deleted: result.deleted,
|
|
226
|
+
});
|
|
227
|
+
if (message !== undefined)
|
|
228
|
+
process.stderr.write(`${message}\n`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
201
231
|
}
|
|
202
232
|
catch (error) {
|
|
203
233
|
log('reticle_gate_failed', { error: error instanceof Error ? error.message : String(error) });
|
|
204
|
-
process.exitCode =
|
|
234
|
+
process.exitCode = GateExit.FAIL;
|
|
205
235
|
}
|
|
206
236
|
}
|
package/dist/cli/cli-kill.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export declare const KillAction: {
|
|
|
25
25
|
readonly REFUSE: "refuse";
|
|
26
26
|
};
|
|
27
27
|
export type KillAction = (typeof KillAction)[keyof typeof KillAction];
|
|
28
|
-
|
|
28
|
+
interface KillPlan {
|
|
29
29
|
action: KillAction;
|
|
30
30
|
/** The single pid to signal. Present only for `kill`. */
|
|
31
31
|
pid?: number;
|
|
@@ -56,3 +56,4 @@ export declare function planKill(input: {
|
|
|
56
56
|
}): KillPlan;
|
|
57
57
|
/** `reticle kill` — free the port. Resolves to whether the port is now free. */
|
|
58
58
|
export declare function runKill(port: number, force: boolean): Promise<boolean>;
|
|
59
|
+
export {};
|
package/dist/cli/cli-parse.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from '.
|
|
|
13
13
|
* telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
|
|
14
14
|
* the usage block rather than being a footnote somewhere else.
|
|
15
15
|
*/
|
|
16
|
-
export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>] (wire Reticle into the project in this directory)\n --app picks WHICH app in a monorepo, when several are found\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N]\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)\n reticle watch [url] (on save, report which saved flows must re-verify)\n reticle drive <url> [--headless] (foreground mode, for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)\n reticle update (install the latest server version and restart)\n reticle rollback (restore the previous server version and restart)\n reticle license (show enterprise license status: active | eval | missing)\n reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't; prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> \"message\"\n (agents: file from anywhere, including a setup that never finished)\n reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]\n (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)\n\nCloud (link this project to Reticle Cloud; runs/flows recorded on the dashboard):\n reticle login --email <e> [--code <c>] [--org <n>] (sign in: mails a code, then exchanges it)\n reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)\n reticle whoami (who am I signed in as, and is this repo attached?)\n reticle project <ls|create <name>> (list or create cloud projects)\n reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]\n reticle push (send local run artifacts to the dashboard)\n reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)\n\n'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay\nand the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
|
|
16
|
+
export declare const CLI_USAGE = "usage: npx @reticlehq/server <command> (or `reticle <command>` once the bin is on your PATH)\n\n reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>] (wire Reticle into the project in this directory)\n --app picks WHICH app in a monorepo, when several are found\n --no-mcp skips MORE than the server registration: also the agent rule files\n (CLAUDE.md / AGENTS.md / .cursor) and the /reticle command, because all three\n only make sense once the tools are reachable.\n reticle serve [--port N] [--drive <url>] [--headless] [--http] [--http-port N] [--http-token T]\n reticle stop [--port N] [--quiet] (stop the daemon we started, by its recorded pid)\n reticle kill [--port N] [--force] (free the port by its LISTENER, never the agent's mcp proxy)\n reticle restart [--port N] [--force] (kill, then start a daemon and wait for a real bind)\n reticle status [--port N]\n reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)\n reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)\n reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)\n reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)\n reticle watch [url] (on save, report which saved flows must re-verify)\n reticle drive <url> [--headless] (foreground mode, for debugging)\n reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)\n reticle update (install the latest server version and restart)\n reticle rollback (restore the previous server version and restart)\n reticle license (show enterprise license status: active | eval | missing)\n reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)\n reticle feedback [--rating 1-5] [--bug] \"message\" (tell us what worked and what didn't; prints exactly what it sends)\n reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> \"message\"\n (agents: file from anywhere, including a setup that never finished)\n reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]\n (OPT-IN: tell us who you are, e.g. for support or an enterprise trial)\n\nCloud (link this project to Reticle Cloud; runs/flows recorded on the dashboard):\n reticle login --email <e> [--code <c>] [--org <n>] (sign in: mails a code, then exchanges it)\n reticle link [--project <name|id>] (bind this repo: mints a scoped key, writes .reticle/cloud.json)\n reticle whoami (who am I signed in as, and is this repo attached?)\n reticle project <ls|create <name>> (list or create cloud projects)\n reticle config [--runs on|off] [--memory on|off] [--flows on|off] [--verify local|server]\n reticle push (send local run artifacts to the dashboard)\n reticle runs | regression | share <runId> (read cloud state; regression exits 3 if any flow broke)\n\n'drive' shows the browser; everything else is hidden (serve/mcp own the pool behind leases, replay\nand the spec runner: batch work). --headed opts any of them in, --headless hides drive, CI hides it.";
|
|
17
17
|
/** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
|
|
18
18
|
export declare const TelemetryAction: {
|
|
19
19
|
readonly STATUS: "status";
|
|
@@ -111,6 +111,7 @@ export type CliResult = {
|
|
|
111
111
|
kind: 'verify';
|
|
112
112
|
url: string;
|
|
113
113
|
headless: boolean;
|
|
114
|
+
port: number;
|
|
114
115
|
timeoutMs?: number;
|
|
115
116
|
storageState?: string;
|
|
116
117
|
} | {
|
|
@@ -126,6 +127,7 @@ export type CliResult = {
|
|
|
126
127
|
kind: 'gate';
|
|
127
128
|
files: string[];
|
|
128
129
|
since?: string;
|
|
130
|
+
hook?: boolean;
|
|
129
131
|
} | {
|
|
130
132
|
kind: 'watch';
|
|
131
133
|
url?: string;
|
package/dist/cli/cli-parse.js
CHANGED
|
@@ -29,7 +29,7 @@ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticl
|
|
|
29
29
|
reticle status [--port N]
|
|
30
30
|
reticle doctor [--port N] (one command to diagnose setup: Chromium, daemon, port)
|
|
31
31
|
reticle open [url] [--port N] (show the app: reuse the connected tab, else open one)
|
|
32
|
-
reticle verify <url> [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)
|
|
32
|
+
reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] (one-shot: drive the URL, verify saved flows, exit 0=pass)
|
|
33
33
|
reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)
|
|
34
34
|
reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)
|
|
35
35
|
reticle watch [url] (on save, report which saved flows must re-verify)
|
|
@@ -69,6 +69,8 @@ const AFFECTED_COMMAND = 'affected';
|
|
|
69
69
|
const HUNT_COMMAND = 'hunt';
|
|
70
70
|
const CAPSULES_COMMAND = 'capsules';
|
|
71
71
|
const GATE_COMMAND = 'gate';
|
|
72
|
+
/** Hook mode: prose a human can read, and silence when there was simply nothing to check. */
|
|
73
|
+
const HOOK_FLAG = '--hook';
|
|
72
74
|
const WATCH_COMMAND = 'watch';
|
|
73
75
|
const UPDATE_COMMAND = 'update';
|
|
74
76
|
const ROLLBACK_COMMAND = 'rollback';
|
|
@@ -288,14 +290,15 @@ function parseDriveSuffix(args, port, defaultHeadless) {
|
|
|
288
290
|
return { kind: 'ok', port, driveUrl, headless };
|
|
289
291
|
}
|
|
290
292
|
/**
|
|
291
|
-
* Parse `verify <url> [--headed] [--timeout N] [--storage-state <file>]`. The first
|
|
292
|
-
* the preview URL.
|
|
293
|
+
* Parse `verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>]`. The first
|
|
294
|
+
* non-flag token is the preview URL. `defaultPort` is already env + `.reticle.json` + 4400.
|
|
293
295
|
*/
|
|
294
|
-
function parseVerifySuffix(args) {
|
|
296
|
+
function parseVerifySuffix(args, defaultPort) {
|
|
295
297
|
let headless = true;
|
|
296
298
|
let url;
|
|
297
299
|
let timeoutMs;
|
|
298
300
|
let storageState;
|
|
301
|
+
let port = defaultPort;
|
|
299
302
|
let i = 0;
|
|
300
303
|
while (i < args.length) {
|
|
301
304
|
const arg = args[i];
|
|
@@ -304,6 +307,16 @@ function parseVerifySuffix(args) {
|
|
|
304
307
|
if (arg === HEADED_FLAG) {
|
|
305
308
|
headless = false;
|
|
306
309
|
}
|
|
310
|
+
else if (arg === PORT_FLAG) {
|
|
311
|
+
i++;
|
|
312
|
+
const n = args[i];
|
|
313
|
+
if (n === undefined)
|
|
314
|
+
return missingValue(PORT_FLAG);
|
|
315
|
+
const parsed = parseInt(n, 10);
|
|
316
|
+
if (isNaN(parsed))
|
|
317
|
+
return notANumber(PORT_FLAG, n);
|
|
318
|
+
port = parsed;
|
|
319
|
+
}
|
|
307
320
|
else if (arg === TIMEOUT_FLAG) {
|
|
308
321
|
i++;
|
|
309
322
|
const n = args[i];
|
|
@@ -338,6 +351,7 @@ function parseVerifySuffix(args) {
|
|
|
338
351
|
kind: 'ok',
|
|
339
352
|
url,
|
|
340
353
|
headless,
|
|
354
|
+
port,
|
|
341
355
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
342
356
|
...(storageState !== undefined ? { storageState } : {}),
|
|
343
357
|
};
|
|
@@ -550,13 +564,14 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
550
564
|
};
|
|
551
565
|
}
|
|
552
566
|
case VERIFY_COMMAND: {
|
|
553
|
-
const r = parseVerifySuffix(rest);
|
|
567
|
+
const r = parseVerifySuffix(rest, defaultPort);
|
|
554
568
|
if ('error' === r.kind)
|
|
555
569
|
return r;
|
|
556
570
|
return {
|
|
557
571
|
kind: 'verify',
|
|
558
572
|
url: r.url,
|
|
559
573
|
headless: r.headless,
|
|
574
|
+
port: r.port,
|
|
560
575
|
...(r.timeoutMs !== undefined ? { timeoutMs: r.timeoutMs } : {}),
|
|
561
576
|
...(r.storageState !== undefined ? { storageState: r.storageState } : {}),
|
|
562
577
|
};
|
|
@@ -575,9 +590,16 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
|
|
|
575
590
|
return { kind: 'affected', files: t.files, ...(since === undefined ? {} : { since }) };
|
|
576
591
|
}
|
|
577
592
|
case GATE_COMMAND: {
|
|
578
|
-
|
|
593
|
+
// `--hook` is stripped before target parsing, which would reject it as an unknown flag.
|
|
594
|
+
const hook = rest.includes(HOOK_FLAG);
|
|
595
|
+
const t = parseTargetArgs(rest.filter((a) => a !== HOOK_FLAG));
|
|
579
596
|
const since = t.since ?? implicitSince(t.files);
|
|
580
|
-
return {
|
|
597
|
+
return {
|
|
598
|
+
kind: 'gate',
|
|
599
|
+
files: t.files,
|
|
600
|
+
...(since === undefined ? {} : { since }),
|
|
601
|
+
...(hook ? { hook: true } : {}),
|
|
602
|
+
};
|
|
581
603
|
}
|
|
582
604
|
case WATCH_COMMAND: {
|
|
583
605
|
// `reticle watch [url]` — on file save, report which saved flows must re-verify.
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
import { checkForUpdate } from '../update/update-checker.js';
|
|
9
9
|
import { updateTarget } from '../update/update-nudge.js';
|
|
10
10
|
import { applyUpdate, rollback } from '../update/updater.js';
|
|
11
|
+
import { refreshAgentRules } from '../init/refresh-rules.js';
|
|
11
12
|
import { SERVER_VERSION } from '../version/server-version.js';
|
|
12
13
|
import { log } from '../log.js';
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
14
15
|
import { join } from 'node:path';
|
|
15
16
|
import { reticleDepsOf, sdkSyncCommand } from '../update/sdk-sync.js';
|
|
16
17
|
import { detectPackageManager } from '../init/detect.js';
|
|
@@ -64,6 +65,24 @@ export async function handleUpdate() {
|
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
67
|
log('reticle_update', { ok: true, from: SERVER_VERSION, to: target });
|
|
68
|
+
// Bring this project's agent rules up with the version.
|
|
69
|
+
//
|
|
70
|
+
// The managed block has always been updatable — marker-delimited, idempotent by comparing
|
|
71
|
+
// content — and nothing called it after the first install. `mergeMarkedInstruction` is reachable
|
|
72
|
+
// from `buildPlan` alone, so a project set up on an older release kept that release's
|
|
73
|
+
// instructions forever and every improvement to them reached new projects only.
|
|
74
|
+
//
|
|
75
|
+
// That is backwards: the people who most need better instructions are the ones already
|
|
76
|
+
// installed and not getting value, and this is the moment they are touching the install anyway.
|
|
77
|
+
// Only files that ALREADY carry the block are touched, and only inside the markers.
|
|
78
|
+
const refreshed = refreshAgentRules(process.cwd(), {
|
|
79
|
+
read: (path) => (existsSync(path) ? readFileSync(path, 'utf8') : null),
|
|
80
|
+
write: (path, content) => {
|
|
81
|
+
writeFileSync(path, content, 'utf8');
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
if (0 !== refreshed.updated.length)
|
|
85
|
+
log('reticle_rules_refreshed', { files: refreshed.updated, to: target });
|
|
67
86
|
// The app's SDK FIRST, then the CLI. `reticle update` used to swap only the CLI, so the command
|
|
68
87
|
// whose job is keeping an install current was itself a way to produce a version-skewed pair —
|
|
69
88
|
// and the skew message told people to run it to fix an outdated SDK, which it could not do.
|
package/dist/cli/cli-verify.d.ts
CHANGED
package/dist/cli/cli-verify.js
CHANGED
|
@@ -16,7 +16,7 @@ import { start } from '../index.js';
|
|
|
16
16
|
import { probePresence, PortPresence } from '../daemon/port-presence.js';
|
|
17
17
|
import { probeDaemon } from '../mcp/mcp-proxy.js';
|
|
18
18
|
import { fetchStatus } from './cli-launch.js';
|
|
19
|
-
import { resolveCloudConfig, syncRunToCloud, SyncOutcome } from '../cloud/cloud-sync.js';
|
|
19
|
+
import { cloudFetch, resolveCloudConfig, syncRunToCloud, SyncOutcome, } from '../cloud/cloud-sync.js';
|
|
20
20
|
import { ReticleRunner } from '../runs/reticle-runner.js';
|
|
21
21
|
import { createRunnerPort } from '../runs/runner-port.js';
|
|
22
22
|
import { RunStore } from '../runs/run-store.js';
|
|
@@ -94,7 +94,7 @@ async function pushRunToCloud(run, ports) {
|
|
|
94
94
|
const config = resolveCloudConfig(process.env);
|
|
95
95
|
if (null === config)
|
|
96
96
|
return;
|
|
97
|
-
const result = await syncRunToCloud(run, config,
|
|
97
|
+
const result = await syncRunToCloud(run, config, cloudFetch);
|
|
98
98
|
if (result.outcome === SyncOutcome.SYNCED) {
|
|
99
99
|
ports.out(`↑ run ${run.runId} recorded on the Reticle Cloud dashboard`);
|
|
100
100
|
}
|