@reticlehq/server 2.6.0 → 2.8.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 (276) hide show
  1. package/README.md +14 -5
  2. package/SKILL.md +251 -33
  3. package/dist/bridge/bridge.d.ts +6 -1
  4. package/dist/bridge/bridge.js +27 -4
  5. package/dist/capsule/causal-summary.d.ts +20 -0
  6. package/dist/capsule/causal-summary.js +31 -6
  7. package/dist/capsule/divergence.js +7 -1
  8. package/dist/cli/chromium-hint.d.ts +68 -0
  9. package/dist/cli/chromium-hint.js +145 -0
  10. package/dist/cli/cli-doctor.js +31 -36
  11. package/dist/cli/cli-kill.d.ts +58 -0
  12. package/dist/cli/cli-kill.js +162 -0
  13. package/dist/cli/cli-launch.d.ts +18 -0
  14. package/dist/cli/cli-launch.js +32 -1
  15. package/dist/cli/cli-parse.d.ts +16 -1
  16. package/dist/cli/cli-parse.js +32 -9
  17. package/dist/cli/cli-port.d.ts +8 -0
  18. package/dist/cli/cli-port.js +36 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/doctor-daemon-line.js +10 -1
  21. package/dist/cli/doctor-sessions-line.d.ts +12 -0
  22. package/dist/cli/doctor-sessions-line.js +66 -0
  23. package/dist/cli/port-holder.d.ts +10 -0
  24. package/dist/cli/port-holder.js +26 -2
  25. package/dist/cli.js +139 -23
  26. package/dist/crawl/crawl-tools.js +12 -2
  27. package/dist/crawl/crawl.d.ts +13 -0
  28. package/dist/crawl/crawl.js +50 -2
  29. package/dist/daemon/daemon-resilience.js +2 -2
  30. package/dist/daemon/loopback-alias.d.ts +32 -0
  31. package/dist/daemon/loopback-alias.js +80 -0
  32. package/dist/events/contradictions.d.ts +14 -0
  33. package/dist/events/contradictions.js +26 -17
  34. package/dist/events/predicate-eval.d.ts +26 -3
  35. package/dist/events/predicate-eval.js +284 -95
  36. package/dist/events/predicate-parse.d.ts +1 -1
  37. package/dist/events/predicate-parse.js +18 -4
  38. package/dist/events/predicate.js +170 -18
  39. package/dist/events/ring-buffer.d.ts +8 -0
  40. package/dist/events/ring-buffer.js +34 -1
  41. package/dist/flows/flow-classify.js +1 -1
  42. package/dist/flows/flow-tools.js +12 -4
  43. package/dist/flows/heal-run.js +1 -1
  44. package/dist/honesty/blind-spots.js +6 -2
  45. package/dist/honesty/honesty.d.ts +11 -0
  46. package/dist/honesty/honesty.js +9 -1
  47. package/dist/honesty/unsettled.d.ts +40 -0
  48. package/dist/honesty/unsettled.js +78 -0
  49. package/dist/honesty/verified.d.ts +7 -0
  50. package/dist/honesty/verified.js +27 -4
  51. package/dist/http-server.js +11 -1
  52. package/dist/index.js +29 -2
  53. package/dist/init/agent-rules.d.ts +16 -0
  54. package/dist/init/agent-rules.js +96 -17
  55. package/dist/init/astro-patch.d.ts +2 -1
  56. package/dist/init/astro-patch.js +11 -5
  57. package/dist/init/capabilities.d.ts +22 -0
  58. package/dist/init/capabilities.js +76 -0
  59. package/dist/init/confirm.d.ts +96 -0
  60. package/dist/init/confirm.js +138 -0
  61. package/dist/init/cra.js +3 -2
  62. package/dist/init/detect.d.ts +8 -0
  63. package/dist/init/detect.js +15 -0
  64. package/dist/init/non-js-project.d.ts +27 -0
  65. package/dist/init/non-js-project.js +84 -0
  66. package/dist/init/plan-framework.d.ts +9 -0
  67. package/dist/init/plan-framework.js +72 -16
  68. package/dist/init/plan.d.ts +40 -4
  69. package/dist/init/plan.js +183 -43
  70. package/dist/init/run.d.ts +24 -2
  71. package/dist/init/run.js +181 -46
  72. package/dist/init/slash-command.d.ts +1 -1
  73. package/dist/init/slash-command.js +46 -6
  74. package/dist/init/snippets.d.ts +64 -5
  75. package/dist/init/snippets.js +220 -31
  76. package/dist/init/vite-config.js +29 -3
  77. package/dist/input/network-mock-tools.d.ts +19 -0
  78. package/dist/input/network-mock-tools.js +29 -4
  79. package/dist/license/license.js +1 -1
  80. package/dist/mcp/mcp-proxy.d.ts +69 -3
  81. package/dist/mcp/mcp-proxy.js +215 -23
  82. package/dist/mcp/mcp.js +58 -7
  83. package/dist/mcp/proxy-lifecycle.d.ts +20 -1
  84. package/dist/mcp/proxy-lifecycle.js +22 -1
  85. package/dist/pool/browser-pool.d.ts +22 -2
  86. package/dist/pool/browser-pool.js +44 -4
  87. package/dist/pool/playwright-launcher.js +10 -2
  88. package/dist/project/learned-routes.d.ts +24 -0
  89. package/dist/project/learned-routes.js +63 -0
  90. package/dist/project/project-store.d.ts +6 -0
  91. package/dist/project/project-store.js +25 -1
  92. package/dist/session/live-control-tools.js +39 -2
  93. package/dist/session/live-control.d.ts +7 -9
  94. package/dist/session/live-control.js +34 -1
  95. package/dist/session/no-session-diagnosis.d.ts +23 -4
  96. package/dist/session/no-session-diagnosis.js +128 -44
  97. package/dist/session/no-session-watch.js +12 -0
  98. package/dist/session/ref-provenance.d.ts +54 -0
  99. package/dist/session/ref-provenance.js +61 -0
  100. package/dist/session/review-store.d.ts +19 -0
  101. package/dist/session/review-store.js +34 -5
  102. package/dist/session/session-lease.js +1 -1
  103. package/dist/session/session-manager.d.ts +9 -0
  104. package/dist/session/session-manager.js +11 -0
  105. package/dist/session/session.d.ts +8 -0
  106. package/dist/session/session.js +12 -0
  107. package/dist/status-payload.d.ts +13 -1
  108. package/dist/status-payload.js +6 -1
  109. package/dist/telemetry/app-instrumented.d.ts +37 -0
  110. package/dist/telemetry/app-instrumented.js +54 -0
  111. package/dist/telemetry/automation-hint.d.ts +17 -0
  112. package/dist/telemetry/automation-hint.js +71 -0
  113. package/dist/telemetry/bug-found.d.ts +0 -20
  114. package/dist/telemetry/bug-found.js +38 -21
  115. package/dist/telemetry/cli-telemetry.js +7 -1
  116. package/dist/telemetry/daemon-telemetry.d.ts +4 -4
  117. package/dist/telemetry/daemon-telemetry.js +8 -4
  118. package/dist/telemetry/feedback.js +1 -1
  119. package/dist/telemetry/init-telemetry.js +9 -1
  120. package/dist/telemetry/install-source.d.ts +36 -0
  121. package/dist/telemetry/install-source.js +40 -0
  122. package/dist/telemetry/run-telemetry.js +4 -1
  123. package/dist/telemetry/session-metrics.d.ts +3 -3
  124. package/dist/telemetry/session-metrics.js +9 -4
  125. package/dist/telemetry/telemetry.d.ts +7 -1
  126. package/dist/telemetry/telemetry.js +17 -2
  127. package/dist/telemetry/tool-refused.d.ts +19 -0
  128. package/dist/telemetry/tool-refused.js +55 -0
  129. package/dist/telemetry/verification-of.js +72 -8
  130. package/dist/temp-dir.d.ts +27 -0
  131. package/dist/temp-dir.js +46 -0
  132. package/dist/tools/act-tools.js +77 -11
  133. package/dist/tools/assert-grade.d.ts +1 -1
  134. package/dist/tools/assert-grade.js +27 -5
  135. package/dist/tools/assert-verdict.js +25 -1
  136. package/dist/tools/contract-tools.js +4 -0
  137. package/dist/tools/dynamic-tools.js +1 -1
  138. package/dist/tools/error-recovery.d.ts +10 -0
  139. package/dist/tools/error-recovery.js +70 -5
  140. package/dist/tools/feedback-invite.js +1 -1
  141. package/dist/tools/invoke-tool.js +149 -32
  142. package/dist/tools/lease-tools.d.ts +29 -1
  143. package/dist/tools/lease-tools.js +58 -5
  144. package/dist/tools/navigate-result.js +1 -1
  145. package/dist/tools/observe-tools.js +1 -1
  146. package/dist/tools/observed-nothing.js +1 -1
  147. package/dist/tools/react-grace.d.ts +48 -0
  148. package/dist/tools/react-grace.js +87 -0
  149. package/dist/tools/read-tools.js +20 -4
  150. package/dist/tools/settle-in-flight.d.ts +79 -0
  151. package/dist/tools/settle-in-flight.js +106 -0
  152. package/dist/tools/snapshot-delta.d.ts +7 -0
  153. package/dist/tools/snapshot-delta.js +130 -6
  154. package/dist/tools/tool-kit.d.ts +52 -5
  155. package/dist/tools/tool-kit.js +55 -24
  156. package/dist/tools/tool-surface.js +28 -9
  157. package/dist/tools/tools.js +38 -4
  158. package/dist/visual/capture-cleanup.d.ts +16 -0
  159. package/dist/visual/capture-cleanup.js +39 -0
  160. package/dist/visual/visual-tools.d.ts +18 -0
  161. package/dist/visual/visual-tools.js +24 -5
  162. package/docs/README.md +56 -11
  163. package/docs/actions.mdx +87 -0
  164. package/docs/agent-cheatsheet.md +102 -26
  165. package/docs/architecture.md +46 -35
  166. package/docs/benchmarks.md +76 -72
  167. package/docs/best-practices.mdx +211 -0
  168. package/docs/capabilities.mdx +387 -0
  169. package/docs/cli/affected.mdx +51 -0
  170. package/docs/cli/capsules.mdx +69 -0
  171. package/docs/cli/cloud.mdx +127 -0
  172. package/docs/cli/doctor.mdx +76 -0
  173. package/docs/cli/drive.mdx +55 -0
  174. package/docs/cli/feedback.mdx +67 -0
  175. package/docs/cli/gate.mdx +64 -0
  176. package/docs/cli/help.mdx +92 -0
  177. package/docs/cli/hunt.mdx +57 -0
  178. package/docs/cli/identify.mdx +62 -0
  179. package/docs/cli/init.mdx +94 -0
  180. package/docs/cli/kill.mdx +91 -0
  181. package/docs/cli/license.mdx +48 -0
  182. package/docs/cli/mcp.mdx +61 -0
  183. package/docs/cli/open.mdx +68 -0
  184. package/docs/cli/restart.mdx +60 -0
  185. package/docs/cli/rollback.mdx +35 -0
  186. package/docs/cli/serve.mdx +62 -0
  187. package/docs/cli/status.mdx +64 -0
  188. package/docs/cli/stop.mdx +56 -0
  189. package/docs/cli/telemetry.mdx +57 -0
  190. package/docs/cli/update.mdx +52 -0
  191. package/docs/cli/verify.mdx +57 -0
  192. package/docs/cli/version.mdx +39 -0
  193. package/docs/cli/watch.mdx +55 -0
  194. package/docs/cli.mdx +219 -0
  195. package/docs/debugging.md +13 -9
  196. package/docs/deploy-checks.md +15 -11
  197. package/docs/desktop-apps.md +49 -40
  198. package/docs/desktop.mdx +188 -0
  199. package/docs/docs.json +363 -0
  200. package/docs/enterprise.md +20 -16
  201. package/docs/faq.mdx +88 -0
  202. package/docs/fixtures.md +11 -7
  203. package/docs/flows.md +26 -22
  204. package/docs/for-agents.md +71 -0
  205. package/docs/frameworks.mdx +199 -0
  206. package/docs/gate-plan.md +45 -41
  207. package/docs/gates.md +23 -19
  208. package/docs/getting-started.md +76 -65
  209. package/docs/human-control.md +22 -18
  210. package/docs/index.mdx +101 -0
  211. package/docs/install-agentic.mdx +215 -0
  212. package/docs/install-manual.mdx +194 -0
  213. package/docs/instrumentation.mdx +232 -0
  214. package/docs/integration-patterns.md +38 -34
  215. package/docs/local-registry.md +16 -12
  216. package/docs/multi-agent-testing.md +16 -12
  217. package/docs/packages/babel-plugin.mdx +70 -0
  218. package/docs/packages/browser.mdx +111 -0
  219. package/docs/packages/core.mdx +106 -0
  220. package/docs/packages/electron.mdx +87 -0
  221. package/docs/packages/eslint-plugin.mdx +84 -0
  222. package/docs/packages/next.mdx +91 -0
  223. package/docs/packages/react.mdx +91 -0
  224. package/docs/packages/server.mdx +98 -0
  225. package/docs/packages/tauri.mdx +63 -0
  226. package/docs/packages/test.mdx +108 -0
  227. package/docs/packages/vite-plugin.mdx +96 -0
  228. package/docs/packages.mdx +217 -0
  229. package/docs/platform-integration.md +33 -26
  230. package/docs/predicates.mdx +179 -0
  231. package/docs/quickstart.mdx +211 -0
  232. package/docs/recipes.mdx +242 -0
  233. package/docs/skill-file.mdx +105 -0
  234. package/docs/state-management.mdx +197 -0
  235. package/docs/style.css +14 -0
  236. package/docs/system-map.md +47 -32
  237. package/docs/telemetry-contract.md +101 -46
  238. package/docs/telemetry-events.mdx +430 -0
  239. package/docs/telemetry.md +63 -50
  240. package/docs/testing.md +10 -6
  241. package/docs/token-efficiency.md +30 -26
  242. package/docs/tools-act-and-wait.mdx +123 -0
  243. package/docs/tools-act-sequence.mdx +120 -0
  244. package/docs/tools-act.mdx +82 -0
  245. package/docs/tools-assert.mdx +70 -0
  246. package/docs/tools-console.mdx +59 -0
  247. package/docs/tools-inspect.mdx +85 -0
  248. package/docs/tools-navigate.mdx +63 -0
  249. package/docs/tools-network.mdx +86 -0
  250. package/docs/tools-observe.mdx +89 -0
  251. package/docs/tools-overview.mdx +157 -0
  252. package/docs/tools-query.mdx +82 -0
  253. package/docs/tools-session-and-feedback.mdx +91 -0
  254. package/docs/tools-sessions.mdx +65 -0
  255. package/docs/tools-snapshot.mdx +75 -0
  256. package/docs/tools-state.mdx +81 -0
  257. package/docs/tools-tools-and-run.mdx +125 -0
  258. package/docs/tools-wait-for.mdx +66 -0
  259. package/docs/troubleshooting.mdx +193 -0
  260. package/docs/usage.md +175 -153
  261. package/docs/vs-chrome-devtools-mcp.mdx +82 -0
  262. package/docs/vs-playwright-mcp.mdx +85 -0
  263. package/docs/vs-screenshots.mdx +96 -0
  264. package/docs/what-is-reticle.mdx +63 -0
  265. package/docs/why-reticle.mdx +107 -0
  266. package/package.json +2 -2
  267. package/docs/matrix/2.5.0/claude-code-machine.json +0 -19
  268. package/docs/matrix/2.5.0/codex-machine.json +0 -20
  269. package/docs/matrix/2.5.0/cursor-machine.json +0 -22
  270. package/docs/matrix/2.5.0/gemini-machine.json +0 -22
  271. package/docs/matrix/2.5.0/opencode-machine.json +0 -22
  272. package/docs/matrix/2.5.0/vscode-machine.json +0 -22
  273. package/docs/matrix/2.5.0/windsurf-machine.json +0 -22
  274. package/docs/matrix/MATRIX.md +0 -26
  275. package/docs/matrix/README.md +0 -59
  276. package/docs/mint.json +0 -61
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Which tab a ref came out of, so the call that spends it cannot be routed to a different one.
3
+ *
4
+ * Reported from the field: three tabs of one project were connected, a snapshot was taken against a
5
+ * named session, and the act that followed omitted `sessionId`. Auto-selection picked a different
6
+ * tab, the ref legitimately did not exist there, and the refusal read "that ref is stale: refs are
7
+ * invalidated whenever the DOM re-renders" — a confident diagnosis of the wrong thing, and one that
8
+ * accuses the app under test. The recovery text then sends the agent to re-snapshot, which mints a
9
+ * fresh ref in whichever tab is chosen next, so the lesson it teaches is wrong either way.
10
+ *
11
+ * The quiet outcome is the worse one. Refs are minted from a per-document block, so the same number
12
+ * is a real, live, DIFFERENT element in the tab that was guessed: the identical call on a luckier day
13
+ * drives the wrong tab and returns a green about a page nobody asked about. That is a false green
14
+ * with extra steps, and it is the failure class this product exists to remove.
15
+ *
16
+ * Every `sessionId` description on the surface already promises that Reticle "refuses rather than
17
+ * guesses when ambiguous". This is the fact that makes the ambiguity visible: the tab a ref was
18
+ * handed out of is known at the moment it is handed out. Resolution is otherwise untouched — a call
19
+ * that names a session, carries no ref, or lands on the tab it looked at resolves silently, which is
20
+ * the cost the surface wording was rewritten to avoid.
21
+ *
22
+ * ponytail: one slot, not a per-ref map. It answers the reported shape — look at one tab, act on
23
+ * what you saw — for the cost of an assignment. A drive that interleaves refs from several tabs
24
+ * inside one turn needs per-ref provenance, and that is the upgrade if it ever shows up.
25
+ */
26
+ /** Record that a tool handed out refs from this session. */
27
+ export declare function noteRefsMinted(sessionId: string): void;
28
+ export declare function refsLastMintedIn(): string | undefined;
29
+ /** Test seam: the module holds one slot for the process, so a test has to be able to clear it. */
30
+ export declare function forgetRefProvenance(): void;
31
+ interface RefRouting {
32
+ /** The ref this call is about to spend, if it carries one. */
33
+ ref: string | undefined;
34
+ /** The `sessionId` the CALLER passed — when present, nothing was guessed. */
35
+ explicitSessionId: string | undefined;
36
+ /** The session resolution actually picked. */
37
+ chosenSessionId: string;
38
+ /**
39
+ * The connected sessions, read ONLY once a refusal is certain. A thunk because listing them walks
40
+ * every session and builds a health block each time, and the answer is wanted on the rare path.
41
+ */
42
+ connected: () => readonly {
43
+ sessionId: string;
44
+ url: string;
45
+ }[];
46
+ }
47
+ /**
48
+ * The refusal for a ref that belongs to another tab, or undefined when there is no ambiguity.
49
+ *
50
+ * Silent when the minting tab is no longer connected: that ref really is dead, and the stale-ref
51
+ * refusal that already exists is the honest answer for it.
52
+ */
53
+ export declare function wrongTabRefusal(routing: RefRouting): string | undefined;
54
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Which tab a ref came out of, so the call that spends it cannot be routed to a different one.
3
+ *
4
+ * Reported from the field: three tabs of one project were connected, a snapshot was taken against a
5
+ * named session, and the act that followed omitted `sessionId`. Auto-selection picked a different
6
+ * tab, the ref legitimately did not exist there, and the refusal read "that ref is stale: refs are
7
+ * invalidated whenever the DOM re-renders" — a confident diagnosis of the wrong thing, and one that
8
+ * accuses the app under test. The recovery text then sends the agent to re-snapshot, which mints a
9
+ * fresh ref in whichever tab is chosen next, so the lesson it teaches is wrong either way.
10
+ *
11
+ * The quiet outcome is the worse one. Refs are minted from a per-document block, so the same number
12
+ * is a real, live, DIFFERENT element in the tab that was guessed: the identical call on a luckier day
13
+ * drives the wrong tab and returns a green about a page nobody asked about. That is a false green
14
+ * with extra steps, and it is the failure class this product exists to remove.
15
+ *
16
+ * Every `sessionId` description on the surface already promises that Reticle "refuses rather than
17
+ * guesses when ambiguous". This is the fact that makes the ambiguity visible: the tab a ref was
18
+ * handed out of is known at the moment it is handed out. Resolution is otherwise untouched — a call
19
+ * that names a session, carries no ref, or lands on the tab it looked at resolves silently, which is
20
+ * the cost the surface wording was rewritten to avoid.
21
+ *
22
+ * ponytail: one slot, not a per-ref map. It answers the reported shape — look at one tab, act on
23
+ * what you saw — for the cost of an assignment. A drive that interleaves refs from several tabs
24
+ * inside one turn needs per-ref provenance, and that is the upgrade if it ever shows up.
25
+ */
26
+ /** The session the last ref-bearing result was produced from. Undefined until something mints one. */
27
+ let mintedIn;
28
+ /** Record that a tool handed out refs from this session. */
29
+ export function noteRefsMinted(sessionId) {
30
+ mintedIn = sessionId;
31
+ }
32
+ export function refsLastMintedIn() {
33
+ return mintedIn;
34
+ }
35
+ /** Test seam: the module holds one slot for the process, so a test has to be able to clear it. */
36
+ export function forgetRefProvenance() {
37
+ mintedIn = undefined;
38
+ }
39
+ /**
40
+ * The refusal for a ref that belongs to another tab, or undefined when there is no ambiguity.
41
+ *
42
+ * Silent when the minting tab is no longer connected: that ref really is dead, and the stale-ref
43
+ * refusal that already exists is the honest answer for it.
44
+ */
45
+ export function wrongTabRefusal(routing) {
46
+ if (routing.ref === undefined || routing.explicitSessionId !== undefined)
47
+ return undefined;
48
+ const source = mintedIn;
49
+ if (source === undefined || source === routing.chosenSessionId)
50
+ return undefined;
51
+ const connected = routing.connected();
52
+ if (!connected.some((s) => s.sessionId === source))
53
+ return undefined;
54
+ const listed = connected.map((s) => `'${s.sessionId}' (${s.url})`).join(', ');
55
+ return (`ref '${routing.ref}' was handed out by session '${source}', and this call named no sessionId so ` +
56
+ `it resolved to '${routing.chosenSessionId}'. A ref only means something in the tab it was taken ` +
57
+ 'in — refs are minted per document, so the same one can point at a different element in another ' +
58
+ 'tab — and Reticle refuses rather than guessing which tab you meant. Connected right now: ' +
59
+ `${listed}. Pass sessionId: '${source}' to act on the tab you looked at, or re-read the tab you ` +
60
+ 'actually want first. Nothing was driven.');
61
+ }
@@ -21,6 +21,15 @@ export interface ReviewMark {
21
21
  at: number;
22
22
  status: MarkStatus;
23
23
  }
24
+ /** Tests only — assertions on `m1` must not depend on which test ran first. */
25
+ export declare function resetMarkIdsForTest(): void;
26
+ /** What `resolve` actually did, including WHICH mark — so a caller can verify it hit the right one. */
27
+ export interface ResolveOutcome {
28
+ resolved: boolean;
29
+ id: string;
30
+ /** The retired mark's note. Absent when nothing was resolved. */
31
+ note?: string;
32
+ }
24
33
  export declare class ReviewStore {
25
34
  #private;
26
35
  /** Store a new mark (status pending) stamped with the caller-supplied session-relative time. */
@@ -36,4 +45,14 @@ export declare class ReviewStore {
36
45
  * false for an unknown id or an already-resolved mark (so resolve is idempotent).
37
46
  */
38
47
  resolve(id: string): boolean;
48
+ /**
49
+ * Resolve, and say WHICH mark was retired.
50
+ *
51
+ * The bare boolean is what made the misattribution silent: `{"resolved":true}` is equally
52
+ * consistent with "I retired the bug you fixed" and "I retired somebody else's". Echoing the note
53
+ * lets a caller check the answer against what it believed it was resolving, which is exactly what
54
+ * the agent that hit this asked for — it had been re-reading the whole list after every resolve
55
+ * and hand-diffing the notes to get the same assurance.
56
+ */
57
+ resolveDetail(id: string): ResolveOutcome;
39
58
  }
@@ -11,14 +11,31 @@ import { MarkStatus } from '@reticlehq/core';
11
11
  */
12
12
  /** Prefix on review-mark ids (m1, m2, …) — distinguishes them from command ids. */
13
13
  const MARK_ID_PREFIX = 'm';
14
+ /**
15
+ * Shared by every store in the process, so an id is never reissued.
16
+ *
17
+ * The sequence used to live on the instance, which looked right and was not: a store is created per
18
+ * Session, and a Session is recreated on every page reload and socket reattach. Each new store began
19
+ * again at `m1`, so an id an agent was still holding silently came to denote a DIFFERENT mark — and
20
+ * `resolve` retired that one instead, reporting success. Reported from the field by an agent that
21
+ * closed a bug nobody had fixed while the two it had fixed vanished unrecorded.
22
+ *
23
+ * Process-wide is deliberately not "forever": a daemon restart resets it, but a restart also
24
+ * destroys every session, so any id from before it resolves to nothing and `resolve` correctly
25
+ * returns false. The identity echo below covers what remains.
26
+ */
27
+ let nextMarkId = 0;
28
+ /** Tests only — assertions on `m1` must not depend on which test ran first. */
29
+ export function resetMarkIdsForTest() {
30
+ nextMarkId = 0;
31
+ }
14
32
  export class ReviewStore {
15
33
  #marks = [];
16
- #seq = 0;
17
34
  /** Store a new mark (status pending) stamped with the caller-supplied session-relative time. */
18
35
  add(data, at) {
19
- this.#seq += 1;
36
+ nextMarkId += 1;
20
37
  const mark = {
21
- id: `${MARK_ID_PREFIX}${String(this.#seq)}`,
38
+ id: `${MARK_ID_PREFIX}${String(nextMarkId)}`,
22
39
  note: data.note,
23
40
  anchor: data.anchor,
24
41
  strategy: data.strategy,
@@ -51,10 +68,22 @@ export class ReviewStore {
51
68
  * false for an unknown id or an already-resolved mark (so resolve is idempotent).
52
69
  */
53
70
  resolve(id) {
71
+ return this.resolveDetail(id).resolved;
72
+ }
73
+ /**
74
+ * Resolve, and say WHICH mark was retired.
75
+ *
76
+ * The bare boolean is what made the misattribution silent: `{"resolved":true}` is equally
77
+ * consistent with "I retired the bug you fixed" and "I retired somebody else's". Echoing the note
78
+ * lets a caller check the answer against what it believed it was resolving, which is exactly what
79
+ * the agent that hit this asked for — it had been re-reading the whole list after every resolve
80
+ * and hand-diffing the notes to get the same assurance.
81
+ */
82
+ resolveDetail(id) {
54
83
  const mark = this.#marks.find((m) => m.id === id);
55
84
  if (mark === undefined || mark.status === MarkStatus.RESOLVED)
56
- return false;
85
+ return { resolved: false, id };
57
86
  mark.status = MarkStatus.RESOLVED;
58
- return true;
87
+ return { resolved: true, id, note: mark.note };
59
88
  }
60
89
  }
@@ -8,7 +8,7 @@
8
8
  *
9
9
  * Any edit here is a per-session tax on every user: say it in as few words as still work.
10
10
  */
11
- const SESSION_LEASE_NOTICE = 'When you stop driving, call reticle_session {action:"yield", mode:"waiting"|"ask"} — the panel reads "live" until you do. {action:"end"} when the task is done; it revives on your next action.';
11
+ const SESSION_LEASE_NOTICE = 'When you stop driving, call reticle_session {action:"yield", mode:"waiting"|"ask"}. The panel reads "live" until you do. {action:"end"} when the task is done; it revives on your next action.';
12
12
  export function buildSessionLease(sessionId, openedAt) {
13
13
  return { sessionId, opened_at: openedAt, IMPORTANT: SESSION_LEASE_NOTICE };
14
14
  }
@@ -26,6 +26,15 @@ export declare class SessionManager {
26
26
  count(): number;
27
27
  /** Wire the diagnosis provider (daemon boot). Absent ⇒ the plain, static message. */
28
28
  setNoSessionHint(hint: (() => string | undefined) | undefined): void;
29
+ /**
30
+ * The same diagnosis, for a caller that is ASKING about sessions rather than failing to get one.
31
+ *
32
+ * `reticle_sessions` is the first thing an agent reaches for and it returned a bare empty array —
33
+ * confidently, with nothing to act on. An agent cannot tell an empty list apart from a daemon that
34
+ * is down, an app with no SDK, or a tab that was closed, so it stops there. The diagnosis was
35
+ * already computed for the error path; this exposes it to the read path too.
36
+ */
37
+ noSessionHint(): string | undefined;
29
38
  everConnected(): boolean;
30
39
  /** Record a bridge-initiated close so `resolve` can explain a session that vanished. */
31
40
  noteClosure(reason: string, at: number): void;
@@ -161,6 +161,17 @@ export class SessionManager {
161
161
  setNoSessionHint(hint) {
162
162
  this.#noSessionHint = hint;
163
163
  }
164
+ /**
165
+ * The same diagnosis, for a caller that is ASKING about sessions rather than failing to get one.
166
+ *
167
+ * `reticle_sessions` is the first thing an agent reaches for and it returned a bare empty array —
168
+ * confidently, with nothing to act on. An agent cannot tell an empty list apart from a daemon that
169
+ * is down, an app with no SDK, or a tab that was closed, so it stops there. The diagnosis was
170
+ * already computed for the error path; this exposes it to the read path too.
171
+ */
172
+ noSessionHint() {
173
+ return this.#noSessionHint?.();
174
+ }
164
175
  /** Whether any session has connected since this daemon booted — half the diagnosis. */
165
176
  #everConnected = false;
166
177
  everConnected() {
@@ -135,6 +135,12 @@ export declare class Session {
135
135
  total: number;
136
136
  dropped: number;
137
137
  };
138
+ /**
139
+ * Did the buffer lose scarce evidence from a window opened at `cursor`? The input to whether a
140
+ * verdict's capture was clean — see `RingBuffer.lostSince`, and never the raw drop counter, which
141
+ * moves for the age and churn evictions that every live page produces continuously.
142
+ */
143
+ lostSince(cursor: number): boolean;
138
144
  onEvent(listener: (event: ReticleEvent) => void): () => void;
139
145
  onDisconnect(listener: () => void): () => void;
140
146
  /** Send a command to the browser and await its reply (or time out). */
@@ -159,6 +165,8 @@ export declare class Session {
159
165
  drainInbox(): InboxMessage[];
160
166
  /** Diagnostic read of the inbox depth (does not clear). */
161
167
  inboxSize(): number;
168
+ /** Everything the human has said this session, delivered or not. Never clears. */
169
+ inboxHistory(): readonly InboxMessage[];
162
170
  /** Human marks still awaiting a fix (oldest first). Reading does not consume — resolveMark does. */
163
171
  pendingMarks(): ReviewMark[];
164
172
  /** Full mark history (pending + resolved), oldest first. */
@@ -370,6 +370,14 @@ export class Session {
370
370
  bufferHealth() {
371
371
  return this.#buffer.bufferHealth();
372
372
  }
373
+ /**
374
+ * Did the buffer lose scarce evidence from a window opened at `cursor`? The input to whether a
375
+ * verdict's capture was clean — see `RingBuffer.lostSince`, and never the raw drop counter, which
376
+ * moves for the age and churn evictions that every live page produces continuously.
377
+ */
378
+ lostSince(cursor) {
379
+ return this.#buffer.lostSince(cursor);
380
+ }
373
381
  onEvent(listener) {
374
382
  this.#listeners.add(listener);
375
383
  return () => this.#listeners.delete(listener);
@@ -470,6 +478,10 @@ export class Session {
470
478
  inboxSize() {
471
479
  return this.#live.size();
472
480
  }
481
+ /** Everything the human has said this session, delivered or not. Never clears. */
482
+ inboxHistory() {
483
+ return this.#live.history();
484
+ }
473
485
  // ── Human review marks: the "annotate the bug where you see it" inbox (server-owned) ──────────
474
486
  /** Human marks still awaiting a fix (oldest first). Reading does not consume — resolveMark does. */
475
487
  pendingMarks() {
@@ -14,6 +14,18 @@ interface StatusPayload {
14
14
  contract: string;
15
15
  sessionCount: number;
16
16
  sessions: SessionInfo[];
17
+ /**
18
+ * Why nothing is connected — present ONLY when `sessionCount` is 0.
19
+ *
20
+ * `reticle status` is the most-run command in the field, and with no sessions it answered
21
+ * `sessionCount: 0` and stopped. That is the same dead end `reticle_sessions` used to be for
22
+ * agents, at the same point in the funnel — the daemon is up, the agent is attached, and the app
23
+ * never arrived — and it is where most installs stop. Agents got the diagnosis in 2.7.0; a human
24
+ * running the command we tell them to run in `init`'s closing line deserves the same sentence.
25
+ */
26
+ why?: string;
17
27
  }
18
- export declare function statusPayload(sessionCount: number, sessions: SessionInfo[]): StatusPayload;
28
+ export declare function statusPayload(sessionCount: number, sessions: SessionInfo[],
29
+ /** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
30
+ why?: string): StatusPayload;
19
31
  export {};
@@ -1,11 +1,16 @@
1
1
  import { CONTRACT_FINGERPRINT } from '@reticlehq/core';
2
2
  import { SERVER_VERSION } from './version/server-version.js';
3
- export function statusPayload(sessionCount, sessions) {
3
+ export function statusPayload(sessionCount, sessions,
4
+ /** The no-session diagnosis, injected so this stays pure and the port probe stays off this path. */
5
+ why) {
4
6
  return {
5
7
  running: true,
6
8
  version: SERVER_VERSION,
7
9
  contract: CONTRACT_FINGERPRINT,
8
10
  sessionCount,
9
11
  sessions,
12
+ // Only when there is nothing to explain away: a diagnosis printed beside a live session would
13
+ // contradict it.
14
+ ...(0 === sessionCount && why !== undefined ? { why } : {}),
10
15
  };
11
16
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * When an app carrying the SDK first reaches this daemon.
3
+ *
4
+ * This is the step the whole funnel turns on, and the one nothing could measure. Registering the MCP
5
+ * server and instrumenting an app are separate acts, done by different commands, and the data says
6
+ * almost everybody does the first and almost nobody does the second — but proving that required
7
+ * inferring it from a window counter that resets on every flush, and which therefore reported fewer
8
+ * instrumented users than there were users calling tools.
9
+ *
10
+ * Once per daemon run, on the FIRST app connect only. A page that reloads, or an app that opens five
11
+ * tabs, must not read as five instrumented installs — the question is "did this install ever get an
12
+ * app wired", and it has one answer per run.
13
+ *
14
+ * Names, never values: whether `init` had been run and whether an agent was attached — never the
15
+ * project path, the URL, or anything from the page. The stack is deliberately NOT repeated here;
16
+ * `project_profiled` already carries it on the same daemon run, so the two join on sessionId.
17
+ */
18
+ export declare function markInstrumentationClock(now: number): void;
19
+ export interface InstrumentationFacts {
20
+ /** Whether this project has a projectId stamped — i.e. `init` has run here. */
21
+ initialized: boolean;
22
+ /**
23
+ * Whether an MCP client was attached when the app arrived.
24
+ *
25
+ * Both halves present is the only state in which Reticle can do anything, and it is worth being
26
+ * able to see the two orders apart: an app that connects with no agent attached is a person
27
+ * setting up, while an agent that has been waiting is a session about to do work.
28
+ */
29
+ agentAttached: boolean;
30
+ }
31
+ /**
32
+ * Report the first instrumented app of this daemon run. Best-effort and idempotent — a metric must
33
+ * never be the reason a session fails to register.
34
+ */
35
+ export declare function reportAppInstrumented(facts: InstrumentationFacts, now?: () => number): void;
36
+ /** Tests only. */
37
+ export declare function resetAppInstrumented(): void;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * When an app carrying the SDK first reaches this daemon.
3
+ *
4
+ * This is the step the whole funnel turns on, and the one nothing could measure. Registering the MCP
5
+ * server and instrumenting an app are separate acts, done by different commands, and the data says
6
+ * almost everybody does the first and almost nobody does the second — but proving that required
7
+ * inferring it from a window counter that resets on every flush, and which therefore reported fewer
8
+ * instrumented users than there were users calling tools.
9
+ *
10
+ * Once per daemon run, on the FIRST app connect only. A page that reloads, or an app that opens five
11
+ * tabs, must not read as five instrumented installs — the question is "did this install ever get an
12
+ * app wired", and it has one answer per run.
13
+ *
14
+ * Names, never values: whether `init` had been run and whether an agent was attached — never the
15
+ * project path, the URL, or anything from the page. The stack is deliberately NOT repeated here;
16
+ * `project_profiled` already carries it on the same daemon run, so the two join on sessionId.
17
+ */
18
+ import { TelemetryEventKind } from '@reticlehq/core';
19
+ import { getTelemetry } from './telemetry.js';
20
+ let reported = false;
21
+ /** Set at daemon start so the first connect can report how long the install sat un-instrumented. */
22
+ let daemonStartedAt;
23
+ export function markInstrumentationClock(now) {
24
+ daemonStartedAt = now;
25
+ reported = false;
26
+ }
27
+ /**
28
+ * Report the first instrumented app of this daemon run. Best-effort and idempotent — a metric must
29
+ * never be the reason a session fails to register.
30
+ */
31
+ export function reportAppInstrumented(facts, now = () => Date.now()) {
32
+ if (reported)
33
+ return;
34
+ reported = true;
35
+ try {
36
+ void getTelemetry().emit(TelemetryEventKind.APP_INSTRUMENTED, {
37
+ instrumentation: {
38
+ initialized: facts.initialized,
39
+ agentAttached: facts.agentAttached,
40
+ // How long the daemon ran before an app showed up. Large values are the interesting ones:
41
+ // Reticle was up, the agent had the tools, and nothing was wired for that whole time.
42
+ msToFirstApp: daemonStartedAt === undefined ? 0 : Math.max(0, now() - daemonStartedAt),
43
+ },
44
+ });
45
+ }
46
+ catch {
47
+ /* never let a metric interfere with a page connecting */
48
+ }
49
+ }
50
+ /** Tests only. */
51
+ export function resetAppInstrumented() {
52
+ reported = false;
53
+ daemonStartedAt = undefined;
54
+ }
@@ -0,0 +1,17 @@
1
+ import { AutomationHint } from '@reticlehq/core';
2
+ export interface AutomationInput {
3
+ env: NodeJS.ProcessEnv;
4
+ fileExists: (path: string) => boolean;
5
+ hasTty: boolean;
6
+ }
7
+ /**
8
+ * The most specific signal that fires, or undefined.
9
+ *
10
+ * Ordered most specific first: a container inside a Codespace with no terminal is one fact about the
11
+ * run, and sending the weakest of the three would waste the field. Omitted rather than sent as a
12
+ * "none" value, so its presence is itself the signal — the convention every other advisory field
13
+ * here follows.
14
+ */
15
+ export declare function resolveAutomationHint(input: AutomationInput): AutomationHint | undefined;
16
+ /** The real reading, taken once per process. */
17
+ export declare function currentAutomationHint(env: NodeJS.ProcessEnv): AutomationHint | undefined;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * An ADVISORY marker that a run looks automated, for the runs `ci` cannot see.
3
+ *
4
+ * `ci` is a single environment variable, which is exactly right for a GitHub Actions runner and
5
+ * blind to everything else: our own gate has run from a datacenter and landed in the user data
6
+ * indistinguishable from somebody at a laptop.
7
+ *
8
+ * ADVISORY, and never a filter. A real person works inside a container, inside a Codespace, and over
9
+ * an ssh session with no terminal attached, and every one of them is a user. Nothing may drop a row
10
+ * because this is set; it exists so a surprising number can be looked at from a second angle, and a
11
+ * hint that starts being used as an exclusion has stopped being a hint. Absent means "nothing here
12
+ * looked automated", never "a human was present".
13
+ *
14
+ * WHAT WAS REJECTED, so nobody re-proposes it:
15
+ * - IP address, ASN or datacenter geolocation. Not knowable client-side, and it is a stronger
16
+ * identifier than anything else we collect. The one signal that would actually separate a cloud
17
+ * sandbox from a laptop is the one we will not collect.
18
+ * - Hostname patterns (`runner-*`, `ip-10-*`). A hostname is user data, and matching names is
19
+ * guesswork dressed as a measurement — a guessed dimension and a measured one are
20
+ * indistinguishable once they are in the same column.
21
+ * - Process ancestry. Costs a spawn on a path that must never tax a command, and reads the name of
22
+ * somebody else's program to classify our own user.
23
+ * - `npm_config_user_agent`. Says which package manager ran us, which every route shares.
24
+ * - An absent `$DISPLAY`. Every Linux server, every ssh session, and a large share of genuine
25
+ * developer machines trip it; it separates nothing.
26
+ */
27
+ import { existsSync } from 'node:fs';
28
+ import { AutomationHint } from '@reticlehq/core';
29
+ /** Container runtimes drop one of these at the filesystem root. */
30
+ const CONTAINER_MARKERS = ['/.dockerenv', '/run/.containerenv'];
31
+ /** Hosted dev environments that declare themselves. Self-declared, never inferred. */
32
+ const HOSTED_WORKSPACE_ENV = [
33
+ 'CODESPACES',
34
+ 'GITPOD_WORKSPACE_ID',
35
+ 'CLOUD_SHELL',
36
+ 'REMOTE_CONTAINERS',
37
+ ];
38
+ /**
39
+ * The most specific signal that fires, or undefined.
40
+ *
41
+ * Ordered most specific first: a container inside a Codespace with no terminal is one fact about the
42
+ * run, and sending the weakest of the three would waste the field. Omitted rather than sent as a
43
+ * "none" value, so its presence is itself the signal — the convention every other advisory field
44
+ * here follows.
45
+ */
46
+ export function resolveAutomationHint(input) {
47
+ if (CONTAINER_MARKERS.some((m) => input.fileExists(m)))
48
+ return AutomationHint.CONTAINER;
49
+ if (HOSTED_WORKSPACE_ENV.some((k) => (input.env[k] ?? '') !== ''))
50
+ return AutomationHint.HOSTED_WORKSPACE;
51
+ if (!input.hasTty)
52
+ return AutomationHint.NO_TTY;
53
+ return undefined;
54
+ }
55
+ /** The real reading, taken once per process. */
56
+ export function currentAutomationHint(env) {
57
+ return resolveAutomationHint({
58
+ env,
59
+ fileExists: (p) => {
60
+ try {
61
+ return existsSync(p);
62
+ }
63
+ catch {
64
+ return false; // an unreadable root is not evidence of anything
65
+ }
66
+ },
67
+ // Both ends, because a piped stdout alone is how `reticle status | jq` looks, and that is a
68
+ // person typing.
69
+ hasTty: true === process.stdout.isTTY || true === process.stdin.isTTY,
70
+ });
71
+ }
@@ -6,24 +6,4 @@ import { type BugFound } from '@reticlehq/core';
6
6
  * nothing about sessions and should stay that way. The emission site owns the dedup flag.
7
7
  */
8
8
  export type BugCandidate = Omit<BugFound, 'repeat'>;
9
- /**
10
- * `bug.attribution` was REMOVED, deliberately, and this note is here so it is not re-added casually.
11
- *
12
- * It shipped twice and was wrong both times. First it stamped `app` on every contradiction, including
13
- * `request-never-settled` — which Reticle raises from the ABSENCE of a settle event, so a dev-overlay
14
- * poll manufactured it with the app doing nothing wrong. That was fixed, and a real drive then found
15
- * it still inverted: across two full runs, EVERY `attribution: 'app'` was a misattribution, while the
16
- * one defect that genuinely was a bad agent predicate carried none. One session would have published
17
- * "2 defects in the app" against a true count of 0.
18
- *
19
- * A metric that is confidently wrong about WHOSE fault a defect is, is worse than no metric: it is
20
- * the number a founder steers on, and it points at the customer. Counting nothing is recoverable;
21
- * publishing a false accusation about somebody's product is not.
22
- *
23
- * Bringing it back needs evidence that actually separates the cases. It does not exist in the payload
24
- * today: `element.present` covers "the button is missing", "the API is down" and "the agent mistyped
25
- * a testid" identically, and `signal.absent` covers both a signal the app never fired and a ref
26
- * Reticle drove into nothing. Until a verdict carries the reason it came out that way, the honest
27
- * output is no owner at all.
28
- */
29
9
  export declare function bugsInResult(toolName: string, result: Record<string, unknown>): BugCandidate[];