@reticlehq/server 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/SKILL.md +16 -13
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/cli-doctor.js +25 -5
  7. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  8. package/dist/command/cli/cli-parse-grammar.js +7 -0
  9. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  10. package/dist/command/cli/cli-parse-verify.js +86 -13
  11. package/dist/command/cli/cli-parse.d.ts +7 -1
  12. package/dist/command/cli/cli-parse.js +33 -5
  13. package/dist/command/cli/cli-verify.d.ts +59 -0
  14. package/dist/command/cli/cli-verify.js +129 -17
  15. package/dist/command/cli/cloud-kit.d.ts +0 -2
  16. package/dist/command/cli/cloud-kit.js +1 -1
  17. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  18. package/dist/command/cli/launch/cli-launch.d.ts +1 -29
  19. package/dist/command/cli/launch/cli-launch.js +6 -40
  20. package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
  21. package/dist/command/cli/ports/resolve/cli-port.js +45 -1
  22. package/dist/command/cli/report-command.d.ts +16 -0
  23. package/dist/command/cli/report-command.js +97 -0
  24. package/dist/command/cli.js +5 -0
  25. package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
  26. package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
  27. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  28. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  29. package/dist/command/setup/init/init-host.js +8 -0
  30. package/dist/features/crawl/crawl.d.ts +2 -0
  31. package/dist/features/crawl/crawl.js +1 -0
  32. package/dist/features/harness/jev-driver.js +6 -9
  33. package/dist/features/harness/platform-config.d.ts +10 -0
  34. package/dist/features/harness/platform-config.js +8 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +40 -95
  37. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  38. package/dist/judgement/capsule/capsule-store.js +58 -6
  39. package/dist/judgement/capsule/capsule.js +5 -0
  40. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  41. package/dist/judgement/capsule/causal-summary.js +7 -0
  42. package/dist/judgement/domain/domain-model.js +38 -9
  43. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  44. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  45. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  46. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  47. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  48. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  49. package/dist/judgement/runs/artifact/render-report.js +4 -1
  50. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  51. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  52. package/dist/judgement/runs/context-tools.js +21 -3
  53. package/dist/judgement/runs/drive-run.js +7 -2
  54. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  55. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  56. package/dist/language/flows/flow-anchor.d.ts +12 -0
  57. package/dist/language/flows/flow-anchor.js +20 -0
  58. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  59. package/dist/language/flows/flow-expect-grammar.js +92 -11
  60. package/dist/language/flows/flow-intent.js +1 -1
  61. package/dist/language/flows/flow-learning.d.ts +3 -0
  62. package/dist/language/flows/flow-learning.js +39 -8
  63. package/dist/language/flows/flow-replay-run.d.ts +31 -9
  64. package/dist/language/flows/flow-replay-run.js +146 -33
  65. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  66. package/dist/language/flows/flow-replay.d.ts +12 -11
  67. package/dist/language/flows/flow-replay.js +79 -109
  68. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  69. package/dist/language/flows/flow-step-runners.js +20 -4
  70. package/dist/language/flows/flow-success.d.ts +21 -6
  71. package/dist/language/flows/flow-success.js +50 -138
  72. package/dist/language/flows/flows.d.ts +8 -4
  73. package/dist/language/flows/flows.js +11 -2
  74. package/dist/language/flows/heal.d.ts +9 -1
  75. package/dist/language/flows/heal.js +24 -2
  76. package/dist/language/flows/learned-sources.d.ts +21 -0
  77. package/dist/language/flows/learned-sources.js +84 -0
  78. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  79. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  80. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  81. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  82. package/dist/language/flows/replay.js +12 -8
  83. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  84. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  85. package/dist/language/flows/stores/flake-store.js +2 -3
  86. package/dist/language/flows/suite/coverage.d.ts +1 -1
  87. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  88. package/dist/language/flows/verify-change-tools.js +14 -0
  89. package/dist/memory/cloud/cloud-sync.d.ts +0 -2
  90. package/dist/memory/cloud/cloud-sync.js +1 -1
  91. package/dist/memory/cloud/harness-config.d.ts +2 -0
  92. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  93. package/dist/memory/cloud/sync-cycle.js +29 -4
  94. package/dist/memory/cloud/sync-daemon.d.ts +13 -0
  95. package/dist/memory/cloud/sync-daemon.js +21 -0
  96. package/dist/memory/cloud/sync-disk.js +11 -6
  97. package/dist/memory/impact/impact-store.js +6 -0
  98. package/dist/memory/intent/inline-intent.js +8 -3
  99. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  100. package/dist/memory/intent/intent-shard-store.js +252 -140
  101. package/dist/memory/intent/intent-shard.d.ts +10 -11
  102. package/dist/memory/intent/intent-shard.js +1 -1
  103. package/dist/memory/intent/intent-store.d.ts +13 -8
  104. package/dist/memory/intent/intent-store.js +46 -90
  105. package/dist/memory/intent/intent-tools.js +34 -23
  106. package/dist/memory/journal/ambient-store.js +2 -3
  107. package/dist/memory/journal/deviation-report.js +4 -1
  108. package/dist/memory/journal/deviation-service.js +10 -2
  109. package/dist/memory/journal/drive-flow.d.ts +2 -2
  110. package/dist/memory/journal/drive-flow.js +21 -12
  111. package/dist/memory/journal/envelope-store.js +2 -3
  112. package/dist/memory/journal/journal-recorder.d.ts +21 -0
  113. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  114. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  115. package/dist/memory/journal/on-disk/retention.d.ts +14 -10
  116. package/dist/memory/journal/on-disk/retention.js +13 -12
  117. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  118. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  119. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
  120. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
  121. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  122. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  123. package/dist/memory/journal/route-template.d.ts +22 -0
  124. package/dist/memory/journal/route-template.js +44 -0
  125. package/dist/memory/journal/session-end.d.ts +4 -1
  126. package/dist/memory/journal/session-end.js +64 -6
  127. package/dist/memory/journal/session-journal.d.ts +8 -1
  128. package/dist/memory/journal/session-journal.js +10 -0
  129. package/dist/memory/project/baselines.d.ts +1 -1
  130. package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
  131. package/dist/memory/project/fs/fs-port.js +44 -5
  132. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  133. package/dist/memory/project/fs/write-atomic.js +48 -0
  134. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  135. package/dist/memory/recall/prior/connection-memory.js +8 -0
  136. package/dist/memory/recall/registered-projects.d.ts +2 -1
  137. package/dist/portal/bridge/auth-failure-reason.js +5 -2
  138. package/dist/portal/input/real-input.js +2 -1
  139. package/dist/portal/pool/browser-pool.d.ts +10 -0
  140. package/dist/portal/pool/browser-pool.js +10 -0
  141. package/dist/portal/pool/playwright-launcher.js +1 -0
  142. package/dist/portal/pool/pool-contract.d.ts +2 -0
  143. package/dist/portal/pool/storage-seed.d.ts +2 -1
  144. package/dist/portal/pool/zero-install.d.ts +27 -0
  145. package/dist/portal/pool/zero-install.js +54 -0
  146. package/dist/portal/realm/web-realm.js +6 -0
  147. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  148. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  149. package/dist/portal/session/dev-server/served-document.js +57 -0
  150. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  151. package/dist/portal/session/facts/background-traffic.js +25 -0
  152. package/dist/portal/session/live-control-tools.js +48 -3
  153. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  154. package/dist/portal/session/session-health.d.ts +61 -0
  155. package/dist/portal/session/session-health.js +40 -1
  156. package/dist/portal/session/session-info.d.ts +6 -0
  157. package/dist/portal/session/session-manager.d.ts +9 -0
  158. package/dist/portal/session/session-manager.js +23 -2
  159. package/dist/portal/session/session-successor.d.ts +11 -0
  160. package/dist/portal/session/session-successor.js +14 -1
  161. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  162. package/dist/portal/session/session-verdict-facts.js +15 -0
  163. package/dist/portal/session/session.d.ts +5 -0
  164. package/dist/portal/session/session.js +19 -7
  165. package/dist/surface/mcp/mcp-proxy.js +3 -3
  166. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  167. package/dist/surface/mcp/server-instructions.js +12 -1
  168. package/dist/surface/tools/act/already-true.d.ts +40 -0
  169. package/dist/surface/tools/act/already-true.js +36 -0
  170. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  171. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  172. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  173. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  174. package/dist/surface/tools/act-capsule.js +22 -7
  175. package/dist/surface/tools/act-tools.js +25 -24
  176. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  177. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  178. package/dist/surface/tools/assert/assert-grade.js +14 -2
  179. package/dist/surface/tools/assert/assert-verdict.js +30 -4
  180. package/dist/surface/tools/browser-tools.js +4 -3
  181. package/dist/surface/tools/dynamic-tools.js +6 -0
  182. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  183. package/dist/surface/tools/feedback-tools.js +1 -1
  184. package/dist/surface/tools/invoke-tool.js +17 -4
  185. package/dist/surface/tools/lease-hint.d.ts +8 -0
  186. package/dist/surface/tools/lease-hint.js +10 -0
  187. package/dist/surface/tools/lease-tools.d.ts +16 -28
  188. package/dist/surface/tools/lease-tools.js +53 -5
  189. package/dist/surface/tools/lineage-tools.js +1 -1
  190. package/dist/surface/tools/observe-tools.js +14 -6
  191. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  192. package/dist/surface/tools/real-input-attempt.js +1 -1
  193. package/dist/surface/tools/tool-kit.d.ts +5 -0
  194. package/dist/surface/tools/tool-kit.js +6 -1
  195. package/dist/surface/tools/tool-surface.js +17 -5
  196. package/dist/surface/tools/tools.js +64 -29
  197. package/dist/telemetry/feedback-local.d.ts +1 -0
  198. package/dist/telemetry/feedback-local.js +1 -3
  199. package/dist/telemetry/feedback.d.ts +1 -1
  200. package/dist/telemetry/hud-metrics.d.ts +32 -0
  201. package/dist/telemetry/hud-metrics.js +95 -0
  202. package/dist/telemetry/session-metrics.d.ts +5 -0
  203. package/dist/telemetry/session-metrics.js +15 -1
  204. package/dist/wire-journal.d.ts +31 -0
  205. package/dist/wire-journal.js +100 -0
  206. package/docs/README.md +1 -0
  207. package/docs/cli/cloud.mdx +9 -7
  208. package/docs/cli/init.mdx +1 -0
  209. package/docs/cli/report.mdx +44 -0
  210. package/docs/desktop.mdx +1 -1
  211. package/docs/flows.md +11 -0
  212. package/docs/frameworks.mdx +113 -1
  213. package/docs/install-agentic.mdx +36 -0
  214. package/docs/install-manual.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages.mdx +1 -1
  217. package/docs/predicates.mdx +128 -4
  218. package/docs/tools/query.mdx +10 -10
  219. package/docs/usage.md +1 -1
  220. package/docs/what-is-recorded.md +62 -0
  221. package/package.json +6 -6
  222. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  223. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
@@ -0,0 +1,95 @@
1
+ import { isHudControl } from '@reticlehq/core/hud';
2
+ import { HudPanel, HudView } from '@reticlehq/core';
3
+ /**
4
+ * How people use the in-page HUD, rolled into the session summary.
5
+ *
6
+ * Windowed like the tool histograms beside it: a periodic flush reports what happened since the
7
+ * last one, so a dashboard sums across a session's events. Names only — every key is a control id
8
+ * the wire schema already checked against core's closed list.
9
+ */
10
+ /** How many steps of the journey one window keeps. A journey longer than this is cut, and says so. */
11
+ export const HUD_JOURNEY_MAX = 60;
12
+ export class HudMetrics {
13
+ #now;
14
+ #controls = new Map();
15
+ #viewMs = new Map();
16
+ #panelMs = new Map();
17
+ #journey = [];
18
+ #cut = false;
19
+ /** Did anything happen this window. A HUD merely sitting open is not worth a flush of its own. */
20
+ #changed = false;
21
+ #view;
22
+ #panel;
23
+ constructor(now) {
24
+ this.#now = now;
25
+ }
26
+ record(use) {
27
+ // A control the HUD never rendered is dropped here, the one place HUD use is counted, so free
28
+ // text cannot ride in on the id.
29
+ const control = use.control !== undefined && isHudControl(use.control) ? use.control : undefined;
30
+ if (control === undefined && use.view === undefined && use.panel === undefined)
31
+ return;
32
+ this.#changed = true;
33
+ if (control !== undefined) {
34
+ const key = use.toggle === undefined ? control : `${control}:${use.toggle}`;
35
+ this.#controls.set(key, (this.#controls.get(key) ?? 0) + 1);
36
+ this.#step(key);
37
+ }
38
+ if (use.view !== undefined && use.view !== this.#view?.value) {
39
+ this.#close(this.#view, this.#viewMs);
40
+ this.#view = { value: use.view, since: this.#now() };
41
+ this.#step(`view:${use.view}`);
42
+ }
43
+ if (use.panel !== undefined && use.panel !== this.#panel?.value) {
44
+ this.#close(this.#panel, this.#panelMs);
45
+ this.#panel = { value: use.panel, since: this.#now() };
46
+ if (use.panel !== HudPanel.NONE)
47
+ this.#step(`panel:${use.panel}`);
48
+ }
49
+ }
50
+ get empty() {
51
+ return !this.#changed;
52
+ }
53
+ /** The window so far. Open view and panel are counted up to now, and keep running after. */
54
+ summarize() {
55
+ const viewMs = this.#withOpen(this.#view, this.#viewMs);
56
+ const panelMs = this.#withOpen(this.#panel, this.#panelMs);
57
+ return {
58
+ ...(this.#controls.size > 0 ? { hudControls: Object.fromEntries(this.#controls) } : {}),
59
+ ...(viewMs.size > 0 ? { hudViewMs: Object.fromEntries(viewMs) } : {}),
60
+ ...(panelMs.size > 0 ? { hudPanelMs: Object.fromEntries(panelMs) } : {}),
61
+ ...(this.#journey.length > 0 ? { hudJourney: [...this.#journey] } : {}),
62
+ ...(this.#cut ? { hudJourneyCut: true } : {}),
63
+ };
64
+ }
65
+ /** Start the next window. The current view and panel carry over, timed from now. */
66
+ reset() {
67
+ const at = this.#now();
68
+ this.#controls.clear();
69
+ this.#viewMs.clear();
70
+ this.#panelMs.clear();
71
+ this.#journey.length = 0;
72
+ this.#cut = false;
73
+ this.#changed = false;
74
+ if (this.#view !== undefined)
75
+ this.#view = { ...this.#view, since: at };
76
+ if (this.#panel !== undefined)
77
+ this.#panel = { ...this.#panel, since: at };
78
+ }
79
+ #step(entry) {
80
+ if (this.#journey.length < HUD_JOURNEY_MAX)
81
+ this.#journey.push(entry);
82
+ else
83
+ this.#cut = true;
84
+ }
85
+ #close(open, into) {
86
+ if (open === undefined)
87
+ return;
88
+ into.set(open.value, (into.get(open.value) ?? 0) + Math.max(0, this.#now() - open.since));
89
+ }
90
+ #withOpen(open, closed) {
91
+ const out = new Map(closed);
92
+ this.#close(open, out);
93
+ return out;
94
+ }
95
+ }
@@ -16,6 +16,7 @@
16
16
  * than a rule saying it shouldn't.
17
17
  */
18
18
  import { BrowserLaunchKind, type ConnectFailure, type SessionSummary } from '@reticlehq/core/telemetry';
19
+ import type { HudUseData } from '@reticlehq/core';
19
20
  /**
20
21
  * Mutable counters for one daemon lifetime. A plain class, not a store: it is process-local, it never
21
22
  * persists, and it dies with the daemon.
@@ -24,6 +25,8 @@ export declare class SessionMetrics {
24
25
  #private;
25
26
  /** Clock injected — this file must stay testable without a real one, per the repo's clock rule. */
26
27
  constructor(now: () => number);
28
+ /** A person used the HUD. Names only; the wire schema has already narrowed it. */
29
+ recordHudUse(use: HudUseData): void;
27
30
  /**
28
31
  * Record a call starting. Returns a function to call when it finishes — that closure is what makes
29
32
  * timing correct under CONCURRENCY: several agents can be inside `runTool` at once, so a single
@@ -162,5 +165,7 @@ export declare const recordSdkFailure: (failure: {
162
165
  site: string;
163
166
  message: string;
164
167
  }) => void;
168
+ /** Narrow-and-record one HUD use. Keeps the session hot path to a single call. */
169
+ export declare const recordHudUse: (use: HudUseData) => void;
165
170
  /** Tests only — drop the singleton so each case starts from zero. */
166
171
  export declare const resetSessionMetrics: () => void;
@@ -20,6 +20,7 @@ import { errorSkeleton, fingerprintError } from './error-fingerprint.js';
20
20
  import { describeToolParams } from './argument-shape.js';
21
21
  import { machineSnapshot } from './machine-snapshot.js';
22
22
  import { classifyError } from './error-class.js';
23
+ import { HudMetrics } from './hud-metrics.js';
23
24
  /** Cap the distinct error shapes held in memory — a pathological loop must not grow unbounded. */
24
25
  const MAX_ERROR_KINDS = 40;
25
26
  /** Distinct tool names whose repeat-runs are tracked. Its own cap: the tool surface, not error shapes. */
@@ -165,10 +166,16 @@ export class SessionMetrics {
165
166
  #surface;
166
167
  #startedAt;
167
168
  #now;
169
+ #hud;
168
170
  /** Clock injected — this file must stay testable without a real one, per the repo's clock rule. */
169
171
  constructor(now) {
170
172
  this.#now = now;
171
173
  this.#startedAt = now();
174
+ this.#hud = new HudMetrics(now);
175
+ }
176
+ /** A person used the HUD. Names only; the wire schema has already narrowed it. */
177
+ recordHudUse(use) {
178
+ this.#hud.record(use);
172
179
  }
173
180
  /**
174
181
  * Record a call starting. Returns a function to call when it finishes — that closure is what makes
@@ -555,6 +562,7 @@ export class SessionMetrics {
555
562
  ...(this.#firstAppAt === undefined
556
563
  ? {}
557
564
  : { msToFirstApp: Math.max(0, this.#firstAppAt - this.#startedAt) }),
565
+ ...this.#hud.summarize(),
558
566
  final,
559
567
  // Only on a real exit. A periodic flush carrying `exit: "unknown"` would read as a daemon that
560
568
  // died without a shutdown path, which is the one thing this field exists to make visible.
@@ -613,7 +621,8 @@ export class SessionMetrics {
613
621
  0 === this.#bugsFound &&
614
622
  0 === this.#sdkFailures &&
615
623
  0 === this.#postSocketFailures &&
616
- 0 === this.#postRetriesSaved);
624
+ 0 === this.#postRetriesSaved &&
625
+ this.#hud.empty);
617
626
  }
618
627
  /** Zero the counters after a non-final flush so the next flush reports the NEXT window, not a total. */
619
628
  reset() {
@@ -641,6 +650,7 @@ export class SessionMetrics {
641
650
  this.#unknownToolCalls = 0;
642
651
  this.#bugsFound = 0;
643
652
  this.#bugKinds.clear();
653
+ this.#hud.reset();
644
654
  // #seenBugKinds is deliberately NOT cleared. It is not a window counter — it is the
645
655
  // session-lifetime memory behind `repeat` on bug_found, and zeroing it made the same defect,
646
656
  // found again after a flush, report as a newly distinct one. Sessions run to 11.5 hours in the
@@ -665,6 +675,10 @@ export const recordBrowserLatency = (ms) => {
665
675
  export const recordSdkFailure = (failure) => {
666
676
  getSessionMetrics().recordSdkFailure(failure.site, failure.message);
667
677
  };
678
+ /** Narrow-and-record one HUD use. Keeps the session hot path to a single call. */
679
+ export const recordHudUse = (use) => {
680
+ getSessionMetrics().recordHudUse(use);
681
+ };
668
682
  /** Tests only — drop the singleton so each case starts from zero. */
669
683
  export const resetSessionMetrics = () => {
670
684
  current = undefined;
@@ -0,0 +1,31 @@
1
+ import { Bridge } from './portal/bridge/bridge.js';
2
+ import { FlowStore } from './language/flows/flows.js';
3
+ import type { FileSystemPort } from './memory/project/fs/fs-port.js';
4
+ import type { TapeStep } from './memory/journal/drive-flow.js';
5
+ import type { OnboardingStep } from '@reticlehq/core/telemetry';
6
+ import { type PruneWorkspaceOptions } from './memory/journal/on-disk/startup-maintenance.js';
7
+ /**
8
+ * Wire journal capture, ambient seeding and the journal-tail flush onto a bridge.
9
+ *
10
+ * Both entry points need all three, and both used to hand-roll them. `startDaemon` only ever wired the
11
+ * first, so on the path every user actually takes (`reticle serve` / `reticle mcp`) the journal tail was
12
+ * dropped at session end and the learned ambient map was never persisted OR seeded — meaning ambient
13
+ * learning could not converge across sessions and the last events of every session were lost. The two
14
+ * call sites had already drifted once before, which is why this is one function rather than a
15
+ * copy-paste both are asked to keep in step.
16
+ */
17
+ export declare function attachJournal(bridge: Bridge, deps: {
18
+ fs: FileSystemPort;
19
+ reticleRoot: string;
20
+ enabled: boolean;
21
+ takeAmbientTape?: () => {
22
+ steps: readonly TapeStep[];
23
+ startPath?: string;
24
+ } | undefined;
25
+ reportStep?: (step: OnboardingStep) => Promise<boolean>;
26
+ /** Tell cloud sync a run landed, so it cycles instead of waiting for its timer. */
27
+ onRunPersisted?: () => void;
28
+ flows?: FlowStore;
29
+ /** What this project is willing to keep — the `retain` block of its `.reticle.json`. */
30
+ retain?: PruneWorkspaceOptions;
31
+ }): void;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Journal wiring for the composition root.
3
+ *
4
+ * A sibling of `index.ts` rather than a move into `memory/journal/`: this is COMPOSITION, not
5
+ * journalling. It reaches the bridge, the flow store, the telemetry funnel and the recording tape to
6
+ * join them together, and putting those reaches inside the journal domain would make that domain
7
+ * depend on four areas it has no business knowing. `index.ts` already holds every one of them, and
8
+ * the 1000-line backstop is what asked for the split.
9
+ */
10
+ import { artifactRootResolver } from './memory/project/artifact-root-resolver.js';
11
+ import { originOf } from './portal/session/session-manager.js';
12
+ import { Bridge } from './portal/bridge/bridge.js';
13
+ import { FlowStore } from './language/flows/flows.js';
14
+ import { makeJournalAttach } from './memory/journal/attach-journal.js';
15
+ import { makeSessionEnd, recordDriveRun } from './memory/journal/session-end.js';
16
+ import { attachDriveRunFlush } from './memory/journal/drive-run-flush.js';
17
+ import { AmbientStore } from './memory/journal/ambient-store.js';
18
+ import { ensureWorkspaceGitignore } from './memory/journal/on-disk/workspace-gitignore.js';
19
+ import { pruneWorkspace, } from './memory/journal/on-disk/startup-maintenance.js';
20
+ import { dirname } from 'node:path';
21
+ import { findProjectConfig } from './command/cli/ports/resolve/cli-port.js';
22
+ import { readBackgroundTraffic } from './portal/session/facts/background-traffic.js';
23
+ /**
24
+ * Wire journal capture, ambient seeding and the journal-tail flush onto a bridge.
25
+ *
26
+ * Both entry points need all three, and both used to hand-roll them. `startDaemon` only ever wired the
27
+ * first, so on the path every user actually takes (`reticle serve` / `reticle mcp`) the journal tail was
28
+ * dropped at session end and the learned ambient map was never persisted OR seeded — meaning ambient
29
+ * learning could not converge across sessions and the last events of every session were lost. The two
30
+ * call sites had already drifted once before, which is why this is one function rather than a
31
+ * copy-paste both are asked to keep in step.
32
+ */
33
+ export function attachJournal(bridge, deps) {
34
+ const journalAttach = makeJournalAttach(deps);
35
+ const ambientStore = new AmbientStore(deps.fs, deps.reticleRoot);
36
+ // Built once, not per session: the resolver walks config discovery and the user-level registry,
37
+ // and neither changes between two tabs connecting a second apart.
38
+ const resolveArtifactRoot = artifactRootResolver(deps.reticleRoot);
39
+ bridge.attachSessionCreate((session) => {
40
+ // Stamp the project's own `.reticle` before ANY counter fires for this session. Without it every
41
+ // verdict is recorded against wherever the daemon was started, which is how one app's evidence
42
+ // reached a different account's production dashboard.
43
+ // The origin is passed for the case where the page never stamped a project id: it is the only
44
+ // distinguishing fact left, and without it every such app shares one bucket.
45
+ session.artifactRoot = resolveArtifactRoot(session.projectId, originOf(session.url)).root;
46
+ // The project's config sits beside its `.reticle/`, so the declaration is read from the tab's
47
+ // own project rather than from wherever the daemon was started.
48
+ session.background = readBackgroundTraffic(findProjectConfig(dirname(session.artifactRoot)));
49
+ // Here rather than on the start path, which was neither the moment we were about to write into a
50
+ // repository nor the root we were about to write into: it created `.reticle/` — holding nothing
51
+ // but the ignore file — wherever the daemon was launched, coming back every boot after the user
52
+ // deleted it, while the journals this ignore protects landed in another tree, uncovered.
53
+ if (deps.enabled)
54
+ void ensureWorkspaceGitignore(deps.fs, session.artifactRoot);
55
+ journalAttach(session);
56
+ // Seed the learned ambient map so a fresh session starts knowing which regions churn, instead of
57
+ // re-learning from zero. Best-effort + async: a late seed still helps, a failure is silent.
58
+ if (deps.enabled) {
59
+ void ambientStore
60
+ .load()
61
+ .then((counts) => session.seedAmbient(counts))
62
+ .catch(() => undefined);
63
+ }
64
+ });
65
+ // Teardown: flush the journal tail to disk + persist what this session learned.
66
+ bridge.attachSessionEnd(makeSessionEnd({
67
+ ...deps,
68
+ ...(deps.retain === undefined ? {} : { retain: deps.retain }),
69
+ // Retention runs from teardown, and it must not delete the journal of a session that is still
70
+ // being written. The registry is the only thing that knows which those are.
71
+ liveSessionIds: () => new Set(bridge.sessions.all().map((s) => s.id)),
72
+ }));
73
+ /*
74
+ * And the same write, DURING the session rather than only at the end of it.
75
+ *
76
+ * Teardown was the only writer of a run artifact, so every verdict produced while a tab stayed
77
+ * open was invisible to cloud sync — which can only push artifacts that exist. Reported as "sync
78
+ * is not happening" and diagnosed as the agent forgetting to sync; there is no sync command to
79
+ * forget. The evidence simply was not on disk yet.
80
+ *
81
+ * Safe to call repeatedly because the run id is derived from the session, so this rewrites one
82
+ * artifact rather than accumulating them — a property `recordDriveRun` already had, for the
83
+ * unrelated reason that a reconnecting tab must not publish two overlapping rows.
84
+ */
85
+ if (deps.enabled) {
86
+ attachDriveRunFlush({
87
+ resolve: (sessionId) => bridge.sessions.get(sessionId),
88
+ write: (session) => recordDriveRun(deps, session),
89
+ });
90
+ }
91
+ // One call, because "what maintenance runs at startup" needs one answer. Inlined here, the byte
92
+ // budget was simply missing — written, tested, and called by nothing, while the three count
93
+ // bounds beside it ran every time. See startup-maintenance.ts.
94
+ //
95
+ // Deliberately NOT behind `deps.enabled`. Switching journalling off is what somebody does BECAUSE
96
+ // the directory got too big, and both sweep sites were gated on it, so that setting was the one
97
+ // under which nothing ever deleted what was already there. Visual diffs, feedback copies and run
98
+ // artifacts do not need the journal to be written at all and kept accumulating regardless.
99
+ void pruneWorkspace(deps.fs, deps.reticleRoot, new Set(bridge.sessions.all().map((s) => s.id)), deps.retain ?? {});
100
+ }
package/docs/README.md CHANGED
@@ -72,6 +72,7 @@ Everything here is published to [docs.reticle.sh](https://docs.reticle.sh) by `d
72
72
  | [vs-playwright-mcp.mdx](vs-playwright-mcp.mdx) | outside-in vs inside-out, and when Playwright is right |
73
73
  | [vs-chrome-devtools-mcp.mdx](vs-chrome-devtools-mcp.mdx) | cheaper per look, catches less: the trade, measured |
74
74
  | [vs-screenshots.mdx](vs-screenshots.mdx) | why a better vision model does not fix a non-visual bug |
75
+ | [what-is-recorded.md](what-is-recorded.md) | what Reticle keeps, what is redacted, and what it takes for any of it to leave your machine |
75
76
  | [telemetry.md](telemetry.md) | what is collected, and how to turn it off |
76
77
  | [local-registry.md](local-registry.md) | installing an unpublished build |
77
78
  | [license-activation.mdx](license-activation.mdx) | you have a key: where to set it, how to confirm it, what to do when it fails |
@@ -101,7 +101,9 @@ reticle: no .reticle/cloud.json here ... run `reticle link` first
101
101
 
102
102
  ### `reticle push`
103
103
 
104
- Sends local run artifacts from `.reticle` to the linked project, honoring the sync policy. With `sync.runs` off it does nothing and says so. Prints `{ pushed, failed, total, project }`.
104
+ Sends local run artifacts from `.reticle` to the linked project, honoring the sync policy. With `sync.runs` off it does nothing and says so. Prints `{ ok, project, sent, pulled }`, where `sent` carries the accepted counts and, when the server refused anything, a `rejected` array naming each artifact's index and reason.
105
+
106
+ **A rejection is a failed push.** If the server refuses any artifact, `ok` is `false` and the command exits `1`, even when other artifacts in the same bundle landed. A refused artifact never reaches the dashboard and is re-offered on every cycle, so partial loss is still loss. `ok: true` and exit `0` mean everything offered was accepted, which includes the common case of a repo that was already up to date. The accepted counts still report what did land, so a partial push is visible as one.
105
107
 
106
108
  ### `reticle runs`
107
109
 
@@ -133,12 +135,12 @@ reticle: cloud not attached here ... run `reticle link` (or set RETICLE_CLOUD_UR
133
135
 
134
136
  ## Exit codes
135
137
 
136
- | Code | Meaning |
137
- | ---- | ----------------------------------------------------------------------------------- |
138
- | `0` | Success |
139
- | `1` | The API call failed, or the repository is not attached where attachment is required |
140
- | `2` | Usage error, or not signed in and no `RETICLE_API_KEY` |
141
- | `3` | `regression` only: at least one flow broke |
138
+ | Code | Meaning |
139
+ | --- | --- |
140
+ | `0` | Success |
141
+ | `1` | The API call failed, the repository is not attached where attachment is required, or `push` had an artifact refused |
142
+ | `2` | Usage error, or not signed in and no `RETICLE_API_KEY` |
143
+ | `3` | `regression` only: at least one flow broke |
142
144
 
143
145
  Output is pretty-printed JSON on stdout. Errors and next-step nudges go to stderr, so an agent parsing stdout is unaffected.
144
146
 
package/docs/cli/init.mdx CHANGED
@@ -19,6 +19,7 @@ npx @reticlehq/server init [--dry-run] [--port N] [--no-mcp] [--no-install] [--a
19
19
  | `--no-mcp` | boolean | `false` (MCP is registered) | Skip the MCP registration **and** the agent rule files (`CLAUDE.md`, `AGENTS.md`, `.cursor`) and the `/reticle` command |
20
20
  | `--no-install` | boolean | `false` (deps are installed) | Do not run the install. The step becomes `[⚠]` and prints the `add -D` command for you to run by hand, rather than vanishing from the plan |
21
21
  | `--app <dir>` | string | unset | Pick which app to wire in a monorepo, without changing directory |
22
+ | `--hooks` | boolean | `false` | Also add a print-only Stop hook to `.claude/settings.json`, merged into what is there. When the agent stops without a claim that held, it prints one line; it never blocks. See [`reticle report`](/cli/report) |
22
23
  | `--yes` | boolean | `false` | Accepted for scripting. `init` has no interactive prompts today, so it is a no-op |
23
24
 
24
25
  <Warning>
@@ -0,0 +1,44 @@
1
+ ---
2
+ title: reticle report
3
+ description: What the latest session claimed, and what held. Also a print-only Stop hook for Claude Code.
4
+ icon: clipboard-check
5
+ ---
6
+
7
+ `reticle report` reads this project's most recent session from `.reticle/` and says how many of its claims held, how many failed, how many came back `unknown` and who can act on each, and how many proved nothing. It reads the journal on disk, so no daemon has to be running.
8
+
9
+ ```bash
10
+ npx @reticlehq/server report [--session <id>] [--hook]
11
+ ```
12
+
13
+ ## Arguments and flags
14
+
15
+ | Flag | Default | What it does |
16
+ | --- | --- | --- |
17
+ | `--session <id>` | the newest session | Report a specific session |
18
+ | `--hook` | off | Print one line, and only when the working tree changed and no claim held |
19
+
20
+ ## What it prints
21
+
22
+ ```text
23
+ session lease-85f8191b-3874-457b-973b-8ee365e5acfb
24
+ 2 of 4 claims held
25
+ 1 failed, 1 of them a green a channel contradicted
26
+ 1 unknown (environment: 1)
27
+ ✗ signal order:placed
28
+ ```
29
+
30
+ An `unknown` is never counted as held. It is Reticle saying it could not tell, and the owner beside it says what to do next.
31
+
32
+ ## As a Claude Code Stop hook
33
+
34
+ With `--hook` it prints nothing when a claim held or nothing in the tree changed, and otherwise one line, such as `Reticle: not verified. 3 claims: 1 failed, 2 unknown`. It prints Claude Code's `systemMessage` shape and always exits `0`: you see the line when the agent stops, and the agent is never blocked by it. `reticle init --hooks` adds it for you, merged into whatever `.claude/settings.json` already holds. By hand:
35
+
36
+ ```json
37
+ {
38
+ "hooks": {
39
+ "Stop": [{ "hooks": [{ "type": "command", "command": "npx @reticlehq/server report --hook" }] }]
40
+ }
41
+ }
42
+ ```
43
+
44
+ A session last written more than an hour ago does not count as this turn, so a morning drive cannot answer for an afternoon edit. If you want the stop blocked until the change is verified, use [`reticle gate --hook`](/cli/gate) instead.
package/docs/desktop.mdx CHANGED
@@ -214,7 +214,7 @@ Alternatively, since an Electron renderer _is_ Chromium, `--remote-debugging-por
214
214
 
215
215
  ```toml
216
216
  [dependencies]
217
- reticle-tauri = "3.2"
217
+ reticle-tauri = "3.3"
218
218
  ```
219
219
 
220
220
  ```rust
package/docs/flows.md CHANGED
@@ -87,6 +87,16 @@ reticle_flow {action:"load"}({ flowName: "create-task" }) // → the flow JSON
87
87
  reticle_flow_replay({ flowName: "create-task" }) // re-resolve each anchor against the LIVE DOM, run it
88
88
  ```
89
89
 
90
+ ## Delete a flow
91
+
92
+ ```jsonc
93
+ reticle_flow {action:"delete"}({ flowName: "create-task" }) // → { deleted: true }
94
+ ```
95
+
96
+ A renamed or obsolete flow otherwise lingers in `reticle_flow {action:"list"}` and in every `reticle_verify {action:"flows"}` suite run, where it fails forever against a screen nobody intends to keep.
97
+
98
+ **Deleting a flow that is not there is an error, not a no-op.** It answers `{ error, code: "not_found" }` rather than `{ deleted: true }`, so a mistyped name cannot read as a completed cleanup while the real flow stays in the suite. Check the spelling against `reticle_flow {action:"list"}` and try again.
99
+
90
100
  **Watch it replay on the page.** When the presenter is on (`present: true`), a replay isn't silent. Each step drives the real page, so the synthetic cursor flies to the element, the focus ring lands, and the activity log streams the journey live. You (or a teammate) literally watch the saved journey re-walk itself on your app, then see the verdict land. It's the fastest way to _see_ that a flow still works, not just read a green checkmark.
91
101
 
92
102
  `reticle_flow_replay` returns a status:
@@ -164,6 +174,7 @@ With `apply: false` the flow file is **never modified**; you get the proposed di
164
174
  | `reticle_flow_save_recorded` | `{ flowName? }` | persist a human-recorded (toolbar) flow |
165
175
  | `reticle_flow {action:"list"}` | `{}` | flows on disk |
166
176
  | `reticle_flow {action:"load"}` | `{ flowName }` | the flow JSON |
177
+ | `reticle_flow {action:"delete"}` | `{ flowName }` | `{ deleted: true }`, or `{ error, code }` (`not_found` when no such flow) |
167
178
  | `reticle_flow_replay` | `{ flowName }` | `{ status, steps, decision? }` (decision on drift/fail) |
168
179
  | `reticle_verify {action:"flows"}` | `{ names?, sessionId? }` | suite verdict `{ status, passed, failed, failures[] }` |
169
180
  | `reticle_verify { action: "heal" }` | `{ flowName, apply? }` | propose / apply nearest-match rebind |
@@ -35,6 +35,7 @@ Two columns say different things and it is worth keeping them apart. **CI gate**
35
35
  | **Plain HTML** | yes | no | no | no | Wired, unverified |
36
36
  | **Vue (non-Nuxt)** | as Vite | no | install only | no | Install gated, drive unverified |
37
37
  | **Preact** | as Vite | no | no | unproven | Sensor, React adapter via `preact/compat` |
38
+ | **Server-rendered (Django, Flask, FastAPI, Streamlit, Rails)** | yes | no | no | no | Wired, unverified |
38
39
 
39
40
  <Warning>
40
41
  "Wired, unverified" means exactly that. The recipe exists and was written from a real integration,
@@ -85,7 +86,41 @@ module.exports = withReticle({/* your config */});
85
86
 
86
87
  ## Remix and Astro
87
88
 
88
- Both run on Vite, so both use the Vite plugin exactly as React does. Astro needs the React integration if you want component identity in `.jsx` islands. Static `.astro` markup gets DOM, network and routing without it.
89
+ Remix uses the Vite plugin with `inject: false`: Remix SSRs its own HTML, so the plugin's connect-injection does not fire, but the stamping half still puts `file:line` on a verdict.
90
+
91
+ Astro is the same on the plugin (wire it with `inject: false` if you want source pointers on JSX islands) and uses a different channel for the pairing token. `vite.define` does not reach Astro's client pipeline on 7.2+, so the identifier stays literal and the bridge refuses the dial. `init` reads the token in frontmatter and puts it on a `<meta name="reticle-pairing-token">`. Reading it per request also means a token written after the dev server started is picked up on the next load, with no restart.
92
+
93
+ Use a processed page `<script>` with a static SDK import. Vite owns that module graph from the first transform:
94
+
95
+ ```astro
96
+ ---
97
+ import { readFileSync } from 'node:fs';
98
+ import { homedir } from 'node:os';
99
+ import { join } from 'node:path';
100
+ const pairingToken = (() => {
101
+ if (!import.meta.env.DEV) return '';
102
+ const dir = process.env['RETICLE_PAIRING_TOKEN_DIR'] || join(homedir(), '.reticle');
103
+ try { return readFileSync(join(dir, 'pairing-token'), 'utf8').trim(); } catch { return ''; }
104
+ })();
105
+ const pairingRoot = import.meta.env.DEV ? process.cwd() : '';
106
+ ---
107
+ <meta name="reticle-pairing-token" content={pairingToken} />
108
+ <meta name="reticle-pairing-root" content={pairingRoot} />
109
+ <script>
110
+ import { reticle, install } from '@reticlehq/react';
111
+ if (import.meta.env.DEV) {
112
+ const token = document.querySelector('meta[name="reticle-pairing-token"]')?.getAttribute('content') ?? '';
113
+ const root = document.querySelector('meta[name="reticle-pairing-root"]')?.getAttribute('content') ?? '';
114
+ install();
115
+ reticle.connect({
116
+ ...(0 < token.length ? { token } : {}),
117
+ ...(0 < root.length ? { root } : {}),
118
+ });
119
+ }
120
+ </script>
121
+ ```
122
+
123
+ The `<script>` tag must not sit inside a conditional. Astro hoists them statically, so `{isDev && (<script />)}` is never hoisted. `init` writes the same `<meta>` tags and a local `src/components/ReticleDev.ts` that the script statically imports. It also raises `vite.build.target` to `es2022` (Astro's default down-levels the SDK), declares the SDK in `optimizeDeps.include`, and keeps `.reticle/` out of the watcher. Static `.astro` markup still gets DOM, network and routing without the React integration.
89
124
 
90
125
  ## SvelteKit
91
126
 
@@ -205,6 +240,83 @@ The same shape works for webpack, Parcel, and the Vue and Svelte CLIs.
205
240
 
206
241
  Keep it out of your production template. Reticle self-disables when the build reports `NODE_ENV=production`, but a second lock on a door leading to your users is a reasonable number.
207
242
 
243
+ ## Server-rendered apps (Django, Flask, FastAPI, Rails)
244
+
245
+ There is no JS build to import the SDK from, and no single template to paste a script tag into: an app has many templates and may not have a base one. The SDK loads from a URL instead, so a page with no build step is not a blocker.
246
+
247
+ `reticle init` detects these and prints the right thing for the stack it found, writing `.reticle.json` as it goes. What follows is the same wiring, written down so you can read it before running anything, or wire it by hand.
248
+
249
+ ### Django
250
+
251
+ Middleware is the Django-shaped answer: one place, runs for every page, no template to find.
252
+
253
+ ```python
254
+ # reticle_dev.py: add to MIDDLEWARE while DEBUG is on:
255
+ # MIDDLEWARE = [..., "reticle_dev.ReticleDevMiddleware"]
256
+ from django.conf import settings
257
+
258
+ _SNIPPET = """<script type="module">
259
+ import { reticle } from 'https://cdn.jsdelivr.net/npm/@reticlehq/browser@<version>/+esm';
260
+ reticle.connect({ projectId: '<your-project-id>', token: '<pairing token>' });
261
+ </script>"""
262
+
263
+
264
+ class ReticleDevMiddleware:
265
+ """Inject the Reticle SDK into HTML responses during development only."""
266
+
267
+ def __init__(self, get_response):
268
+ self.get_response = get_response
269
+
270
+ def __call__(self, request):
271
+ response = self.get_response(request)
272
+ if not settings.DEBUG:
273
+ return response
274
+ if "text/html" not in response.get("Content-Type", ""):
275
+ return response
276
+ # Streaming responses have no .content to rewrite; leave them alone.
277
+ if getattr(response, "streaming", False):
278
+ return response
279
+ body = response.content.decode(response.charset)
280
+ if "</body>" not in body:
281
+ return response
282
+ response.content = body.replace("</body>", _SNIPPET + "</body>", 1).encode(
283
+ response.charset
284
+ )
285
+ if response.has_header("Content-Length"):
286
+ response["Content-Length"] = str(len(response.content))
287
+ return response
288
+ ```
289
+
290
+ `reticle init` emits this with your real version, project id and pairing token filled in. Copy it from there rather than from this page: the placeholders above are the three things that have to be right.
291
+
292
+ Four details in that middleware are load-bearing, and each is there because the obvious version is wrong:
293
+
294
+ - **`settings.DEBUG`** is the guard. Reticle is dev-only, and this is the lock that cannot be forgotten at deploy time.
295
+ - **the `text/html` check** keeps it from corrupting a JSON API response or a file download.
296
+ - **the `streaming` check** exists because a `StreamingHttpResponse` has no `.content` to rewrite, and touching it raises.
297
+ - **`</body>`, not `<head>`** so the SDK sees a parsed document.
298
+
299
+ Re-setting `Content-Length` matters for the same reason: the body grew, and a stale length truncates the page in the browser rather than failing loudly.
300
+
301
+ ### Flask and FastAPI with Jinja
302
+
303
+ Same shape, one hook instead of a middleware class: an `@app.after_request` in Flask, or a middleware in FastAPI, with the same four guards: a debug flag, `text/html` only, not streaming, and injected before `</body>`.
304
+
305
+ ### Streamlit
306
+
307
+ Different again: Streamlit owns the top-level document, so there is no served template, and scripts passed to `st.markdown` do not run. Use `st.html` on Streamlit 1.63 or newer. `reticle init` prints the helper, which also guards against connecting twice across reruns.
308
+
309
+ ### What you get, and what you do not
310
+
311
+ Everything that comes from reading the live page works: queries, snapshots, actions, network and console channels, verdicts.
312
+
313
+ What is missing is the **source pointer**. `file:line` on a verdict comes from the build plugin stamping `data-reticle-source`, and there is no build step here to stamp it. You get every other signal and no `source` field, the same way a Vue single-file component does.
314
+
315
+ <Warning>
316
+ Wired, unverified. No app in CI drives one of these to a verdict, so nothing will tell us if it
317
+ breaks. If a page does not register a session, that is worth reporting.
318
+ </Warning>
319
+
208
320
  ## Desktop is a different shape
209
321
 
210
322
  Electron and Tauri invert the direction: your app dials the daemon, and there is no URL for an agent to open. Reticle also reaches the main-process and Rust IPC boundary, which the renderer cannot see.
@@ -150,6 +150,42 @@ Four files in your project, none of them mysterious.
150
150
  ```
151
151
  The `projectId` is how Reticle scopes sessions to this project, so two apps running at once
152
152
  don't get confused for each other.
153
+
154
+ **Keeping `.reticle/` small.** Reticle bounds the directory for you, by count and by total size.
155
+ Add a `retain` block to change what it keeps:
156
+
157
+ ```json
158
+ {
159
+ "retain": { "sessions": 20, "visual": 10, "feedback": 20, "budgetMb": 512 }
160
+ }
161
+ ```
162
+
163
+ Those are the defaults, and every field is optional. `sessions` is how many session journals
164
+ survive, `visual` how many overlay diffs (pixel baselines are never deleted, because the next
165
+ comparison needs them), `feedback` how many local copies of reports already sent, and `budgetMb`
166
+ the total the whole evidence tier may occupy. `0` means keep none, and `"sessions": 0` also stops
167
+ journals being written in the first place.
168
+
169
+ Saved flows, capsules, baselines, the contract and the intent ledger are not touched by any of
170
+ this. They are the part meant to be committed, and a bound that deleted them would be deleting
171
+ the regression checks your team shares.
172
+
173
+ **Your app's own background calls.** An analytics or heartbeat endpoint on your app's own host
174
+ looks exactly like the app's work, so Reticle never guesses. If one of them keeps a verdict
175
+ waiting, or fails and gets blamed on an unrelated action, name it:
176
+
177
+ ```json
178
+ {
179
+ "background": ["/api/analytics/events", "/api/heartbeat"]
180
+ }
181
+ ```
182
+
183
+ Each entry is matched the way `urlContains` is. A listed request stops holding the wait open and
184
+ stops contradicting a claim; when a contradiction is still reported, it lists what was set aside.
185
+ It is never hidden from an assertion that names it: `net { urlContains: "/api/heartbeat" }` still
186
+ sees it. Keep entries specific: `/api` would set aside your whole backend.
187
+ Third-party calls (another site's beacon or SDK) need no entry; they are already set aside.
188
+
153
189
  </Accordion>
154
190
 
155
191
  <Accordion title="vite.config.ts (the plugin)">
@@ -175,7 +175,7 @@ Screenshots and headless mode need the Rust crate, which is versioned independen
175
175
 
176
176
  ```toml
177
177
  [dependencies]
178
- reticle-tauri = "3.2"
178
+ reticle-tauri = "3.3"
179
179
  ```
180
180
 
181
181
  ```rust
@@ -21,7 +21,7 @@ Tauri IPC observation needs nothing on the Rust side. An `invoke('load_todos')`
21
21
 
22
22
  ```toml
23
23
  [dependencies]
24
- reticle-tauri = "3.2"
24
+ reticle-tauri = "3.3"
25
25
  ```
26
26
 
27
27
  Its own dependencies are `tauri = "2"` plus the per-platform capture backends: `objc2` and friends on macOS, `webview2-com` and `windows` on Windows, `webkit2gtk` and `cairo-rs` on Linux and the BSDs.