@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
@@ -1,7 +1,7 @@
1
1
  import { basename, join } from 'node:path';
2
2
  import { ReticleDir } from '@reticlehq/core';
3
3
  import { reticleDirPaths, visualDir } from '../../project/dir/reticle-dir.js';
4
- import { TRANSIENT_DIRS } from './workspace-gitignore.js';
4
+ import { EVIDENCE_DIRS } from './workspace-tiers.js';
5
5
  /** Keep at most this many session journals on disk; older ones are pruned by recency. */
6
6
  export const DEFAULT_SESSION_RETENTION = 20;
7
7
  /**
@@ -21,9 +21,10 @@ const PNG_SUFFIX = '.png';
21
21
  * and feedback copies together.
22
22
  *
23
23
  * Every bound above this line counts things, and a count cannot see a size: twenty session
24
- * directories is twenty UNBOUNDED directories. A user's `.reticle/sessions` reached 8 GB with all
25
- * three counts being honoured the whole time, because one drive against a chatty app appends
26
- * response bodies and DOM text for as long as the drive lasts, and nothing was looking at the total.
24
+ * directories is twenty UNBOUNDED directories. A workspace in the field grew to multiple gigabytes
25
+ * with all three counts being honoured the whole time, because one drive against a chatty app
26
+ * appends response bodies and DOM text for as long as the drive lasts, and nothing was looking at
27
+ * the total.
27
28
  *
28
29
  * Why this number and not one an order of magnitude either side. An ordinary session journal is
29
30
  * hundreds of kilobytes to a few megabytes; a long drive against an app with large JSON responses
@@ -60,7 +61,7 @@ function isOpenSession(entry, live) {
60
61
  * `selectPrunable` and `pruneByRecency` already split it.
61
62
  */
62
63
  export function selectOverBudget(entries, budgetBytes, live = NO_LIVE_SESSIONS) {
63
- const evidence = entries.filter((entry) => TRANSIENT_DIRS.includes(entry.under));
64
+ const evidence = entries.filter((entry) => EVIDENCE_DIRS.includes(entry.under));
64
65
  // Live sessions still COUNT — their bytes are on disk and the budget is about the disk. They are
65
66
  // only excluded from the eviction list, so a session over budget on its own is left alone rather
66
67
  // than deleted under its own writer.
@@ -78,8 +79,6 @@ export function selectOverBudget(entries, budgetBytes, live = NO_LIVE_SESSIONS)
78
79
  }
79
80
  return doomed;
80
81
  }
81
- /** Where refused feedback reports are copied. Named here because the writer lives in another area. */
82
- const FEEDBACK_SUBDIR = 'feedback';
83
82
  /**
84
83
  * Pure: given session dirs with their mtimes, the names to remove so only the `retention` most-recent
85
84
  * remain. Oldest-first. Exported for testing (no IO).
@@ -138,6 +137,11 @@ async function pruneByRecency(fs, dir, retention, keep = () => true) {
138
137
  * so a stat/rm failure on one dir (or a missing sessions/ dir) is swallowed, never crashing a session.
139
138
  * Mirrors RunStore's amortized pruning; run on daemon start and at the end of every session.
140
139
  *
140
+ * The COUNT bound only. This used to sweep the tier total as well, from when it was the single
141
+ * sweep both call sites used; `pruneWorkspace` is that single place now and runs the budget last,
142
+ * on purpose, so the leftover call here was a second walk of every evidence entry on every teardown
143
+ * — and it ran FIRST, sizing everything the counts were about to delete anyway.
144
+ *
141
145
  * An open session is excluded from BOTH bounds — see `RetentionOptions.live`.
142
146
  *
143
147
  * ponytail: excluded via `keep`, so an open session is not COUNTED toward the retention cap either.
@@ -148,9 +152,6 @@ export async function pruneSessions(fs, root, options = {}) {
148
152
  const live = options.live ?? NO_LIVE_SESSIONS;
149
153
  const retention = options.retention ?? DEFAULT_SESSION_RETENTION;
150
154
  await pruneByRecency(fs, reticleDirPaths(root).sessions, retention, (name) => !live.has(name));
151
- // The tier total is swept from here because this is the one sweep that runs on BOTH the daemon's
152
- // start path and the end of every session, and the end of a session is when the bytes arrive.
153
- await pruneEvidenceBudget(fs, root, options.budgetBytes ?? DEFAULT_EVIDENCE_BUDGET_BYTES, live);
154
155
  }
155
156
  /**
156
157
  * Bound `.reticle/visual/` — the largest thing in the workspace, and the only one with no delete
@@ -224,7 +225,7 @@ async function sizeOf(fs, path) {
224
225
  */
225
226
  async function collectEvidence(fs, root) {
226
227
  const entries = [];
227
- for (const under of TRANSIENT_DIRS) {
228
+ for (const under of EVIDENCE_DIRS) {
228
229
  const isVisual = ReticleDir.VISUAL_SUBDIR === under;
229
230
  const dirs = isVisual ? await visualDirs(fs, root) : [join(root, under)];
230
231
  for (const dir of dirs) {
@@ -287,5 +288,5 @@ export async function pruneEvidenceBudget(fs, root, budgetBytes = DEFAULT_EVIDEN
287
288
  * retention rule is the same rule whether the directory holds journals, diffs or reports.
288
289
  */
289
290
  export async function pruneFeedback(fs, root, retention = DEFAULT_FEEDBACK_RETENTION) {
290
- await pruneByRecency(fs, join(root, FEEDBACK_SUBDIR), retention);
291
+ await pruneByRecency(fs, join(root, ReticleDir.FEEDBACK_SUBDIR), retention);
291
292
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Everything Reticle prunes in ONE `.reticle/` workspace, in one place.
3
+ *
4
+ * Called from two moments and both matter: daemon start (the daemon's own tree) and session end
5
+ * (the tree that SESSION wrote into). Those are usually different directories — a globally
6
+ * registered daemon stands in `$HOME` — and the per-project one is where the bytes actually
7
+ * accumulate, so sweeping only the first swept the wrong tree for the common case.
8
+ *
9
+ * The four bounds were inlined at the call site and one of them was simply missing:
10
+ * `pruneEvidenceBudget` had been written, tested, and never called by anything. The three COUNT
11
+ * bounds were wired and the BYTE bound was not, which is the combination the budget's own header
12
+ * warns about -- "twenty session directories is twenty UNBOUNDED directories", beside a field
13
+ * report of a multi-gigabyte `.reticle/sessions` with all three counts honoured throughout.
14
+ *
15
+ * Counts and bytes answer different questions and neither substitutes for the other. A count says
16
+ * how MANY sessions are kept; it says nothing about how large one is allowed to get. A chatty app
17
+ * writes one enormous journal and never trips a count.
18
+ *
19
+ * Gathered into a function rather than left as four statements so that "what maintenance runs at
20
+ * startup" has one answer that can be TESTED. Inlined, the only way to check the budget was wired
21
+ * was to grep the call site for its name -- and a grep passes on a comment that quotes the code,
22
+ * which has already happened in this repository.
23
+ *
24
+ * Order: counts first, budget last. The counts are cheap and bounded; the budget walks and sizes
25
+ * what the counts leave behind, so running it second means it sizes less.
26
+ *
27
+ * NEVER THROWS. Maintenance must not be the reason a daemon fails to come up, and each prune below
28
+ * already swallows its own errors -- this only has to avoid adding a new way to fail.
29
+ */
30
+ import type { FileSystemPort } from '../../project/fs/fs-port.js';
31
+ import { type RetainPolicy } from './retain-policy.js';
32
+ /**
33
+ * What this sweep is allowed to keep. Every field optional, defaulting to the built-in bound.
34
+ *
35
+ * Partial rather than a whole `RetainPolicy` for two reasons that happen to agree: a test states
36
+ * only the bound it is exercising instead of writing half a gigabyte to disk, and a project's
37
+ * `.reticle.json` states only the bounds it cares about. Same reason the clock is injected
38
+ * everywhere else here — a bound that can only be exercised by producing the real quantity is a
39
+ * bound nothing exercises.
40
+ */
41
+ export type PruneWorkspaceOptions = Partial<RetainPolicy>;
42
+ export declare function pruneWorkspace(fs: FileSystemPort, root: string, live: ReadonlySet<string>, options?: PruneWorkspaceOptions): Promise<void>;
@@ -0,0 +1,14 @@
1
+ import { pruneEvidenceBudget, pruneFeedback, pruneSessions, pruneVisualDiffs, } from './retention.js';
2
+ import { DEFAULT_RETAIN } from './retain-policy.js';
3
+ export async function pruneWorkspace(fs, root, live, options = {}) {
4
+ const retain = { ...DEFAULT_RETAIN, ...options };
5
+ // Empty in the ordinary case (nothing has connected yet); this path also runs on a daemon that is
6
+ // already serving sessions, which is why `live` is threaded rather than assumed empty.
7
+ await pruneSessions(fs, root, { live, retention: retain.sessions });
8
+ // The largest thing in the workspace, and for a long time the only one with no delete path.
9
+ await pruneVisualDiffs(fs, root, retain.visual);
10
+ // Write-only local copies of reports the outbox already carries.
11
+ await pruneFeedback(fs, root, retain.feedback);
12
+ // LAST, and the one that was missing: the only bound that limits SIZE rather than count.
13
+ await pruneEvidenceBudget(fs, root, retain.budgetBytes, live);
14
+ }
@@ -1,15 +1,4 @@
1
1
  import type { FileSystemPort } from '../../project/fs/fs-port.js';
2
- /**
3
- * Local directories, without the trailing slash the ignore file wants.
4
- *
5
- * Exported because this is also the EVIDENCE tier: the per-session churn that retention is allowed
6
- * to delete. Read from here rather than restated next to the byte budget — the partition of what
7
- * `.reticle/` holds already exists, is guarded as exhaustive, and a second copy of it would be the
8
- * list that silently disagrees.
9
- */
10
- export declare const TRANSIENT_DIRS: readonly string[];
11
- /** Every name Reticle treats as machine-local, however it is spelled in the ignore file. */
12
- export declare const TRANSIENT_NAMES: readonly string[];
13
2
  /**
14
3
  * Write `.reticle/.gitignore` if it is not already there.
15
4
  *
@@ -1,60 +1,21 @@
1
1
  import { join } from 'node:path';
2
2
  import { ReticleDir } from '@reticlehq/core';
3
+ import { LOCAL_DIRS, LOCAL_FILES } from './workspace-tiers.js';
3
4
  /**
4
5
  * The `.reticle/` entries that are LOCAL state, and must never be committed by accident.
5
6
  *
6
- * Split from the durable half deliberately, and the split is the whole design: `contract.json`,
7
- * `flows/`, `intent.json` and `capsules/` are meant to be checked in — a flow that cannot be shared
8
- * is not a regression check anybody else can run — while everything below is per-machine churn.
7
+ * Read from the tier table rather than restated here: which half a name belongs to is one decision,
8
+ * and a second copy of it is the list that silently disagrees. This file asks the table exactly one
9
+ * of its two questions — `sharing` — and has no opinion at all about what retention may delete.
9
10
  *
10
11
  * The reason this matters beyond tidiness: a session journal carries URLs, request and response
11
12
  * bodies, and DOM text from the app under test. `.reticle/` is created by the daemon rather than by
12
13
  * the user, so it arrives untracked and unexplained, and one `git add -A` puts an app's traffic into
13
14
  * a shared repository. Reticle created the directory; the ignore for it is Reticle's to write.
14
15
  */
15
- /**
16
- * The local feedback copies — `.reticle/feedback/`.
17
- *
18
- * Named here rather than imported: the directory is spelled inline where it is written, in files
19
- * another agent owns, and the ignore for a directory Reticle creates is Reticle's to write whether
20
- * or not that name has been hoisted yet.
21
- */
22
- const FEEDBACK_SUBDIR = 'feedback';
23
- /**
24
- * Local directories, without the trailing slash the ignore file wants.
25
- *
26
- * Exported because this is also the EVIDENCE tier: the per-session churn that retention is allowed
27
- * to delete. Read from here rather than restated next to the byte budget — the partition of what
28
- * `.reticle/` holds already exists, is guarded as exhaustive, and a second copy of it would be the
29
- * list that silently disagrees.
30
- */
31
- export const TRANSIENT_DIRS = [
32
- ReticleDir.SESSIONS_SUBDIR,
33
- ReticleDir.RUNS_SUBDIR,
34
- ReticleDir.VISUAL_SUBDIR,
35
- // Write-only: a local copy of what was already sent. The outbox is the record.
36
- FEEDBACK_SUBDIR,
37
- ];
38
- /** Local files. Exported with the directories as `TRANSIENT_NAMES`, which the partition guard reads. */
39
- const TRANSIENT_FILES = [
40
- ReticleDir.PROJECT_FILE,
41
- ReticleDir.AMBIENT_FILE,
42
- ReticleDir.ENVELOPES_FILE,
43
- ReticleDir.FLAKE_FILE,
44
- ReticleDir.TIERS_FILE,
45
- // The user's own record of what Reticle did for them, on THIS machine.
46
- ReticleDir.IMPACT_FILE,
47
- // This machine's conversation with the server. Its own doc comment says why committing it is
48
- // harmful: one machine's pull cursor makes every other machine skip what it has not seen.
49
- ReticleDir.CLOUD_STATE_FILE,
50
- // Triage pulled back from the dashboard — a cache of somebody's decisions, re-pullable at will.
51
- ReticleDir.ISSUES_FILE,
52
- ];
53
- /** Every name Reticle treats as machine-local, however it is spelled in the ignore file. */
54
- export const TRANSIENT_NAMES = [...TRANSIENT_DIRS, ...TRANSIENT_FILES];
55
16
  const TRANSIENT = [
56
- ...TRANSIENT_DIRS.map((dir) => `${dir}/`),
57
- ...TRANSIENT_FILES,
17
+ ...LOCAL_DIRS.map((dir) => `${dir}/`),
18
+ ...LOCAL_FILES,
58
19
  '*.log',
59
20
  '*.tmp',
60
21
  ];
@@ -0,0 +1,52 @@
1
+ /**
2
+ * May retention delete this to reclaim disk?
3
+ *
4
+ * MEMORY is why `.reticle/` exists — what a flow replays from, what a deviation is measured
5
+ * against. It is small, it is never evicted, and it must keep being written however full the disk
6
+ * is. EVIDENCE is what one drive left behind: journals, run artifacts, pixel diffs. It is large, it
7
+ * ages out, and losing the oldest of it costs nothing anybody can name.
8
+ */
9
+ export declare const Durability: {
10
+ readonly MEMORY: "memory";
11
+ readonly EVIDENCE: "evidence";
12
+ };
13
+ export type Durability = (typeof Durability)[keyof typeof Durability];
14
+ /**
15
+ * Does this belong in the repository, or only on the machine that wrote it?
16
+ *
17
+ * COMMIT is the shareable half: a flow nobody else can replay is not a regression check. LOCAL is
18
+ * per-machine churn, and journals carry URLs, request and response bodies and page text from the
19
+ * app under test, so keeping them out of a shared repository is worth doing on its own account.
20
+ */
21
+ export declare const Sharing: {
22
+ readonly COMMIT: "commit";
23
+ readonly LOCAL: "local";
24
+ };
25
+ export type Sharing = (typeof Sharing)[keyof typeof Sharing];
26
+ /** What one `.reticle/` entry is, on both axes at once. */
27
+ export interface WorkspaceTier {
28
+ readonly durability: Durability;
29
+ readonly sharing: Sharing;
30
+ /** A directory is ignored as `name/` and swept entry by entry; a file is neither. */
31
+ readonly isDirectory: boolean;
32
+ }
33
+ /**
34
+ * Every top-level name `.reticle/` can hold, classified on both axes.
35
+ *
36
+ * One list used to answer both questions with one answer, and they are not the same question.
37
+ * `capsules/` is committed — a fail-to-pass capsule a teammate cannot replay is not a regression
38
+ * check — AND it is evidence a drive left behind, which grows without bound. That cell did not
39
+ * exist, so the byte budget, which read the gitignore's list, structurally could not see capsules
40
+ * however large they got. Splitting the axes is what makes that sayable; it does not by itself say
41
+ * it, and `capsules/` stays MEMORY here until the budget is measured against it.
42
+ *
43
+ * Declaration order is the order the ignore file lists them, so the file a user already has on disk
44
+ * and the one written today are the same file.
45
+ */
46
+ export declare const WORKSPACE_TIERS: Readonly<Record<string, WorkspaceTier>>;
47
+ /** Machine-local directories, without the trailing slash the ignore file wants. */
48
+ export declare const LOCAL_DIRS: readonly string[];
49
+ /** Machine-local files. */
50
+ export declare const LOCAL_FILES: readonly string[];
51
+ /** The directories retention sweeps, ignored or not. The byte budget and the count bounds read this. */
52
+ export declare const EVIDENCE_DIRS: readonly string[];
@@ -0,0 +1,88 @@
1
+ import { ReticleDir } from '@reticlehq/core';
2
+ /**
3
+ * May retention delete this to reclaim disk?
4
+ *
5
+ * MEMORY is why `.reticle/` exists — what a flow replays from, what a deviation is measured
6
+ * against. It is small, it is never evicted, and it must keep being written however full the disk
7
+ * is. EVIDENCE is what one drive left behind: journals, run artifacts, pixel diffs. It is large, it
8
+ * ages out, and losing the oldest of it costs nothing anybody can name.
9
+ */
10
+ export const Durability = {
11
+ MEMORY: 'memory',
12
+ EVIDENCE: 'evidence',
13
+ };
14
+ /**
15
+ * Does this belong in the repository, or only on the machine that wrote it?
16
+ *
17
+ * COMMIT is the shareable half: a flow nobody else can replay is not a regression check. LOCAL is
18
+ * per-machine churn, and journals carry URLs, request and response bodies and page text from the
19
+ * app under test, so keeping them out of a shared repository is worth doing on its own account.
20
+ */
21
+ export const Sharing = {
22
+ COMMIT: 'commit',
23
+ LOCAL: 'local',
24
+ };
25
+ const localDir = { durability: Durability.EVIDENCE, sharing: Sharing.LOCAL, isDirectory: true };
26
+ const localFile = { durability: Durability.MEMORY, sharing: Sharing.LOCAL, isDirectory: false };
27
+ const sharedDir = { durability: Durability.MEMORY, sharing: Sharing.COMMIT, isDirectory: true };
28
+ const sharedFile = { durability: Durability.MEMORY, sharing: Sharing.COMMIT, isDirectory: false };
29
+ /**
30
+ * Every top-level name `.reticle/` can hold, classified on both axes.
31
+ *
32
+ * One list used to answer both questions with one answer, and they are not the same question.
33
+ * `capsules/` is committed — a fail-to-pass capsule a teammate cannot replay is not a regression
34
+ * check — AND it is evidence a drive left behind, which grows without bound. That cell did not
35
+ * exist, so the byte budget, which read the gitignore's list, structurally could not see capsules
36
+ * however large they got. Splitting the axes is what makes that sayable; it does not by itself say
37
+ * it, and `capsules/` stays MEMORY here until the budget is measured against it.
38
+ *
39
+ * Declaration order is the order the ignore file lists them, so the file a user already has on disk
40
+ * and the one written today are the same file.
41
+ */
42
+ export const WORKSPACE_TIERS = {
43
+ [ReticleDir.SESSIONS_SUBDIR]: localDir,
44
+ [ReticleDir.RUNS_SUBDIR]: localDir,
45
+ [ReticleDir.VISUAL_SUBDIR]: localDir,
46
+ // Write-only: a local copy of what was already sent. The outbox is the record.
47
+ [ReticleDir.FEEDBACK_SUBDIR]: localDir,
48
+ [ReticleDir.PROJECT_FILE]: localFile,
49
+ [ReticleDir.AMBIENT_FILE]: localFile,
50
+ [ReticleDir.ENVELOPES_FILE]: localFile,
51
+ [ReticleDir.FLAKE_FILE]: localFile,
52
+ [ReticleDir.TIERS_FILE]: localFile,
53
+ // The user's own record of what Reticle did for them, on THIS machine.
54
+ [ReticleDir.IMPACT_FILE]: localFile,
55
+ // This machine's conversation with the server. Its own doc comment says why committing it is
56
+ // harmful: one machine's pull cursor makes every other machine skip what it has not seen.
57
+ [ReticleDir.CLOUD_STATE_FILE]: localFile,
58
+ // Triage pulled back from the dashboard — a cache of somebody's decisions, re-pullable at will.
59
+ [ReticleDir.ISSUES_FILE]: localFile,
60
+ [ReticleDir.CONTRACT_FILE]: sharedFile,
61
+ [ReticleDir.FLOWS_SUBDIR]: sharedDir,
62
+ [ReticleDir.BASELINES_SUBDIR]: sharedDir,
63
+ [ReticleDir.CAPSULES_SUBDIR]: sharedDir,
64
+ [ReticleDir.INTENT_FILE]: sharedFile,
65
+ // The sharded form of the same ledger, and shared for the same reason.
66
+ [ReticleDir.INTENT_SUBDIR]: sharedDir,
67
+ [ReticleDir.CLOUD_LINK_FILE]: sharedFile,
68
+ // A hook is a decision the whole team shares, exactly like a package script: one that exists only
69
+ // on the machine that wrote it is a rule nobody else is following. It names COMMANDS and never a
70
+ // credential, so it is safe to commit — the same reasoning that puts `cloud.json` here while its
71
+ // API key stays in ~/.reticle.
72
+ [ReticleDir.HOOKS_FILE]: sharedFile,
73
+ // What a drive types into each labelled field. Committed for two reasons: a replay must send
74
+ // exactly what the recording sent or it is not a replay, and a value a model wrote once should be
75
+ // reviewable and editable by the team rather than regenerated differently on every machine.
76
+ [ReticleDir.FILL_VALUES_FILE]: sharedFile,
77
+ };
78
+ function namesWhere(match) {
79
+ return Object.entries(WORKSPACE_TIERS)
80
+ .filter(([, tier]) => match(tier))
81
+ .map(([name]) => name);
82
+ }
83
+ /** Machine-local directories, without the trailing slash the ignore file wants. */
84
+ export const LOCAL_DIRS = namesWhere((tier) => Sharing.LOCAL === tier.sharing && tier.isDirectory);
85
+ /** Machine-local files. */
86
+ export const LOCAL_FILES = namesWhere((tier) => Sharing.LOCAL === tier.sharing && !tier.isDirectory);
87
+ /** The directories retention sweeps, ignored or not. The byte budget and the count bounds read this. */
88
+ export const EVIDENCE_DIRS = namesWhere((tier) => Durability.EVIDENCE === tier.durability && tier.isDirectory);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The key an envelope's statistics accumulate under.
3
+ *
4
+ * The envelope map was keyed on the raw `pathname`. On any app whose URLs carry ids — most of them —
5
+ * `/orders/1001` and `/orders/1002` are different keys, so every visit mints a fresh envelope
6
+ * holding one sample.
7
+ *
8
+ * The cost is not the file size. `deviation-report` skips any envelope below
9
+ * `MIN_ENVELOPE_SAMPLES`, so on such an app no envelope ever qualifies, every report answers
10
+ * "envelope too new", and the deviation feature never turns on at all — silently, permanently, with
11
+ * nothing anywhere saying why. The unbounded file and the dead feature are one defect.
12
+ *
13
+ * CONSERVATIVE on purpose. Over-collapsing is the worse error: merging two genuinely different
14
+ * routes builds a baseline from two different pages, and a confident comparison against the wrong
15
+ * population is more expensive than no comparison. So a segment is an id only when it could not
16
+ * plausibly be a route NAME.
17
+ *
18
+ * This is the statistics key alone. The observed path is untouched, because a report has to be able
19
+ * to say "/orders/1001 was slow" — the template tells you where to look, the path tells you what to
20
+ * open.
21
+ */
22
+ export declare function routeTemplate(pathname: string): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The key an envelope's statistics accumulate under.
3
+ *
4
+ * The envelope map was keyed on the raw `pathname`. On any app whose URLs carry ids — most of them —
5
+ * `/orders/1001` and `/orders/1002` are different keys, so every visit mints a fresh envelope
6
+ * holding one sample.
7
+ *
8
+ * The cost is not the file size. `deviation-report` skips any envelope below
9
+ * `MIN_ENVELOPE_SAMPLES`, so on such an app no envelope ever qualifies, every report answers
10
+ * "envelope too new", and the deviation feature never turns on at all — silently, permanently, with
11
+ * nothing anywhere saying why. The unbounded file and the dead feature are one defect.
12
+ *
13
+ * CONSERVATIVE on purpose. Over-collapsing is the worse error: merging two genuinely different
14
+ * routes builds a baseline from two different pages, and a confident comparison against the wrong
15
+ * population is more expensive than no comparison. So a segment is an id only when it could not
16
+ * plausibly be a route NAME.
17
+ *
18
+ * This is the statistics key alone. The observed path is untouched, because a report has to be able
19
+ * to say "/orders/1001 was slow" — the template tells you where to look, the path tells you what to
20
+ * open.
21
+ */
22
+ /** Stands in for any id segment. Not a valid path segment itself, so it cannot collide with one. */
23
+ const ID = ':id';
24
+ /** 8+ hex characters: a token, a hash, a short uuid. Below that it is as likely to be a word. */
25
+ const LONG_HEX = /^[0-9a-f]{8,}$/i;
26
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
27
+ const ALL_DIGITS = /^\d+$/;
28
+ /**
29
+ * A year collapses too, and that is a deliberate trade rather than an oversight: `/reports/2024`
30
+ * and `/reports/2025` are the same page with different data, which is exactly what one envelope
31
+ * should describe. Distinguishing a year from an id needs to know the app, and guessing would
32
+ * reintroduce the split this exists to remove.
33
+ */
34
+ function isIdSegment(segment) {
35
+ return ALL_DIGITS.test(segment) || UUID.test(segment) || LONG_HEX.test(segment);
36
+ }
37
+ export function routeTemplate(pathname) {
38
+ if (0 === pathname.length)
39
+ return pathname;
40
+ return pathname
41
+ .split('/')
42
+ .map((segment) => (isIdSegment(segment) ? ID : segment))
43
+ .join('/');
44
+ }
@@ -3,6 +3,7 @@ import { type OnboardingStep } from '@reticlehq/core/telemetry';
3
3
  import type { AmbientCounts } from '@reticlehq/engine/window/ambient.js';
4
4
  import { type ProjectId, type JournalAction } from '@reticlehq/core';
5
5
  import type { FileSystemPort } from '../project/fs/fs-port.js';
6
+ import { type PruneWorkspaceOptions } from './on-disk/startup-maintenance.js';
6
7
  /**
7
8
  * Session teardown: the durable half of ending a session. Two things must happen when a tab disconnects,
8
9
  * and neither did before — the journal batches events and only writes at its flush threshold, so the tail
@@ -44,8 +45,10 @@ export interface SessionEndTarget {
44
45
  interface SessionEndDeps {
45
46
  fs: FileSystemPort;
46
47
  reticleRoot: string;
47
- /** Journaling/persistence off (opt-out) → teardown is a no-op. */
48
+ /** Journaling/persistence off (opt-out) → teardown is a no-op. Retention still runs. */
48
49
  enabled: boolean;
50
+ /** What this project is willing to keep — the `retain` block of its `.reticle.json`. */
51
+ retain?: PruneWorkspaceOptions;
49
52
  /**
50
53
  * The one clock in this file, injected — the run artifact stamps `createdAt` from it.
51
54
  *
@@ -1,8 +1,10 @@
1
1
  import { driveFlowsFrom } from './drive-flow.js';
2
+ import { journalActionsPath, sessionDirPath } from '../project/dir/reticle-dir.js';
3
+ import { asSessionId } from '@reticlehq/core';
2
4
  import { OnboardingPhase, OnboardingStepStatus, } from '@reticlehq/core/telemetry';
3
5
  import { AmbientStore } from './ambient-store.js';
4
6
  import { subjectOf } from '@reticlehq/core';
5
- import { pruneSessions } from './on-disk/retention.js';
7
+ import { pruneWorkspace } from './on-disk/startup-maintenance.js';
6
8
  import { buildVerificationRun } from '../../judgement/runs/artifact/build-verification-run.js';
7
9
  import { driveRunFrom, driveRunId } from '../../judgement/runs/drive-run.js';
8
10
  import { RunStore } from '../../judgement/runs/artifact/run-store.js';
@@ -13,8 +15,28 @@ import { RunStore } from '../../judgement/runs/artifact/run-store.js';
13
15
  */
14
16
  export function makeSessionEnd(deps) {
15
17
  return async (session) => {
16
- if (!deps.enabled)
18
+ /*
19
+ * Retention is maintenance of a directory, not a part of journalling, so it runs either way.
20
+ *
21
+ * Teardown used to return here, and this is the one setting under which that mattered most:
22
+ * somebody switches journalling off BECAUSE `.reticle/` got too big, and switching it off was
23
+ * what stopped anything ever deleting what was already there. It also stopped the sweep of
24
+ * visual diffs, feedback copies and run artifacts, none of which need the journal to be written
25
+ * at all. The daemon's own start-path sweep was gated on the same flag, so there was no second
26
+ * site still running — both are ungated now.
27
+ */
28
+ const sweep = async () => {
29
+ try {
30
+ await pruneWorkspace(deps.fs, session.artifactRoot ?? deps.reticleRoot, deps.liveSessionIds?.() ?? new Set(), deps.retain ?? {});
31
+ }
32
+ catch {
33
+ // retention is best-effort maintenance; never surface at teardown
34
+ }
35
+ };
36
+ if (!deps.enabled) {
37
+ await sweep();
17
38
  return;
39
+ }
18
40
  try {
19
41
  await session.flushJournal();
20
42
  }
@@ -101,13 +123,31 @@ export function makeSessionEnd(deps) {
101
123
  // The root this session actually journalled into. Pruning the daemon's tree instead meant a
102
124
  // per-project workspace was never swept at all, so the one place journals really accumulate
103
125
  // was the one place retention never ran.
104
- await pruneSessions(deps.fs, session.artifactRoot ?? deps.reticleRoot, {
105
- live: deps.liveSessionIds?.(),
106
- });
126
+ /*
127
+ * A session nobody ever drove keeps no journal.
128
+ *
129
+ * The observers run from the moment the SDK attaches, so an idle tab on a busy page
130
+ * accumulates megabytes without a single tool call. The cost is not the disk: those
131
+ * directories occupy retention SLOTS, so a journal that could answer a verdict question is
132
+ * evicted by one that was never asked one.
133
+ *
134
+ * Decided at the END and never by gating CAPTURE. Whether a tool call will happen is not
135
+ * knowable while the events that would answer it are being recorded, and gating capture on it
136
+ * would leave the first assertion of a session with nothing to read.
137
+ */
138
+ await dropUndrivenJournal(deps.fs, session.artifactRoot ?? deps.reticleRoot, asSessionId(session.id));
107
139
  }
108
140
  catch {
109
- // retention is best-effort maintenance; never surface at teardown
141
+ // teardown must never throw: the tab is already gone
110
142
  }
143
+ // EVERY tier, not just sessions. Visual diffs and feedback copies were pruned only at daemon
144
+ // start, against the DAEMON's root — which for a globally registered daemon is `$HOME` and not
145
+ // the project at all, so the two tiers that only ever grow in a project workspace were the two
146
+ // never swept there. Same defect as the one this line already fixed for sessions, one move
147
+ // behind. The byte budget rides along for the same reason: it was wired at daemon start too.
148
+ //
149
+ // Outside the try above, so a failure anywhere in teardown still leaves the directory swept.
150
+ await sweep();
111
151
  };
112
152
  }
113
153
  /**
@@ -177,3 +217,21 @@ async function saveDrivenFlow(deps, session) {
177
217
  ...(outcome.unprovenSteps === undefined ? {} : { reason: 'no_declared_consequence' }),
178
218
  });
179
219
  }
220
+ /**
221
+ * Remove this session's journal when it served no tool call.
222
+ *
223
+ * "Served no tool call" is read off the actions ledger, which is written one line per call: absent
224
+ * or empty means nothing was ever driven here. Best-effort like every other maintenance step —
225
+ * a session that cannot be tidied is not a session that failed.
226
+ */
227
+ async function dropUndrivenJournal(fs, root, sessionId) {
228
+ try {
229
+ const actions = await fs.readFile(journalActionsPath(root, sessionId)).catch(() => '');
230
+ if (actions.trim().length > 0)
231
+ return;
232
+ await fs.rm(sessionDirPath(root, sessionId));
233
+ }
234
+ catch {
235
+ // tidying is never the reason a teardown fails
236
+ }
237
+ }
@@ -1,6 +1,6 @@
1
1
  import { type JournalAction, type JournalWriteLoss, type ReticleEvent } from '@reticlehq/core';
2
2
  import type { FileSystemPort } from '../project/fs/fs-port.js';
3
- import type { JournalReadLoss } from './journal-recorder.js';
3
+ import type { JournalLedgerSize, JournalReadLoss } from './journal-recorder.js';
4
4
  /**
5
5
  * The two ceilings a durable read is held to.
6
6
  *
@@ -115,6 +115,13 @@ export declare class SessionJournal {
115
115
  * file, as opposed to what a bounded read declined to hand back.
116
116
  */
117
117
  readWriteLoss(): Promise<JournalWriteLoss | undefined>;
118
+ /**
119
+ * The ledger's size against its ceiling, once the size is known.
120
+ *
121
+ * `undefined` before the first append, because `#eventBytes` is seeded from `stat` at that point
122
+ * and a zero reported before it is measured would be a claim rather than a reading.
123
+ */
124
+ ledger(): JournalLedgerSize | undefined;
118
125
  appendAction(action: JournalAction): Promise<void>;
119
126
  readEvents(): Promise<ReticleEvent[]>;
120
127
  /**
@@ -281,6 +281,16 @@ export class SessionJournal {
281
281
  async readWriteLoss() {
282
282
  return this.#closureReport();
283
283
  }
284
+ /**
285
+ * The ledger's size against its ceiling, once the size is known.
286
+ *
287
+ * `undefined` before the first append, because `#eventBytes` is seeded from `stat` at that point
288
+ * and a zero reported before it is measured would be a claim rather than a reading.
289
+ */
290
+ ledger() {
291
+ const bytes = this.#eventBytes;
292
+ return bytes === undefined ? undefined : { bytes, capBytes: this.#eventBytesCap };
293
+ }
284
294
  /**
285
295
  * Close the ledger for good, and declare the loss twice — durably, then in-band.
286
296
  *
@@ -1,4 +1,4 @@
1
- export interface Baseline {
1
+ interface Baseline {
2
2
  name: string;
3
3
  lines: string[];
4
4
  route: string;
@@ -15,7 +15,7 @@ export interface ReticleDirPaths {
15
15
  project: string;
16
16
  /**.../.reticle/impact.json (what Reticle has done for this user, local only) */
17
17
  impact: string;
18
- /**.../.reticle/intent.json (what changes were supposed to make true — git-checked) */
18
+ /**.../.reticle/intent.json — the LEGACY single ledger, read until the first write migrates it into .reticle/intent/ */
19
19
  intent: string;
20
20
  /**.../.reticle/fill-values.json (what a drive types into each labelled field — git-checked) */
21
21
  fillValues: string;