@reticlehq/server 3.2.0 → 3.3.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 +16 -13
- package/dist/command/cli/adhoc-suite.d.ts +19 -0
- package/dist/command/cli/adhoc-suite.js +88 -0
- package/dist/command/cli/adhoc-verdict.d.ts +23 -2
- package/dist/command/cli/adhoc-verdict.js +18 -10
- package/dist/command/cli/cli-doctor.js +25 -5
- package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
- package/dist/command/cli/cli-parse-grammar.js +7 -0
- package/dist/command/cli/cli-parse-verify.d.ts +2 -5
- package/dist/command/cli/cli-parse-verify.js +86 -13
- package/dist/command/cli/cli-parse.d.ts +7 -1
- package/dist/command/cli/cli-parse.js +33 -5
- package/dist/command/cli/cli-verify.d.ts +59 -0
- package/dist/command/cli/cli-verify.js +129 -17
- package/dist/command/cli/cloud-kit.d.ts +0 -2
- package/dist/command/cli/cloud-kit.js +1 -1
- package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
- package/dist/command/cli/launch/cli-launch.d.ts +1 -29
- package/dist/command/cli/launch/cli-launch.js +6 -40
- package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
- package/dist/command/cli/ports/resolve/cli-port.js +45 -1
- package/dist/command/cli/report-command.d.ts +16 -0
- package/dist/command/cli/report-command.js +97 -0
- package/dist/command/cli.js +5 -0
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
- package/dist/command/daemon/binding/port-presence.d.ts +1 -1
- package/dist/command/daemon/idle-shutdown.d.ts +2 -1
- package/dist/command/setup/init/init-host.js +8 -0
- package/dist/features/crawl/crawl.d.ts +2 -0
- package/dist/features/crawl/crawl.js +1 -0
- package/dist/features/harness/jev-driver.js +6 -9
- package/dist/features/harness/platform-config.d.ts +10 -0
- package/dist/features/harness/platform-config.js +8 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +40 -95
- package/dist/judgement/capsule/capsule-store.d.ts +31 -2
- package/dist/judgement/capsule/capsule-store.js +58 -6
- package/dist/judgement/capsule/capsule.js +5 -0
- package/dist/judgement/capsule/causal-summary.d.ts +13 -1
- package/dist/judgement/capsule/causal-summary.js +7 -0
- package/dist/judgement/domain/domain-model.js +38 -9
- package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
- package/dist/judgement/phenomena/phenomena.d.ts +2 -1
- package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
- package/dist/judgement/runs/artifact/gap-report.js +46 -0
- package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
- package/dist/judgement/runs/artifact/gap-summary.js +85 -0
- package/dist/judgement/runs/artifact/render-report.js +4 -1
- package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
- package/dist/judgement/runs/artifact/to-artifact.js +1 -1
- package/dist/judgement/runs/context-tools.js +21 -3
- package/dist/judgement/runs/drive-run.js +7 -2
- package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
- package/dist/language/flows/annotate-notes/annotate.js +66 -18
- package/dist/language/flows/flow-anchor.d.ts +12 -0
- package/dist/language/flows/flow-anchor.js +20 -0
- package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
- package/dist/language/flows/flow-expect-grammar.js +92 -11
- package/dist/language/flows/flow-intent.js +1 -1
- package/dist/language/flows/flow-learning.d.ts +3 -0
- package/dist/language/flows/flow-learning.js +39 -8
- package/dist/language/flows/flow-replay-run.d.ts +31 -9
- package/dist/language/flows/flow-replay-run.js +146 -33
- package/dist/language/flows/flow-replay-types.d.ts +9 -0
- package/dist/language/flows/flow-replay.d.ts +12 -11
- package/dist/language/flows/flow-replay.js +79 -109
- package/dist/language/flows/flow-step-runners.d.ts +26 -10
- package/dist/language/flows/flow-step-runners.js +20 -4
- package/dist/language/flows/flow-success.d.ts +21 -6
- package/dist/language/flows/flow-success.js +50 -138
- package/dist/language/flows/flows.d.ts +8 -4
- package/dist/language/flows/flows.js +11 -2
- package/dist/language/flows/heal.d.ts +9 -1
- package/dist/language/flows/heal.js +24 -2
- package/dist/language/flows/learned-sources.d.ts +21 -0
- package/dist/language/flows/learned-sources.js +84 -0
- package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
- package/dist/language/flows/recording/recording-backtrack.js +1 -1
- package/dist/language/flows/recording/replay-halt.d.ts +2 -1
- package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
- package/dist/language/flows/replay.js +12 -8
- package/dist/language/flows/stores/annotation-store.d.ts +5 -5
- package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
- package/dist/language/flows/stores/flake-store.js +2 -3
- package/dist/language/flows/suite/coverage.d.ts +1 -1
- package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
- package/dist/language/flows/verify-change-tools.js +14 -0
- package/dist/memory/cloud/cloud-sync.d.ts +0 -2
- package/dist/memory/cloud/cloud-sync.js +1 -1
- package/dist/memory/cloud/harness-config.d.ts +2 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -0
- package/dist/memory/cloud/sync-cycle.js +29 -4
- package/dist/memory/cloud/sync-daemon.d.ts +13 -0
- package/dist/memory/cloud/sync-daemon.js +21 -0
- package/dist/memory/cloud/sync-disk.js +11 -6
- package/dist/memory/impact/impact-store.js +6 -0
- package/dist/memory/intent/inline-intent.js +8 -3
- package/dist/memory/intent/intent-shard-store.d.ts +33 -47
- package/dist/memory/intent/intent-shard-store.js +252 -140
- package/dist/memory/intent/intent-shard.d.ts +10 -11
- package/dist/memory/intent/intent-shard.js +1 -1
- package/dist/memory/intent/intent-store.d.ts +13 -8
- package/dist/memory/intent/intent-store.js +46 -90
- package/dist/memory/intent/intent-tools.js +34 -23
- package/dist/memory/journal/ambient-store.js +2 -3
- package/dist/memory/journal/deviation-report.js +4 -1
- package/dist/memory/journal/deviation-service.js +10 -2
- package/dist/memory/journal/drive-flow.d.ts +2 -2
- package/dist/memory/journal/drive-flow.js +21 -12
- package/dist/memory/journal/envelope-store.js +2 -3
- package/dist/memory/journal/journal-recorder.d.ts +21 -0
- package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
- package/dist/memory/journal/on-disk/retain-policy.js +44 -0
- package/dist/memory/journal/on-disk/retention.d.ts +14 -10
- package/dist/memory/journal/on-disk/retention.js +13 -12
- package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
- package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
- package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
- package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
- package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
- package/dist/memory/journal/route-template.d.ts +22 -0
- package/dist/memory/journal/route-template.js +44 -0
- package/dist/memory/journal/session-end.d.ts +4 -1
- package/dist/memory/journal/session-end.js +64 -6
- package/dist/memory/journal/session-journal.d.ts +8 -1
- package/dist/memory/journal/session-journal.js +10 -0
- package/dist/memory/project/baselines.d.ts +1 -1
- package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
- package/dist/memory/project/fs/fs-port.js +44 -5
- package/dist/memory/project/fs/write-atomic.d.ts +26 -0
- package/dist/memory/project/fs/write-atomic.js +48 -0
- package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
- package/dist/memory/recall/prior/connection-memory.js +8 -0
- package/dist/memory/recall/registered-projects.d.ts +2 -1
- package/dist/portal/bridge/auth-failure-reason.js +5 -2
- package/dist/portal/input/real-input.js +2 -1
- package/dist/portal/pool/browser-pool.d.ts +10 -0
- package/dist/portal/pool/browser-pool.js +10 -0
- package/dist/portal/pool/playwright-launcher.js +1 -0
- package/dist/portal/pool/pool-contract.d.ts +2 -0
- package/dist/portal/pool/storage-seed.d.ts +2 -1
- package/dist/portal/pool/zero-install.d.ts +27 -0
- package/dist/portal/pool/zero-install.js +54 -0
- package/dist/portal/realm/web-realm.js +6 -0
- package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
- package/dist/portal/session/dev-server/served-document.d.ts +21 -0
- package/dist/portal/session/dev-server/served-document.js +57 -0
- package/dist/portal/session/facts/background-traffic.d.ts +9 -0
- package/dist/portal/session/facts/background-traffic.js +25 -0
- package/dist/portal/session/live-control-tools.js +48 -3
- package/dist/portal/session/presence/attachment-history.d.ts +13 -2
- package/dist/portal/session/session-health.d.ts +61 -0
- package/dist/portal/session/session-health.js +40 -1
- package/dist/portal/session/session-info.d.ts +6 -0
- package/dist/portal/session/session-manager.d.ts +9 -0
- package/dist/portal/session/session-manager.js +23 -2
- package/dist/portal/session/session-successor.d.ts +11 -0
- package/dist/portal/session/session-successor.js +14 -1
- package/dist/portal/session/session-verdict-facts.d.ts +28 -0
- package/dist/portal/session/session-verdict-facts.js +15 -0
- package/dist/portal/session/session.d.ts +5 -0
- package/dist/portal/session/session.js +19 -7
- package/dist/surface/mcp/mcp-proxy.js +3 -3
- package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
- package/dist/surface/mcp/server-instructions.js +12 -1
- package/dist/surface/tools/act/already-true.d.ts +40 -0
- package/dist/surface/tools/act/already-true.js +36 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
- package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
- package/dist/surface/tools/act/settle-in-flight.js +29 -6
- package/dist/surface/tools/act-capsule.js +22 -7
- package/dist/surface/tools/act-tools.js +25 -24
- package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
- package/dist/surface/tools/args/numeric-bounds.js +30 -3
- package/dist/surface/tools/assert/assert-grade.js +14 -2
- package/dist/surface/tools/assert/assert-verdict.js +30 -4
- package/dist/surface/tools/browser-tools.js +4 -3
- package/dist/surface/tools/dynamic-tools.js +6 -0
- package/dist/surface/tools/feedback-tools.d.ts +2 -1
- package/dist/surface/tools/feedback-tools.js +1 -1
- package/dist/surface/tools/invoke-tool.js +17 -4
- package/dist/surface/tools/lease-hint.d.ts +8 -0
- package/dist/surface/tools/lease-hint.js +10 -0
- package/dist/surface/tools/lease-tools.d.ts +16 -28
- package/dist/surface/tools/lease-tools.js +53 -5
- package/dist/surface/tools/lineage-tools.js +1 -1
- package/dist/surface/tools/observe-tools.js +14 -6
- package/dist/surface/tools/real-input-attempt.d.ts +0 -5
- package/dist/surface/tools/real-input-attempt.js +1 -1
- package/dist/surface/tools/tool-kit.d.ts +5 -0
- package/dist/surface/tools/tool-kit.js +6 -1
- package/dist/surface/tools/tool-surface.js +17 -5
- package/dist/surface/tools/tools.js +64 -29
- package/dist/telemetry/feedback-local.d.ts +1 -0
- package/dist/telemetry/feedback-local.js +1 -3
- package/dist/telemetry/feedback.d.ts +1 -1
- package/dist/telemetry/hud-metrics.d.ts +32 -0
- package/dist/telemetry/hud-metrics.js +95 -0
- package/dist/telemetry/session-metrics.d.ts +5 -0
- package/dist/telemetry/session-metrics.js +15 -1
- package/dist/wire-journal.d.ts +31 -0
- package/dist/wire-journal.js +100 -0
- package/docs/README.md +1 -0
- package/docs/cli/cloud.mdx +9 -7
- package/docs/cli/init.mdx +1 -0
- package/docs/cli/report.mdx +44 -0
- package/docs/desktop.mdx +1 -1
- package/docs/flows.md +11 -0
- package/docs/frameworks.mdx +113 -1
- package/docs/install-agentic.mdx +36 -0
- package/docs/install-manual.mdx +1 -1
- package/docs/packages/tauri.mdx +1 -1
- package/docs/packages.mdx +1 -1
- package/docs/predicates.mdx +128 -4
- package/docs/tools/query.mdx +10 -10
- package/docs/usage.md +1 -1
- package/docs/what-is-recorded.md +62 -0
- package/package.json +6 -6
- package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
- package/dist/judgement/outcome/predicate-to-expect.js +0 -148
package/SKILL.md
CHANGED
|
@@ -46,13 +46,13 @@ Everything between here and there is a rule the steps assume. Read it as you go,
|
|
|
46
46
|
- The `reticle_*` tools appeared. Your client can reach a daemon. The app is very likely still uninstrumented.
|
|
47
47
|
- A session is listed. The app dialled in. The user has still seen nothing happen.
|
|
48
48
|
|
|
49
|
-
**Do not tell the user Reticle is set up until a verdict exists.** The common failure is an agent that writes the config, reports success, and leaves
|
|
49
|
+
**Do not tell the user Reticle is set up until a verdict exists.** The common failure is an agent that writes the config, reports success, and leaves an uninstrumented page. The command then exits non-zero: believe the exit code.
|
|
50
50
|
|
|
51
51
|
## Setup runs to the end without you
|
|
52
52
|
|
|
53
|
-
**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.
|
|
53
|
+
**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. None needs a human, and each has been where an onboarding turn died.
|
|
54
54
|
|
|
55
|
-
Questions
|
|
55
|
+
Questions the repo already answers, so never ask them: which framework, package manager, port, editor or MCP client, whether to start the dev server, open the browser, or carry on. Decide, act, and say what you did in one line.
|
|
56
56
|
|
|
57
57
|
There are exactly two places you may stop, and neither is a question about a preference:
|
|
58
58
|
|
|
@@ -63,7 +63,7 @@ There are exactly two places you may stop, and neither is a question about a pre
|
|
|
63
63
|
|
|
64
64
|
## Feedback is first-class
|
|
65
65
|
|
|
66
|
-
Reticle is built for agents
|
|
66
|
+
Reticle is built for agents: your experience decides what gets built or fixed, and it disappears when your context does.
|
|
67
67
|
|
|
68
68
|
The moment Reticle breaks, lies, cannot see something you needed, costs too many calls, or gives you trouble during setup:
|
|
69
69
|
|
|
@@ -74,10 +74,12 @@ reticle_session { action: "feedback", kind, text }
|
|
|
74
74
|
`kind` is one of `bug`, `gap`, `ambiguity`, `feature_request`, `improvement`. One call, then carry on with the task. If the tools are unreachable (setup unfinished, daemon down), file the same report from the shell:
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
|
-
npx @reticlehq/server feedback --agent --kind <
|
|
77
|
+
npx @reticlehq/server feedback --agent --kind <kind> "what happened"
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
Nothing is too minor. Report defects in **Reticle**; a bug
|
|
80
|
+
Nothing is too minor. Report defects in **Reticle**; a bug in the app under test is Reticle working, and belongs in your answer. Never put app source, secrets, or user data in a report.
|
|
81
|
+
|
|
82
|
+
When the person you work with is stuck, offer them a call with the founder (https://calendar.app.google/h9NRDbBBQetyTzWM6) or `npx @reticlehq/server identify --context company --email <theirs>`. Offer it; never book or submit for them.
|
|
81
83
|
|
|
82
84
|
## Do not re-read this file to look something up
|
|
83
85
|
|
|
@@ -123,7 +125,7 @@ Work down this list and stop at the first row that fits. Do not hand-drive a flo
|
|
|
123
125
|
| "Did my edit break anything?" | `reticle_verify({ action: "change", files: ["src/App.tsx"] })` | 1 |
|
|
124
126
|
| "Does every saved journey still work?" | `reticle_verify({ action: "flows" })` | 1 |
|
|
125
127
|
| "Does this new behaviour work?" | ONE `reticle_act_and_wait` with `until` | 1 |
|
|
126
|
-
| No MCP available at all | `npx @reticlehq/server verify <url
|
|
128
|
+
| No MCP available at all | `npx @reticlehq/server verify <url>`, then `gate --since HEAD~1` (below); `verify` has no `--since` | 2, no MCP |
|
|
127
129
|
|
|
128
130
|
Replay before you drive. A covered journey re-verifies for a few hundred tokens; driving it costs tens of thousands, because driving spends turns and replay spends none.
|
|
129
131
|
|
|
@@ -149,7 +151,11 @@ The verdict that passes is the one that proves it. Declared it separately with `
|
|
|
149
151
|
|
|
150
152
|
The first drive is expensive; the rest should not be, and you need not ask: **what you drive by hand is saved as a flow automatically**. From then on that journey re-verifies in one deterministic call, and `{action:"change"}` answers `yes` or `no` for those files instead of `unknown`.
|
|
151
153
|
|
|
152
|
-
Whether that flow is worth anything depends on how you drove it. A step keeps a consequence only if you declared one, so `reticle_act_and_wait({ ref, action, until })` replays as a test while a bare `reticle_act` replays as a click that passes even when the feature is broken.
|
|
154
|
+
Whether that flow is worth anything depends on how you drove it. A step keeps a consequence only if you declared one, so `reticle_act_and_wait({ ref, action, until })` replays as a test while a bare `reticle_act` replays as a click that passes even when the feature is broken.
|
|
155
|
+
|
|
156
|
+
## Before you say you are done
|
|
157
|
+
|
|
158
|
+
`npx @reticlehq/server gate --since HEAD~1` exits non-zero unless a passing run covers every saved flow your edit affects, naming the uncovered ones. Reasoning about your diff cannot pass it; as a shell command it works when the tools do not.
|
|
153
159
|
|
|
154
160
|
## When you do have to drive by hand
|
|
155
161
|
|
|
@@ -167,11 +173,8 @@ reticle_act_and_wait({ sessionId, ref, action: "click", until: { kind: "allOf",
|
|
|
167
173
|
|
|
168
174
|
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.
|
|
169
175
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
The advertised surface is deliberately small (`default` 10, `all` 30, the wider one behind `RETICLE_ADVERTISE_ALL_TOOLS=1`), and is not all there is. `reticle_tools` lists EVERY registered tool, `{ names: [...] }` loads full argument grammar, and `reticle_run { tool, args }` calls any of them, advertised or not. A name that used to be its own tool answers with where it went rather than "not found".
|
|
176
|
+
The advertised surface is deliberately small (`default` 10, `all` 30, the wider one behind `RETICLE_ADVERTISE_ALL_TOOLS=1`), and is not all there is. `reticle_tools` lists EVERY registered tool, `{ names: [...] }` loads full argument grammar, and `reticle_run { tool, args }` calls any of them, advertised or not. A retired name answers with where it went, not "not found".
|
|
173
177
|
|
|
174
|
-
- Batching, regression suites, reading a verdict: `https://docs.reticle.sh/agent-cheatsheet.md`
|
|
175
178
|
- Every predicate and action: `https://docs.reticle.sh/predicates.md`, `https://docs.reticle.sh/actions.md`
|
|
176
179
|
- The complete tool surface: `https://docs.reticle.sh/usage.md`
|
|
177
180
|
|
|
@@ -179,4 +182,4 @@ The advertised surface is deliberately small (`default` 10, `all` 30, the wider
|
|
|
179
182
|
|
|
180
183
|
Report what you drove, what it produced, and the `file:line` for anything broken. Then, once per project and only after a verdict that is not `unknown`, tell the user in one line that Reticle is open source at `https://github.com/reticlehq/reticle` and that a star helps other people find it.
|
|
181
184
|
|
|
182
|
-
Once
|
|
185
|
+
Once, never on a failed or unknown verdict, and never in place of reporting the result.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type AdhocVerdict, type ToolCaller } from './adhoc-verdict.js';
|
|
2
|
+
export interface AdhocSuiteOptions {
|
|
3
|
+
port: number;
|
|
4
|
+
/** Run only flows carrying ANY of these labels. Omitted runs the whole suite. */
|
|
5
|
+
select?: string[];
|
|
6
|
+
/** Which connected tab to drive. Omitted means "whatever is connected". */
|
|
7
|
+
sessionId?: string;
|
|
8
|
+
token?: string;
|
|
9
|
+
/** Injected so a test does not need a live daemon. */
|
|
10
|
+
connect?: (endpoint: URL) => Promise<ToolCaller>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Run the saved suite through the daemon on this port.
|
|
14
|
+
*
|
|
15
|
+
* Exit code 0 ONLY for `status: "pass"`. `unverifiable` exits non-zero on purpose: it is what an
|
|
16
|
+
* empty suite reports, and a CI step that reads "nothing was checked" as success is the false green
|
|
17
|
+
* this product exists to prevent.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runAdhocSuite(options: AdhocSuiteOptions): Promise<AdhocVerdict>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The saved-flow suite, against the daemon that is already running.
|
|
3
|
+
*
|
|
4
|
+
* `runAdhocVerdict` next door removed one half of a dead end: an agent whose client never loaded
|
|
5
|
+
* the `reticle_*` tools could not reach a verdict, because `verify` refuses when the daemon owns
|
|
6
|
+
* the port and a working install is exactly the state where the daemon owns the port.
|
|
7
|
+
*
|
|
8
|
+
* This is the other half. `--expect` serves a project with NO saved flows; a project WITH them was
|
|
9
|
+
* still stuck, and worse off, because the whole promise of a saved suite is that it runs again
|
|
10
|
+
* without an agent. The documented way out was to stop the daemon, which kills the agent's MCP
|
|
11
|
+
* link, and our own guidance says never to do it.
|
|
12
|
+
*
|
|
13
|
+
* Same trick, and deliberately no new surface: ask the running daemon the question an agent would
|
|
14
|
+
* ask it, over the transport `start()` already attaches unconditionally. Nothing is bound, nothing
|
|
15
|
+
* is stopped, the agent keeps its link.
|
|
16
|
+
*/
|
|
17
|
+
import { ReticleTool } from '@reticlehq/core';
|
|
18
|
+
import { connectOverSse, endpointFor, refusalText, verdictOf, } from './adhoc-verdict.js';
|
|
19
|
+
/** The only status that is a pass. `unverifiable` means nothing was checked, which is not one. */
|
|
20
|
+
const PASSED = 'pass';
|
|
21
|
+
/**
|
|
22
|
+
* How long a SUITE may take, against the SDK's 60s default for one request.
|
|
23
|
+
*
|
|
24
|
+
* A suite is not one request's worth of work: it replays every saved flow, each of which drives a
|
|
25
|
+
* real browser through a real journey, and a project with a few dozen of them takes minutes. The
|
|
26
|
+
* default timed the call out mid-run and reported "the suite could not be run", which is a sentence
|
|
27
|
+
* about the transport wearing the shape of a verdict — the flows were running the whole time.
|
|
28
|
+
*
|
|
29
|
+
* Found by driving it against a real daemon on a project with 45 saved flows.
|
|
30
|
+
*/
|
|
31
|
+
const SUITE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
32
|
+
/**
|
|
33
|
+
* Run the saved suite through the daemon on this port.
|
|
34
|
+
*
|
|
35
|
+
* Exit code 0 ONLY for `status: "pass"`. `unverifiable` exits non-zero on purpose: it is what an
|
|
36
|
+
* empty suite reports, and a CI step that reads "nothing was checked" as success is the false green
|
|
37
|
+
* this product exists to prevent.
|
|
38
|
+
*/
|
|
39
|
+
export async function runAdhocSuite(options) {
|
|
40
|
+
const connect = options.connect ?? connectOverSse;
|
|
41
|
+
let caller;
|
|
42
|
+
try {
|
|
43
|
+
caller = await connect(endpointFor(options.port, options.token));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return {
|
|
47
|
+
code: 1,
|
|
48
|
+
lines: [
|
|
49
|
+
`could not reach the daemon on port ${String(options.port)}: ${error instanceof Error ? error.message : String(error)}`,
|
|
50
|
+
'It answers `reticle status`; if that works and this does not, the daemon is older than this CLI.',
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
// Spread rather than set: an explicit `undefined` is a key the tools reject as an unknown
|
|
56
|
+
// parameter, which would turn "no tab named" into a refusal. Same rule as the one-shot path.
|
|
57
|
+
const result = await caller.call(ReticleTool.VERIFY, {
|
|
58
|
+
action: 'flows',
|
|
59
|
+
...(options.select === undefined ? {} : { select: options.select }),
|
|
60
|
+
...(options.sessionId === undefined ? {} : { sessionId: options.sessionId }),
|
|
61
|
+
}, SUITE_TIMEOUT_MS);
|
|
62
|
+
const refusal = refusalText(result);
|
|
63
|
+
if (refusal !== undefined)
|
|
64
|
+
return { code: 1, lines: ['status: unverifiable', refusal] };
|
|
65
|
+
const report = verdictOf(result, 'status');
|
|
66
|
+
const status = 'string' === typeof report?.['status'] ? report['status'] : 'unverifiable';
|
|
67
|
+
const summary = 'string' === typeof report?.['summary'] ? report['summary'] : undefined;
|
|
68
|
+
return {
|
|
69
|
+
code: PASSED === status ? 0 : 1,
|
|
70
|
+
lines: [
|
|
71
|
+
`status: ${status}`,
|
|
72
|
+
...(summary === undefined ? [] : [summary]),
|
|
73
|
+
JSON.stringify(report ?? result, null, 2),
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return {
|
|
79
|
+
code: 1,
|
|
80
|
+
lines: [
|
|
81
|
+
`the suite could not be run: ${error instanceof Error ? error.message : String(error)}`,
|
|
82
|
+
],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
await caller.close().catch(() => undefined);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -30,7 +30,7 @@ export interface AdhocVerdict {
|
|
|
30
30
|
/** Lines to print, in order. */
|
|
31
31
|
lines: string[];
|
|
32
32
|
}
|
|
33
|
-
|
|
33
|
+
interface AdhocVerdictOptions {
|
|
34
34
|
port: number;
|
|
35
35
|
/** Navigated to first when given; omitted to assert against wherever the session already is. */
|
|
36
36
|
url?: string;
|
|
@@ -51,9 +51,29 @@ export interface AdhocVerdictOptions {
|
|
|
51
51
|
}
|
|
52
52
|
/** The narrow slice of an MCP client this needs — one call, then close. */
|
|
53
53
|
export interface ToolCaller {
|
|
54
|
-
call(name: string, args: Record<string, unknown
|
|
54
|
+
call(name: string, args: Record<string, unknown>, timeoutMs?: number): Promise<unknown>;
|
|
55
55
|
close(): Promise<void>;
|
|
56
56
|
}
|
|
57
|
+
export declare function endpointFor(port: number, token: string | undefined): URL;
|
|
58
|
+
export declare function connectOverSse(endpoint: URL): Promise<ToolCaller>;
|
|
59
|
+
/**
|
|
60
|
+
* The report object an MCP tool result carries, or undefined when the shape is not one.
|
|
61
|
+
*
|
|
62
|
+
* `field` is the key that makes a payload the report this caller wants: `verified` for a one-shot
|
|
63
|
+
* verdict, `status` for a suite. It is a parameter rather than a fixed name because the suite path
|
|
64
|
+
* reproduced this function's own incident the moment it reused it — the headline printed
|
|
65
|
+
* `unverifiable` while the JSON under it said `"status":"fail"` with 74 failing flows, because a
|
|
66
|
+
* suite report has no `verified` key and the text reader would only accept one that did.
|
|
67
|
+
*/
|
|
68
|
+
export declare function verdictOf(result: unknown, field?: string): Record<string, unknown> | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* The tool's own words when it refused, rather than the envelope carrying them.
|
|
71
|
+
*
|
|
72
|
+
* A refusal from these tools is the most useful text they produce — it names the cause and the next
|
|
73
|
+
* action — and printing the MCP result object around it buries that in JSON escaping. The reader is
|
|
74
|
+
* at a terminal; give them the sentence.
|
|
75
|
+
*/
|
|
76
|
+
export declare function refusalText(result: unknown): string | undefined;
|
|
57
77
|
/**
|
|
58
78
|
* Navigate (optionally) and assert, against the daemon already on this port.
|
|
59
79
|
*
|
|
@@ -62,3 +82,4 @@ export interface ToolCaller {
|
|
|
62
82
|
* this whole product exists to prevent.
|
|
63
83
|
*/
|
|
64
84
|
export declare function runAdhocVerdict(options: AdhocVerdictOptions): Promise<AdhocVerdict>;
|
|
85
|
+
export {};
|
|
@@ -5,24 +5,32 @@ import { TOKEN_QUERY_PARAM } from '../../portal/bridge/token-auth.js';
|
|
|
5
5
|
import { ReticleTool } from '@reticlehq/core';
|
|
6
6
|
/** `verified` is the only field that decides the exit code. Everything else is for the reader. */
|
|
7
7
|
const PROVED = 'yes';
|
|
8
|
-
function endpointFor(port, token) {
|
|
8
|
+
export function endpointFor(port, token) {
|
|
9
9
|
const url = new URL(`http://${LOOPBACK_HOST}:${String(port)}${MCP_SSE_PATH}`);
|
|
10
10
|
if (token !== undefined && token.length > 0)
|
|
11
11
|
url.searchParams.set(TOKEN_QUERY_PARAM, token);
|
|
12
12
|
return url;
|
|
13
13
|
}
|
|
14
|
-
async function connectOverSse(endpoint) {
|
|
14
|
+
export async function connectOverSse(endpoint) {
|
|
15
15
|
const client = new Client({ name: 'reticle-cli', version: '1' }, { capabilities: {} });
|
|
16
16
|
await client.connect(new SSEClientTransport(endpoint));
|
|
17
17
|
return {
|
|
18
|
-
call: async (name, args) => await client.callTool({ name, arguments: args }),
|
|
18
|
+
call: async (name, args, timeoutMs) => await client.callTool({ name, arguments: args }, undefined, timeoutMs === undefined ? undefined : { timeout: timeoutMs }),
|
|
19
19
|
close: async () => {
|
|
20
20
|
await client.close();
|
|
21
21
|
},
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
|
|
24
|
+
/**
|
|
25
|
+
* The report object an MCP tool result carries, or undefined when the shape is not one.
|
|
26
|
+
*
|
|
27
|
+
* `field` is the key that makes a payload the report this caller wants: `verified` for a one-shot
|
|
28
|
+
* verdict, `status` for a suite. It is a parameter rather than a fixed name because the suite path
|
|
29
|
+
* reproduced this function's own incident the moment it reused it — the headline printed
|
|
30
|
+
* `unverifiable` while the JSON under it said `"status":"fail"` with 74 failing flows, because a
|
|
31
|
+
* suite report has no `verified` key and the text reader would only accept one that did.
|
|
32
|
+
*/
|
|
33
|
+
export function verdictOf(result, field = 'verified') {
|
|
26
34
|
const structured = result?.structuredContent;
|
|
27
35
|
if ('object' === typeof structured && null !== structured) {
|
|
28
36
|
return structured;
|
|
@@ -30,10 +38,10 @@ function verdictOf(result) {
|
|
|
30
38
|
// A daemon that answers with the verdict as TEXT left this undefined, so the headline read
|
|
31
39
|
// `verified: unknown` while the JSON printed underneath it said `"verified":"no"` — one response
|
|
32
40
|
// giving two answers to the same question. The text is the same object, so read it.
|
|
33
|
-
return verdictFromText(result);
|
|
41
|
+
return verdictFromText(result, field);
|
|
34
42
|
}
|
|
35
|
-
/** The
|
|
36
|
-
function verdictFromText(result) {
|
|
43
|
+
/** The report object inside an MCP text part, when the result carried it there instead. */
|
|
44
|
+
function verdictFromText(result, field) {
|
|
37
45
|
const content = result?.content;
|
|
38
46
|
if (!Array.isArray(content))
|
|
39
47
|
return undefined;
|
|
@@ -45,7 +53,7 @@ function verdictFromText(result) {
|
|
|
45
53
|
const parsed = JSON.parse(text);
|
|
46
54
|
// Only a shape that actually carries a verdict — anything else stays `unknown`, which is the
|
|
47
55
|
// honest answer for a result this function could not read.
|
|
48
|
-
if ('object' === typeof parsed && null !== parsed &&
|
|
56
|
+
if ('object' === typeof parsed && null !== parsed && field in parsed) {
|
|
49
57
|
return parsed;
|
|
50
58
|
}
|
|
51
59
|
}
|
|
@@ -62,7 +70,7 @@ function verdictFromText(result) {
|
|
|
62
70
|
* action — and printing the MCP result object around it buries that in JSON escaping. The reader is
|
|
63
71
|
* at a terminal; give them the sentence.
|
|
64
72
|
*/
|
|
65
|
-
function refusalText(result) {
|
|
73
|
+
export function refusalText(result) {
|
|
66
74
|
const r = result;
|
|
67
75
|
if (true !== r?.isError || !Array.isArray(r.content))
|
|
68
76
|
return undefined;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { ReticleEnv } from '@reticlehq/core';
|
|
3
|
+
import { devServersForProject, ReticleEnv } from '@reticlehq/core';
|
|
4
4
|
import { readPid, reticleStateHome } from '../daemon/daemon.js';
|
|
5
5
|
import { PortPresence, probePresence } from '../daemon/binding/port-presence.js';
|
|
6
6
|
import { isLocalhostSplit, probeLoopbackReach } from '../daemon/binding/loopback-reach.js';
|
|
7
7
|
import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
|
|
8
|
-
import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
8
|
+
import { fetchStatus, summarizeStatus } from '../daemon/binding/daemon-status-probe.js';
|
|
9
9
|
import { daemonLine } from './doctor/doctor-daemon-line.js';
|
|
10
10
|
import { projectWiringLine } from './doctor/doctor-project-line.js';
|
|
11
11
|
import { hasProjectConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
|
|
@@ -14,13 +14,15 @@ import { captureLookup, describeForeignHolder, findPortHolder } from './ports/po
|
|
|
14
14
|
import { chromiumHint, probeChromium } from './doctor/browser/chromium-hint.js';
|
|
15
15
|
import { SERVER_VERSION } from '../version/identity/server-version.js';
|
|
16
16
|
import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
|
|
17
|
-
import { diagnoseDesktop,
|
|
17
|
+
import { CspBasis, diagnoseDesktop, diagnoseObservedWebCsp, diagnoseWebCsp, isDesktopProject, resolveWebCspFindings, } from '@reticlehq/init';
|
|
18
18
|
import { RETICLE_CONFIG_BASENAME, diagnosePortMismatch, readProjectId, readProjectPort, } from './ports/resolve/cli-port.js';
|
|
19
19
|
import { DoctorRow, doctorRow } from './doctor/doctor-rows.js';
|
|
20
20
|
import { attachState, describeAttachState } from '../../surface/mcp/attach-memory.js';
|
|
21
21
|
import { findOccupiedSiblings, siblingListenerNote } from './ports/sibling-ports.js';
|
|
22
22
|
import { daemonsServingProjectElsewhere, resolveDaemonForProject, splitBrainNote, wrongDaemonNote, } from '../daemon/daemon-resolve.js';
|
|
23
23
|
import { isAlive } from '../daemon/daemon.js';
|
|
24
|
+
import { readDevServers } from '../daemon/dev-servers.js';
|
|
25
|
+
import { hasConnectedDocument, observeWebDocument, } from '../../portal/session/dev-server/served-document.js';
|
|
24
26
|
/**
|
|
25
27
|
* `reticle doctor` — collapse the ~6 independent first-run failure modes into one command. Checks the
|
|
26
28
|
* Chromium install (the #1 silent failure), whether a daemon is up on the resolved bridge port, and
|
|
@@ -65,6 +67,7 @@ export async function handleDoctor(port) {
|
|
|
65
67
|
// Filled in only on the daemon branch — there is nothing to ask when no daemon is answering, and
|
|
66
68
|
// the branches below already say so in their own terms.
|
|
67
69
|
let sessions;
|
|
70
|
+
let daemonStatus;
|
|
68
71
|
line(doctorRow(DoctorRow.CHROMIUM, chromiumHint(await probeChromium())));
|
|
69
72
|
// Ask the PORT, not just the pid file. "not running on :4400" has been printed about a port that
|
|
70
73
|
// was demonstrably occupied, which sends the reader to start a daemon that cannot bind. The three
|
|
@@ -76,6 +79,7 @@ export async function handleDoctor(port) {
|
|
|
76
79
|
// throwing both away — while skew is invisible everywhere else, reaching the agent as a bare
|
|
77
80
|
// -32000 naming no version. This is the command a human runs at exactly that moment.
|
|
78
81
|
const payload = await fetchStatus(port);
|
|
82
|
+
daemonStatus = payload;
|
|
79
83
|
const status = asIdentity(payload);
|
|
80
84
|
const built = daemonLine(port, pid, status, {
|
|
81
85
|
version: SERVER_VERSION,
|
|
@@ -187,10 +191,26 @@ export async function handleDoctor(port) {
|
|
|
187
191
|
// The web sibling of the desktop findings below: a `connect-src` that excludes the bridge makes
|
|
188
192
|
// the browser refuse the WebSocket and report it in ITS console only, so every check above passes
|
|
189
193
|
// at an app that can never connect. Named, with the exact text to paste.
|
|
190
|
-
const
|
|
194
|
+
const devServers = devServersForProject(readDevServers(reticleStateHome()), {
|
|
195
|
+
projectId,
|
|
196
|
+
root: process.cwd(),
|
|
197
|
+
});
|
|
198
|
+
const observedDocuments = (await Promise.all(devServers.map((server) => observeWebDocument(server.url)))).filter((document) => document !== undefined);
|
|
199
|
+
const observedCsp = 0 === observedDocuments.length
|
|
200
|
+
? undefined
|
|
201
|
+
: observedDocuments.flatMap((document) => diagnoseObservedWebCsp(document, port));
|
|
202
|
+
const connected = hasConnectedDocument(summarizeStatus(daemonStatus).sessions, devServers.map((server) => server.url), projectId);
|
|
203
|
+
const csp = resolveWebCspFindings({
|
|
204
|
+
predicted: diagnoseWebCsp(readProjectFile, port),
|
|
205
|
+
...(observedCsp === undefined ? {} : { observed: observedCsp }),
|
|
206
|
+
connected,
|
|
207
|
+
});
|
|
191
208
|
if (csp.length > 0) {
|
|
192
209
|
line('');
|
|
193
|
-
|
|
210
|
+
const observed = csp.some((finding) => finding.basis === CspBasis.OBSERVED);
|
|
211
|
+
line(observed
|
|
212
|
+
? ` csp ✗ a Content-Security-Policy is blocking the Reticle bridge:`
|
|
213
|
+
: ` csp ? a configuration may block the Reticle bridge (predicted, not observed):`);
|
|
194
214
|
for (const finding of csp) {
|
|
195
215
|
line(` ${finding.file}`);
|
|
196
216
|
line(` ${finding.problem}`);
|
|
@@ -27,3 +27,10 @@ export declare const missingOperand: (command: string, what: string) => ParseErr
|
|
|
27
27
|
export declare const HEADED_FLAG = "--headed";
|
|
28
28
|
export declare const PORT_FLAG = "--port";
|
|
29
29
|
export declare const VERIFY_COMMAND = "verify";
|
|
30
|
+
/**
|
|
31
|
+
* The predicate flag, shared for the same reason the rest of this file is: two places have to name
|
|
32
|
+
* it. The grammar reads it, and `verify` itself has to say IN PROSE which flag it could not honour
|
|
33
|
+
* when the port is not serving a daemon — a refusal that never names the flag is how this one was
|
|
34
|
+
* missed for a release.
|
|
35
|
+
*/
|
|
36
|
+
export declare const EXPECT_FLAG = "--expect";
|
|
@@ -37,3 +37,10 @@ export const missingOperand = (command, what) => ({
|
|
|
37
37
|
export const HEADED_FLAG = '--headed';
|
|
38
38
|
export const PORT_FLAG = '--port';
|
|
39
39
|
export const VERIFY_COMMAND = 'verify';
|
|
40
|
+
/**
|
|
41
|
+
* The predicate flag, shared for the same reason the rest of this file is: two places have to name
|
|
42
|
+
* it. The grammar reads it, and `verify` itself has to say IN PROSE which flag it could not honour
|
|
43
|
+
* when the port is not serving a daemon — a refusal that never names the flag is how this one was
|
|
44
|
+
* missed for a release.
|
|
45
|
+
*/
|
|
46
|
+
export const EXPECT_FLAG = '--expect';
|
|
@@ -7,6 +7,8 @@ export type VerifySuffix = {
|
|
|
7
7
|
storageState?: string;
|
|
8
8
|
sessionId?: string;
|
|
9
9
|
expect?: unknown;
|
|
10
|
+
/** A path to read the predicate from, when `--expect-file` was used instead of `--expect`. */
|
|
11
|
+
expectFile?: string;
|
|
10
12
|
explore?: boolean;
|
|
11
13
|
persona?: string;
|
|
12
14
|
select?: string[];
|
|
@@ -14,9 +16,4 @@ export type VerifySuffix = {
|
|
|
14
16
|
kind: 'error';
|
|
15
17
|
message: string;
|
|
16
18
|
};
|
|
17
|
-
/**
|
|
18
|
-
* Parse `verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>]
|
|
19
|
-
* [--session-id <id>]`. The first non-flag token is the preview URL. `defaultPort` is already
|
|
20
|
-
* env + `.reticle.json` + 4400.
|
|
21
|
-
*/
|
|
22
19
|
export declare function parseVerifySuffix(args: string[], defaultPort: number): VerifySuffix;
|
|
@@ -6,11 +6,28 @@
|
|
|
6
6
|
* pin, a predicate to assert AND a persona to drive as — so it carries more grammar than the rest of
|
|
7
7
|
* the CLI put together.
|
|
8
8
|
*/
|
|
9
|
-
import { HEADED_FLAG, PORT_FLAG, VERIFY_COMMAND, missingOperand, missingValue, notANumber, unknownArgument, } from './cli-parse-grammar.js';
|
|
9
|
+
import { EXPECT_FLAG, HEADED_FLAG, PORT_FLAG, VERIFY_COMMAND, missingOperand, missingValue, notANumber, unknownArgument, } from './cli-parse-grammar.js';
|
|
10
10
|
const TIMEOUT_FLAG = '--timeout';
|
|
11
11
|
const STORAGE_STATE_FLAG = '--storage-state';
|
|
12
|
-
|
|
12
|
+
/** The same predicate, read from a file, which no shell can mangle. See `parseExpect`. */
|
|
13
|
+
const EXPECT_FILE_FLAG = '--expect-file';
|
|
13
14
|
const SESSION_ID_FLAG = '--session-id';
|
|
15
|
+
/**
|
|
16
|
+
* `--expect` and `--storage-state` cannot both be honoured, so the pair is refused rather than
|
|
17
|
+
* half-applied.
|
|
18
|
+
*
|
|
19
|
+
* `--expect` takes its verdict from the tab a running daemon already owns — it binds nothing and
|
|
20
|
+
* launches nothing, which is exactly what makes it work while the port is busy. There is nowhere in
|
|
21
|
+
* that path to load a storage state, and the flag was not refused, it was dropped: a restricted-user
|
|
22
|
+
* absence assertion was graded against a logged-in admin tab and PASSED. A false green is worse than
|
|
23
|
+
* a missing answer, and the only honest reply to a question this command cannot ask is to say so.
|
|
24
|
+
*/
|
|
25
|
+
const MSG_EXPECT_WITH_STORAGE_STATE = `${EXPECT_FLAG} cannot be combined with ${STORAGE_STATE_FLAG}: the predicate is graded against ` +
|
|
26
|
+
'the tab the running daemon already owns, so the storage state would never be loaded and the ' +
|
|
27
|
+
'verdict would be about whoever is signed in there.\n' +
|
|
28
|
+
` Use one of them: ${EXPECT_FLAG} on its own asserts against the session you have, and ` +
|
|
29
|
+
`${STORAGE_STATE_FLAG} without ${EXPECT_FLAG} makes Reticle drive the url with that state and ` +
|
|
30
|
+
'replay the saved flows.';
|
|
14
31
|
/** Let Reticle drive the app itself and record what it drove, when nothing is saved yet. */
|
|
15
32
|
const EXPLORE_FLAG = '--explore';
|
|
16
33
|
/** Who to be while exploring — a persona, or the business outcome to reach. Implies --explore. */
|
|
@@ -22,6 +39,46 @@ const SELECT_FLAG = '--select';
|
|
|
22
39
|
* [--session-id <id>]`. The first non-flag token is the preview URL. `defaultPort` is already
|
|
23
40
|
* env + `.reticle.json` + 4400.
|
|
24
41
|
*/
|
|
42
|
+
/**
|
|
43
|
+
* Read a predicate off the command line, allowing for what a Windows shell did to it on the way.
|
|
44
|
+
*
|
|
45
|
+
* `cmd.exe` does not treat `'` as a quote character, so the command every doc and README shows
|
|
46
|
+
* arrives with LITERAL single quotes around the JSON. That is unambiguous, so it is undone: a
|
|
47
|
+
* matching pair, and only when what is inside then parses.
|
|
48
|
+
*
|
|
49
|
+
* PowerShell calling a native executable strips the INNER double quotes instead, and `{kind:signal}`
|
|
50
|
+
* is not JSON and cannot be repaired without guessing which bare words were keys and which were
|
|
51
|
+
* string values. Guessing there would assert something the caller did not write, so it is named
|
|
52
|
+
* instead — with the two forms that do work, because a predicate that does not parse produces NO
|
|
53
|
+
* verdict at all, which is the worst of the three possible outcomes.
|
|
54
|
+
*/
|
|
55
|
+
function parseExpect(raw) {
|
|
56
|
+
const unwrapped = raw.length > 1 && raw.startsWith("'") && raw.endsWith("'") ? raw.slice(1, -1) : raw;
|
|
57
|
+
try {
|
|
58
|
+
return { kind: 'ok', value: JSON.parse(unwrapped) };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
if (looksQuoteStripped(unwrapped)) {
|
|
62
|
+
return {
|
|
63
|
+
kind: 'error',
|
|
64
|
+
message: `${EXPECT_FLAG} received ${unwrapped}, which has had its double quotes removed — that is ` +
|
|
65
|
+
'PowerShell passing an argument to a native program, not a problem with the predicate. ' +
|
|
66
|
+
`Use ${EXPECT_FILE_FLAG} <path> (no quoting at all), or escape them: ` +
|
|
67
|
+
`${EXPECT_FLAG} "{\\"kind\\":\\"signal\\",\\"name\\":\\"order:placed\\"}"`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
// Named as a JSON problem rather than an unknown argument: the value IS the predicate, and
|
|
71
|
+
// "unknown argument" would send the reader looking at the flag instead of at their quoting.
|
|
72
|
+
return {
|
|
73
|
+
kind: 'error',
|
|
74
|
+
message: `${EXPECT_FLAG} needs a JSON predicate; could not parse: ${unwrapped}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** A JSON object with every double quote gone: braces, a colon, and not one `"` anywhere. */
|
|
79
|
+
function looksQuoteStripped(value) {
|
|
80
|
+
return (value.startsWith('{') && value.endsWith('}') && value.includes(':') && !value.includes('"'));
|
|
81
|
+
}
|
|
25
82
|
export function parseVerifySuffix(args, defaultPort) {
|
|
26
83
|
let headless = true;
|
|
27
84
|
let url;
|
|
@@ -29,6 +86,7 @@ export function parseVerifySuffix(args, defaultPort) {
|
|
|
29
86
|
let storageState;
|
|
30
87
|
let sessionId;
|
|
31
88
|
let expect;
|
|
89
|
+
let expectFile;
|
|
32
90
|
let explore = false;
|
|
33
91
|
let persona;
|
|
34
92
|
const select = [];
|
|
@@ -102,17 +160,17 @@ export function parseVerifySuffix(args, defaultPort) {
|
|
|
102
160
|
const v = args[i];
|
|
103
161
|
if (v === undefined)
|
|
104
162
|
return missingValue(EXPECT_FLAG);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
163
|
+
const read = parseExpect(v);
|
|
164
|
+
if ('error' === read.kind)
|
|
165
|
+
return read;
|
|
166
|
+
expect = read.value;
|
|
167
|
+
}
|
|
168
|
+
else if (arg === EXPECT_FILE_FLAG) {
|
|
169
|
+
i++;
|
|
170
|
+
const v = args[i];
|
|
171
|
+
if (v === undefined)
|
|
172
|
+
return missingValue(EXPECT_FILE_FLAG);
|
|
173
|
+
expectFile = v;
|
|
116
174
|
}
|
|
117
175
|
else if (arg.startsWith('--')) {
|
|
118
176
|
return unknownArgument(arg);
|
|
@@ -127,12 +185,27 @@ export function parseVerifySuffix(args, defaultPort) {
|
|
|
127
185
|
}
|
|
128
186
|
if (url === undefined)
|
|
129
187
|
return missingOperand(VERIFY_COMMAND, 'a url');
|
|
188
|
+
// Two ways to say the same thing, and no way to say two different things. Preferring one
|
|
189
|
+
// silently is how a run asserts something the person did not write.
|
|
190
|
+
if (expect !== undefined && expectFile !== undefined) {
|
|
191
|
+
return {
|
|
192
|
+
kind: 'error',
|
|
193
|
+
message: `${EXPECT_FLAG} and ${EXPECT_FILE_FLAG} both name the predicate; pass one`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
// Refused here, where nothing has been bound and no browser exists yet, because the damage this
|
|
197
|
+
// pair does is a verdict taken against the wrong user. A predicate from a file is the same
|
|
198
|
+
// predicate, so it is refused the same way.
|
|
199
|
+
if ((expect !== undefined || expectFile !== undefined) && storageState !== undefined) {
|
|
200
|
+
return { kind: 'error', message: MSG_EXPECT_WITH_STORAGE_STATE };
|
|
201
|
+
}
|
|
130
202
|
return {
|
|
131
203
|
kind: 'ok',
|
|
132
204
|
url,
|
|
133
205
|
headless,
|
|
134
206
|
port,
|
|
135
207
|
...(expect !== undefined ? { expect } : {}),
|
|
208
|
+
...(expectFile !== undefined ? { expectFile } : {}),
|
|
136
209
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
137
210
|
...(storageState !== undefined ? { storageState } : {}),
|
|
138
211
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
@@ -15,7 +15,7 @@ export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from '.
|
|
|
15
15
|
* telling readers to do exactly that on 110 lines before it was caught, so the invocation now leads
|
|
16
16
|
* the usage block rather than being a footnote somewhere else.
|
|
17
17
|
*/
|
|
18
|
-
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>]\n [--env KEY=VALUE]... [--files-only] (wire Reticle into the project in this directory)\n init is ONBOARDING: it wires the project, boots the app and proves a session\n connected. It does not drive. The FIRST RUN is the stage that proves a flow:\n reticle verify <url> --explore --persona \"<who does what>\", or the same thing\n from an agent as reticle_verify { action: \"explore\", persona: \"\u2026\" }\n --app picks WHICH app in a monorepo, when several are found\n --env is what the app needs to reach a usable state: the key from\n .env.example, the mock backend, the variable that skips an auth wall.\n Repeatable, and a value may contain spaces and equals signs\n --files-only writes the files and stops, which is what init did before it\n learned to boot the app and prove the install works\n --relaunch prints the exact command that restarts THIS conversation with\n the tools loaded, so the restart is not a chore handed to a human. It\n refuses when the session id has no transcript behind it, because\n --resume on an empty id opens a blank conversation that looks like\n success. Works with --files-only\n --license writes the key to .env and keeps .env out of git\n --json puts the result on stdout, so an agent reads one object\n --no-open / --no-agents / --url / --timeout are the runtime dials: CI, a\n headless box, or an app you already run\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] [--json] (a readable block; --json for the event)\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>] [--session-id <id>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n [--explore] [--persona <who>] (no saved flows? let Reticle drive the app itself and record them)\n [--select <label>] (repeatable: verify only flows carrying these labels \u2014 no model, exit 0=pass)\n [--expect '<json predicate>']
|
|
18
|
+
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>]\n [--env KEY=VALUE]... [--files-only] [--hooks] (wire Reticle into the project in this directory)\n init is ONBOARDING: it wires the project, boots the app and proves a session\n connected. It does not drive. The FIRST RUN is the stage that proves a flow:\n reticle verify <url> --explore --persona \"<who does what>\", or the same thing\n from an agent as reticle_verify { action: \"explore\", persona: \"\u2026\" }\n --app picks WHICH app in a monorepo, when several are found\n --env is what the app needs to reach a usable state: the key from\n .env.example, the mock backend, the variable that skips an auth wall.\n Repeatable, and a value may contain spaces and equals signs\n --files-only writes the files and stops, which is what init did before it\n learned to boot the app and prove the install works\n --relaunch prints the exact command that restarts THIS conversation with\n the tools loaded, so the restart is not a chore handed to a human. It\n refuses when the session id has no transcript behind it, because\n --resume on an empty id opens a blank conversation that looks like\n success. Works with --files-only\n --license writes the key to .env and keeps .env out of git\n --json puts the result on stdout, so an agent reads one object\n --no-open / --no-agents / --url / --timeout are the runtime dials: CI, a\n headless box, or an app you already run\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] [--json] (a readable block; --json for the event)\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>] [--session-id <id>] (one-shot: drive the URL, verify saved flows, exit 0=pass)\n [--explore] [--persona <who>] (no saved flows? let Reticle drive the app itself and record them)\n [--select <label>] (repeatable: verify only flows carrying these labels \u2014 no model, exit 0=pass)\n [--expect '<json predicate>' | --expect-file <path>] (one verdict, no saved\n flows needed. --expect-file is the form no shell can mangle: cmd.exe keeps the\n single quotes the docs show, and PowerShell strips the inner double quotes, and\n a predicate that does not parse produces no verdict at all \u2014 asks the daemon that\n is already running, so nothing is bound and nothing is stopped. It NEEDS that\n daemon: with none on the port it refuses and names the flag, rather than reporting\n on your saved flows instead. Cannot be combined with --storage-state, which this\n path has nowhere to load. This is the path when your client never loaded the\n reticle_* tools. exit 0 ONLY on verified:\"yes\" \u2014 \"unknown\" is not a 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 report [--session <id>] [--hook] (what the latest session claimed, and what held)\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; runs/flows recorded on the dashboard):\n reticle login [--url <u>] [--email <e>] [--code <c>] [--org <n>]\n (browser device flow by default; --email mails a code)\n reticle logout [--url <u>] (sign out of ONE host; others stay signed in)\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 | sync [--watch] (one sync cycle: send the difference, collect decisions)\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.";
|
|
19
19
|
/** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
|
|
20
20
|
export declare const TelemetryAction: {
|
|
21
21
|
readonly STATUS: "status";
|
|
@@ -58,6 +58,7 @@ export type CliResult = {
|
|
|
58
58
|
env: string[];
|
|
59
59
|
filesOnly: boolean;
|
|
60
60
|
captureBodies: boolean;
|
|
61
|
+
hooks: boolean;
|
|
61
62
|
json: boolean;
|
|
62
63
|
relaunch: boolean;
|
|
63
64
|
open: boolean;
|
|
@@ -148,6 +149,7 @@ export type CliResult = {
|
|
|
148
149
|
storageState?: string;
|
|
149
150
|
sessionId?: string;
|
|
150
151
|
expect?: unknown;
|
|
152
|
+
expectFile?: string;
|
|
151
153
|
explore?: boolean;
|
|
152
154
|
persona?: string;
|
|
153
155
|
select?: string[];
|
|
@@ -165,6 +167,10 @@ export type CliResult = {
|
|
|
165
167
|
files: string[];
|
|
166
168
|
since?: string;
|
|
167
169
|
hook?: boolean;
|
|
170
|
+
} | {
|
|
171
|
+
kind: 'report';
|
|
172
|
+
session?: string;
|
|
173
|
+
hook: boolean;
|
|
168
174
|
} | {
|
|
169
175
|
kind: 'watch';
|
|
170
176
|
url?: string;
|