@reticlehq/server 2.9.0 → 2.11.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 +64 -26
- package/dist/capsule/capsule-store.d.ts +2 -2
- package/dist/cli/cli-parse.d.ts +1 -1
- package/dist/cli/cli-parse.js +1 -1
- package/dist/cli/cli-verify.js +1 -1
- package/dist/crawl/crawl.d.ts +7 -2
- package/dist/crawl/crawl.js +52 -9
- package/dist/dev/stale-issue-guard.d.ts +43 -0
- package/dist/dev/stale-issue-guard.js +75 -0
- package/dist/domain/domain-tools.js +3 -2
- package/dist/ee/audit-log.js +2 -2
- package/dist/events/body-failures.d.ts +2 -2
- package/dist/events/contradiction-folds.d.ts +45 -0
- package/dist/events/contradiction-folds.js +42 -0
- package/dist/events/contradictions.d.ts +19 -1
- package/dist/events/contradictions.js +17 -2
- package/dist/events/echo-mismatch.d.ts +2 -2
- package/dist/events/predicate-asks.d.ts +17 -0
- package/dist/events/predicate-asks.js +26 -0
- package/dist/events/predicate-eval.js +211 -24
- package/dist/events/predicate-parse.js +9 -3
- package/dist/events/predicate-schema.d.ts +18 -0
- package/dist/events/predicate-schema.js +55 -0
- package/dist/events/stale-response.d.ts +2 -3
- package/dist/events/stale-response.js +49 -2
- package/dist/events/unit-mismatch.d.ts +2 -2
- package/dist/flows/flow-tools.js +34 -5
- package/dist/flows/predicate-to-expect.js +8 -0
- package/dist/flows/verify-change-tools.js +6 -2
- package/dist/honesty/blind-spots.d.ts +9 -0
- package/dist/honesty/blind-spots.js +17 -4
- package/dist/honesty/gap-ledger.d.ts +52 -0
- package/dist/honesty/gap-ledger.js +63 -0
- package/dist/honesty/instrumentation-gaps.d.ts +55 -0
- package/dist/honesty/instrumentation-gaps.js +21 -0
- package/dist/honesty/observability.d.ts +37 -0
- package/dist/honesty/observability.js +40 -0
- package/dist/honesty/verified.js +16 -0
- package/dist/index.d.ts +18 -1
- package/dist/index.js +57 -0
- package/dist/init/mcp.d.ts +1 -2
- package/dist/init/mcp.js +4 -1
- package/dist/init/node-io.js +3 -0
- package/dist/init/run.d.ts +18 -0
- package/dist/init/run.js +34 -3
- package/dist/init/slash-command.d.ts +1 -1
- package/dist/init/slash-command.js +33 -10
- package/dist/journal/envelope.d.ts +24 -24
- package/dist/license/license.d.ts +76 -5
- package/dist/license/license.js +93 -29
- package/dist/mcp/mcp-proxy.d.ts +2 -27
- package/dist/mcp/mcp-proxy.js +4 -88
- package/dist/mcp/mcp.d.ts +18 -2
- package/dist/mcp/mcp.js +31 -10
- package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
- package/dist/mcp/proxy-daemon-probe.js +48 -0
- package/dist/mcp/proxy-handshake.js +1 -1
- package/dist/mcp/server-instructions.js +2 -2
- package/dist/mcp/sse-frame-parser.d.ts +19 -0
- package/dist/mcp/sse-frame-parser.js +41 -0
- package/dist/project/artifact-root.d.ts +83 -0
- package/dist/project/artifact-root.js +112 -0
- package/dist/project/memory-fs.d.ts +9 -0
- package/dist/project/memory-fs.js +88 -0
- package/dist/project/project-store.d.ts +17 -0
- package/dist/project/project-store.js +33 -0
- package/dist/project/remember-project.d.ts +24 -0
- package/dist/project/remember-project.js +40 -0
- package/dist/project/session-root.d.ts +15 -0
- package/dist/project/session-root.js +31 -0
- package/dist/runs/run-tools.js +2 -1
- package/dist/session/lease-availability.d.ts +40 -0
- package/dist/session/lease-availability.js +43 -0
- package/dist/session/no-session-diagnosis.d.ts +17 -0
- package/dist/session/no-session-diagnosis.js +41 -8
- package/dist/session/no-session-watch.js +7 -0
- package/dist/session/session-readiness.js +1 -1
- package/dist/session/session.d.ts +9 -0
- package/dist/session/session.js +9 -0
- package/dist/session/stall-clock.d.ts +18 -0
- package/dist/session/stall-clock.js +48 -0
- package/dist/telemetry/daemon-telemetry.js +16 -22
- package/dist/telemetry/license-activation.d.ts +48 -0
- package/dist/telemetry/license-activation.js +57 -0
- 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 +34 -5
- package/dist/tools/act-tools.js +34 -4
- package/dist/tools/assert-verdict.d.ts +2 -0
- package/dist/tools/assert-verdict.js +24 -1
- package/dist/tools/contract-tools.js +5 -3
- package/dist/tools/coverage-tools.js +33 -0
- package/dist/tools/dynamic-tools.js +1 -1
- package/dist/tools/error-recovery.js +1 -1
- package/dist/tools/feedback-tools.js +31 -6
- package/dist/tools/invoke-tool.d.ts +1 -1
- package/dist/tools/invoke-tool.js +10 -5
- package/dist/tools/observe-tools.js +8 -2
- package/dist/tools/tool-kit.d.ts +46 -4
- package/dist/tools/tool-names.d.ts +2 -0
- package/dist/tools/tool-names.js +2 -0
- package/dist/tools/tool-surface.js +2 -3
- package/dist/tools/tools.js +16 -1
- package/docs/README.md +1 -0
- package/docs/agent-cheatsheet.md +2 -2
- package/docs/capabilities.mdx +4 -4
- package/docs/cli/hunt.mdx +2 -2
- package/docs/deploy-checks.md +2 -2
- package/docs/docs.json +2 -2
- package/docs/enterprise.md +9 -3
- package/docs/flows.md +4 -4
- package/docs/gates.md +1 -1
- package/docs/license-activation.mdx +145 -0
- package/docs/recipes.mdx +2 -2
- package/docs/telemetry-contract.md +18 -12
- package/docs/telemetry-events.mdx +1 -34
- package/docs/telemetry.md +0 -1
- package/docs/tools-overview.mdx +1 -1
- package/docs/tools-session-and-feedback.mdx +5 -0
- package/docs/tools-tools-and-run.mdx +15 -15
- package/docs/usage.md +3 -3
- package/package.json +2 -2
- package/dist/telemetry/instrumentation-stall.d.ts +0 -50
- package/dist/telemetry/instrumentation-stall.js +0 -90
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Reticle
|
|
2
2
|
|
|
3
|
-
Reticle embeds a dev-only SDK in the user's running app and exposes it to you as `reticle_*` MCP tools. You look, act, observe, and assert against the real app. No screenshots, and no browser download for the verify loop
|
|
3
|
+
Reticle embeds a dev-only SDK in the user's running app and exposes it to you as `reticle_*` MCP tools. You look, act, observe, and assert against the real app. No screenshots, and no browser download for the verify loop: it drives the tab the user already has open.
|
|
4
4
|
|
|
5
5
|
This file is the whole critical path and nothing else. Everything it leaves out is at `https://docs.reticle.sh`, one page at a time.
|
|
6
6
|
|
|
@@ -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
|
-
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
|
|
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
|
|
|
@@ -109,7 +125,7 @@ This is the step the funnel dies on. The daemon runs, the MCP server registers,
|
|
|
109
125
|
- **Vite + React**: `reticle()` is in the `plugins` array of `vite.config.*`.
|
|
110
126
|
- **Next.js**: `withReticle` wraps the export in `next.config.*`, `reticle-dev.tsx` exists, and it is mounted in the root layout or `_app`.
|
|
111
127
|
- **Remix**, **Astro**: wired by `init`, each with an app in this repo that a gate drives.
|
|
112
|
-
- **Anything else**: nothing is wired. Do it by hand
|
|
128
|
+
- **Anything else**: nothing is wired. Do it by hand: fetch `https://docs.reticle.sh/frameworks.md`.
|
|
113
129
|
|
|
114
130
|
Those four are the frameworks with an app and a CI gate behind them. The SDK is framework-agnostic and usually connects elsewhere, but say so honestly rather than reporting a proven install.
|
|
115
131
|
|
|
@@ -120,7 +136,7 @@ Two rules that cause silent failures if broken:
|
|
|
120
136
|
|
|
121
137
|
Then make sure something is serving the app.
|
|
122
138
|
|
|
123
|
-
**If a dev server is already listening, use it. If none is, start one yourself
|
|
139
|
+
**If a dev server is already listening, use it. If none is, start one yourself**: read the project's own dev script out of `package.json` (`dev`, `start`, whatever this project calls it), run it in the BACKGROUND, and tell the user in one line that it is running and how to stop it. Stopping here to ask is how a setup turn ends with nothing verified.
|
|
124
140
|
|
|
125
141
|
The daemon deliberately will not do this for you. A build process started by a long-lived background daemon is invisible to the person whose machine it runs on and orphans when the daemon exits; a dev server YOU start is in the transcript, attributable, and stoppable.
|
|
126
142
|
|
|
@@ -128,11 +144,21 @@ Five guards, none optional:
|
|
|
128
144
|
|
|
129
145
|
1. **Never start a second one.** If something is already listening on the app's port, use it.
|
|
130
146
|
2. **Never guess the command.** It comes from `package.json` scripts. No recognisable dev script means say so and stop, not invent one.
|
|
131
|
-
3. **Never kill anything.** Not a dev server, not a daemon, not a port holder
|
|
147
|
+
3. **Never kill anything.** Not a dev server, not a daemon, not a port holder, including one you started.
|
|
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
|
|
|
@@ -142,7 +168,7 @@ reticle_sessions()
|
|
|
142
168
|
|
|
143
169
|
You need a session whose URL matches the app's localhost address. **Nothing below this line is meaningful until you have one, and you may not report setup complete without one.**
|
|
144
170
|
|
|
145
|
-
**Empty list?** Read `next_action` first, then `why`. `next_action` is the machine-readable half: it names which of the four cases this is and, when there is one, the literal command to run and the port
|
|
171
|
+
**Empty list?** Read `next_action` first, then `why`. `next_action` is the machine-readable half: it names which of the four cases this is and, when there is one, the literal command to run and the port, sourced from this project's own scripts, never guessed. `why` is the same thing in prose for the human. The daemon can see whether a session was ever here and whether a dev server is listening. Then, in order: is the SDK imported and called in the app entry, is the dev server actually serving that entry, is the connect guarded on `hostname === 'localhost'`, and is the bridge port the same number on both sides. **Nothing listening at all? That one is yours: start it, per step 3.** If something IS listening, do not tell the user to start what they are already running; the fault is the SDK not loading in the page. Full checklist: `https://docs.reticle.sh/troubleshooting.md`.
|
|
146
172
|
|
|
147
173
|
## 5. Drive one real flow and produce a verdict.
|
|
148
174
|
|
|
@@ -152,14 +178,14 @@ A connected session is not a result. The user has installed something and seen n
|
|
|
152
178
|
|
|
153
179
|
Tell the user to keep the tab visible. The HUD is on by default (glow border, animated cursor, narration per step) and watching you drive their own app is the demo.
|
|
154
180
|
|
|
155
|
-
Drive it in as few calls as you can. Every call is a full model turn, and in a client that asks the user to approve each one it is also a click
|
|
181
|
+
Drive it in as few calls as you can. Every call is a full model turn, and in a client that asks the user to approve each one it is also a click. A flow driven one call at a time is how a person gives up before they ever see a verdict.
|
|
156
182
|
|
|
157
183
|
1. `reticle_snapshot({ mode: "interactive" })` **once**, for the whole flow. Not once per step.
|
|
158
|
-
2. `reticle_act_sequence` for the setup
|
|
184
|
+
2. `reticle_act_sequence` for the setup: every fill and every intermediate click in ONE call.
|
|
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
|
|
|
@@ -170,7 +196,7 @@ reticle_run({ tool: "reticle_record", args: { action: "stop", recordingName: "<
|
|
|
170
196
|
reticle_run({ tool: "reticle_flow_save", args: { flowName: "<flow>" } })
|
|
171
197
|
```
|
|
172
198
|
|
|
173
|
-
`reticle_flow_save` returns `assertions.grade`. If it is not `asserted`, the flow only clicks
|
|
199
|
+
`reticle_flow_save` returns `assertions.grade`. If it is not `asserted`, the flow only clicks: it will pass even when the feature is broken. Say that in one line rather than presenting it as a regression check.
|
|
174
200
|
|
|
175
201
|
Tell the user plainly: that flow is now saved to `.reticle/flows/`, and re-verifying it after any future change is one call with no model in the loop.
|
|
176
202
|
|
|
@@ -178,7 +204,7 @@ Tell the user plainly: that flow is now saved to `.reticle/flows/`, and re-verif
|
|
|
178
204
|
|
|
179
205
|
`init` writes a managed block into the project's `CLAUDE.md` and `AGENTS.md` (and `.cursor/rules/reticle.mdc` where it applies), plus a `RETICLE.md` reference file and the `/reticle` command. That block is what makes the next agent verify its own work instead of guessing.
|
|
180
206
|
|
|
181
|
-
Confirm the block is present, between `<!-- reticle:begin ... -->` and `<!-- reticle:end -->`. **Do not hand-write your own copy
|
|
207
|
+
Confirm the block is present, between `<!-- reticle:begin ... -->` and `<!-- reticle:end -->`. **Do not hand-write your own copy.** The markers are how a re-run stays idempotent, and a second unmarked copy will never be updated again. If it is missing, re-run `init`.
|
|
182
208
|
|
|
183
209
|
**Only after step 5 has produced a verdict**, report what you drove and what it produced, then:
|
|
184
210
|
|
|
@@ -190,11 +216,23 @@ 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.
|
|
196
234
|
|
|
197
|
-
A verdict of `verified: "unknown"` is not a pass. It means Reticle drove the app and could not tell what happened. Report it as unknown. **Never weaken a check to make it pass.**
|
|
235
|
+
A verdict of `verified: "unknown"` is not a pass. It means Reticle drove the app and could not tell what happened. Report it as unknown. `verified: "no-fault"` is not a pass either. It means the page settled and no channel reported a problem, but nothing was declared to prove, so assert a consequence the action CHANGES. **Never weaken a check to make it pass.**
|
|
198
236
|
|
|
199
237
|
## Take the cheapest path that answers the question
|
|
200
238
|
|
|
@@ -202,14 +240,14 @@ Work down this list and stop at the first row that fits. Do not hand-drive a flo
|
|
|
202
240
|
|
|
203
241
|
| The question | The call | Calls |
|
|
204
242
|
| --- | --- | --- |
|
|
205
|
-
| "Did my edit break anything?" | `reticle_run({ tool: "
|
|
243
|
+
| "Did my edit break anything?" | `reticle_run({ tool: "reticle_verify", args: { action: "change", files: ["src/App.tsx"] } })` | 1 |
|
|
206
244
|
| "Does this known journey still work?" | `reticle_run({ tool: "reticle_flow_replay", args: { flowName: "login" } })` | 1 |
|
|
207
245
|
| "Does this new behaviour work?" | `reticle_act_sequence` for the setup, then ONE `reticle_act_and_wait` | 2 |
|
|
208
246
|
| No MCP available at all | `npx @reticlehq/server verify <url>` in the shell | 1, no MCP |
|
|
209
247
|
|
|
210
|
-
`
|
|
248
|
+
`reticle_verify` and `reticle_flow_replay` are **not on the advertised tool list**: they are reached through `reticle_run` exactly as written above. That is the supported call shape, not a workaround, and it is why you have to be told they exist at all.
|
|
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
|
|
|
214
252
|
## Record once, replay cheaply
|
|
215
253
|
|
|
@@ -222,7 +260,7 @@ reticle_run({ tool: "reticle_record", args: { action: "stop", recordingName: "c
|
|
|
222
260
|
reticle_run({ tool: "reticle_flow_save", args: { flowName: "checkout" } })
|
|
223
261
|
```
|
|
224
262
|
|
|
225
|
-
From then on that journey re-verifies in one call, deterministically, and `
|
|
263
|
+
From then on that journey re-verifies in one call, deterministically, and `reticle_verify {action:"change"}` can start answering `yes` or `no` for the files it touches instead of `unknown`.
|
|
226
264
|
|
|
227
265
|
Check `assertions.grade` on the save. Anything other than `asserted` means the flow only acts, so it will pass even if the feature breaks.
|
|
228
266
|
|
|
@@ -243,11 +281,11 @@ reticle_act_and_wait({ sessionId, ref, action: "click", until: { kind: "allOf",
|
|
|
243
281
|
|
|
244
282
|
Prefer `reticle_act_and_wait({ ref, action, until })`. It names the expected consequence **before** the action, which is the difference between a check and a rationalisation.
|
|
245
283
|
|
|
246
|
-
A verdict of `verified: "unknown"` is not a pass. It means Reticle drove the app and could not tell what happened. Report it as unknown. **Never weaken a check to make it pass.**
|
|
284
|
+
A verdict of `verified: "unknown"` is not a pass. It means Reticle drove the app and could not tell what happened. Report it as unknown. `verified: "no-fault"` is not a pass either. It means the page settled and no channel reported a problem, but nothing was declared to prove, so assert a consequence the action CHANGES. **Never weaken a check to make it pass.**
|
|
247
285
|
|
|
248
286
|
Then report what you drove, what it produced, and the `file:line` for anything broken.
|
|
249
287
|
|
|
250
|
-
The surface is deliberately small: `default` 18, `all`
|
|
288
|
+
The surface is deliberately small: `default` 18, `all` 28. 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
289
|
|
|
252
290
|
- Batching, regression suites, reading a verdict: `https://docs.reticle.sh/agent-cheatsheet.md`
|
|
253
291
|
- Every predicate and action: `https://docs.reticle.sh/predicates.md`, `https://docs.reticle.sh/actions.md`
|
|
@@ -31,19 +31,19 @@ export declare const CapsuleSchema: z.ZodObject<{
|
|
|
31
31
|
/** Minimal steps to reproduce (prefix-trimmed). */
|
|
32
32
|
steps: z.ZodArray<z.ZodType<FlowStep, z.ZodTypeDef, FlowStep>, "many">;
|
|
33
33
|
}, "strip", z.ZodTypeAny, {
|
|
34
|
+
expected: string;
|
|
34
35
|
origin: string;
|
|
35
36
|
version: 1;
|
|
36
37
|
id: string;
|
|
37
|
-
expected: string;
|
|
38
38
|
observed: string;
|
|
39
39
|
steps: FlowStep[];
|
|
40
40
|
createdAt: number;
|
|
41
41
|
flow?: string | undefined;
|
|
42
42
|
}, {
|
|
43
|
+
expected: string;
|
|
43
44
|
origin: string;
|
|
44
45
|
version: 1;
|
|
45
46
|
id: string;
|
|
46
|
-
expected: string;
|
|
47
47
|
observed: string;
|
|
48
48
|
steps: FlowStep[];
|
|
49
49
|
createdAt: number;
|
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> \"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> [--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";
|
package/dist/cli/cli-parse.js
CHANGED
|
@@ -40,7 +40,7 @@ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticl
|
|
|
40
40
|
reticle license (show enterprise license status: active | eval | missing)
|
|
41
41
|
reticle telemetry [status|enable|disable] (anonymous usage metrics; status shows what's sent + the policy)
|
|
42
42
|
reticle feedback [--rating 1-5] [--bug] "message" (tell us what worked and what didn't; prints exactly what it sends)
|
|
43
|
-
reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement> "message"
|
|
43
|
+
reticle feedback --agent --kind <bug|gap|ambiguity|feature_request|improvement|experience> "message"
|
|
44
44
|
(agents: file from anywhere, including a setup that never finished)
|
|
45
45
|
reticle identify --context company|side_project|open_source|learning [--company N] [--email E] [--forget]
|
|
46
46
|
(OPT-IN: tell us who you are, e.g. for support or an enterprise trial)
|
package/dist/cli/cli-verify.js
CHANGED
|
@@ -226,7 +226,7 @@ export function portBusyMessage(port) {
|
|
|
226
226
|
return (`reticle verify needs port ${String(port)} — the port your app dials — and a Reticle daemon is ` +
|
|
227
227
|
'already listening on it. It did not start a second one.\n\n' +
|
|
228
228
|
' • If you have the Reticle tools, ask the daemon that is already running instead: ' +
|
|
229
|
-
'reticle_run { tool: "
|
|
229
|
+
'reticle_run { tool: "reticle_verify", args: { action: "change", files: ["..."] } }\n' +
|
|
230
230
|
` • Or stop it and re-run: npx @reticlehq/server stop --port ${String(port)}\n` +
|
|
231
231
|
` • Or run both on another port, if your app is configured for it: RETICLE_PORT=<port> ` +
|
|
232
232
|
'npx @reticlehq/server verify <url>');
|
package/dist/crawl/crawl.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type CommandResult, type ReticleEvent } from '@reticlehq/core';
|
|
2
2
|
/** The slice of Session the crawler needs — so tests inject a fake without a live browser. */
|
|
3
3
|
export interface CrawlSession {
|
|
4
4
|
command(name: string, args?: Record<string, unknown>): Promise<CommandResult>;
|
|
@@ -21,8 +21,13 @@ export interface CrawlAnomaly {
|
|
|
21
21
|
* disagreeing about the same click). The second kind is why an autonomous crawl is worth more
|
|
22
22
|
* than a human clicking the same buttons: a person sees only the screen, so a UI that advanced
|
|
23
23
|
* while its write failed looks like success to them and always will.
|
|
24
|
+
*
|
|
25
|
+
* `CrawlAnomalyKind` and `ContradictionKind` are what THIS package puts here, and both are still
|
|
26
|
+
* enforced at the emit sites below. The type is a plain string because a rule registered by a
|
|
27
|
+
* consumer contributes kinds this package has never heard of, and a closed union here would mean
|
|
28
|
+
* a consumer's private vocabulary had to ship in the free product to be reportable.
|
|
24
29
|
*/
|
|
25
|
-
kind:
|
|
30
|
+
kind: string;
|
|
26
31
|
/** The control that triggered it. */
|
|
27
32
|
ref: string;
|
|
28
33
|
desc: string;
|
package/dist/crawl/crawl.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { span } from '../trace.js';
|
|
2
|
-
import { ActionType, CRAWL_DEFAULTS, CrawlAnomalyKind,
|
|
2
|
+
import { ActionType, CRAWL_DEFAULTS, CrawlAnomalyKind, DANGEROUS_ACTION_CONFIRM_ARG, EventType, ReticleCommand, } from '@reticlehq/core';
|
|
3
3
|
import { crawlEmptyNote } from './crawl-empty.js';
|
|
4
4
|
import { parseInteractive, asRecord, asNumber, asString, sourceOf, } from '../tools/tools-helpers.js';
|
|
5
5
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
@@ -86,6 +86,37 @@ async function countRevealed(session, opts, before) {
|
|
|
86
86
|
const known = new Set(before.map((i) => (i.ref !== '' ? i.ref : i.desc)));
|
|
87
87
|
return parseInteractive(tree).filter((i) => !known.has(i.ref !== '' ? i.ref : i.desc)).length;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* The confirmation half of the dead-control rule. Kept separate so the rule above reads as one
|
|
91
|
+
* question, and so the second click gets its own window: reusing the first would let the first
|
|
92
|
+
* click's own (absent) activity decide the second, which is not a second sample at all.
|
|
93
|
+
*
|
|
94
|
+
* `true` means "silent both times" — the only state the anomaly is reported in. Anything that goes
|
|
95
|
+
* wrong here returns `false`: a confirmation that cannot run is not evidence, and the direction that
|
|
96
|
+
* costs a missed finding is better than the one that invents one.
|
|
97
|
+
*/
|
|
98
|
+
async function stillSilent(session, item, settleMs, opts, sleep) {
|
|
99
|
+
const since = session.elapsed();
|
|
100
|
+
session.beginAction?.(ReticleTool.CRAWL, { ref: item.ref, action: ActionType.CLICK });
|
|
101
|
+
try {
|
|
102
|
+
const again = await session.command(ReticleCommand.ACT, {
|
|
103
|
+
ref: item.ref,
|
|
104
|
+
action: ActionType.CLICK,
|
|
105
|
+
args: true === opts.confirmDangerous ? { [DANGEROUS_ACTION_CONFIRM_ARG]: true } : {},
|
|
106
|
+
});
|
|
107
|
+
await sleep(settleMs);
|
|
108
|
+
// A control that has GONE (the first click removed it) is not a dead control — it did something.
|
|
109
|
+
if (!again.ok || false === asRecord(again.result)['dispatched'])
|
|
110
|
+
return false;
|
|
111
|
+
return !session.eventsSince(since).some(isActivity);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
session.finishAction?.();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
89
120
|
export async function crawl(session, opts, sleep) {
|
|
90
121
|
const maxSteps = opts.maxSteps ?? CRAWL_DEFAULTS.MAX_STEPS;
|
|
91
122
|
const settleMs = opts.settleMs ?? CRAWL_DEFAULTS.SETTLE_MS;
|
|
@@ -194,14 +225,26 @@ export async function crawl(session, opts, sleep) {
|
|
|
194
225
|
0 === errs.length &&
|
|
195
226
|
!events.some(isActivity) &&
|
|
196
227
|
!legitimatelyInert(item.desc)) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
228
|
+
// CONFIRM IT. One silent sample is a sample, not a fact, and this is the strongest claim the
|
|
229
|
+
// crawler makes — "your button does nothing" is the finding a reader acts on immediately.
|
|
230
|
+
//
|
|
231
|
+
// Measured on a fixture whose FIXED twin has a working primary CTA: the crawl reported it dead
|
|
232
|
+
// on 2 of 3 runs, always a control the drive had already clicked successfully moments earlier.
|
|
233
|
+
// A false positive in this tier is the most expensive kind there is — it is the tier a verdict
|
|
234
|
+
// is built from, and the twin column is the whole claim this product makes.
|
|
235
|
+
//
|
|
236
|
+
// A second click costs one round trip on the rare control that looked dead, and nothing at all
|
|
237
|
+
// on every control that did not. A genuinely dead control is silent twice; the flake is not.
|
|
238
|
+
if (await stillSilent(session, item, settleMs, opts, sleep)) {
|
|
239
|
+
counts.deadControls += 1;
|
|
240
|
+
anomalies.push({
|
|
241
|
+
kind: CrawlAnomalyKind.DEAD_CONTROL,
|
|
242
|
+
ref: item.ref,
|
|
243
|
+
desc: item.desc,
|
|
244
|
+
detail: 'clicked TWICE and the app did nothing either time (no DOM/network/route/signal change)',
|
|
245
|
+
...source,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
205
248
|
}
|
|
206
249
|
}
|
|
207
250
|
// One more look, AFTER the clicks. The enumeration above happened before any of them, so anything
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An issue we have already fixed must not still read as available work.
|
|
3
|
+
*
|
|
4
|
+
* Three contributor PRs died on arrival in one week for this. #373 was filed at 09:52 and fixed by
|
|
5
|
+
* us at 12:33 the same day, then left open for three more days — somebody started on it on day two.
|
|
6
|
+
* #398 shipped and stayed open; a second contributor rebuilt it from scratch. `GITHUB.md` already
|
|
7
|
+
* documents the same failure costing three contributors before either of those, which makes this the
|
|
8
|
+
* third recurrence of a defect we have written down and asked ourselves to remember.
|
|
9
|
+
*
|
|
10
|
+
* Remembering is not a mechanism, and this is the smallest one that works: a commit saying it closes
|
|
11
|
+
* an issue is a claim that the issue is done. If that issue is still open and unlabelled afterwards,
|
|
12
|
+
* the tracker is lying to whoever reads it next — and the person it lies to is by definition someone
|
|
13
|
+
* looking for something to work on.
|
|
14
|
+
*
|
|
15
|
+
* ## Why `fixed-pending-release` is an accepted answer
|
|
16
|
+
*
|
|
17
|
+
* Work fixed on an unreleased branch is legitimately still open; the issue closes when the version
|
|
18
|
+
* ships. The label is what tells a contributor not to start, so the label — not the closure — is
|
|
19
|
+
* what this guard is actually asking for. That is also why the fix is cheap: nobody has to change
|
|
20
|
+
* when they close things, only to say so.
|
|
21
|
+
*
|
|
22
|
+
* Pure. The git log and the GitHub lookup belong to the caller, which is what makes every branch
|
|
23
|
+
* below testable without a network or a repo.
|
|
24
|
+
*/
|
|
25
|
+
export interface IssueState {
|
|
26
|
+
number: number;
|
|
27
|
+
state: 'open' | 'closed';
|
|
28
|
+
labels: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/** The label that means "fixed, waiting for a release" — an honest reason to still be open. */
|
|
31
|
+
export declare const PENDING_RELEASE_LABEL = "fixed-pending-release";
|
|
32
|
+
export declare function closedRefsIn(commitMessage: string): number[];
|
|
33
|
+
/**
|
|
34
|
+
* Of the issues these commits claimed to close, the ones still reading as available work.
|
|
35
|
+
*
|
|
36
|
+
* An issue that could not be looked up is deliberately silent rather than failing. A rate limit, a
|
|
37
|
+
* transferred issue or a deleted one would otherwise turn this red for a reason unrelated to what it
|
|
38
|
+
* guards, and a guard that goes red for unrelated reasons gets disabled — which costs more than the
|
|
39
|
+
* defect it was catching.
|
|
40
|
+
*/
|
|
41
|
+
export declare function staleIssues(claimed: readonly number[], issues: readonly IssueState[]): number[];
|
|
42
|
+
/** The message the guard prints. Held here so the check and its tests cannot describe it differently. */
|
|
43
|
+
export declare function staleIssueReport(stale: readonly number[]): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An issue we have already fixed must not still read as available work.
|
|
3
|
+
*
|
|
4
|
+
* Three contributor PRs died on arrival in one week for this. #373 was filed at 09:52 and fixed by
|
|
5
|
+
* us at 12:33 the same day, then left open for three more days — somebody started on it on day two.
|
|
6
|
+
* #398 shipped and stayed open; a second contributor rebuilt it from scratch. `GITHUB.md` already
|
|
7
|
+
* documents the same failure costing three contributors before either of those, which makes this the
|
|
8
|
+
* third recurrence of a defect we have written down and asked ourselves to remember.
|
|
9
|
+
*
|
|
10
|
+
* Remembering is not a mechanism, and this is the smallest one that works: a commit saying it closes
|
|
11
|
+
* an issue is a claim that the issue is done. If that issue is still open and unlabelled afterwards,
|
|
12
|
+
* the tracker is lying to whoever reads it next — and the person it lies to is by definition someone
|
|
13
|
+
* looking for something to work on.
|
|
14
|
+
*
|
|
15
|
+
* ## Why `fixed-pending-release` is an accepted answer
|
|
16
|
+
*
|
|
17
|
+
* Work fixed on an unreleased branch is legitimately still open; the issue closes when the version
|
|
18
|
+
* ships. The label is what tells a contributor not to start, so the label — not the closure — is
|
|
19
|
+
* what this guard is actually asking for. That is also why the fix is cheap: nobody has to change
|
|
20
|
+
* when they close things, only to say so.
|
|
21
|
+
*
|
|
22
|
+
* Pure. The git log and the GitHub lookup belong to the caller, which is what makes every branch
|
|
23
|
+
* below testable without a network or a repo.
|
|
24
|
+
*/
|
|
25
|
+
/** The label that means "fixed, waiting for a release" — an honest reason to still be open. */
|
|
26
|
+
export const PENDING_RELEASE_LABEL = 'fixed-pending-release';
|
|
27
|
+
/**
|
|
28
|
+
* Issue numbers a commit message CLAIMS to have closed.
|
|
29
|
+
*
|
|
30
|
+
* Only the closing verbs GitHub itself acts on. A bare `#447`, a `Refs #447` or a `see #447` is how
|
|
31
|
+
* a change points at context it did not close — the exact habit we want to encourage — so counting
|
|
32
|
+
* those would make the guard cry wolf on good behaviour, and a check that cries wolf is one people
|
|
33
|
+
* learn to skip.
|
|
34
|
+
*/
|
|
35
|
+
const CLOSING_REF = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi;
|
|
36
|
+
export function closedRefsIn(commitMessage) {
|
|
37
|
+
const seen = new Set();
|
|
38
|
+
for (const match of commitMessage.matchAll(CLOSING_REF)) {
|
|
39
|
+
const raw = match[1];
|
|
40
|
+
if (raw === undefined)
|
|
41
|
+
continue;
|
|
42
|
+
const parsed = Number.parseInt(raw, 10);
|
|
43
|
+
if (Number.isFinite(parsed))
|
|
44
|
+
seen.add(parsed);
|
|
45
|
+
}
|
|
46
|
+
return [...seen];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Of the issues these commits claimed to close, the ones still reading as available work.
|
|
50
|
+
*
|
|
51
|
+
* An issue that could not be looked up is deliberately silent rather than failing. A rate limit, a
|
|
52
|
+
* transferred issue or a deleted one would otherwise turn this red for a reason unrelated to what it
|
|
53
|
+
* guards, and a guard that goes red for unrelated reasons gets disabled — which costs more than the
|
|
54
|
+
* defect it was catching.
|
|
55
|
+
*/
|
|
56
|
+
export function staleIssues(claimed, issues) {
|
|
57
|
+
const byNumber = new Map(issues.map((issue) => [issue.number, issue]));
|
|
58
|
+
return claimed.filter((number) => {
|
|
59
|
+
const issue = byNumber.get(number);
|
|
60
|
+
if (issue === undefined)
|
|
61
|
+
return false;
|
|
62
|
+
if ('closed' === issue.state)
|
|
63
|
+
return false;
|
|
64
|
+
return !issue.labels.includes(PENDING_RELEASE_LABEL);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** The message the guard prints. Held here so the check and its tests cannot describe it differently. */
|
|
68
|
+
export function staleIssueReport(stale) {
|
|
69
|
+
const list = stale.map((n) => `#${String(n)}`).join(', ');
|
|
70
|
+
return (`${list} ${1 === stale.length ? 'is' : 'are'} still OPEN, and a commit on this branch says it ` +
|
|
71
|
+
`closes ${1 === stale.length ? 'it' : 'them'}. Anyone browsing the tracker reads that as work ` +
|
|
72
|
+
`nobody has started. Close ${1 === stale.length ? 'it' : 'them'}, or label ` +
|
|
73
|
+
`${1 === stale.length ? 'it' : 'them'} \`${PENDING_RELEASE_LABEL}\` if the fix is waiting on a ` +
|
|
74
|
+
`release. This has cost three contributors an evening each; it is not bookkeeping.`);
|
|
75
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { sessionRoot } from '../project/session-root.js';
|
|
3
|
+
import { asNumber, asString } from '../tools/tools-helpers.js';
|
|
3
4
|
import { ReticleTool } from '../tools/tool-names.js';
|
|
4
5
|
import { readContract } from '../project/reticle-dir.js';
|
|
5
6
|
import { buildDomainModel } from './domain-model.js';
|
|
@@ -74,7 +75,7 @@ export const DOMAIN_TOOLS = [
|
|
|
74
75
|
if (loaded.ok)
|
|
75
76
|
flows.push(loaded.value);
|
|
76
77
|
}
|
|
77
|
-
const contract = await readContract(deps.fs, deps
|
|
78
|
+
const contract = await readContract(deps.fs, sessionRoot(deps, asString(args['sessionId'])));
|
|
78
79
|
const project = await deps.project.read();
|
|
79
80
|
const runs = project.ok ? project.file.runs : [];
|
|
80
81
|
const model = buildDomainModel(flows, contract.ok ? contract.capabilities : null, runs);
|
package/dist/ee/audit-log.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* ee feature follows: call assertEnterprise...) first, then do the privileged work. The body here is a
|
|
4
4
|
* stub — the real audit sink is the hosted control plane. It exists now to prove the gate.
|
|
5
5
|
*/
|
|
6
|
-
import { assertEnterprise } from '../license/license.js';
|
|
6
|
+
import { assertEnterprise, EnterpriseFeature } from '../license/license.js';
|
|
7
7
|
/** Record an audit event — gated. Throws EnterpriseLicenseError in production without a valid license. */
|
|
8
8
|
export function recordAuditEvent(event, ctx) {
|
|
9
|
-
assertEnterprise(
|
|
9
|
+
assertEnterprise(EnterpriseFeature.AUDIT_LOG, ctx);
|
|
10
10
|
return event; // stub: a real implementation persists to the enterprise audit store
|
|
11
11
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { type ReticleEvent } from '@reticlehq/core';
|
|
2
|
-
import type {
|
|
2
|
+
import type { OwnContradiction } from './contradictions.js';
|
|
3
3
|
/** Contradictions for 2xx responses whose bodies report failure. One per offending call. */
|
|
4
|
-
export declare function findBodyFailures(events: readonly ReticleEvent[]):
|
|
4
|
+
export declare function findBodyFailures(events: readonly ReticleEvent[]): OwnContradiction[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ReticleEvent } from '@reticlehq/core';
|
|
2
|
+
import type { Contradiction, ContradictionOptions } from './contradictions.js';
|
|
3
|
+
/**
|
|
4
|
+
* Rules a CONSUMER adds to the contradiction pass, without editing the ones this package ships.
|
|
5
|
+
*
|
|
6
|
+
* Every rule in `contradictions.ts` is a pure fold over a recording — events in, findings out, no
|
|
7
|
+
* session, no clock, no IO. A service embedding this engine wants rules of exactly that shape for
|
|
8
|
+
* itself, and the only thing standing between it and one was that `findContradictions` folds over a
|
|
9
|
+
* list written into its own body. Appending to that list means editing this package, which means a
|
|
10
|
+
* fork, which means the consumer's rules and this package's rules become one tree to merge forever.
|
|
11
|
+
*
|
|
12
|
+
* A registry rather than a parameter on `findContradictions`, deliberately. There are six call sites
|
|
13
|
+
* and a consumer reaches none of them: the tools call the pass internally, and a tool handler is
|
|
14
|
+
* handed a deps bag, not options. Threading a parameter would mean touching every path a verdict can
|
|
15
|
+
* take — and would still miss the one inside `crawl`. Registration happens once, at boot, by whoever
|
|
16
|
+
* composed the process.
|
|
17
|
+
*
|
|
18
|
+
* This is not a public plugin API and should not be documented as one. It has a single intended
|
|
19
|
+
* consumer, which is a service we own, and its shape is free to change in lockstep with it.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* One consumer rule.
|
|
23
|
+
*
|
|
24
|
+
* Receives the app's OWN events — dev-tooling traffic is already removed, exactly as it is for the
|
|
25
|
+
* shipped rules. A second answer to "which events count" would be a second product.
|
|
26
|
+
*/
|
|
27
|
+
export type ContradictionFold = (events: readonly ReticleEvent[], options: ContradictionOptions) => readonly Contradiction[];
|
|
28
|
+
/**
|
|
29
|
+
* Add a rule to every verdict this engine produces. Returns the undo.
|
|
30
|
+
*
|
|
31
|
+
* The undo is not a convenience: without it a registry is process-global state that tests cannot
|
|
32
|
+
* clean up, so each one starts depending on the order it ran in.
|
|
33
|
+
*/
|
|
34
|
+
export declare function registerContradictionFold(fold: ContradictionFold): () => void;
|
|
35
|
+
/** What is registered right now. Exported so a guard can assert the registry is empty by default. */
|
|
36
|
+
export declare function registeredContradictionFolds(): readonly ContradictionFold[];
|
|
37
|
+
/**
|
|
38
|
+
* Run every registered rule, containing anything one of them throws.
|
|
39
|
+
*
|
|
40
|
+
* A registered fold is somebody else's code running inside every verdict path this package has. If a
|
|
41
|
+
* throw could propagate, one defect in a consumer's rule would take down `assert`, `act_and_wait`,
|
|
42
|
+
* `observe` and `crawl` at once — turning a missing finding into a dead engine. Contained and logged:
|
|
43
|
+
* the rules that did run still report, and the failure is visible rather than swallowed.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runRegisteredFolds(events: readonly ReticleEvent[], options: ContradictionOptions): Contradiction[];
|