@reticlehq/server 2.5.0 → 2.6.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 +27 -10
- package/dist/bridge/bridge.d.ts +12 -0
- package/dist/bridge/bridge.js +32 -2
- package/dist/bridge/bridge.test-harness.d.ts +8 -0
- package/dist/bridge/bridge.test-harness.js +9 -1
- package/dist/cli/cli-doctor.d.ts +0 -5
- package/dist/cli/cli-doctor.js +71 -3
- package/dist/cli/cli-parse-feedback.d.ts +31 -0
- package/dist/cli/cli-parse-feedback.js +56 -0
- package/dist/cli/cli-parse.d.ts +5 -9
- package/dist/cli/cli-parse.js +7 -23
- package/dist/cli/doctor-daemon-line.d.ts +37 -0
- package/dist/cli/doctor-daemon-line.js +35 -0
- package/dist/cli/port-holder.d.ts +43 -0
- package/dist/cli/port-holder.js +99 -0
- package/dist/cli.js +131 -11
- package/dist/daemon/daemon-resilience.d.ts +24 -0
- package/dist/daemon/daemon-resilience.js +105 -17
- package/dist/daemon/daemon.js +26 -1
- package/dist/daemon/heartbeat.d.ts +115 -0
- package/dist/daemon/heartbeat.js +141 -0
- package/dist/daemon/idle-shutdown.d.ts +13 -5
- package/dist/daemon/idle-shutdown.js +14 -6
- package/dist/daemon/port-presence.d.ts +76 -0
- package/dist/daemon/port-presence.js +93 -0
- package/dist/daemon/state-dir.d.ts +7 -0
- package/dist/daemon/state-dir.js +59 -0
- package/dist/daemon/unclean-predecessor.d.ts +23 -0
- package/dist/daemon/unclean-predecessor.js +27 -0
- package/dist/events/predicate-eval.d.ts +10 -0
- package/dist/events/predicate-eval.js +24 -4
- package/dist/events/predicate-parse.d.ts +23 -0
- package/dist/events/predicate-parse.js +54 -0
- package/dist/events/predicate.d.ts +5 -0
- package/dist/events/predicate.js +17 -3
- package/dist/events/ring-buffer.js +1 -16
- package/dist/flows/flow-replay.d.ts +1 -1
- package/dist/flows/flow-replay.js +19 -2
- package/dist/flows/flow-step-runners.js +2 -3
- package/dist/flows/flow-tools.js +10 -14
- package/dist/flows/role-drift-reason.d.ts +20 -0
- package/dist/flows/role-drift-reason.js +39 -0
- package/dist/flows/suite-flakes.d.ts +33 -0
- package/dist/flows/suite-flakes.js +32 -0
- package/dist/honesty/blind-spots.d.ts +19 -0
- package/dist/honesty/blind-spots.js +33 -0
- package/dist/honesty/verified.d.ts +10 -1
- package/dist/honesty/verified.js +16 -0
- package/dist/http-server.js +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -1
- package/dist/init/agent-rules.d.ts +18 -0
- package/dist/init/agent-rules.js +22 -4
- package/dist/init/cra.d.ts +13 -0
- package/dist/init/cra.js +16 -0
- package/dist/init/detect.d.ts +6 -0
- package/dist/init/detect.js +8 -0
- package/dist/init/mcp-clients.d.ts +81 -0
- package/dist/init/mcp-clients.js +248 -0
- package/dist/init/node-io.js +8 -2
- package/dist/init/plan-framework.js +36 -2
- package/dist/init/plan.d.ts +14 -0
- package/dist/init/plan.js +73 -9
- package/dist/init/run.d.ts +18 -1
- package/dist/init/run.js +133 -16
- package/dist/init/windows-quote.d.ts +38 -0
- package/dist/init/windows-quote.js +70 -0
- package/dist/mcp/mcp-outage.d.ts +1 -0
- package/dist/mcp/mcp-outage.js +8 -1
- package/dist/mcp/mcp-proxy.d.ts +11 -1
- package/dist/mcp/mcp-proxy.js +30 -7
- package/dist/mcp/mcp.js +29 -2
- package/dist/pool/browser-pool.d.ts +9 -0
- package/dist/pool/browser-pool.js +38 -9
- package/dist/session/attachment-history.d.ts +46 -0
- package/dist/session/attachment-history.js +65 -0
- package/dist/session/no-session-diagnosis.d.ts +7 -0
- package/dist/session/no-session-diagnosis.js +53 -3
- package/dist/session/no-session-watch.d.ts +3 -1
- package/dist/session/no-session-watch.js +10 -2
- package/dist/session/pending-commands.js +1 -0
- package/dist/session/session-info.d.ts +17 -1
- package/dist/session/session-info.js +5 -1
- package/dist/session/session-manager.d.ts +3 -0
- package/dist/session/session-manager.js +72 -3
- package/dist/session/session.d.ts +1 -0
- package/dist/session/session.js +8 -0
- package/dist/telemetry/crash-cause.d.ts +47 -0
- package/dist/telemetry/crash-cause.js +104 -0
- package/dist/telemetry/daemon-telemetry.d.ts +25 -7
- package/dist/telemetry/daemon-telemetry.js +28 -13
- package/dist/telemetry/error-class.d.ts +26 -0
- package/dist/telemetry/error-class.js +52 -0
- package/dist/telemetry/feedback-cli.d.ts +7 -2
- package/dist/telemetry/feedback-cli.js +49 -5
- package/dist/telemetry/feedback-context.d.ts +1 -6
- package/dist/telemetry/feedback-context.js +64 -4
- package/dist/telemetry/git-facts.d.ts +7 -0
- package/dist/telemetry/git-facts.js +4 -4
- package/dist/telemetry/project-profile.js +2 -1
- package/dist/telemetry/session-metrics.d.ts +45 -3
- package/dist/telemetry/session-metrics.js +247 -9
- package/dist/telemetry/telemetry.d.ts +19 -1
- package/dist/telemetry/telemetry.js +31 -8
- package/dist/tools/act-tools.js +33 -6
- package/dist/tools/assert-verdict.d.ts +7 -1
- package/dist/tools/assert-verdict.js +15 -3
- package/dist/tools/browser-tools.js +8 -2
- package/dist/tools/contract-tools.d.ts +0 -5
- package/dist/tools/contract-tools.js +26 -0
- package/dist/tools/dynamic-tools.js +6 -1
- package/dist/tools/error-recovery.d.ts +14 -0
- package/dist/tools/error-recovery.js +83 -7
- package/dist/tools/feedback-invite.d.ts +48 -0
- package/dist/tools/feedback-invite.js +71 -0
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +69 -1
- package/dist/tools/navigate-arrival.d.ts +26 -0
- package/dist/tools/navigate-arrival.js +60 -0
- package/dist/tools/navigate-result.d.ts +15 -1
- package/dist/tools/navigate-result.js +17 -10
- package/dist/tools/observe-tools.js +22 -4
- package/dist/tools/real-input-attempt.js +10 -5
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.js +13 -0
- package/docs/README.md +55 -0
- package/docs/agent-cheatsheet.md +2 -2
- package/docs/benchmarks.md +1 -1
- package/docs/fixtures-dispatch-receiver.yml +76 -0
- package/docs/fixtures.md +17 -0
- package/docs/gate-plan.md +98 -0
- package/docs/gates.md +117 -0
- package/docs/local-registry.md +1 -1
- package/docs/matrix/2.5.0/claude-code-machine.json +19 -0
- package/docs/matrix/2.5.0/codex-machine.json +20 -0
- package/docs/matrix/2.5.0/cursor-machine.json +22 -0
- package/docs/matrix/2.5.0/gemini-machine.json +22 -0
- package/docs/matrix/2.5.0/opencode-machine.json +22 -0
- package/docs/matrix/2.5.0/vscode-machine.json +22 -0
- package/docs/matrix/2.5.0/windsurf-machine.json +22 -0
- package/docs/matrix/MATRIX.md +26 -0
- package/docs/matrix/README.md +59 -0
- package/docs/mint.json +1 -1
- package/docs/system-map.md +152 -0
- package/docs/telemetry-contract.md +2 -2
- package/docs/telemetry.md +7 -0
- package/package.json +2 -2
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# System map
|
|
2
|
+
|
|
3
|
+
> How a tool call gets from a coding agent to your app and back, what each hop can do wrong, and which of those failures are **silent**. Written for anyone touching the transport, the daemon, the bridge, or a gate — and for contributors trying to work out where their change lives.
|
|
4
|
+
>
|
|
5
|
+
> Companion pages: [`harness-rules.md`](../apps/e2e/harness-rules.md) (what a gate must do about all this) and [`telemetry-contract.md`](./telemetry-contract.md) (the rules for anything that emits).
|
|
6
|
+
|
|
7
|
+
## 1. Topology — 5 processes, 4 hops
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
coding agent ──stdio──▶ reticle mcp ──HTTP POST──▶ daemon :4400 ──WebSocket──▶ browser SDK ──▶ your app
|
|
11
|
+
(client) JSON-RPC (proxy) ◀───SSE───── (bridge+tools) ◀───────── (@reticlehq/browser)
|
|
12
|
+
│
|
|
13
|
+
└─▶ .reticle/ flows · capsules · baselines · runs
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Each hop has its own failure vocabulary, its own recovery, and its own way of lying. Nothing stated this end to end until this page, which is why gate results have been contradictory.
|
|
17
|
+
|
|
18
|
+
## 2. Connection establishment
|
|
19
|
+
|
|
20
|
+
| # | Step | Owner | Fails how |
|
|
21
|
+
| --- | --- | --- | --- |
|
|
22
|
+
| 1 | client spawns `reticle mcp` over stdio | client | the client's own startup timeout; **no host respawns a dead stdio server** |
|
|
23
|
+
| 2 | proxy probes `:4400` (`probeDaemon`, plain TCP, 500ms) | proxy | a squatter that accepts and never serves looks alive |
|
|
24
|
+
| 3 | absent → spawn daemon detached, poll to `DAEMON_READY_TIMEOUT_MS` (10s) | proxy | slow CI; raise with `RETICLE_DAEMON_READY_TIMEOUT_MS` |
|
|
25
|
+
| 4 | proxy opens SSE, waits for the `endpoint` frame → POST URL | proxy | an empty frame used to read as "connected"; now refused (`buildSessionUrl`) |
|
|
26
|
+
| 5 | `initialize` — answered **locally** after `LOCAL_HANDSHAKE_MS` (12s) when the daemon cannot | proxy | any client with a startup budget under ~12s loses Reticle for the whole session |
|
|
27
|
+
| 6 | daemon binds `:4400`, writes the pid file, starts bridge + idle watcher | daemon | `EADDRINUSE` → child exits 1, **parent already exited 0** |
|
|
28
|
+
| 7 | app calls `reticle.connect()` → `ws://localhost:4400/reticle` | SDK | wrong port; missing pairing token; mixed content (Safari throws synchronously on `new WebSocket`) |
|
|
29
|
+
| 8 | HELLO: `sessionId`, `projectId`, `protocolVersion`, capabilities | SDK → bridge | version mismatch → `1008` close → **the SDK stops retrying, permanently** |
|
|
30
|
+
| 9 | bridge authenticates: pairing token / loopback tier / Origin allow-list | bridge | another project's daemon answers → `authFailureReason` names it |
|
|
31
|
+
| 10 | Session registered, `everConnected = true` | daemon | — |
|
|
32
|
+
| 11 | tool call → `runTool` → `session.send` → WS command → SDK → reply | all | any hop above, mid-flight |
|
|
33
|
+
|
|
34
|
+
**Sequencing hazard:** steps 6 and 7 race. The product absorbs it internally — the first live tool call blocks briefly for a session to appear rather than failing — but a gate that boots an app and immediately asks whether a session exists is outside that protection and has been burned by it. Poll for the session; never sample once.
|
|
35
|
+
|
|
36
|
+
## 3. The tool graph — what produces what
|
|
37
|
+
|
|
38
|
+
Three different counts, all measured, and the difference between them matters:
|
|
39
|
+
|
|
40
|
+
| Count | What it is |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| **68** | name constants in `ReticleTool` |
|
|
43
|
+
| **48** | tools advertised under `RETICLE_ADVERTISE_ALL_TOOLS=1` — what `tool-surface-sweep-test` drives (51 calls) |
|
|
44
|
+
| **17** | advertised by default; the rest are reached through `reticle_run` |
|
|
45
|
+
|
|
46
|
+
68 → 48 is **family folding**: `reticle_baseline`, `reticle_session`, `reticle_record`, `reticle_flow` and `reticle_lease` each absorb their members behind an `action` parameter. So "every tool is callable" is asserted over 48 surfaces, not 68 behaviours — a family member reachable only through an `action` value the sweep never passes is not covered by it. Worth closing when the per-tool budget lands (see [`gate-plan.md`](./gate-plan.md), Phase 4).
|
|
47
|
+
|
|
48
|
+
| Token | Produced by | Consumed by | Invalidated by |
|
|
49
|
+
| --- | --- | --- | --- |
|
|
50
|
+
| `sessionId` | `reticle_sessions`, HELLO | ~every tool | tab close; **survives reload** via `sessionStorage` |
|
|
51
|
+
| `ref` (`e115`) | `snapshot`, `query`, `scroll_to` | `inspect`, `act`, `act_and_wait`, `state` | any re-render — refuses cleanly rather than clicking the new occupant |
|
|
52
|
+
| `actionId` (`a2`) | `act`, `act_and_wait` | `observe`, `network`, `console` filters | window expiry |
|
|
53
|
+
| `since` cursor | every read | every read | ring-buffer eviction |
|
|
54
|
+
| `capsuleSaved` | `act_and_wait` | `replay` | — |
|
|
55
|
+
| flow | `record_start` → acts → `flow_save_recorded` | `flow_verify`, `flow_replay`, `flow_heal`, `affected`, `gate` | DOM drift → `flow_heal` |
|
|
56
|
+
| baseline | `baseline_save`, `viewport` | `visual_diff`, `diff` | viewport change |
|
|
57
|
+
| `session_lease` | direct tool results | the agent (`yield` / `end`) | **dropped on the `reticle_run` path** (#119) |
|
|
58
|
+
| run artifact | `run_export`, `reticle verify` | CI, `reticle gate` | — |
|
|
59
|
+
|
|
60
|
+
Two structural facts:
|
|
61
|
+
|
|
62
|
+
- **`reticle_run` is a second dispatch path.** It routes through `runTool`, so telemetry is intact, but anything decorating results _above_ `runTool` silently does not apply. The session lease is the instance we know about; the next decorator inherits the same bug.
|
|
63
|
+
- **`record → save → verify → heal` is the only model-free loop**, and it is what makes a cheap gate possible at all.
|
|
64
|
+
|
|
65
|
+
## 4. Fragility inventory
|
|
66
|
+
|
|
67
|
+
**Silent** = fails with no error surfaced to the agent or the user.
|
|
68
|
+
|
|
69
|
+
### Hop 1 — client ↔ proxy
|
|
70
|
+
|
|
71
|
+
| Failure | Silent | Guarded by |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| proxy killed externally (the `lsof -ti` recipe) | **yes, total** | nothing — needs `reticle kill` (#114). See [harness rule 1](../apps/e2e/harness-rules.md) |
|
|
74
|
+
| proxy uncaught exception | no | `installProxyResilience` + `proxyLog` |
|
|
75
|
+
| proxy exits when the reconnect budget is spent | _was the worst bug in the product_ | `onReconnectBudgetSpent` → DORMANT, never exit |
|
|
76
|
+
| client closes stdin with calls in flight | no | `SHUTDOWN_DRAIN_MS` (5s) |
|
|
77
|
+
|
|
78
|
+
### Hop 2 — proxy ↔ daemon
|
|
79
|
+
|
|
80
|
+
Three distinct populations of unanswered call, **all covered**:
|
|
81
|
+
|
|
82
|
+
| Population | Answered by |
|
|
83
|
+
| --------------------------------------------- | ---------------------------------- |
|
|
84
|
+
| forwarded, then the SSE stream dropped | `streamLossReplies` → `-32001` |
|
|
85
|
+
| queued while dormant/reconnecting, never sent | `QUEUE_WAIT_MS` (20s) → `-32001` |
|
|
86
|
+
| POST-leg `ECONNRESET` while SSE is healthy | `postToSession` returns the reason |
|
|
87
|
+
|
|
88
|
+
Still fragile:
|
|
89
|
+
|
|
90
|
+
| Failure | Silent | Note |
|
|
91
|
+
| --- | --- | --- |
|
|
92
|
+
| squatter on `:4400` | partly | 12s handshake, then the first tool call diagnoses it |
|
|
93
|
+
| stale tool catalog served while dormant | **yes** | `ToolCatalogCache` answers `tools/list` from cache; after a version change a client can hold a catalog the daemon no longer implements |
|
|
94
|
+
| handshake replay regresses | **yes** | the daemon builds a fresh `McpServer` per SSE connection, so every `tools/call` would hit an uninitialized server |
|
|
95
|
+
|
|
96
|
+
### Hop 3 — daemon lifecycle
|
|
97
|
+
|
|
98
|
+
| Failure | Silent | Note |
|
|
99
|
+
| --- | --- | --- |
|
|
100
|
+
| `serve` exits 0 on `EADDRINUSE` | **yes** | #115 — the parent reports the spawn, the child reports the bind, nothing joins them |
|
|
101
|
+
| liveness is the pid file, never the port | **yes** | `daemon/daemon.ts:156` |
|
|
102
|
+
| idle shutdown fires mid-install | **yes** | 5min base / 30min attached — already scored three fixtures as install failures |
|
|
103
|
+
| `isUselessDaemon` ends a daemon that served nothing | **yes** | lifetime facts, but a gate that installs for six minutes before its first call is that daemon |
|
|
104
|
+
| a daemon spawned inside a tool call is reaped when the call ends | **yes** | `SIGTERM` within ~1s, `detached` or not |
|
|
105
|
+
| no `daemon_alive` heartbeat | **fixed** | a killed daemon and a tidy exit used to read identically; the daemon now beats every 30s and `classifyDaemonLife` turns silence into `died_silently` (#123) |
|
|
106
|
+
| no port arbitration | **yes** | a second daemon can bind a port a first one held |
|
|
107
|
+
|
|
108
|
+
### Hop 4 — bridge ↔ SDK
|
|
109
|
+
|
|
110
|
+
| Failure | Silent | Note |
|
|
111
|
+
| --- | --- | --- |
|
|
112
|
+
| protocol mismatch → `1008` | no, but **terminal** | the SDK stops retrying by design; the app is dark until reload (#127) |
|
|
113
|
+
| another project's daemon answers | no | `authFailureReason` — evidence-based, only claims it when the daemon has demonstrably served another project |
|
|
114
|
+
| `rejectAll('session disconnected')` | **fixed** | used to become `verified:"no" / assertion_failed` — Reticle blaming the app, by file and line, for its own lost connection. Now `verified:"unknown" / observation_lost` (#124) |
|
|
115
|
+
| reaper ends a quiet session | no | revivable on the agent's next action |
|
|
116
|
+
| opaque origins (`tauri://localhost`, `file://`) | no | kept verbatim in the allow-list rather than collapsed to `"null"` |
|
|
117
|
+
|
|
118
|
+
### Hop 5 — SDK internals
|
|
119
|
+
|
|
120
|
+
| Failure | Silent | Note |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| fixed 1s reconnect, no backoff, unbounded | **yes** | and the noise lands in the console channel we report to the agent (#116) |
|
|
123
|
+
| `onUnreachable` suppressed after the first success | **yes** | a **mid-session** outage never warns at all |
|
|
124
|
+
| offline queue overflow (`MAX_QUEUE = 500`) | no | gap marker stamped at the **oldest** drop — the honest floor for where the hole starts |
|
|
125
|
+
| `sessionStorage` throws in a sandboxed iframe | no | falls back; a session must still start |
|
|
126
|
+
|
|
127
|
+
## 5. Where the gates are
|
|
128
|
+
|
|
129
|
+
| Layer | Class | Covered by | Gap |
|
|
130
|
+
| --- | --- | --- | --- |
|
|
131
|
+
| stdio | proxy survival | `mcp-survives-test` | external SIGKILL |
|
|
132
|
+
| SSE/POST | unanswered calls | `mcp-stress-test` | fault injection is `kill -9`, not specified toxics |
|
|
133
|
+
| daemon | lifecycle, idle, port | `daemon-lifecycle-test`, `status-honesty-test` | port arbitration, heartbeat |
|
|
134
|
+
| bridge | auth, origins, version | `version-skew-test` | desktop origins under install |
|
|
135
|
+
| SDK | queue, gaps, reconnect | browser unit tests | mid-session outage → verdict |
|
|
136
|
+
| tools | surface, schema, refusals | `tool-surface-sweep-test`, `tool-fuzz-test` | latency/failure budget |
|
|
137
|
+
| verdict | honesty | `release-smoke-test` | transport ≠ app (#124); false-green rate |
|
|
138
|
+
| telemetry | silent absence | `telemetry-events-test`, `telemetry-contract.test.ts` | chokepoint coverage |
|
|
139
|
+
| install | pristine-surface wiring | — | the whole class |
|
|
140
|
+
| client | per-agent MCP compatibility | — | the whole class |
|
|
141
|
+
|
|
142
|
+
Nine of eleven layers have a gate. The two that do not are exactly the two that need a second repo and a human. The plan for both is in [`gate-plan.md`](./gate-plan.md).
|
|
143
|
+
|
|
144
|
+
## 6. Traces already encode the invariants
|
|
145
|
+
|
|
146
|
+
`runTool` opens one `tool.handler` root span per call and every stage underneath inherits it. With `RETICLE_TRACE=1` that makes three assertions available from any existing battery run, at the cost of one log parser:
|
|
147
|
+
|
|
148
|
+
- exactly one root span at `depth: 0` per tool call
|
|
149
|
+
- **zero orphan spans** — 23 orphans on one healthy run is the documented signature of a hung call
|
|
150
|
+
- `settleTiming` always fires; it sits in a `finally` so a thrown call cannot leak a concurrency slot
|
|
151
|
+
|
|
152
|
+
Today that output is produced and discarded.
|
|
@@ -123,7 +123,7 @@ Four counters and one flag were added because the data could not answer question
|
|
|
123
123
|
| `abandonedActions` | session summary | actions driven with no verdict AFTER them — the trailing unsettled run, not `actions - verifications`. That difference ignores order, so a verdict that drove nothing (a `flow_verify` over saved flows) silently paid for an abandoned action elsewhere. |
|
|
124
124
|
| `verification.browser` | `verification_completed` | `headless` \| `headed` \| `attached` — who DROVE the browser. `attached` (Reticle launched nothing, the SDK connected from a browser somebody else opened) is the common case in production, so on its own this is mostly "somebody's own browser". |
|
|
125
125
|
| `verification.brand` | `verification_completed` | WHICH browser it was: `chrome` \| `edge` \| `arc` \| `dia` \| `brave` \| `opera` \| `firefox` \| `safari` \| `other` — the closed `BrowserBrand` list in core. The axis `engine` cannot answer, since Chrome, Edge, Arc, Dia and Brave are all `blink`. The SDK reads `navigator.userAgentData.brands` (and the UA string on Firefox/Safari, which expose no `userAgentData`) and normalises IN THE PAGE: a raw brand or UA string is unbounded and fingerprintable and never leaves. Anything unrecognised is `other`. **Omitted rather than `"unknown"`** when the page did not say — a desktop webview has no brand and an older SDK does not report one, and a guess is indistinguishable from a measurement on a dashboard. |
|
|
126
|
-
| `verification.reason` | `verification_completed` | WHICH clause of `decideVerified` produced the verdict, from core's closed `VerifiedReason`: `inconclusive` \| `assertion_failed` \| `contradicted` \| `already_true` \| `unclean_capture` \| `vacuous_grade` \| `outcome_pending` \| `outcome_unread` \| `unsettled` \| `proved`. See below. |
|
|
126
|
+
| `verification.reason` | `verification_completed` | WHICH clause of `decideVerified` produced the verdict, from core's closed `VerifiedReason`: `inconclusive` \| `observation_lost` \| `assertion_failed` \| `contradicted` \| `already_true` \| `unclean_capture` \| `vacuous_grade` \| `outcome_pending` \| `outcome_unread` \| `unsettled` \| `proved`. See below. |
|
|
127
127
|
| `bug.attribution` | `bug_found` | `app` \| `request` \| `reticle` — whose fault the defect was. **Absent means unclassified**, never `app`. See below. |
|
|
128
128
|
| `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. |
|
|
129
129
|
| `tzOffsetMin` | every event | minutes offset from UTC. One integer, no location. |
|
|
@@ -131,7 +131,7 @@ Four counters and one flag were added because the data could not answer question
|
|
|
131
131
|
|
|
132
132
|
## Why a verdict came out that way — `verification.reason`
|
|
133
133
|
|
|
134
|
-
`verified` has three values. The rule that produces it has **
|
|
134
|
+
`verified` has three values. The rule that produces it has **eleven clauses**. Everything in between was thrown away at the moment it was known.
|
|
135
135
|
|
|
136
136
|
Captured against the real classifier: `verified: 'unknown'` covered "the agent malformed the call", "the consequence was already true", "the app answered 202", "a 2xx body went unread", "the capture was not clean", "nothing was asserted at a real grade" and "the page never settled" — **seven causes, two wire payloads**. They belong to three different owners (the agent, the app, Reticle) and need opposite responses: teach the agent, wait and re-check, or ship a fix. On a dashboard they were one bar. `verified: 'no'` collapsed the same way — "channels disagree" (Reticle earning its keep) and "the agent's predicate failed" were the same string.
|
|
137
137
|
|
package/docs/telemetry.md
CHANGED
|
@@ -65,6 +65,10 @@ at doCheckout (/Users/ada/secret-app/src/checkout.tsx:42:9) ← dropped
|
|
|
65
65
|
at resolveAnchor (…/@reticlehq/server/dist/tools/act-tools.js:142:19) ← sent
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
**One narrow exception, for the crash that otherwise says nothing.** A refused connection — `connect ECONNREFUSED` — has a stack that is _entirely_ node internals, so the rule above correctly keeps nothing and the report arrives with no location at all. Those crashes now also carry the failing **syscall** (`connect`), the **errno** (`ECONNREFUSED`), whether the target was **loopback** (one boolean), whether the port was **one of Reticle's own** (the enum `reticle` / `other`), and the innermost frame naming **Node's own source** (`node:net:1637`).
|
|
69
|
+
|
|
70
|
+
The address and the port number are used to compute those two answers and are then discarded — neither is ever sent. The Node frame is a line in Node's published source, not yours: it says a connect failed rather than a DNS lookup, and carries nothing about your machine, your app, or your directory layout. The frame is included **only** when the crash is a system error and no Reticle frame survived — the report that would otherwise be blind.
|
|
71
|
+
|
|
68
72
|
`project_profiled` carries: the framework and its major version, a size **bucket** (`tiny` … `huge`, never a file count), whether it is a monorepo, its age in whole **weeks**, how many saved flows/baselines/runs exist, and which Reticle feature families have been used. It also carries whether the project has git at all, whether it has ever been pushed (`none` / `local_only` / `remote`), and which forge hosts it — `github`, `gitlab`, `bitbucket`, `azure`, `sourcehut`, `codeberg`, or just `self_hosted` for anything else. A private git host is usually `git.<company>.com`, so self-hosted repos report **only** that they are self-hosted, never the hostname.
|
|
69
73
|
|
|
70
74
|
This is how we learn whether people are getting value from the whole product or only a corner of it. No file names, no paths, no flow names, no dependency list.
|
|
@@ -104,6 +108,7 @@ Reticle verifies apps for AI agents, and for a long time it had no way to hear w
|
|
|
104
108
|
| --- | --- | --- |
|
|
105
109
|
| Your agent | Calls the `reticle_feedback` MCP tool — after a failure, **or to ask for a feature** | What it wrote: an analysis, or a request with the goal behind it, what would improve, and how it works around the gap today. Plus the model it is running, which it tells us because MCP cannot |
|
|
106
110
|
| You | Run `reticle feedback [--rating 1-5] [--bug] "your words"` | Your words and your rating |
|
|
111
|
+
| Your agent, before Reticle works | Runs `reticle feedback --agent --kind <bug\|gap\|ambiguity\|feature_request\|improvement> "what happened"` | The same words, filed from the shell. This exists for the phase where there is no daemon and no MCP tools — a failed `init`, half-finished wiring — which is the failure we are otherwise never told about |
|
|
107
112
|
|
|
108
113
|
Agents are instructed, in the tool description itself, never to include app source, secrets, or user data — to describe the failure in their own words instead.
|
|
109
114
|
|
|
@@ -145,6 +150,8 @@ reticle identify --context company --company "Acme" --email you@acme.com
|
|
|
145
150
|
|
|
146
151
|
Before it sends anything, it prints what it will send and one thing worth reading carefully: **the identity is linked to this machine's anonymous id, so identifying yourself also connects the anonymous usage already recorded from this machine to what you enter.** That is what makes it useful to us, and it is why you are told before choosing rather than after.
|
|
147
152
|
|
|
153
|
+
**One place mentions it to you.** After you send feedback as a human — never as an agent, and never if you have already identified or already declined — the receipt prints one line offering this command, because a bug report we cannot reply to is a conversation that ends after one sentence. Your feedback has already been sent by then: the line is an offer, not a question, and nothing is gated on it. **No address is ever attached to the feedback itself** — it would put personal data on the anonymous stream, which is exactly what the rest of this page promises not to do.
|
|
154
|
+
|
|
148
155
|
To undo it:
|
|
149
156
|
|
|
150
157
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.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.13.0",
|
|
23
23
|
"ws": "^8.18.0",
|
|
24
24
|
"zod": "^3.24.1",
|
|
25
|
-
"@reticlehq/core": "2.
|
|
25
|
+
"@reticlehq/core": "2.6.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/pngjs": "^6.0.5",
|