@reticlehq/server 2.10.0 → 2.12.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 (290) hide show
  1. package/SKILL.md +73 -11
  2. package/dist/bridge/bridge.d.ts +0 -13
  3. package/dist/bridge/bridge.js +1 -1
  4. package/dist/bridge/bridge.test-harness.d.ts +2 -0
  5. package/dist/bridge/bridge.test-harness.js +8 -1
  6. package/dist/capsule/capsule-store.d.ts +2 -1
  7. package/dist/capsule/capsule-store.js +1 -1
  8. package/dist/capsule/causal-summary.d.ts +2 -2
  9. package/dist/chromium-launch-options.d.ts +28 -0
  10. package/dist/chromium-launch-options.js +29 -0
  11. package/dist/cli/cli-doctor.js +8 -0
  12. package/dist/cli/cli-flow-commands.d.ts +3 -1
  13. package/dist/cli/cli-flow-commands.js +34 -4
  14. package/dist/cli/cli-kill.d.ts +2 -1
  15. package/dist/cli/cli-parse.d.ts +3 -1
  16. package/dist/cli/cli-parse.js +29 -7
  17. package/dist/cli/cli-update-commands.js +20 -1
  18. package/dist/cli/cli-verify.d.ts +1 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/cloud-cli.js +12 -6
  21. package/dist/cli/config-discovery.d.ts +2 -1
  22. package/dist/cli/doctor-rows.d.ts +3 -1
  23. package/dist/cli/doctor-rows.js +3 -1
  24. package/dist/cli/gate-exit.d.ts +21 -0
  25. package/dist/cli/gate-exit.js +21 -0
  26. package/dist/cli/gate-hook-message.d.ts +33 -0
  27. package/dist/cli/gate-hook-message.js +46 -0
  28. package/dist/cli/mcp-command.d.ts +25 -0
  29. package/dist/cli/mcp-command.js +143 -0
  30. package/dist/cli/sibling-ports.d.ts +37 -0
  31. package/dist/cli/sibling-ports.js +56 -0
  32. package/dist/cli.js +37 -110
  33. package/dist/cloud/cloud-config.d.ts +2 -1
  34. package/dist/cloud/cloud-sync.d.ts +11 -0
  35. package/dist/cloud/cloud-sync.js +32 -0
  36. package/dist/crawl/crawl.d.ts +9 -13
  37. package/dist/crawl/crawl.js +20 -3
  38. package/dist/daemon/daemon-resolve.d.ts +79 -0
  39. package/dist/daemon/daemon-resolve.js +118 -0
  40. package/dist/daemon/free-port.d.ts +29 -0
  41. package/dist/daemon/free-port.js +55 -0
  42. package/dist/daemon/heartbeat.d.ts +0 -4
  43. package/dist/daemon/heartbeat.js +2 -2
  44. package/dist/dev/stale-issue-guard.d.ts +41 -0
  45. package/dist/dev/stale-issue-guard.js +75 -0
  46. package/dist/domain/domain-tools.js +3 -2
  47. package/dist/events/contradictions.d.ts +41 -0
  48. package/dist/events/contradictions.js +125 -12
  49. package/dist/events/declared.d.ts +14 -0
  50. package/dist/events/declared.js +33 -0
  51. package/dist/events/echo-mismatch.js +14 -0
  52. package/dist/events/observed-in-window.d.ts +12 -0
  53. package/dist/events/observed-in-window.js +17 -0
  54. package/dist/events/predicate-asks.d.ts +30 -0
  55. package/dist/events/predicate-asks.js +48 -0
  56. package/dist/events/predicate-eval.js +254 -25
  57. package/dist/events/predicate-parse.d.ts +0 -6
  58. package/dist/events/predicate-parse.js +51 -5
  59. package/dist/events/predicate-schema.d.ts +62 -0
  60. package/dist/events/predicate-schema.js +162 -4
  61. package/dist/events/predicate.d.ts +15 -0
  62. package/dist/events/predicate.js +41 -2
  63. package/dist/flows/coverage.d.ts +0 -8
  64. package/dist/flows/coverage.js +1 -1
  65. package/dist/flows/decision.d.ts +5 -1
  66. package/dist/flows/decision.js +33 -7
  67. package/dist/flows/flake.d.ts +0 -2
  68. package/dist/flows/flake.js +1 -1
  69. package/dist/flows/flow-intent.d.ts +52 -0
  70. package/dist/flows/flow-intent.js +123 -0
  71. package/dist/flows/flow-replay-run.js +27 -4
  72. package/dist/flows/flow-success.js +15 -3
  73. package/dist/flows/flow-tools.js +65 -8
  74. package/dist/flows/flows.d.ts +6 -1
  75. package/dist/flows/flows.js +34 -22
  76. package/dist/flows/predicate-to-expect.js +14 -3
  77. package/dist/flows/server-verify.js +16 -2
  78. package/dist/flows/verify-change-tools.js +9 -2
  79. package/dist/honesty/blind-spots.d.ts +8 -16
  80. package/dist/honesty/blind-spots.js +18 -5
  81. package/dist/honesty/feature-capture.d.ts +150 -0
  82. package/dist/honesty/feature-capture.js +233 -0
  83. package/dist/honesty/gap-ledger.d.ts +52 -0
  84. package/dist/honesty/gap-ledger.js +63 -0
  85. package/dist/honesty/instrumentation-gaps.d.ts +79 -0
  86. package/dist/honesty/instrumentation-gaps.js +77 -0
  87. package/dist/honesty/observability.d.ts +37 -0
  88. package/dist/honesty/observability.js +40 -0
  89. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  90. package/dist/honesty/tool-hit-rate.js +90 -0
  91. package/dist/honesty/undeclared-change.d.ts +51 -0
  92. package/dist/honesty/undeclared-change.js +56 -0
  93. package/dist/honesty/verified.d.ts +15 -0
  94. package/dist/honesty/verified.js +68 -4
  95. package/dist/http-server.d.ts +0 -1
  96. package/dist/http-server.js +1 -1
  97. package/dist/hunt/hunt-report.d.ts +0 -2
  98. package/dist/hunt/hunt-report.js +1 -1
  99. package/dist/impact/impact-store.d.ts +0 -5
  100. package/dist/impact/impact-store.js +1 -1
  101. package/dist/index.d.ts +4 -2
  102. package/dist/index.js +41 -0
  103. package/dist/init/agent-rules.d.ts +17 -0
  104. package/dist/init/agent-rules.js +31 -1
  105. package/dist/init/capabilities.d.ts +0 -2
  106. package/dist/init/capabilities.js +1 -1
  107. package/dist/init/closing-hint.d.ts +36 -0
  108. package/dist/init/closing-hint.js +120 -0
  109. package/dist/init/confirm.d.ts +0 -25
  110. package/dist/init/confirm.js +3 -3
  111. package/dist/init/cra.d.ts +14 -1
  112. package/dist/init/cra.js +43 -12
  113. package/dist/init/dev-script.d.ts +48 -0
  114. package/dist/init/dev-script.js +63 -0
  115. package/dist/init/mcp.d.ts +16 -5
  116. package/dist/init/mcp.js +26 -10
  117. package/dist/init/node-io.js +3 -0
  118. package/dist/init/plan-framework.d.ts +0 -5
  119. package/dist/init/plan-framework.js +83 -9
  120. package/dist/init/plan.d.ts +10 -30
  121. package/dist/init/plan.js +30 -15
  122. package/dist/init/refresh-rules.d.ts +39 -0
  123. package/dist/init/refresh-rules.js +51 -0
  124. package/dist/init/run.d.ts +2 -10
  125. package/dist/init/run.js +26 -68
  126. package/dist/init/slash-command.d.ts +1 -1
  127. package/dist/init/slash-command.js +33 -10
  128. package/dist/init/snippets.js +53 -10
  129. package/dist/input/real-input.d.ts +2 -0
  130. package/dist/input/real-input.js +3 -2
  131. package/dist/input/scroll-find.d.ts +7 -4
  132. package/dist/input/scroll-find.js +63 -25
  133. package/dist/intent/inline-intent.d.ts +46 -0
  134. package/dist/intent/inline-intent.js +103 -0
  135. package/dist/intent/intent-store.d.ts +37 -0
  136. package/dist/intent/intent-store.js +112 -0
  137. package/dist/intent/intent-tools.d.ts +2 -0
  138. package/dist/intent/intent-tools.js +80 -0
  139. package/dist/intent/open-intents.d.ts +21 -0
  140. package/dist/intent/open-intents.js +25 -0
  141. package/dist/journal/envelope.d.ts +2 -2
  142. package/dist/journal/journal-recorder.d.ts +23 -0
  143. package/dist/journal/journal-recorder.js +35 -0
  144. package/dist/license/license.d.ts +8 -2
  145. package/dist/license/license.js +10 -15
  146. package/dist/mcp/attach-memory.d.ts +67 -0
  147. package/dist/mcp/attach-memory.js +142 -0
  148. package/dist/mcp/mcp-proxy.d.ts +0 -7
  149. package/dist/mcp/mcp-proxy.js +14 -2
  150. package/dist/mcp/mcp.d.ts +2 -0
  151. package/dist/mcp/mcp.js +45 -23
  152. package/dist/mcp/server-instructions.js +45 -3
  153. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  154. package/dist/mcp/tool-catalog-cache.js +10 -0
  155. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  156. package/dist/oracles/flow-instrument-gaps.js +4 -5
  157. package/dist/oracles/self-instrument.d.ts +9 -9
  158. package/dist/oracles/self-instrument.js +19 -13
  159. package/dist/pool/browser-pool.d.ts +12 -1
  160. package/dist/pool/browser-pool.js +31 -1
  161. package/dist/pool/playwright-launcher.js +3 -1
  162. package/dist/project/artifact-root.d.ts +83 -0
  163. package/dist/project/artifact-root.js +112 -0
  164. package/dist/project/fs-port.d.ts +3 -0
  165. package/dist/project/fs-port.js +3 -2
  166. package/dist/project/memory-fs.d.ts +9 -0
  167. package/dist/project/memory-fs.js +93 -0
  168. package/dist/project/project-store.d.ts +17 -0
  169. package/dist/project/project-store.js +33 -0
  170. package/dist/project/project-tools.js +3 -32
  171. package/dist/project/remember-project.d.ts +24 -0
  172. package/dist/project/remember-project.js +40 -0
  173. package/dist/project/reticle-dir.d.ts +2 -0
  174. package/dist/project/reticle-dir.js +1 -0
  175. package/dist/project/session-root.d.ts +15 -0
  176. package/dist/project/session-root.js +31 -0
  177. package/dist/runs/context-tools.d.ts +2 -0
  178. package/dist/runs/context-tools.js +89 -0
  179. package/dist/runs/run-context.d.ts +40 -0
  180. package/dist/runs/run-context.js +158 -0
  181. package/dist/runs/run-diff.d.ts +2 -1
  182. package/dist/runs/run-tools.js +2 -1
  183. package/dist/runs/verification-sync.js +2 -2
  184. package/dist/session/dev-server-probe.d.ts +0 -8
  185. package/dist/session/dev-server-probe.js +1 -1
  186. package/dist/session/last-act.d.ts +7 -1
  187. package/dist/session/last-act.js +2 -2
  188. package/dist/session/lease-availability.d.ts +40 -0
  189. package/dist/session/lease-availability.js +43 -0
  190. package/dist/session/live-control-tools.js +1 -42
  191. package/dist/session/no-session-diagnosis.d.ts +20 -0
  192. package/dist/session/no-session-diagnosis.js +41 -12
  193. package/dist/session/no-session-next-action.d.ts +5 -0
  194. package/dist/session/no-session-next-action.js +35 -3
  195. package/dist/session/no-session-watch.d.ts +8 -0
  196. package/dist/session/no-session-watch.js +83 -65
  197. package/dist/session/output-budget.js +4 -1
  198. package/dist/session/session-health.d.ts +9 -0
  199. package/dist/session/session-health.js +23 -0
  200. package/dist/session/session-manager.js +50 -1
  201. package/dist/session/session-successor.d.ts +37 -0
  202. package/dist/session/session-successor.js +78 -0
  203. package/dist/session/session.d.ts +40 -1
  204. package/dist/session/session.js +84 -2
  205. package/dist/session/stall-clock.d.ts +18 -0
  206. package/dist/session/stall-clock.js +48 -0
  207. package/dist/telemetry/automation-hint.d.ts +2 -1
  208. package/dist/telemetry/daemon-telemetry.js +12 -23
  209. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  210. package/dist/telemetry/project-profile.d.ts +0 -8
  211. package/dist/telemetry/project-profile.js +1 -1
  212. package/dist/telemetry/run-telemetry.d.ts +0 -2
  213. package/dist/telemetry/run-telemetry.js +1 -1
  214. package/dist/telemetry/session-metrics.d.ts +1 -1
  215. package/dist/telemetry/session-metrics.js +1 -1
  216. package/dist/telemetry/telemetry.d.ts +12 -3
  217. package/dist/telemetry/telemetry.js +25 -5
  218. package/dist/telemetry/verification-of.js +2 -2
  219. package/dist/tools/act-tools.js +159 -24
  220. package/dist/tools/assert-source.d.ts +22 -0
  221. package/dist/tools/assert-source.js +53 -0
  222. package/dist/tools/assert-verdict.d.ts +21 -2
  223. package/dist/tools/assert-verdict.js +59 -4
  224. package/dist/tools/browser-tools.js +0 -20
  225. package/dist/tools/complete-snapshot.d.ts +48 -0
  226. package/dist/tools/complete-snapshot.js +75 -0
  227. package/dist/tools/contract-tools.js +5 -3
  228. package/dist/tools/coverage-tools.d.ts +10 -1
  229. package/dist/tools/coverage-tools.js +124 -37
  230. package/dist/tools/dynamic-tools.js +29 -8
  231. package/dist/tools/error-recovery.d.ts +24 -14
  232. package/dist/tools/error-recovery.js +70 -9
  233. package/dist/tools/feedback-tools.d.ts +1 -1
  234. package/dist/tools/feedback-tools.js +2 -2
  235. package/dist/tools/invoke-tool.js +62 -2
  236. package/dist/tools/lease-hint.d.ts +10 -0
  237. package/dist/tools/lease-hint.js +53 -4
  238. package/dist/tools/lease-tools.d.ts +5 -1
  239. package/dist/tools/lease-tools.js +22 -8
  240. package/dist/tools/navigate-arrival.d.ts +0 -2
  241. package/dist/tools/navigate-arrival.js +1 -1
  242. package/dist/tools/observe-tools.js +70 -27
  243. package/dist/tools/read-tools.js +5 -3
  244. package/dist/tools/real-input-attempt.d.ts +22 -0
  245. package/dist/tools/real-input-attempt.js +153 -1
  246. package/dist/tools/reconcile-tools.js +18 -4
  247. package/dist/tools/resolve-target.d.ts +0 -7
  248. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  249. package/dist/tools/sdk-marker-probe.js +57 -0
  250. package/dist/tools/snapshot-delta.d.ts +1 -1
  251. package/dist/tools/tool-kit.d.ts +28 -1
  252. package/dist/tools/tool-kit.js +16 -0
  253. package/dist/tools/tool-names.d.ts +3 -0
  254. package/dist/tools/tool-names.js +3 -0
  255. package/dist/tools/tool-surface.d.ts +85 -6
  256. package/dist/tools/tool-surface.js +122 -2
  257. package/dist/tools/tools.js +22 -4
  258. package/dist/tools/unadvertised-help.js +4 -4
  259. package/dist/tools/verify-next-baton.d.ts +53 -0
  260. package/dist/tools/verify-next-baton.js +52 -0
  261. package/dist/update/update-nudge.d.ts +7 -10
  262. package/dist/update/update-nudge.js +12 -3
  263. package/dist/update/updater.d.ts +1 -1
  264. package/docs/README.md +3 -1
  265. package/docs/cli/cloud.mdx +1 -1
  266. package/docs/cli/doctor.mdx +1 -0
  267. package/docs/cli/verify.mdx +4 -5
  268. package/docs/desktop.mdx +1 -1
  269. package/docs/docs.json +3 -3
  270. package/docs/enterprise.md +3 -1
  271. package/docs/gates.md +1 -1
  272. package/docs/install-manual.mdx +1 -1
  273. package/docs/license-activation.mdx +145 -0
  274. package/docs/packages/browser.mdx +2 -2
  275. package/docs/packages/server.mdx +1 -1
  276. package/docs/packages.mdx +8 -6
  277. package/docs/platform-integration.md +17 -0
  278. package/docs/predicates.mdx +8 -0
  279. package/docs/reticle-manual.md +106 -0
  280. package/docs/telemetry-contract.md +1 -12
  281. package/docs/telemetry-events.mdx +1 -34
  282. package/docs/telemetry.md +0 -1
  283. package/docs/tools-tools-and-run.mdx +2 -4
  284. package/docs/troubleshooting.mdx +9 -0
  285. package/docs/usage.md +3 -2
  286. package/package.json +2 -2
  287. package/dist/session/session-readiness.d.ts +0 -34
  288. package/dist/session/session-readiness.js +0 -36
  289. package/dist/telemetry/instrumentation-stall.d.ts +0 -57
  290. package/dist/telemetry/instrumentation-stall.js +0 -103
@@ -1,5 +1,7 @@
1
1
  import { recordImpact } from '../impact/impact-recorder.js';
2
2
  import { LastAct } from './last-act.js';
3
+ import { GapLedger } from '../honesty/gap-ledger.js';
4
+ import { CaptureLedger } from '../honesty/feature-capture.js';
3
5
  import { commandTimeoutMessage } from './command-timeout.js';
4
6
  import { readHealthEvent } from './session-health.js';
5
7
  /** The HUD does not need 200 frames to watch a counter climb; the last state always lands. */
@@ -106,6 +108,30 @@ export class Session {
106
108
  #journalReader;
107
109
  /** What this session has learned by watching its own stream: ambient churn + blind-spot levels. */
108
110
  #observed = new ObservedState();
111
+ /**
112
+ * Which document is on screen right now — the one the most recent stamped event was observed under.
113
+ *
114
+ * DERIVED, never announced. The SDK mints a document id once per real document and stamps it on
115
+ * every event, so the stream already carries the answer; asking the page for it separately would be
116
+ * a second source of truth that can disagree with the evidence it is supposed to scope. A full
117
+ * navigation or a reload builds a new document, mints a new id, and the first event carrying it
118
+ * moves this forward — which is exactly the moment the previous document's evidence stops being
119
+ * about the world. An SPA route change keeps the same document and so keeps the same id, which is
120
+ * correct rather than a limitation: same JavaScript context, same in-flight requests, same evidence.
121
+ *
122
+ * An UNSTAMPED event never clears this. An SDK older than the field stamps nothing, and letting one
123
+ * such event blank the current document would make every later comparison vacuous.
124
+ */
125
+ #documentId;
126
+ /**
127
+ * The edit epoch of the most recent stamped event. DERIVED for the same reason `#documentId` is:
128
+ * the stream already carries it, and asking the page separately would be a second source of truth.
129
+ *
130
+ * A hot update advances it INSIDE the same document, which is the case `#documentId` structurally
131
+ * cannot see — no navigation, same page, replaced code. An unstamped event never clears it: most
132
+ * pages have no hot-update channel at all, so absence is "unknown", never "back to no edits".
133
+ */
134
+ #editEpoch;
109
135
  constructor(hello, socket, clock) {
110
136
  this.id = hello.sessionId;
111
137
  this.projectId = hello.projectId;
@@ -200,8 +226,20 @@ export class Session {
200
226
  staleMs() {
201
227
  return this.#clock() - this.#startedAt;
202
228
  }
229
+ /** The document the most recent stamped event was observed under. See `#documentId`. */
230
+ get currentDocumentId() {
231
+ return this.#documentId;
232
+ }
233
+ /** The edit epoch the most recent stamped event was observed under. See `#editEpoch`. */
234
+ get currentEditEpoch() {
235
+ return this.#editEpoch;
236
+ }
203
237
  /** Re-stamp an incoming event with server-relative time, buffer it, and fan out. */
204
238
  pushEvent(event, byteSize) {
239
+ if (event.documentId !== undefined)
240
+ this.#documentId = event.documentId;
241
+ if (event.editEpoch !== undefined)
242
+ this.#editEpoch = event.editEpoch;
205
243
  if (event.type === EventType.PAGE_HEALTH) {
206
244
  const r = readHealthEvent(event.data);
207
245
  this.applyHealth(r.hidden ?? this.#hidden, r.focused ?? this.#focused, r.runtime, r.engine, r.brand);
@@ -343,19 +381,50 @@ export class Session {
343
381
  * action id. Ids are minted independently of command correlation ids so the journal is self-contained.
344
382
  */
345
383
  beginAction(tool, args) {
346
- this.#actionSeq += 1;
347
- const actionId = `${ACTION_ID_PREFIX}${String(this.#actionSeq)}`;
384
+ const actionId = this.#mintActionId();
348
385
  // Held here, not only in the journal: with the journal off every event was unattributed, and
349
386
  // pushEvent reads that as ambient churn the settle oracle then ignores. See action-attribution test.
350
387
  this.#activeActionId = actionId;
351
388
  this.#journal?.beginAction(actionId, tool, args);
352
389
  return actionId;
353
390
  }
391
+ /**
392
+ * Record one action WITHOUT opening an attribution window — for a tool that returns a verdict but
393
+ * drives nothing, so no event it observes was caused by it. See `JournalRecorder.recordAction`.
394
+ */
395
+ recordAction(tool, args, effect) {
396
+ const actionId = this.#mintActionId();
397
+ this.#journal?.recordAction(actionId, tool, args, effect);
398
+ return actionId;
399
+ }
400
+ #mintActionId() {
401
+ this.#actionSeq += 1;
402
+ return `${ACTION_ID_PREFIX}${String(this.#actionSeq)}`;
403
+ }
404
+ /**
405
+ * How many actions this session has dispatched.
406
+ *
407
+ * The counter that mints journal action ids, not a second one — so it equals the journal's own
408
+ * length, and `reticle_context`'s `step`, whenever this session journals at all.
409
+ */
410
+ get actionCount() {
411
+ return this.#actionSeq;
412
+ }
354
413
  /** Close the active action window, persisting its action record with the settle outcome. */
355
414
  finishAction(effect, settled, settledInMs) {
356
415
  this.#activeActionId = undefined;
357
416
  this.#journal?.finishAction(effect, settled, settledInMs);
358
417
  }
418
+ /**
419
+ * The durable action ledger for this session — the ledger the run context is folded out of.
420
+ *
421
+ * `[]` when nothing journals this session (journaling disabled, or an id that is not a safe path
422
+ * segment). An absent ledger reads as an empty run, never as a missing one: the fold simply has
423
+ * nothing to say, which is the honest answer.
424
+ */
425
+ async readJournalActions() {
426
+ return (await this.#journalReader?.readActions?.()) ?? [];
427
+ }
359
428
  /** Flush any buffered journal events to disk (call on session end). */
360
429
  async flushJournal() {
361
430
  await this.#journal?.flush();
@@ -370,6 +439,19 @@ export class Session {
370
439
  * from it, and the split between those two tool calls is the whole reason it exists.
371
440
  */
372
441
  lastAct = new LastAct();
442
+ /**
443
+ * What this app still cannot tell Reticle, as of the most recent verdict.
444
+ *
445
+ * Lives on the session because that is the scope the question belongs to: "am I done with this
446
+ * app" is a session question, and a gap closed halfway through must not follow the agent to the
447
+ * end of it.
448
+ */
449
+ gaps = new GapLedger();
450
+ /**
451
+ * The read-only calls the journal does not keep, so "was `reticle_context` ever called" has
452
+ * something to fold. See honesty/feature-capture.ts — it is the only state that instrument adds.
453
+ */
454
+ capture = new CaptureLedger();
373
455
  // ── Server-authoritative liveness (immune to browser-tab throttling) ──────────────
374
456
  /**
375
457
  * Stamp the wall-clock of the latest AGENT command (called whenever a tool resolves this session).
@@ -0,0 +1,18 @@
1
+ /**
2
+ * When a run with nothing connected is worth mentioning to a human.
3
+ *
4
+ * Ten minutes sits well past setup latency — reading a doc, restarting a dev server, opening a tab —
5
+ * and short enough that the warning still arrives while somebody is at the keyboard.
6
+ */
7
+ export declare const STALL_AFTER_MS: number;
8
+ /** Start the clock, from the same place the instrumentation clock is marked. */
9
+ export declare function markStallClock(now: number): void;
10
+ /**
11
+ * How long this daemon has waited with no app, or undefined if the question is moot.
12
+ *
13
+ * Moot means the clock was never started, or an app already connected. A run that starts slowly and
14
+ * then works has nothing to report, which is why this returns undefined rather than a duration.
15
+ */
16
+ export declare function stallUptime(now: number): number | undefined;
17
+ /** Tests only. */
18
+ export declare function resetStallClock(): void;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * How long this daemon has waited with no app connected.
3
+ *
4
+ * This is what survived `instrumentation_stalled`. The EVENT was removed: it claimed to separate
5
+ * "never wired" from "died before reporting" and did not, because it only fired on a flush tick or a
6
+ * graceful shutdown, so a killed daemon was invisible to it exactly as before. Everything it carried
7
+ * is derivable from `daemon_started` minus `app_instrumented`, joined on `sessionId` — and that set
8
+ * difference is strictly MORE complete, because it does catch the killed daemons. A second way to
9
+ * compute the same number that disagrees with the first is worse than not having it.
10
+ *
11
+ * The CLOCK is a different thing and stays. Two live features read it, neither of them a metric: the
12
+ * no-session diagnosis tells an agent how long this daemon has been waiting, and the daemon warns on
13
+ * stderr when a run passes the threshold with nothing connected. Both are help for somebody stuck
14
+ * mid-install, which is the moment they are least able to work it out themselves.
15
+ *
16
+ * It lives under `session/` rather than `telemetry/` now, beside the two files that read it. The old
17
+ * home is why the removal looked like it would take the diagnosis with it.
18
+ */
19
+ import { appEverConnected } from '../telemetry/app-instrumented.js';
20
+ /**
21
+ * When a run with nothing connected is worth mentioning to a human.
22
+ *
23
+ * Ten minutes sits well past setup latency — reading a doc, restarting a dev server, opening a tab —
24
+ * and short enough that the warning still arrives while somebody is at the keyboard.
25
+ */
26
+ export const STALL_AFTER_MS = 10 * 60 * 1000;
27
+ let daemonStartedAt;
28
+ /** Start the clock, from the same place the instrumentation clock is marked. */
29
+ export function markStallClock(now) {
30
+ daemonStartedAt = now;
31
+ }
32
+ /**
33
+ * How long this daemon has waited with no app, or undefined if the question is moot.
34
+ *
35
+ * Moot means the clock was never started, or an app already connected. A run that starts slowly and
36
+ * then works has nothing to report, which is why this returns undefined rather than a duration.
37
+ */
38
+ export function stallUptime(now) {
39
+ if (daemonStartedAt === undefined)
40
+ return undefined;
41
+ if (appEverConnected())
42
+ return undefined;
43
+ return now - daemonStartedAt;
44
+ }
45
+ /** Tests only. */
46
+ export function resetStallClock() {
47
+ daemonStartedAt = undefined;
48
+ }
@@ -1,5 +1,5 @@
1
1
  import { AutomationHint } from '@reticlehq/core';
2
- export interface AutomationInput {
2
+ interface AutomationInput {
3
3
  env: NodeJS.ProcessEnv;
4
4
  fileExists: (path: string) => boolean;
5
5
  hasTty: boolean;
@@ -15,3 +15,4 @@ export interface AutomationInput {
15
15
  export declare function resolveAutomationHint(input: AutomationInput): AutomationHint | undefined;
16
16
  /** The real reading, taken once per process. */
17
17
  export declare function currentAutomationHint(env: NodeJS.ProcessEnv): AutomationHint | undefined;
18
+ export {};
@@ -13,8 +13,7 @@ import { profileProject } from './project-profile.js';
13
13
  import { startUpdateCheck } from '../update/update-nudge.js';
14
14
  import { markDaemonStart } from './mcp-connection.js';
15
15
  import { markInstrumentationClock } from './app-instrumented.js';
16
- import { markStallClock, reportInstrumentationStall, STALL_AFTER_MS, } from './instrumentation-stall.js';
17
- import { readProjectId } from '../cli/cli-port.js';
16
+ import { markStallClock, STALL_AFTER_MS, stallUptime } from '../session/stall-clock.js';
18
17
  /** Let the daemon finish coming up before walking the source tree for a profile. */
19
18
  const PROJECT_PROFILE_DELAY_MS = 5_000;
20
19
  /**
@@ -74,19 +73,18 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
74
73
  /* a profile is a nice-to-have; it must never touch the daemon */
75
74
  }
76
75
  }, PROJECT_PROFILE_DELAY_MS).unref();
76
+ // Once per daemon run: a warning repeated every ten minutes at somebody already stuck is noise.
77
+ let stallWarned = false;
77
78
  const flushWindow = () => {
78
- // BEFORE the idle return, and that ordering is the whole point: a stalled install produces an
79
- // empty window by definition — no app, so no tool calls, so nothing to roll up. Checking it
80
- // after the early return would make the one case this event exists for the one case it never
81
- // sees. It is self-limiting (once per run, never after an app connects), so running it on every
82
- // tick costs a comparison.
83
- const stalled = reportInstrumentationStall({
84
- // Same test `app_instrumented` uses for the same field, so the success and failure events
85
- // stay directly comparable: a stamped projectId means `init` has run here.
86
- initialized: readProjectId(cwd) !== undefined,
87
- agentAttached: metrics.agentEverAttached,
88
- }, now);
89
- if (stalled) {
79
+ // BEFORE the idle return, and that ordering still matters for the reason the removed event
80
+ // documented: a stalled install produces an empty window by definition — no app, so no tool
81
+ // calls, so nothing to roll up — so checking after the early return would never fire on the one
82
+ // case worth warning about. What is left here is the WARNING, for a human watching the log; the
83
+ // metric that used to ride alongside it was removed as a second, less complete way to compute
84
+ // `daemon_started` minus `app_instrumented`.
85
+ const waited = stallUptime(now());
86
+ if (!stallWarned && waited !== undefined && waited >= STALL_AFTER_MS) {
87
+ stallWarned = true;
90
88
  const minutes = Math.round(STALL_AFTER_MS / 60_000);
91
89
  process.stderr.write(`[reticle] no app has connected after ${String(minutes)} minutes — ` +
92
90
  'the install may be incomplete. Restart the dev server if you added the Reticle plugin.\n');
@@ -113,15 +111,6 @@ export function installDaemonTelemetry(cwd, now = () => Date.now()) {
113
111
  stopped ??= (async () => {
114
112
  clearInterval(flush);
115
113
  clearTimeout(firstFlush);
116
- // Last chance to say that no app ever arrived. The periodic path cannot report an
117
- // UNATTENDED stall at all — that daemon idle-exits at five minutes and the periodic
118
- // threshold is ten — so without this the event only ever describes daemons with an agent
119
- // attached, which is not the population it was built to measure. Self-limiting: if the
120
- // interval already reported, this returns false and emits nothing.
121
- reportInstrumentationStall({
122
- initialized: readProjectId(cwd) !== undefined,
123
- agentAttached: metrics.agentEverAttached,
124
- }, now, { atShutdown: true });
125
114
  // The rich one: the whole session in a single event — duration, the tool histogram, error
126
115
  // shapes, verifications, browser launches. This is what replaced the per-tool-call event.
127
116
  await getTelemetry().emit(TelemetryEventKind.DAEMON_STOPPED, {
@@ -1,5 +1,5 @@
1
1
  /** One queued report: the payload, plus what is needed to find and remove it again. */
2
- export interface OutboxEntry {
2
+ interface OutboxEntry {
3
3
  id: string;
4
4
  t: string;
5
5
  payload: unknown;
@@ -20,3 +20,4 @@ export declare function readOutbox(home?: string): OutboxEntry[];
20
20
  * Volume is a handful of lines a week, so the cost is irrelevant.
21
21
  */
22
22
  export declare function markDelivered(id: string | null, home?: string): void;
23
+ export {};
@@ -13,9 +13,6 @@ export declare const FeatureFamily: {
13
13
  readonly BUG_CAPSULES: "bug_capsules";
14
14
  readonly CROSS_RUN_HISTORY: "cross_run_history";
15
15
  };
16
- export type FeatureFamily = (typeof FeatureFamily)[keyof typeof FeatureFamily];
17
- /** Count source files, stopping at the cap (the cap itself lands in the largest bucket, correctly). */
18
- export declare function countSourceFiles(root: string, readDir?: DirReader): number;
19
16
  export declare function sizeBucket(sourceFiles: number): ProjectSize;
20
17
  /**
21
18
  * Project age in whole WEEKS, from the repo's first commit.
@@ -30,11 +27,6 @@ export declare function sizeBucket(sourceFiles: number): ProjectSize;
30
27
  export declare function projectAgeWeeks(root: string, now: number, read?: FileReader): number | undefined;
31
28
  /** True when package.json declares workspaces — monorepos exercise very different code paths. */
32
29
  export declare function isMonorepo(root: string, read?: FileReader): boolean;
33
- interface DirEntry {
34
- name: string;
35
- isDirectory: boolean;
36
- }
37
- type DirReader = (dir: string) => DirEntry[];
38
30
  type FileReader = (path: string) => string;
39
31
  /**
40
32
  * Build the full profile. Best-effort throughout: an unreadable project yields a profile of zeroes
@@ -45,7 +45,7 @@ const MAX_FILES_SCANNED = 6000;
45
45
  const MAX_WALK_DEPTH = 8;
46
46
  const MS_PER_WEEK = 7 * 24 * 60 * 60 * 1000;
47
47
  /** Count source files, stopping at the cap (the cap itself lands in the largest bucket, correctly). */
48
- export function countSourceFiles(root, readDir = defaultReadDir) {
48
+ function countSourceFiles(root, readDir = defaultReadDir) {
49
49
  let count = 0;
50
50
  const walk = (dir, depth) => {
51
51
  if (depth > MAX_WALK_DEPTH || count >= MAX_FILES_SCANNED)
@@ -11,8 +11,6 @@
11
11
  * how a replay works.
12
12
  */
13
13
  import { type ReticleVerificationRun } from '@reticlehq/core';
14
- /** The tool name reported for this path, so CI runs are distinguishable from agent-driven ones. */
15
- export declare const VERIFY_SURFACE = "reticle_verify";
16
14
  /**
17
15
  * Report one completed verification run: the verdict, and one bug per failing flow.
18
16
  *
@@ -14,7 +14,7 @@ import { BugAttribution, BugSource, RunFlowStatus, TelemetryActor, TelemetryEven
14
14
  import { getSessionMetrics } from './session-metrics.js';
15
15
  import { getTelemetry } from './telemetry.js';
16
16
  /** The tool name reported for this path, so CI runs are distinguishable from agent-driven ones. */
17
- export const VERIFY_SURFACE = 'reticle_verify';
17
+ const VERIFY_SURFACE = 'reticle_verify';
18
18
  /**
19
19
  * Report one completed verification run: the verdict, and one bug per failing flow.
20
20
  *
@@ -133,7 +133,7 @@ export declare class SessionMetrics {
133
133
  /**
134
134
  * Has an agent ever attached to this daemon?
135
135
  *
136
- * Read by the instrumentation-stall check, which needs to tell two very different silences apart:
136
+ * Read by the stall clock, which needs to tell two very different silences apart:
137
137
  * a daemon nobody is using, and a daemon with an agent sitting on it that has no app to drive.
138
138
  * The second is the funnel's failure case and the first is not a problem at all.
139
139
  */
@@ -575,7 +575,7 @@ export class SessionMetrics {
575
575
  /**
576
576
  * Has an agent ever attached to this daemon?
577
577
  *
578
- * Read by the instrumentation-stall check, which needs to tell two very different silences apart:
578
+ * Read by the stall clock, which needs to tell two very different silences apart:
579
579
  * a daemon nobody is using, and a daemon with an agent sitting on it that has no app to drive.
580
580
  * The second is the funnel's failure case and the first is not a problem at all.
581
581
  */
@@ -1,4 +1,15 @@
1
- import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type InstallSource, type McpConnection, type McpOutage, type ToolRefusal, type AppInstrumentation, type InstrumentationStall, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
1
+ import { TelemetryEventKind, ProjectIdSource, type Crash, type Feedback, type Identity, type BugFound, type InitOutcome, type InstallSource, type McpConnection, type McpOutage, type ToolRefusal, type AppInstrumentation, type ProjectProfile, type SessionSummary, type TelemetryActor, type Verification, type VersionChange } from '@reticlehq/core';
2
+ /**
3
+ * PostHog's group type for a licensed customer. Sent as `$groups` so PostHog can aggregate a whole
4
+ * organisation natively: per-company retention, "which orgs went quiet", one row per customer rather
5
+ * than one per machine.
6
+ *
7
+ * The KEY is the licence id and nothing else. The organisation's NAME is never sent from a customer's
8
+ * machine, here or anywhere; the name reaches PostHog only if we push it ourselves from the issuance
9
+ * ledger, which is the one place that mapping lives. So an event still carries no identity, and the
10
+ * analytics side learns a customer list only when we deliberately upload one.
11
+ */
12
+ export declare const POSTHOG_GROUP_ORGANIZATION = "organization";
2
13
  /**
3
14
  * Feedback gets its own, much larger budget — and a retry.
4
15
  *
@@ -78,8 +89,6 @@ export interface TelemetryExtra {
78
89
  outage?: McpOutage;
79
90
  /** `app_instrumented`: an app carrying the SDK reached this daemon for the first time. */
80
91
  instrumentation?: AppInstrumentation;
81
- /** `instrumentation_stalled`: the daemon waited and no app ever arrived. */
82
- stall?: InstrumentationStall;
83
92
  /** `reticle_installed` / `init_completed`: which published route brought this install in. */
84
93
  installSource?: InstallSource;
85
94
  }
@@ -42,6 +42,17 @@ const POSTHOG_KEY = 'phc_q4yCxKMXHfWQ43VAz4HxJXZFLqFi4nBbg5v3avemggYo';
42
42
  * still key on distinct_id.
43
43
  */
44
44
  const POSTHOG_PERSONLESS = { $process_person_profile: false };
45
+ /**
46
+ * PostHog's group type for a licensed customer. Sent as `$groups` so PostHog can aggregate a whole
47
+ * organisation natively: per-company retention, "which orgs went quiet", one row per customer rather
48
+ * than one per machine.
49
+ *
50
+ * The KEY is the licence id and nothing else. The organisation's NAME is never sent from a customer's
51
+ * machine, here or anywhere; the name reaches PostHog only if we push it ourselves from the issuance
52
+ * ledger, which is the one place that mapping lives. So an event still carries no identity, and the
53
+ * analytics side learns a customer list only when we deliberately upload one.
54
+ */
55
+ export const POSTHOG_GROUP_ORGANIZATION = 'organization';
45
56
  const SEND_TIMEOUT_MS = 2000;
46
57
  /**
47
58
  * Feedback gets its own, much larger budget — and a retry.
@@ -337,7 +348,6 @@ export const createTelemetry = (opts) => {
337
348
  ...(extra?.refusal !== undefined ? { refusal: extra.refusal } : {}),
338
349
  ...(extra?.outage !== undefined ? { outage: extra.outage } : {}),
339
350
  ...(extra?.instrumentation !== undefined ? { instrumentation: extra.instrumentation } : {}),
340
- ...(extra?.stall !== undefined ? { stall: extra.stall } : {}),
341
351
  // A SCALAR, so it needs the event build and the wire schema but no entry in `blocks` below —
342
352
  // there is nothing to flatten. It still needs both of those, which is the trap `outage` fell
343
353
  // into: a field declared on TelemetryExtra and missing from either one is dropped in silence.
@@ -348,14 +358,17 @@ export const createTelemetry = (opts) => {
348
358
  // `blocks` entry. Resolved per event off the event's own clock rather than once at startup — an
349
359
  // eleven-hour session must not report the status it booted with. All three are absent unless a
350
360
  // release has an issuer key baked, so an OSS payload is byte-identical to before.
351
- ...licenseFacts(eventTs),
361
+ // The RESOLVED env, not process.env: createTelemetry takes an env for injection, and reading
362
+ // the ambient one here would silently ignore it. Identical in production, wrong everywhere the
363
+ // seam is actually used.
364
+ ...licenseFacts(eventTs, env),
352
365
  };
353
366
  // Map the core contract onto PostHog's capture shape: id/name/time move up, the rest are properties.
354
367
  // The feedback body is FLATTENED into `feedback_*` properties rather than sent as a nested object:
355
368
  // PostHog filters and breakdowns operate on top-level properties, so a nested `feedback.stack`
356
369
  // would be invisible to the exact "which stacks report the most bugs" question this exists to
357
370
  // answer. One prefix keeps them grouped in the UI without a nested path.
358
- const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, refusal, outage, instrumentation, stall, ...rest } = event;
371
+ const { anonymousId: distinctId, event: name, ts, sessionId: eventSessionId, feedback, session, project, verification, versionChange, crash, identity, connection, init, bug, refusal, outage, instrumentation, ...rest } = event;
359
372
  // `$session_id` is PostHog's OWN session property, so sending ours under that name lights up
360
373
  // its built-in session views and funnels for free rather than requiring custom HogQL everywhere.
361
374
  // `sessionId` rides only on events that happened inside a daemon run. A one-shot CLI command
@@ -386,7 +399,6 @@ export const createTelemetry = (opts) => {
386
399
  refusal,
387
400
  outage,
388
401
  instrumentation,
389
- stall,
390
402
  };
391
403
  for (const [prefix, block] of Object.entries(blocks)) {
392
404
  for (const [key, value] of Object.entries(block ?? {})) {
@@ -397,7 +409,15 @@ export const createTelemetry = (opts) => {
397
409
  event: name,
398
410
  distinct_id: distinctId,
399
411
  timestamp: new Date(ts).toISOString(),
400
- properties: { ...properties, ...POSTHOG_PERSONLESS },
412
+ properties: {
413
+ ...properties,
414
+ ...POSTHOG_PERSONLESS,
415
+ // Only on a licensed build. An unlicensed event has no organisation to belong to, and an
416
+ // empty group key would mint a phantom "no org" bucket that every OSS install falls into.
417
+ ...(event.licenseId !== undefined
418
+ ? { $groups: { [POSTHOG_GROUP_ORGANIZATION]: event.licenseId } }
419
+ : {}),
420
+ },
401
421
  };
402
422
  const body = JSON.stringify({ api_key: apiKey, batch: [capture] });
403
423
  const filePath = env[Env.FILE];
@@ -13,7 +13,7 @@
13
13
  * be readable and testable on its own rather than inferred from a ternary inside a dispatcher.
14
14
  */
15
15
  import { CaptureLoss, VerifiedReason } from '@reticlehq/core';
16
- import { VERIFICATION_TOOLS } from '../tools/feedback-tools.js';
16
+ import { VERDICT_TOOLS } from '../tools/feedback-tools.js';
17
17
  import { getBrowserMode } from './browser-mode.js';
18
18
  /**
19
19
  * The deciding clause, read off the result `decideVerified` wrote it onto.
@@ -92,7 +92,7 @@ function passedOf(result, status) {
92
92
  * it verified. That is the product's thesis reduced to a boolean.
93
93
  */
94
94
  export function verificationOf(toolName, result, durationMs, brand) {
95
- if (!VERIFICATION_TOOLS.has(toolName))
95
+ if (!VERDICT_TOOLS.has(toolName))
96
96
  return undefined;
97
97
  // A paused session REFUSES the call — nothing driven, nothing asserted. The refusal carries a
98
98
  // verdict field so the agent never reads undefined off it, and that field must not be mistaken