@reticlehq/server 2.9.0 → 2.10.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.
Files changed (71) hide show
  1. package/SKILL.md +17 -17
  2. package/dist/capsule/capsule-store.d.ts +2 -2
  3. package/dist/cli/cli-parse.d.ts +1 -1
  4. package/dist/cli/cli-parse.js +1 -1
  5. package/dist/cli/cli-verify.js +1 -1
  6. package/dist/crawl/crawl.d.ts +7 -2
  7. package/dist/crawl/crawl.js +52 -9
  8. package/dist/ee/audit-log.js +2 -2
  9. package/dist/events/body-failures.d.ts +2 -2
  10. package/dist/events/contradiction-folds.d.ts +45 -0
  11. package/dist/events/contradiction-folds.js +42 -0
  12. package/dist/events/contradictions.d.ts +19 -1
  13. package/dist/events/contradictions.js +17 -2
  14. package/dist/events/echo-mismatch.d.ts +2 -2
  15. package/dist/events/stale-response.d.ts +2 -3
  16. package/dist/events/stale-response.js +49 -2
  17. package/dist/events/unit-mismatch.d.ts +2 -2
  18. package/dist/flows/flow-tools.js +1 -1
  19. package/dist/flows/verify-change-tools.js +1 -1
  20. package/dist/honesty/verified.js +16 -0
  21. package/dist/index.d.ts +18 -0
  22. package/dist/index.js +16 -0
  23. package/dist/init/mcp.d.ts +1 -2
  24. package/dist/init/mcp.js +4 -1
  25. package/dist/license/license.d.ts +69 -4
  26. package/dist/license/license.js +84 -15
  27. package/dist/mcp/mcp-proxy.d.ts +2 -27
  28. package/dist/mcp/mcp-proxy.js +4 -88
  29. package/dist/mcp/mcp.d.ts +18 -2
  30. package/dist/mcp/mcp.js +31 -10
  31. package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
  32. package/dist/mcp/proxy-daemon-probe.js +48 -0
  33. package/dist/mcp/proxy-handshake.js +1 -1
  34. package/dist/mcp/server-instructions.js +2 -2
  35. package/dist/mcp/sse-frame-parser.d.ts +19 -0
  36. package/dist/mcp/sse-frame-parser.js +41 -0
  37. package/dist/session/no-session-diagnosis.d.ts +7 -0
  38. package/dist/session/no-session-diagnosis.js +23 -2
  39. package/dist/session/no-session-watch.js +7 -0
  40. package/dist/session/session-readiness.js +1 -1
  41. package/dist/telemetry/daemon-telemetry.js +7 -2
  42. package/dist/telemetry/instrumentation-stall.d.ts +7 -0
  43. package/dist/telemetry/instrumentation-stall.js +13 -0
  44. package/dist/telemetry/license-activation.d.ts +48 -0
  45. package/dist/telemetry/license-activation.js +57 -0
  46. package/dist/telemetry/telemetry.js +10 -1
  47. package/dist/tools/dynamic-tools.js +1 -1
  48. package/dist/tools/error-recovery.js +1 -1
  49. package/dist/tools/feedback-tools.js +31 -6
  50. package/dist/tools/invoke-tool.d.ts +1 -1
  51. package/dist/tools/invoke-tool.js +10 -5
  52. package/dist/tools/tool-kit.d.ts +30 -3
  53. package/dist/tools/tool-names.d.ts +2 -0
  54. package/dist/tools/tool-names.js +2 -0
  55. package/dist/tools/tool-surface.js +2 -3
  56. package/dist/tools/tools.js +16 -1
  57. package/docs/agent-cheatsheet.md +2 -2
  58. package/docs/capabilities.mdx +4 -4
  59. package/docs/cli/hunt.mdx +2 -2
  60. package/docs/deploy-checks.md +2 -2
  61. package/docs/docs.json +1 -1
  62. package/docs/enterprise.md +7 -3
  63. package/docs/flows.md +4 -4
  64. package/docs/gates.md +1 -1
  65. package/docs/recipes.mdx +2 -2
  66. package/docs/telemetry-contract.md +17 -0
  67. package/docs/tools-overview.mdx +1 -1
  68. package/docs/tools-session-and-feedback.mdx +5 -0
  69. package/docs/tools-tools-and-run.mdx +15 -15
  70. package/docs/usage.md +3 -3
  71. package/package.json +2 -2
@@ -12,9 +12,9 @@
12
12
  * tokens on that and not on politeness.
13
13
  */
14
14
  import { z } from 'zod';
15
- import { FeedbackKind, FeedbackSource } from '@reticlehq/core';
15
+ import { FeedbackKind, FeedbackSource, FEEDBACK_RATING_MAX, FEEDBACK_RATING_MIN, } from '@reticlehq/core';
16
16
  import { ReticleTool } from './tool-names.js';
17
- import { asString } from './tools-helpers.js';
17
+ import { asNumber, asString } from './tools-helpers.js';
18
18
  import { submitFeedback, feedbackDisabled } from '../telemetry/feedback.js';
19
19
  import { getTelemetry } from '../telemetry/telemetry.js';
20
20
  /** One optional string arg, present only when the agent actually supplied it. */
@@ -22,6 +22,19 @@ function optionalText(args, key) {
22
22
  const value = asString(args[key]);
23
23
  return value !== undefined && value !== '' ? { [key]: value } : {};
24
24
  }
25
+ /**
26
+ * The star rating, present only when the agent actually supplied one, and only when it is on the
27
+ * scale core defines. Bounds-checked HERE as well as in the schema because a number that survives
28
+ * to the wire off-scale is worse than an absent one: it still averages.
29
+ */
30
+ function optionalRating(args) {
31
+ const value = asNumber(args['rating']);
32
+ if (value === undefined || !Number.isInteger(value))
33
+ return {};
34
+ if (value < FEEDBACK_RATING_MIN || value > FEEDBACK_RATING_MAX)
35
+ return {};
36
+ return { rating: value };
37
+ }
25
38
  /** Read the connected session's self-reported facts, or nothing when no app is attached. */
26
39
  function sessionFacts(deps, sessionId) {
27
40
  const driven = deps.realInput !== undefined;
@@ -45,7 +58,7 @@ function sessionFacts(deps, sessionId) {
45
58
  export const FEEDBACK_TOOLS = [
46
59
  {
47
60
  name: ReticleTool.FEEDBACK,
48
- description: "Tell the Reticle maintainers something — a failure, OR a feature you wish existed. Reticle is built FOR agents, so your experience is the one that decides what gets built, and this is the only channel that reaches us. File a `bug` when a tool returned something wrong, a `gap` when Reticle could not observe what you needed, an `ambiguity` when a verdict was not decidable, a `feature_request` when you thought 'I wish Reticle could do X', or an `improvement` when something works but is awkward (too many calls, a confusing shape, a slow path). Do not save these up or judge them as too minor — file it the moment you notice, then carry on with your task. For a failure, write `text` as a root-cause analysis (what you called, expected, got). For a request, write what you want and use `need` for the GOAL behind it, `impact` for what gets better, and `currentApproach` for how you are working around it today — that workaround is usually the most useful thing in the report. Set `model` to the model you are running. Never include app source, secrets, user data, or customer records. Do NOT use this for bugs you found in the app under test — that is a successful verification, which is Reticle working.",
61
+ description: "Tell the Reticle maintainers something — a failure, OR a feature you wish existed. Reticle is built FOR agents, so your experience is the one that decides what gets built, and this is the only channel that reaches us. File a `bug` when a tool returned something wrong, a `gap` when Reticle could not observe what you needed, an `ambiguity` when a verdict was not decidable, a `feature_request` when you thought 'I wish Reticle could do X', an `improvement` when something works but is awkward (too many calls, a confusing shape, a slow path), or an `experience` when something worked notably WELL (carry `rating` 1-5). File that one unprompted, when you actually notice it, and say which call and what it saved you — every other kind here is a complaint, so it is the only evidence for what is worth protecting when we change things. Do not save these up or judge them as too minor — file it the moment you notice, then carry on with your task. For a failure, write `text` as a root-cause analysis (what you called, expected, got). For a request, write what you want and use `need` for the GOAL behind it, `impact` for what gets better, and `currentApproach` for how you are working around it today — that workaround is usually the most useful thing in the report. Set `model` to the model you are running. Never include app source, secrets, user data, or customer records. Do NOT use this for bugs you found in the app under test — that is a successful verification, which is Reticle working.",
49
62
  inputSchema: {
50
63
  kind: z
51
64
  .enum([
@@ -54,8 +67,9 @@ export const FEEDBACK_TOOLS = [
54
67
  FeedbackKind.AMBIGUITY,
55
68
  FeedbackKind.FEATURE_REQUEST,
56
69
  FeedbackKind.IMPROVEMENT,
70
+ FeedbackKind.EXPERIENCE,
57
71
  ])
58
- .describe('bug = a Reticle tool returned a wrong or broken result; gap = Reticle could not observe something you needed at all; ambiguity = the verification ran but you could not tell pass from fail; feature_request = something that does not exist and would have helped; improvement = something that exists but is awkward or inefficient.'),
72
+ .describe('bug = a Reticle tool returned a wrong or broken result; gap = Reticle could not observe something you needed at all; ambiguity = the verification ran but you could not tell pass from fail; feature_request = something that does not exist and would have helped; improvement = something that exists but is awkward or inefficient; experience = something worked notably well and is worth keeping (pair it with `rating`).'),
59
73
  text: z
60
74
  .string()
61
75
  .min(1)
@@ -72,6 +86,13 @@ export const FEEDBACK_TOOLS = [
72
86
  .string()
73
87
  .optional()
74
88
  .describe('How you work around it TODAY. Usually the most useful field in the report: it proves the need is real, and often shows a cheaper fix than the feature being asked for.'),
89
+ rating: z
90
+ .number()
91
+ .int()
92
+ .min(FEEDBACK_RATING_MIN)
93
+ .max(FEEDBACK_RATING_MAX)
94
+ .optional()
95
+ .describe(`How well it worked, ${FEEDBACK_RATING_MIN}-${FEEDBACK_RATING_MAX}, with an \`experience\`. Only when you formed the judgement while working, never because you were asked: an agreeable number and an earned one are indistinguishable once both sit in the same column. The \`text\` is the part we can act on.`),
75
96
  model: z
76
97
  .string()
77
98
  .optional()
@@ -112,6 +133,7 @@ export const FEEDBACK_TOOLS = [
112
133
  ...optionalText(args, 'impact'),
113
134
  ...optionalText(args, 'currentApproach'),
114
135
  ...optionalText(args, 'model'),
136
+ ...optionalRating(args),
115
137
  }, {
116
138
  session: sessionFacts(deps, asString(args['sessionId'])),
117
139
  // The AGENT does not wait for the network. `reticle feedback` (a human, at a terminal,
@@ -154,8 +176,11 @@ export const FEEDBACK_PROMPT = {
154
176
  export const VERIFICATION_TOOLS = new Set([
155
177
  ReticleTool.ACT_AND_WAIT,
156
178
  ReticleTool.ASSERT,
157
- ReticleTool.FLOW_VERIFY,
158
- ReticleTool.VERIFY_CHANGE,
179
+ // Merged: `flow_verify` and `verify_change` are actions on this tool now. Naming the merged tool
180
+ // does NOT make every action a verification — `verificationOf` still requires the result to carry
181
+ // a `verified` or a pass/fail `status`, so `coverage` and `crawl` contribute nothing and cannot
182
+ // inflate the count.
183
+ ReticleTool.VERIFY,
159
184
  ReticleTool.RECONCILE,
160
185
  ]);
161
186
  /**
@@ -7,4 +7,4 @@ export declare const SESSION_BOUND_TOOLS: ReadonlySet<string>;
7
7
  * `sessionId`-bearing tool to be classified into exactly one set (bound XOR exempt).
8
8
  */
9
9
  export declare const SESSION_EXEMPT_TOOLS: ReadonlySet<string>;
10
- export declare function runTool(tool: ToolDef, deps: ToolDeps, args: Record<string, unknown>): Promise<unknown>;
10
+ export declare function runTool<Ext>(tool: ToolDef<Ext>, deps: ToolDeps<Ext>, args: Record<string, unknown>): Promise<unknown>;
@@ -47,14 +47,21 @@ const REF_MINTING_TOOLS = new Set([
47
47
  ReticleTool.QUERY,
48
48
  ReticleTool.INSPECT,
49
49
  ReticleTool.EXPLORE,
50
- ReticleTool.CRAWL,
50
+ // `crawl` mints refs for the controls it drives and `coverage` for the ones it reports untouched.
51
+ // Both are actions on the merged verify tool now.
52
+ ReticleTool.VERIFY,
51
53
  ]);
52
54
  export const SESSION_BOUND_TOOLS = new Set([
53
55
  ReticleTool.SNAPSHOT,
54
56
  ReticleTool.QUERY,
55
57
  ReticleTool.INSPECT,
56
- ReticleTool.COVERAGE,
57
- ReticleTool.VERIFY_CHANGE,
58
+ // Merged change/flows/affected/coverage/crawl. Its members were SPLIT across bound and exempt —
59
+ // coverage/change/crawl bound, flow_verify exempt because it returns its own suite contract — and
60
+ // a merged tool has to be one or the other. Bound is the right side: `crawl` drives the app
61
+ // itself, so a throttled or event-dropping tab is exactly the condition its verdict depends on,
62
+ // and dropping the health disclosure there to protect a sibling's output shape would trade an
63
+ // honesty signal for a formatting preference.
64
+ ReticleTool.VERIFY,
58
65
  ReticleTool.ACT,
59
66
  ReticleTool.ACT_SEQUENCE,
60
67
  ReticleTool.ACT_AND_WAIT,
@@ -74,7 +81,6 @@ export const SESSION_BOUND_TOOLS = new Set([
74
81
  ReticleTool.STATE,
75
82
  ReticleTool.STORAGE,
76
83
  ReticleTool.EXPLORE,
77
- ReticleTool.CRAWL,
78
84
  ReticleTool.SCROLL_TO,
79
85
  ReticleTool.NAVIGATE,
80
86
  ]);
@@ -90,7 +96,6 @@ export const SESSION_EXEMPT_TOOLS = new Set([
90
96
  ReticleTool.FLOW_SAVE, // sessionId only scopes the write to the app's flow subdir; disk-side
91
97
  ReticleTool.FLOW, // merged list/load/delete — sessionId only scopes the project; all disk-side
92
98
  ReticleTool.FLOW_REPLAY, // returns its own FlowReplayResult contract (+ auto-records a run)
93
- ReticleTool.FLOW_VERIFY, // returns its own SuiteVerdict contract (replays the whole suite)
94
99
  ReticleTool.FLOW_SAVE_RECORDED, // reads the recording buffer, writes disk
95
100
  ReticleTool.FLOW_HEAL, // returns its own FlowHealResult contract
96
101
  ReticleTool.PROJECT, // reads .reticle/project.json
@@ -16,7 +16,7 @@ import type { ProjectStore } from '../project/project-store.js';
16
16
  import type { AnnotationStore } from '../flows/annotation-store.js';
17
17
  import type { BrowserPool } from '../pool/browser-pool.js';
18
18
  import type { ChromiumProbe } from '../cli/chromium-hint.js';
19
- export interface ToolDeps {
19
+ export interface ToolDeps<Ext = unknown> {
20
20
  sessions: SessionManager;
21
21
  /** shared one-browser/N-context pool for headless leases. undefined ⇒ lease tools report unavailable. */
22
22
  pool?: BrowserPool;
@@ -59,8 +59,23 @@ export interface ToolDeps {
59
59
  * ToolDeps keeps working; absent falls back to the default port.
60
60
  */
61
61
  bridgePort?: number;
62
+ /**
63
+ * A consumer's OWN dependencies, carried through untouched.
64
+ *
65
+ * Nothing in this package reads it. It exists so a consumer embedding the engine can add tools of
66
+ * its own — answering out of a store this package has never heard of — without either widening the
67
+ * fields above with a stranger's vocabulary or forking to get a handle through.
68
+ *
69
+ * The type parameter, rather than `unknown`, is what keeps the seam typed: a consumer writes
70
+ * `ToolDef<TheirExt>` once and their handlers see their own type. Casting out of `unknown` in every
71
+ * handler is how a typed seam quietly becomes an untyped one.
72
+ *
73
+ * Optional, and defaulted to `unknown`, so every existing construction of this bag — and every test
74
+ * that builds one — is unchanged.
75
+ */
76
+ ext?: Ext;
62
77
  }
63
- export interface ToolDef {
78
+ export interface ToolDef<Ext = unknown> {
64
79
  name: string;
65
80
  description: string;
66
81
  inputSchema: z.ZodRawShape;
@@ -86,7 +101,19 @@ export interface ToolDef {
86
101
  * example that does not validate is worse than none at all.
87
102
  */
88
103
  example?: Record<string, unknown>;
89
- handler: (deps: ToolDeps, args: Record<string, unknown>) => Promise<unknown>;
104
+ /**
105
+ * Declared method-style, not as a property, and that is load-bearing rather than stylistic.
106
+ *
107
+ * Under `strictFunctionTypes` a property holding a function has CONTRAVARIANT parameters, so
108
+ * `ToolDef<TheirExt>` and `ToolDef<unknown>` become mutually unassignable and a consumer composing
109
+ * `[...TOOLS, ...theirTools]` needs a cast — as does every internal helper that then touches the
110
+ * array. Method-style parameters are checked bivariantly, which collapses all of that.
111
+ *
112
+ * The unsoundness bivariance admits is that a handler written against `ToolDeps<TheirExt>` could be
113
+ * handed a bag carrying no `ext` at all. That costs nothing here, because `ext` is optional by
114
+ * design: any handler reading it already has to survive `undefined`.
115
+ */
116
+ handler(deps: ToolDeps<Ext>, args: Record<string, unknown>): Promise<unknown>;
90
117
  }
91
118
  export declare const sessionIdShape: {
92
119
  sessionId: z.ZodOptional<z.ZodString>;
@@ -46,6 +46,8 @@ export declare const ReticleTool: {
46
46
  readonly AFFECTED: "reticle_affected";
47
47
  readonly COVERAGE: "reticle_coverage";
48
48
  readonly VERIFY_CHANGE: "reticle_verify_change";
49
+ /** Merged: change/flows/affected/coverage/crawl — "what is proved, and what is not". */
50
+ readonly VERIFY: "reticle_verify";
49
51
  /** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
50
52
  readonly PROJECT: "reticle_project";
51
53
  /** explicitly record a run outcome (the manual companion to auto-recording). */
@@ -47,6 +47,8 @@ export const ReticleTool = {
47
47
  AFFECTED: 'reticle_affected',
48
48
  COVERAGE: 'reticle_coverage',
49
49
  VERIFY_CHANGE: 'reticle_verify_change',
50
+ /** Merged: change/flows/affected/coverage/crawl — "what is proved, and what is not". */
51
+ VERIFY: 'reticle_verify',
50
52
  /** read cross-run history (.reticle/project.json) + diff-vs-last for a name. */
51
53
  PROJECT: 'reticle_project',
52
54
  /** explicitly record a run outcome (the manual companion to auto-recording). */
@@ -206,15 +206,14 @@ export const EXTENDED_TOOL_NAMES = new Set([
206
206
  ReticleTool.CAPABILITIES,
207
207
  ReticleTool.FLOW_SAVE,
208
208
  ReticleTool.FLOW_REPLAY,
209
- ReticleTool.FLOW_VERIFY,
210
209
  ReticleTool.RECORD,
211
210
  ReticleTool.SCREENSHOT,
212
211
  ReticleTool.VISUAL_DIFF,
213
212
  ReticleTool.CLOCK,
214
213
  ReticleTool.NETWORK_MOCK,
215
214
  ReticleTool.STORAGE,
216
- ReticleTool.VERIFY_CHANGE,
217
- ReticleTool.CRAWL,
215
+ // Merged change/flows/affected/coverage/crawl — one name where three of these used to sit.
216
+ ReticleTool.VERIFY,
218
217
  ]);
219
218
  /**
220
219
  * The verify surface: one acting tool that returns a verdict, plus the two meta-tools that reach
@@ -508,7 +508,7 @@ export const MERGE_PLANS = [
508
508
  },
509
509
  {
510
510
  name: ReticleTool.FLOW,
511
- description: 'Saved-flow management: { action: "list" } names every saved flow, "load" reads+validates one by flowName, "delete" removes one. The hot verbs (flow_save, flow_replay, flow_verify, flow_heal) stay separate.',
511
+ description: 'Saved-flow management: { action: "list" } names every saved flow, "load" reads+validates one by flowName, "delete" removes one. The hot verbs (flow_save, flow_replay, flow_heal) stay separate; whole-suite replay is reticle_verify { action: "flows" }.',
512
512
  members: {
513
513
  list: ReticleTool.FLOW_LIST,
514
514
  load: ReticleTool.FLOW_LOAD,
@@ -531,6 +531,21 @@ export const MERGE_PLANS = [
531
531
  // is the one that must not need a schema lookup first.
532
532
  example: { action: 'yield', mode: 'waiting' },
533
533
  },
534
+ {
535
+ name: ReticleTool.VERIFY,
536
+ description: 'What is proved, and what is not — by action. Start from a change: { action: "affected" } names which saved flows must re-verify for the files you edited (pass `files`, and/or `since` for a git ref), and "change" goes further and actually replays them, answering with one `verified` plus `because`. `unknown` there is the honest answer when NO saved flow covers the change: nothing ran, so nothing was proved, and it is never reported as green. Without a change to start from: "flows" replays every saved flow for one consolidated suite verdict (deterministic, no LLM per flow), and "coverage" lists the interactive controls you have and have NOT driven this session — an untouched list still holding the controls your change affects means you are not done. "crawl" is the no-script option: it drives every reachable control itself and reports single-channel faults (console errors, failed requests, dead controls) and CONTRADICTIONS, two channels disagreeing about the same click — the false greens a human cannot see, because a human watches the screen and the screen looks correct. DESTRUCTIVE: crawl really clicks, and may navigate or mutate state.',
537
+ members: {
538
+ change: ReticleTool.VERIFY_CHANGE,
539
+ flows: ReticleTool.FLOW_VERIFY,
540
+ affected: ReticleTool.AFFECTED,
541
+ coverage: ReticleTool.COVERAGE,
542
+ crawl: ReticleTool.CRAWL,
543
+ },
544
+ // No default action ON PURPOSE. The name implies no single member, and one of them really
545
+ // clicks: a fallthrough that guessed wrong would drive the app rather than read it. Every action
546
+ // is explicit, which for `crawl` is the whole safety story.
547
+ example: { action: 'change', files: ['src/App.tsx'] },
548
+ },
534
549
  {
535
550
  name: ReticleTool.LEASE,
536
551
  description: 'Isolated headless contexts from the shared pool: { action: "acquire" } leases one and navigates it to the app URL, "release" closes it and frees the slot.',
@@ -115,7 +115,7 @@ The tools advertised DIRECTLY, i.e. what you'll use 90% of the time. Everything
115
115
 
116
116
  Frequently useful but NOT core, so reach them through `reticle_run({ tool, args })`: `reticle_capabilities` (the app's whole testable surface in one call; `{fromDisk:true}` needs no browser), `reticle_domain` (learn the app + gaps), `reticle_baseline {action:"diff"}`, `reticle_project` (run history).
117
117
 
118
- **Reach past core when…** you need to record/replay a journey (`reticle_record {action:"start"}/stop`, `reticle_replay`), persist a self-healing golden flow (`reticle_flow_save*` / `reticle_flow_replay` / `reticle_flow_heal`), compile annotations (`reticle_annotate`), explore autonomously (`reticle_explore` lists controls; `reticle_crawl` clicks them all and reports anomalies, and is **destructive**), reveal a virtualized off-screen row (`reticle_scroll_to`, for when `reticle_query` finds nothing because a windowed list hasn't rendered it yet), visual-check (`reticle_screenshot` / `reticle_visual_diff`, pinned with `reticle_viewport` for reproducible baselines), test error/edge states by stubbing the network (`reticle_network_mock`: 500 / offline / delay, driven mode), control time for toasts/debounces/auto-dismiss (`reticle_clock { freeze | advanceMs | reset }`), or work with a human (`reticle_session {action:"end"}` / `reticle_session {action:"resume"}` / `reticle_session {action:"messages"}`, and **`reticle_session {action:"review"}`** to drain + fix the bugs the human flagged from the panel).
118
+ **Reach past core when…** you need to record/replay a journey (`reticle_record {action:"start"}/stop`, `reticle_replay`), persist a self-healing golden flow (`reticle_flow_save*` / `reticle_flow_replay` / `reticle_flow_heal`), compile annotations (`reticle_annotate`), explore autonomously (`reticle_explore` lists controls; `reticle_verify {action:"crawl"}` clicks them all and reports anomalies, and is **destructive**), reveal a virtualized off-screen row (`reticle_scroll_to`, for when `reticle_query` finds nothing because a windowed list hasn't rendered it yet), visual-check (`reticle_screenshot` / `reticle_visual_diff`, pinned with `reticle_viewport` for reproducible baselines), test error/edge states by stubbing the network (`reticle_network_mock`: 500 / offline / delay, driven mode), control time for toasts/debounces/auto-dismiss (`reticle_clock { freeze | advanceMs | reset }`), or work with a human (`reticle_session {action:"end"}` / `reticle_session {action:"resume"}` / `reticle_session {action:"messages"}`, and **`reticle_session {action:"review"}`** to drain + fix the bugs the human flagged from the panel).
119
119
 
120
120
  ## flows vs baselines vs project.json (the persistence layers)
121
121
 
@@ -160,4 +160,4 @@ Recent additions, each of which answers a question agents were previously asking
160
160
  - **Cap broad reads.** `reticle_query` takes `limit` (caps descriptors; reports `total`/`truncated`) and `count_only` (just the match count). `reticle_network` / `reticle_console` take `limit` (most-recent-N, reports `droppedOldest`) and carry the same `cost` hint, so a busy page or wide window never floods your context unnoticed.
161
161
  - **A saved flow tells you if it's a real test.** `reticle_flow_save` returns `assertions.grade` (`asserted` / `presence-only` / `assertion-free`); if it's not `asserted`, add a consequence (`reticle_annotate` assert-signal/assert-net or a success-state) so it can't pass while broken. On replay, an ambiguous heal (two testids tie) is surfaced, never auto-applied. And an `apply` heal re-replays the rebound flow and **refuses to write** if the success consequence no longer fires (`status:consequence_broken`): it heals the locator, never the intent.
162
162
  - **Predicate schema is not bloated.** The recursive predicate DSL used by `reticle_assert` / `reticle_wait_for` / `reticle_act_and_wait` is **factored, not inlined**: when converted to the JSON Schema MCP sends, the predicate body is emitted **once** (~2.7k chars ≈ **~685 tokens** per tool) and recursion is handled by self-`$ref` (`#/properties/predicate`), with no per-recursion duplication. No action needed.
163
- - **One tool surface.** Reticle advertises the core set directly (sessions/navigate/snapshot/query/act/act_and_wait/observe/network/console/wait_for/assert/state/inspect/feedback/session: the whole detect loop, plus the file-pointer, the feedback channel and the handback) PLUS two meta-tools that keep every other tool one call away: `reticle_tools` (discover; no args lists every tool name + summary, `names:[…]` loads full params on demand) and `reticle_run({ tool, args })` (invoke any tool by name; a top-level `sessionId` is forwarded to the target, so you need not nest it in `args`). So to record/replay/verify a flow, call `reticle_run({ tool:"reticle_flow_verify", sessionId })` (or `reticle_tools` first to see params). There is nothing to pick. `RETICLE_ADVERTISE_ALL_TOOLS=1` (read by the daemon at startup, so restart it) advertises everything with output schemas: a verification switch for suites, not a way to run agents. **Sizes are deliberately not quoted here.** A count in prose goes stale, and has three times already. `reticle_tools` reports the live surface, and SKILL.md carries the one gated table.
163
+ - **One tool surface.** Reticle advertises the core set directly (sessions/navigate/snapshot/query/act/act_and_wait/observe/network/console/wait_for/assert/state/inspect/feedback/session: the whole detect loop, plus the file-pointer, the feedback channel and the handback) PLUS two meta-tools that keep every other tool one call away: `reticle_tools` (discover; no args lists every tool name + summary, `names:[…]` loads full params on demand) and `reticle_run({ tool, args })` (invoke any tool by name; a top-level `sessionId` is forwarded to the target, so you need not nest it in `args`). So to record/replay/verify a flow, call `reticle_run({ tool:"reticle_verify", args:{ action:"flows" }, sessionId })` (or `reticle_tools` first to see params). There is nothing to pick. `RETICLE_ADVERTISE_ALL_TOOLS=1` (read by the daemon at startup, so restart it) advertises everything with output schemas: a verification switch for suites, not a way to run agents. **Sizes are deliberately not quoted here.** A count in prose goes stale, and has three times already. `reticle_tools` reports the live surface, and SKILL.md carries the one gated table.
@@ -15,7 +15,7 @@ These work through the always-on SDK, in any connected session.
15
15
  ### Coverage: what you have not driven yet
16
16
 
17
17
  ```json
18
- { "tool": "reticle_coverage" }
18
+ { "tool": "reticle_verify", "args": { "action": "coverage" } }
19
19
  ```
20
20
 
21
21
  ```json
@@ -90,7 +90,7 @@ Freeze the clock, advance it by exactly the interval, assert the consequence. No
90
90
  ### Crawl: click everything, report anomalies
91
91
 
92
92
  ```json
93
- { "tool": "reticle_crawl", "args": { "maxSteps": 6 } }
93
+ { "tool": "reticle_verify", "args": { "action": "crawl", "maxSteps": 6 } }
94
94
  ```
95
95
 
96
96
  ```json
@@ -327,7 +327,7 @@ The loop closer, and the reason Reticle is not only an interactive tool.
327
327
  { "tool": "reticle_record", "args": { "action": "start", "recordingName": "ship-a-deploy" } }
328
328
  { "tool": "reticle_record", "args": { "action": "stop", "recordingName": "ship-a-deploy" } }
329
329
  { "tool": "reticle_flow_save", "args": { "flowName": "ship-a-deploy" } }
330
- { "tool": "reticle_flow_verify" }
330
+ { "tool": "reticle_verify", "args": { "action": "flows" } }
331
331
  ```
332
332
 
333
333
  <Warning>
@@ -396,7 +396,7 @@ Nine hundred and fifty-three bytes for 445 recorded events. `timeline_omitted` h
396
396
 
397
397
  Reticle grades the flow as you save it, and tells you when you have just created a test that cannot fail. Take the warning seriously: an assertion-free flow replays green whatever the app does.
398
398
 
399
- A replay against a drifted app names what moved and where. This is `reticle_flow_verify` reporting the same flow after the app changed underneath it:
399
+ A replay against a drifted app names what moved and where. This is `reticle_verify {action:"flows"}` reporting the same flow after the app changed underneath it:
400
400
 
401
401
  ```json
402
402
  {
package/docs/cli/hunt.mdx CHANGED
@@ -4,7 +4,7 @@ description: Aggregate many crawl reports into one false-green rate across alrea
4
4
  icon: crosshairs
5
5
  ---
6
6
 
7
- `reticle hunt` is arithmetic, not driving. You point it at a directory of crawl reports (one JSON file per checkout, each produced by `reticle_crawl`) and it turns the pile into the one number the core claim needs: across N already-merged, already-green changes, how many carried a candidate false green?
7
+ `reticle hunt` is arithmetic, not driving. You point it at a directory of crawl reports (one JSON file per checkout, each produced by `reticle_verify {action:"crawl"}`) and it turns the pile into the one number the core claim needs: across N already-merged, already-green changes, how many carried a candidate false green?
8
8
 
9
9
  ```bash
10
10
  npx @reticlehq/server hunt <dir-of-crawl-reports>
@@ -66,7 +66,7 @@ That path exits `1`.
66
66
  ```bash
67
67
  for sha in $(git log --format=%H -n 30 main); do
68
68
  git checkout "$sha" && npm run dev &
69
- # drive reticle_crawl over the app, write the JSON:
69
+ # drive reticle_verify {action:"crawl"} over the app, write the JSON:
70
70
  # .../reports/$sha.json
71
71
  done
72
72
  npx @reticlehq/server hunt ./reports
@@ -76,7 +76,7 @@ Use both: `gate` in the agent's Stop hook (see `agent-cheatsheet`), `verify` at
76
76
  ## Honest limits
77
77
 
78
78
  - **`verify` needs one connected session.** If several tabs of the app are open against the same daemon it refuses rather than guessing which to drive.
79
- - **It replays flows sequentially against one tab**, so flows must not depend on each other's leftover state (a flow that logs in contaminates the next one). Author self-contained flows, or use `reticle_flow_verify { parallel }`, which gives each flow an isolated context.
79
+ - **It replays flows sequentially against one tab**, so flows must not depend on each other's leftover state (a flow that logs in contaminates the next one). Author self-contained flows, or use `reticle_verify { action: "flows", parallel }`, which gives each flow an isolated context.
80
80
  - **No saved flows means nothing to verify.** `verify` fails rather than reporting a vacuous pass.
81
81
 
82
82
  ## FAQ
@@ -91,7 +91,7 @@ Both. For a non-loopback origin Reticle injects `reticle.connect()` with a one-t
91
91
 
92
92
  ### One of my flows logs in and the next one starts logged in. How do I stop that?
93
93
 
94
- `verify` replays flows sequentially against one tab, so leftover state carries. Either author each flow to be self-contained, or use `reticle_flow_verify { parallel: true }`, which gives every flow its own isolated browser context with its own cookies and storage.
94
+ `verify` replays flows sequentially against one tab, so leftover state carries. Either author each flow to be self-contained, or use `reticle_verify { action: "flows", parallel: true }`, which gives every flow its own isolated browser context with its own cookies and storage.
95
95
 
96
96
  ### Should I run reticle gate as well as reticle verify?
97
97
 
package/docs/docs.json CHANGED
@@ -32,7 +32,7 @@
32
32
  "navigation": {
33
33
  "versions": [
34
34
  {
35
- "version": "2.9.0",
35
+ "version": "2.10.0",
36
36
  "default": true,
37
37
  "tabs": [
38
38
  {
@@ -14,19 +14,23 @@ Enterprise (`ee/`) features ship **inside the open package**; they're source-ava
14
14
 
15
15
  **The flow, end to end:**
16
16
 
17
- 1. **Buy.** Contact **[hey@reticle.sh](mailto:hey@reticle.sh)**; we issue you a signed license key (org, plan, expiry, feature set).
17
+ 1. **Buy.** Contact **[hey@reticle.sh](mailto:hey@reticle.sh)**; we issue you a signed license key (license id, org, plan, expiry, feature set). The **license id** is the stable handle on your subscription: it survives a rename, and it is what a renewal continues.
18
18
  2. **Install.** Set it on the machine running the Reticle server:
19
19
  ```bash
20
20
  export RETICLE_LICENSE_KEY="<your key>"
21
21
  # the release already bakes the issuer public key (RETICLE_LICENSE_PUBLIC_KEY)
22
22
  ```
23
23
  3. **Verify.** `reticle license` shows your status:
24
+
24
25
  ```
25
26
  active licensed to Acme Corp (enterprise), expires 2027-06-20 · features: sso, audit
26
27
  eval evaluation mode: enterprise features run free (no issuer key configured)
27
- missing set RETICLE_LICENSE_KEY to activate enterprise features in production
28
- expired renew to keep using enterprise features
28
+ missing set RETICLE_LICENSE_KEY to activate enterprise features in production. Request a key from hey@reticle.sh
29
+ expired license expired. Renew with hey@reticle.sh to keep using enterprise features
29
30
  ```
31
+
32
+ Every status also reports `gated` (what a licence unlocks in the build you are running) and `contact`, so the command answers "what would this buy me, and how do I get one" without anybody having to find this page first. `gated` is derived from the features the gate actually enforces, so it lists what your build will really refuse rather than a roadmap.
33
+
30
34
  4. **Unlock.** Enterprise features now run in production; without a valid key they refuse to run there (a clear error, never a silent half-feature). In eval/dev they always run free.
31
35
  5. **Renew.** Keys carry an expiry; `reticle license` warns before it lapses.
32
36
 
package/docs/flows.md CHANGED
@@ -115,17 +115,17 @@ This is the feedback a human reviewer used to give, made machine-actionable, so
115
115
 
116
116
  ## Verify the whole suite in one call
117
117
 
118
- `reticle_flow_verify` replays **every** saved flow (or a named subset) deterministically, with no LLM per flow, and returns one consolidated verdict. This is the regression check to run after any change:
118
+ `reticle_verify {action:"flows"}` replays **every** saved flow (or a named subset) deterministically, with no LLM per flow, and returns one consolidated verdict. This is the regression check to run after any change:
119
119
 
120
120
  ```jsonc
121
- reticle_flow_verify()
121
+ reticle_verify({ action: "flows" })
122
122
  // → { status: "fail", total: 4, passed: 3, failed: 1,
123
123
  // summary: "3/4 flows pass, 1 needs attention: ship-deploy",
124
124
  // failures: [{ flow: "ship-deploy", verdict: "drift",
125
125
  // whatChanged: "...", whereInSource: "src/...:NN", nextAction: "..." }] }
126
126
  ```
127
127
 
128
- Passing flows are counted; only failures carry detail (token-cheap). Build → `reticle_flow_verify` → fix from each failure's `nextAction` → repeat: the autonomous regression loop.
128
+ Passing flows are counted; only failures carry detail (token-cheap). Build → `reticle_verify {action:"flows"}` → fix from each failure's `nextAction` → repeat: the autonomous regression loop.
129
129
 
130
130
  ## Self-healing: the agent maintains the flow
131
131
 
@@ -165,7 +165,7 @@ With `apply: false` the flow file is **never modified**; you get the proposed di
165
165
  | `reticle_flow {action:"list"}` | `{}` | flows on disk |
166
166
  | `reticle_flow {action:"load"}` | `{ flowName }` | the flow JSON |
167
167
  | `reticle_flow_replay` | `{ flowName }` | `{ status, steps, decision? }` (decision on drift/fail) |
168
- | `reticle_flow_verify` | `{ names?, sessionId? }` | suite verdict `{ status, passed, failed, failures[] }` |
168
+ | `reticle_verify {action:"flows"}` | `{ names?, sessionId? }` | suite verdict `{ status, passed, failed, failures[] }` |
169
169
  | `reticle_flow_heal` | `{ flowName, apply? }` | propose / apply nearest-match rebind |
170
170
  | `reticle_annotate` | `{ kind, … }` | compile a structured annotation into the flow |
171
171
 
package/docs/gates.md CHANGED
@@ -31,7 +31,7 @@ Every gate below was executed end to end against `main` on **2026-08-12** (macOS
31
31
  | `pnpm matrix:compat --only cursor` | ✅ 4/4 | ~10s |
32
32
  | `pnpm bench` | ✅ 10/10 (**was failing before this sweep**, see below) | 279s |
33
33
 
34
- **`pnpm bench` was broken and nobody knew.** `suite-rre.mjs` recorded four flows that asserted no observable consequence and then demanded a `pass` verdict from `reticle_flow_verify`, which correctly grades an assertion-free suite `unverifiable`. The product got more honest about false greens; the benchmark measuring it did not follow, so the whole run aborted at script 9 of 10 and `replay-determinism` never ran at all. Each flow now carries a success oracle. This is the failure mode `bench/` is most exposed to: **nothing in CI runs it**, so it can only rot silently.
34
+ **`pnpm bench` was broken and nobody knew.** `suite-rre.mjs` recorded four flows that asserted no observable consequence and then demanded a `pass` verdict from whole-suite replay, which correctly grades an assertion-free suite `unverifiable`. The product got more honest about false greens; the benchmark measuring it did not follow, so the whole run aborted at script 9 of 10 and `replay-determinism` never ran at all. Each flow now carries a success oracle. This is the failure mode `bench/` is most exposed to: **nothing in CI runs it**, so it can only rot silently.
35
35
 
36
36
  `pnpm gate:install` (~15 min) and the Windows / Rust jobs were **not** run in this sweep; they are CI-only or network-bound. They are green on `main` per the last CI run, which is a weaker claim than every row above, and is stated that way on purpose.
37
37
 
package/docs/recipes.mdx CHANGED
@@ -179,7 +179,7 @@ Confirm it explicitly when you mean it:
179
179
  ## "Did my change break anything else?"
180
180
 
181
181
  ```json
182
- { "tool": "reticle_verify_change", "args": { "since": "HEAD~1" } }
182
+ { "tool": "reticle_verify", "args": { "action": "change", "since": "HEAD~1" } }
183
183
  ```
184
184
 
185
185
  Give it the files you edited, or a git ref. It works out which saved flows cover them and replays only those.
@@ -246,7 +246,7 @@ Under a driven browser the same call answers `{ "applied": true, "count": 1 }`.
246
246
  Record it once, replay it forever:
247
247
 
248
248
  ```json
249
- { "tool": "reticle_flow_verify", "args": { "names": ["docs-check"] } }
249
+ { "tool": "reticle_verify", "args": { "action": "flows", "names": ["docs-check"] } }
250
250
  ```
251
251
 
252
252
  Omit `names` to replay every saved flow. Either way you get one consolidated verdict, and only the failures carry detail:
@@ -156,11 +156,28 @@ Four counters and one flag were added because the data could not answer question
156
156
  | `bug.attribution` | `bug_found` | `app` \| `request` \| `reticle`: whose fault the defect was. **Absent means unclassified**, never `app`. See below. |
157
157
  | `outage.stage` / `outage.reason` / `outage.attempts` | `mcp_connection_lost` | which stage of the outage, why the stream went away (closed `OutageReason`, `other` for anything unnamed), and how many reconnects had been tried. See below. |
158
158
  | `installSource` | `reticle_installed`, `init_completed` | WHICH published route brought this install in, from core's closed `InstallSource`. Read from one self-declared marker (`RETICLE_INSTALL_SOURCE`) and NEVER inferred, so `unknown` is expected to dominate until every channel's own copy of the install command carries it. See below. |
159
+ | `licenseId` / `licenseStatus` | every event, on a licensed build | Enterprise activation. `licenseStatus` is core's closed `LicenseActivation` (`active` \| `missing` \| `invalid` \| `expired`) and rides through the FAILURE states too, which is what makes a lapse distinguishable from a churn. `licenseId` is present only while a key verifies, so on identity alone a customer whose key expired and one who left are the same silence. `licenseId` is an opaque uuid that resolves to a company only against the issuance ledger held locally, so the analytics backend never holds a customer list. The organisation NAME is never sent. **All three absent on a build with no issuer key baked**, which is every OSS install, so absence means "not a licensed build" and costs nothing to say. See below. |
159
160
  | `init.confirmation` | `init_completed` | what `init` SAW after writing, from core's closed `InitConfirmation`: `connected` (an app carrying the SDK reached the daemon while it watched, and it is the only value that means installed) \| `no_daemon` (nothing was listening, so no session could arrive) \| `no_session` (a daemon was up and no app connected inside the window). **Absent means it never looked**, which is every scripted run: `init` waits only when a human is at the terminal. Read absent as "not measured", never as a failure to connect. |
160
161
  | `automation` | every event | ADVISORY hint that the run looks automated when `CI` does not say so, from core's closed `AutomationHint`: `container` \| `hosted_workspace` \| `no_tty`. `ci` reads one environment variable set only by a runner, so a gate driven from a cloud sandbox lands as a human at a machine. **Never a filter**: people work in containers, in Codespaces, and over ssh with no terminal, and dropping a row because this is set drops real users. Absent means nothing looked automated, not that a human was present. |
161
162
  | `tzOffsetMin` | every event | minutes offset from UTC. One integer, no location. |
162
163
  | `versionChange.nudged` | `version_changed` | an agent had been told about exactly this version recently, so the nudge plausibly caused the update. The daemon that nudges and the `reticle update` that acts are different processes, so a marker file joins them. |
163
164
 
165
+ ## Enterprise activation: `licenseId`, `licenseStatus`
166
+
167
+ A licensed deployment reports which licence it is running under, so per-customer usage can be answered at all. Three things make this different from every other property here, and all three are deliberate:
168
+
169
+ **It is on every event, not on an activation event.** The questions a licensed customer generates are "how much is this org using it", "what is breaking for them", "did their key lapse", and every one of those is answered by an event that has nothing to do with licensing. A status riding only its own event would say a key verified once and nothing about the sessions it covered.
170
+
171
+ **Status is separate from identity, and reports failures.** `licenseId` exists only while a key verifies. If that were the whole signal, a lapse would look exactly like a departure, and the renewal conversation would start after the customer noticed rather than before. `licenseStatus` keeps arriving through `expired` and `invalid`. A mis-built release (production with no issuer key baked) reports `invalid` rather than staying quiet, because that one is ours to fix.
172
+
173
+ **The plan is not sent, and neither is the organisation name.** They are left out for different reasons and both are worth stating. The name is free text somebody typed when the key was signed, so rule 3 forbids it. The plan is merely redundant, which is the quieter reason and the easier one to lose sight of: the issuance ledger already holds it against this same id, so sending it would be a per-event cost, from every machine, forever, for something the join that resolves the id resolves at the same moment. `reticle license` still reports it locally, where it costs nothing.
174
+
175
+ **The organisation name never goes on the wire.** It is free text somebody typed when the key was signed, so it falls under rule 3. The id is opaque; the map from id to company is a local ledger. An analytics-side breach therefore cannot expose who is evaluating Reticle.
176
+
177
+ Resolution reads the EVENT's clock, not one captured at daemon start: sessions here run to eleven hours, and a key that expires mid-session has to start reporting `expired` from the event it expired on.
178
+
179
+ > **This changes what a licensed deployment sends, so it is a contract term, not a quiet addition.** The enterprise agreement has to say that licensed deployments report usage attributed to their licence id, and list these fields. `RETICLE_TELEMETRY=0` and `DO_NOT_TRACK` still switch it off exactly as they switch off everything else. There is no exception for licensed installs, and adding one would put a hole in the kill switch that a security review is entitled to find.
180
+
164
181
  ## Why they stopped: `tool_refused`
165
182
 
166
183
  The refusal path computes a precise diagnosis, hands it to the agent as prose, and throws it away. So the biggest cohort in the funnel, the users who attach an agent and never drive, emitted nothing at all and was reachable only by subtracting two other numbers. Half of issue #172.
@@ -75,7 +75,7 @@ Direct `reticle_network` and `reticle_console` stay advertised even though `reti
75
75
  ## Reaching the other 29
76
76
 
77
77
  ```json
78
- { "tool": "reticle_flow_verify", "args": {} }
78
+ { "tool": "reticle_verify", "args": { "action": "flows" } }
79
79
  ```
80
80
 
81
81
  `reticle_run` takes a tool name and its arguments. `reticle_tools` lists what's available and returns the full argument grammar for any of them, so your agent can discover a tool mid-task without you having pre-loaded its schema.
@@ -70,6 +70,11 @@ Reticle is built for agents, which creates an unusual problem: the primary user'
70
70
  | `ambiguity` | The check ran but you could not tell what the answer meant |
71
71
  | `feature_request` | You wished a capability existed |
72
72
  | `improvement` | It works, but costs too many calls |
73
+ | `experience` | Something worked notably well (pair it with `rating`, 1-5) |
74
+
75
+ `experience` is the only kind here that is not a complaint, and that is why it exists. Every other row grows the corpus into a defect list, which says nothing about which parts are worth protecting when we change them. File it unprompted, when you actually notice it, and name the concrete moment: which call, what it saved you, what you would have done instead.
76
+
77
+ The `rating` is secondary to that text on purpose. A score produced because something asked for one is indistinguishable from a score that was earned, and once both sit in the same column neither means anything. The text is the part that can be acted on or quoted.
73
78
 
74
79
  ### It will tell you when it cannot send
75
80
 
@@ -18,21 +18,21 @@ Reticle ships 46 tools plus these two meta-tools. On the default profile your ag
18
18
 
19
19
  Returns every tool with a one-line summary. A sample of what lives in the cold tail:
20
20
 
21
- | Tool | What it does |
22
- | ----------------------- | ---------------------------------------------------------------------- |
23
- | `reticle_capabilities` | The app's advertised testable surface, testids, signals, stores, flows |
24
- | `reticle_flow_verify` | Replay every saved flow, return one consolidated suite verdict |
25
- | `reticle_screenshot` | Pixel screenshot of the driven page, saved as a visual baseline |
26
- | `reticle_visual_diff` | Perceptually diff the live page against that baseline |
27
- | `reticle_network_mock` | Force a 500, go offline, or delay a response |
28
- | `reticle_viewport` | Pin the viewport so screenshots are reproducible across machines |
29
- | `reticle_crawl` | Click every reachable control and report anomalies, with no script |
30
- | `reticle_clock` | Freeze time or fast-forward timers, toasts, debounces, auto-dismiss |
31
- | `reticle_storage` | localStorage, sessionStorage and readable cookies |
32
- | `reticle_coverage` | Which controls you have driven this session, and which you have not |
33
- | `reticle_verify_change` | Give it the files you edited; it replays the flows that cover them |
34
- | `reticle_reconcile` | Compare what the API returned against what the page actually renders |
35
- | `reticle_scroll_to` | Find a row in a virtualised list that has not rendered yet |
21
+ | Tool | What it does |
22
+ | --- | --- |
23
+ | `reticle_capabilities` | The app's advertised testable surface, testids, signals, stores, flows |
24
+ | `reticle_verify` (`flows`) | Replay every saved flow, return one consolidated suite verdict |
25
+ | `reticle_screenshot` | Pixel screenshot of the driven page, saved as a visual baseline |
26
+ | `reticle_visual_diff` | Perceptually diff the live page against that baseline |
27
+ | `reticle_network_mock` | Force a 500, go offline, or delay a response |
28
+ | `reticle_viewport` | Pin the viewport so screenshots are reproducible across machines |
29
+ | `reticle_verify` (`crawl`) | Click every reachable control and report anomalies, with no script |
30
+ | `reticle_clock` | Freeze time or fast-forward timers, toasts, debounces, auto-dismiss |
31
+ | `reticle_storage` | localStorage, sessionStorage and readable cookies |
32
+ | `reticle_verify` (`coverage`) | Which controls you have driven this session, and which you have not |
33
+ | `reticle_verify` (`change`) | Give it the files you edited; it replays the flows that cover them |
34
+ | `reticle_reconcile` | Compare what the API returned against what the page actually renders |
35
+ | `reticle_scroll_to` | Find a row in a virtualised list that has not rendered yet |
36
36
 
37
37
  The response also tells you which surface is active:
38
38
 
package/docs/usage.md CHANGED
@@ -286,10 +286,10 @@ List interactive elements + console-error count for autonomous exploration. See
286
286
 
287
287
  ### Flows, recorder & self-healing (`.reticle/`)
288
288
 
289
- `reticle_contract_save`, `reticle_flow_save` / `reticle_flow_save_recorded` / `reticle_flow {action:"list"}` / `reticle_flow {action:"load"}` / `reticle_flow_replay` / `reticle_flow_verify`, `reticle_flow_heal`, `reticle_annotate`: record once, replay forever (anchored on testid/signal, or on an auto-derived component/source anchor when there's no testid), with legible drift + self-heal. Full guide: [Flows, the recorder & self-healing](flows.md).
289
+ `reticle_contract_save`, `reticle_flow_save` / `reticle_flow_save_recorded` / `reticle_flow {action:"list"}` / `reticle_flow {action:"load"}` / `reticle_flow_replay` / `reticle_verify {action:"flows"}`, `reticle_flow_heal`, `reticle_annotate`: record once, replay forever (anchored on testid/signal, or on an auto-derived component/source anchor when there's no testid), with legible drift + self-heal. Full guide: [Flows, the recorder & self-healing](flows.md).
290
290
 
291
- - **`reticle_flow_verify({ names?, sessionId? })`** is the regression-suite call: it replays EVERY saved flow (or a subset) deterministically and returns one verdict `{ status, passed, failed, failures: [{ flow, verdict, whatChanged, whereInSource, nextAction }] }`. Passing flows are counted; only failures carry detail. Run it after any change: one call, no LLM per flow.
292
- - **Decision envelope:** on a drift/fail, `reticle_flow_replay` (and each `reticle_flow_verify` failure) returns the actionable fix: `whatChanged`, `whereInSource` (`file:line`), and a one-line `nextAction` (e.g. "rebind the anchor to 'new-deploy', or update the flow if intended").
291
+ - **`reticle_verify({ action: "flows", names?, sessionId? })`** is the regression-suite call: it replays EVERY saved flow (or a subset) deterministically and returns one verdict `{ status, passed, failed, failures: [{ flow, verdict, whatChanged, whereInSource, nextAction }] }`. Passing flows are counted; only failures carry detail. Run it after any change: one call, no LLM per flow.
292
+ - **Decision envelope:** on a drift/fail, `reticle_flow_replay` (and each `reticle_verify {action:"flows"}` failure) returns the actionable fix: `whatChanged`, `whereInSource` (`file:line`), and a one-line `nextAction` (e.g. "rebind the anchor to 'new-deploy', or update the flow if intended").
293
293
 
294
294
  ### Human-in-the-loop control
295
295
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reticlehq/server",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Reticle bridge + MCP server. Hosts the browser WS endpoint and exposes MCP tools to the coding agent.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -22,7 +22,7 @@
22
22
  "@modelcontextprotocol/sdk": "^1.30.0",
23
23
  "ws": "^8.21.3",
24
24
  "zod": "^3.24.1",
25
- "@reticlehq/core": "2.9.0"
25
+ "@reticlehq/core": "2.10.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@types/pngjs": "^6.0.5",