@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
@@ -29,6 +29,18 @@ declare const CapsuleSchema: z.ZodObject<{
29
29
  observed: z.ZodString;
30
30
  /** Minimal steps to reproduce (prefix-trimmed). */
31
31
  steps: z.ZodArray<z.ZodType<FlowStep, z.ZodTypeDef, FlowStep>, "many">;
32
+ /**
33
+ * How many times this exact failure has been captured, and when it last was.
34
+ *
35
+ * A repeat is not a new capsule -- it is the same one, again. "This failed 37 times, most
36
+ * recently at T" is strictly MORE than 37 files each holding one timestamp, so folding the
37
+ * duplicates in adds information rather than discarding it.
38
+ *
39
+ * Optional, because a capsule written before these existed has neither and must not be read as
40
+ * though it were seen once when nobody counted.
41
+ */
42
+ seen: z.ZodOptional<z.ZodNumber>;
43
+ lastSeenAt: z.ZodOptional<z.ZodNumber>;
32
44
  }, "strip", z.ZodTypeAny, {
33
45
  expected: string;
34
46
  origin: string;
@@ -38,6 +50,8 @@ declare const CapsuleSchema: z.ZodObject<{
38
50
  createdAt: number;
39
51
  observed: string;
40
52
  flow?: string | undefined;
53
+ seen?: number | undefined;
54
+ lastSeenAt?: number | undefined;
41
55
  }, {
42
56
  expected: string;
43
57
  origin: string;
@@ -47,6 +61,8 @@ declare const CapsuleSchema: z.ZodObject<{
47
61
  createdAt: number;
48
62
  observed: string;
49
63
  flow?: string | undefined;
64
+ seen?: number | undefined;
65
+ lastSeenAt?: number | undefined;
50
66
  }>;
51
67
  export type Capsule = z.infer<typeof CapsuleSchema>;
52
68
  /** Capsule ids are filenames — refuse anything that could escape the capsules directory. */
@@ -63,8 +79,21 @@ export declare class CapsuleStore {
63
79
  /** Every readable capsule, newest-first — what `reticle capsules` prints. */
64
80
  all(): Promise<Capsule[]>;
65
81
  }
66
- /** Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free. */
67
- export declare function capsuleId(now: number, label: string): string;
82
+ /**
83
+ * What makes two captures the SAME failure.
84
+ *
85
+ * Everything that describes the failure, and nothing that describes the capture: `createdAt` is
86
+ * excluded because it is precisely what turned every retry of a broken step into a new file, and
87
+ * `id` because it is derived from this. `flow` is included -- the same assertion failing in two
88
+ * different journeys is two findings, not one.
89
+ */
90
+ export declare function capsuleFingerprint(capsule: Omit<Capsule, 'id' | 'createdAt' | 'seen' | 'lastSeenAt'>): string;
91
+ /**
92
+ * Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free, with the
93
+ * fingerprint on the END so a duplicate can be found by listing the directory rather than reading
94
+ * every file in it.
95
+ */
96
+ export declare function capsuleId(now: number, label: string, fingerprint: string): string;
68
97
  /** The minimal reproduction: the steps up to and including the one that failed (prefix trim). */
69
98
  export declare function minimalSteps(steps: readonly FlowStep[], failedIndex: number): FlowStep[];
70
99
  export {};
@@ -1,4 +1,6 @@
1
1
  import { join } from 'node:path';
2
+ import { fnv1a } from '@reticlehq/core';
3
+ import { writeFileAtomic } from '../../memory/project/fs/write-atomic.js';
2
4
  import { z } from 'zod';
3
5
  import { FlowStepSchema } from '@reticlehq/core';
4
6
  import { reticleDirPaths } from '../../memory/project/dir/reticle-dir.js';
@@ -30,6 +32,18 @@ const CapsuleSchema = z.object({
30
32
  observed: z.string(),
31
33
  /** Minimal steps to reproduce (prefix-trimmed). */
32
34
  steps: z.array(FlowStepSchema),
35
+ /**
36
+ * How many times this exact failure has been captured, and when it last was.
37
+ *
38
+ * A repeat is not a new capsule -- it is the same one, again. "This failed 37 times, most
39
+ * recently at T" is strictly MORE than 37 files each holding one timestamp, so folding the
40
+ * duplicates in adds information rather than discarding it.
41
+ *
42
+ * Optional, because a capsule written before these existed has neither and must not be read as
43
+ * though it were seen once when nobody counted.
44
+ */
45
+ seen: z.number().int().positive().optional(),
46
+ lastSeenAt: z.number().optional(),
33
47
  });
34
48
  /** Capsule ids are filenames — refuse anything that could escape the capsules directory. */
35
49
  export function isValidCapsuleId(id) {
@@ -51,9 +65,26 @@ export class CapsuleStore {
51
65
  return false;
52
66
  try {
53
67
  await this.#fs.mkdir(this.#dir);
54
- const tmp = `${this.#pathFor(capsule.id)}.tmp`;
55
- await this.#fs.writeFile(tmp, `${JSON.stringify(capsule, null, 2)}\n`);
56
- await this.#fs.rename(tmp, this.#pathFor(capsule.id));
68
+ /*
69
+ * A capture of a failure already on disk folds into it rather than adding a file. Found by
70
+ * LISTING: the fingerprint is the id's suffix, so this costs one directory read instead of
71
+ * one read per capsule. An id written before fingerprints existed simply never matches, which
72
+ * is the correct outcome -- it is left exactly as it was.
73
+ *
74
+ * `createdAt` keeps the FIRST sighting. A repeat is the same failure happening again, not a
75
+ * new one, and the interesting pair is when it started and when it last happened.
76
+ */
77
+ const fingerprint = capsuleFingerprint(capsule);
78
+ const existingId = (await this.list()).find((id) => id.endsWith(`-${fingerprint}`));
79
+ const previous = existingId === undefined ? undefined : await this.read(existingId);
80
+ const folded = previous === undefined
81
+ ? { ...capsule, seen: 1, lastSeenAt: capsule.createdAt }
82
+ : {
83
+ ...previous,
84
+ seen: (previous.seen ?? 1) + 1,
85
+ lastSeenAt: capsule.createdAt,
86
+ };
87
+ await writeFileAtomic(this.#fs, this.#pathFor(folded.id), `${JSON.stringify(folded, null, 2)}\n`);
57
88
  return true;
58
89
  }
59
90
  catch {
@@ -100,10 +131,31 @@ export class CapsuleStore {
100
131
  return out;
101
132
  }
102
133
  }
103
- /** Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free. */
104
- export function capsuleId(now, label) {
134
+ /**
135
+ * What makes two captures the SAME failure.
136
+ *
137
+ * Everything that describes the failure, and nothing that describes the capture: `createdAt` is
138
+ * excluded because it is precisely what turned every retry of a broken step into a new file, and
139
+ * `id` because it is derived from this. `flow` is included -- the same assertion failing in two
140
+ * different journeys is two findings, not one.
141
+ */
142
+ export function capsuleFingerprint(capsule) {
143
+ return fnv1a(JSON.stringify([
144
+ capsule.flow ?? '',
145
+ capsule.origin,
146
+ capsule.expected,
147
+ capsule.observed,
148
+ capsule.steps,
149
+ ]));
150
+ }
151
+ /**
152
+ * Deterministic, path-safe capsule id: time-ordered so `list` sorts newest-first for free, with the
153
+ * fingerprint on the END so a duplicate can be found by listing the directory rather than reading
154
+ * every file in it.
155
+ */
156
+ export function capsuleId(now, label, fingerprint) {
105
157
  const safe = label.replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 40);
106
- return `${String(now)}-${safe.length > 0 ? safe : 'capsule'}`;
158
+ return `${String(now)}-${safe.length > 0 ? safe : 'capsule'}-${fingerprint}`;
107
159
  }
108
160
  /** The minimal reproduction: the steps up to and including the one that failed (prefix trim). */
109
161
  export function minimalSteps(steps, failedIndex) {
@@ -1,4 +1,5 @@
1
1
  import { ConsequenceKind, EventType } from '@reticlehq/core';
2
+ import { isDocumentInitiated } from '@reticlehq/engine/question/predicate/predicate-eval.js';
2
3
  import { causalSummary } from './causal-summary.js';
3
4
  import { firstDivergence } from './divergence.js';
4
5
  /**
@@ -34,6 +35,10 @@ export function blastRadius(expected, observed) {
34
35
  continue;
35
36
  if (declaredUrls.some((fragment) => url.includes(fragment)))
36
37
  continue;
38
+ // A subresource the document fetched is the page loading, not a request the action sent: one
39
+ // navigation is a couple of hundred of them. IPC and beacons carry an initiator too, and stay.
40
+ if (isDocumentInitiated(event))
41
+ continue;
37
42
  const method = event.data['method'];
38
43
  add(`net ${'string' === typeof method ? method : 'request'} ${url}`);
39
44
  continue;
@@ -63,6 +63,18 @@ export interface CausalSummary {
63
63
  elided?: Partial<Record<CappedList, number>>;
64
64
  route?: string;
65
65
  signals: string[];
66
+ /**
67
+ * Form fields whose value moved in this window, by NAME.
68
+ *
69
+ * Names only, never values: a form is where somebody types their password, and the observer that
70
+ * reports this already refuses to carry one. The names are what an assertion is written against,
71
+ * and a field moving when nothing should have touched it (or not moving when something should)
72
+ * is the silently-cleared-field class — which until this channel existed left no trace at all,
73
+ * because React sets the input PROPERTY and MutationObserver watches attributes.
74
+ *
75
+ * Present only when a field moved, so a page with no form pays nothing.
76
+ */
77
+ fieldsChanged?: string[];
66
78
  layoutShift?: number;
67
79
  longTasks: number;
68
80
  /**
@@ -114,6 +126,6 @@ interface SummaryContext {
114
126
  */
115
127
  export declare const MAX_SUMMARY_ENTRIES = 20;
116
128
  /** The lists that are capped, named so `elided` can say which one lost entries. */
117
- type CappedList = 'statePathsChanged' | 'storageKeysChanged' | 'stateDiffs' | 'storageDiffs' | 'signals';
129
+ type CappedList = 'statePathsChanged' | 'storageKeysChanged' | 'stateDiffs' | 'storageDiffs' | 'signals' | 'fieldsChanged';
118
130
  export declare function causalSummary(events: readonly ReticleEvent[], context?: SummaryContext): CausalSummary;
119
131
  export {};
@@ -69,6 +69,7 @@ export function causalSummary(events, context = {}) {
69
69
  const storageDiffs = [];
70
70
  let route;
71
71
  const signals = [];
72
+ const fieldsChanged = [];
72
73
  let layoutShift;
73
74
  let longTasks = 0;
74
75
  for (const event of events) {
@@ -131,6 +132,9 @@ export function causalSummary(events, context = {}) {
131
132
  case EventType.SIGNAL:
132
133
  pushUnique(signals, data['name']);
133
134
  break;
135
+ case EventType.FIELD_CHANGE:
136
+ pushUnique(fieldsChanged, data['field']);
137
+ break;
134
138
  case EventType.PERF:
135
139
  if (data['metric'] === PerfMetric.CLS && 'number' === typeof data['value']) {
136
140
  layoutShift = Math.max(layoutShift ?? 0, data['value']);
@@ -163,6 +167,9 @@ export function causalSummary(events, context = {}) {
163
167
  storageDiffs: cap(storageDiffs, 'storageDiffs', elided),
164
168
  ...(route === undefined ? {} : { route }),
165
169
  signals: cap(signals, 'signals', elided),
170
+ ...(0 === fieldsChanged.length
171
+ ? {}
172
+ : { fieldsChanged: cap(fieldsChanged, 'fieldsChanged', elided) }),
166
173
  ...(0 === Object.keys(elided).length ? {} : { elided }),
167
174
  ...(layoutShift === undefined ? {} : { layoutShift }),
168
175
  longTasks,
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * Pure: no IO, no clock. The tool layer loads flows + contract and calls this.
14
14
  */
15
- import { AnchorKind, } from '@reticlehq/core';
15
+ import { AnchorKind, PredicateKind, } from '@reticlehq/core';
16
16
  import { classifyFlowAssertions, FlowAssertionGrade } from '../../language/flows/flow-classify.js';
17
17
  import { successLabel } from '../../language/flows/flow-success.js';
18
18
  import { flowRisk, latestRun, rankByRisk, RiskLevel } from './flow-risk.js';
@@ -25,16 +25,45 @@ function flatten(steps) {
25
25
  }
26
26
  return out;
27
27
  }
28
+ /*
29
+ * The vocabulary a flow uses, read out of a predicate TREE.
30
+ *
31
+ * These were property reads when `expect` was a struct with one slot per kind. A predicate nests, and
32
+ * the domain model is about what a flow TALKS ABOUT, so the walk goes all the way down: a signal
33
+ * named inside an `allOf` two levels deep is still a signal this flow depends on, and missing it
34
+ * would quietly shrink the model that decides which flows a change affects.
35
+ */
36
+ function walkClauses(predicate) {
37
+ if (predicate === undefined)
38
+ return [];
39
+ if (PredicateKind.ALL_OF === predicate.kind || PredicateKind.ANY_OF === predicate.kind) {
40
+ return predicate.predicates.flatMap(walkClauses);
41
+ }
42
+ if (PredicateKind.NOT === predicate.kind)
43
+ return walkClauses(predicate.predicate);
44
+ return [predicate];
45
+ }
46
+ function signalNamesIn(predicate) {
47
+ return walkClauses(predicate)
48
+ .filter((c) => PredicateKind.SIGNAL === c.kind)
49
+ .map((c) => (PredicateKind.SIGNAL === c.kind ? c.name : undefined))
50
+ .filter((name) => name !== undefined);
51
+ }
52
+ function elementTestidsIn(predicate) {
53
+ return walkClauses(predicate)
54
+ .map((c) => (PredicateKind.ELEMENT === c.kind ? c.query.testid : undefined))
55
+ .filter((testid) => testid !== undefined);
56
+ }
28
57
  function flowSignals(flow) {
29
58
  const set = new Set();
30
59
  for (const step of flatten(flow.steps)) {
31
60
  if (step.anchor.kind === AnchorKind.SIGNAL)
32
61
  set.add(step.anchor.name);
33
- if (step.expect?.signal !== undefined)
34
- set.add(step.expect.signal);
62
+ for (const name of signalNamesIn(step.expect))
63
+ set.add(name);
35
64
  }
36
- if (flow.success?.signal !== undefined)
37
- set.add(flow.success.signal);
65
+ for (const name of signalNamesIn(flow.success))
66
+ set.add(name);
38
67
  return [...set];
39
68
  }
40
69
  function flowTestids(flow) {
@@ -42,11 +71,11 @@ function flowTestids(flow) {
42
71
  for (const step of flatten(flow.steps)) {
43
72
  if (step.anchor.kind === AnchorKind.TESTID)
44
73
  set.add(step.anchor.value);
45
- if (step.expect?.element?.testid !== undefined)
46
- set.add(step.expect.element.testid);
74
+ for (const testid of elementTestidsIn(step.expect))
75
+ set.add(testid);
47
76
  }
48
- if (flow.success?.element?.testid !== undefined)
49
- set.add(flow.success.element.testid);
77
+ for (const testid of elementTestidsIn(flow.success))
78
+ set.add(testid);
50
79
  return [...set];
51
80
  }
52
81
  const EMPTY_CONTRACT = { testids: [], signals: [], stores: [], flows: [] };
@@ -1,4 +1,4 @@
1
- import { type FlowExpect } from '@reticlehq/core';
1
+ import { type Predicate } from '@reticlehq/core';
2
2
  /**
3
3
  * Anti-reward-hacking (research-mandated). Test-gated agents pass gates by WEAKENING assertions — the
4
4
  * canonical documented failure. Flows are git-checked and agent-editable, so a mustHold that dropped
@@ -7,10 +7,10 @@ import { type FlowExpect } from '@reticlehq/core';
7
7
  * and flow_verify surface it.
8
8
  */
9
9
  /** True when `after` weakened `before` — a real consequence became a fakeable presence-only check. */
10
- export declare function isAssertionDowngrade(before: FlowExpect | undefined, after: FlowExpect | undefined): boolean;
10
+ export declare function isAssertionDowngrade(before: Predicate | undefined, after: Predicate | undefined): boolean;
11
11
  export interface StepExpect {
12
12
  step: number;
13
- expect?: FlowExpect;
13
+ expect?: Predicate;
14
14
  }
15
15
  interface DowngradeFinding {
16
16
  step: number;
@@ -4,7 +4,7 @@ import { PhenomenonType, type JournalAction, type ReticleEvent } from '@reticleh
4
4
  * scanning by the agent: the layer names the fault, the agent acts on the name. `pre-hydration-click`
5
5
  * lands once the hydration signal (a later item) exists; the three here need only journal data.
6
6
  */
7
- export interface Finding {
7
+ interface Finding {
8
8
  phenomenon: PhenomenonType;
9
9
  evidence: Record<string, unknown>;
10
10
  }
@@ -24,3 +24,4 @@ export declare function detectPreHydrationClicks(events: readonly ReticleEvent[]
24
24
  export declare function detectSwallowedErrors(events: readonly ReticleEvent[]): Finding[];
25
25
  /** Run every journal-only matcher over a session's events + actions. */
26
26
  export declare function detectPhenomena(events: readonly ReticleEvent[], actions: readonly JournalAction[]): Finding[];
27
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The session gap, as a person reads it — and as a Stop hook prints it.
3
+ *
4
+ * `gapSummary` folds the journal into what was claimed and what held. This is its only renderer, so
5
+ * `reticle report` and the hook cannot describe the same session two different ways.
6
+ */
7
+ import type { GapSummary } from './gap-summary.js';
8
+ /**
9
+ * One line when the agent finishes without a single `yes`, and nothing otherwise.
10
+ *
11
+ * Silent after a `yes` on purpose: a hook that speaks on every stop is a hook people turn off, and the
12
+ * question it exists to raise is "did anything get proved at all". Every answer that is not a yes is
13
+ * counted by name — an `unknown` is never folded into anything, because that fold is the false green.
14
+ */
15
+ export declare function gapHookLine(gap: GapSummary): string | undefined;
16
+ /** The whole gap: held of claimed first, then each kind of not-held with who can act on it. */
17
+ export declare function gapReportLines(gap: GapSummary): string[];
@@ -0,0 +1,46 @@
1
+ const HOOK_PREFIX = 'Reticle: not verified.';
2
+ const NO_CLAIM_HOOK = `${HOOK_PREFIX} No claim was checked this session`;
3
+ const NO_CLAIM_REPORT = 'no claims this session, so nothing was verified';
4
+ const plural = (n, word) => `${String(n)} ${word}${1 === n ? '' : 's'}`;
5
+ /**
6
+ * One line when the agent finishes without a single `yes`, and nothing otherwise.
7
+ *
8
+ * Silent after a `yes` on purpose: a hook that speaks on every stop is a hook people turn off, and the
9
+ * question it exists to raise is "did anything get proved at all". Every answer that is not a yes is
10
+ * counted by name — an `unknown` is never folded into anything, because that fold is the false green.
11
+ */
12
+ export function gapHookLine(gap) {
13
+ if (0 < gap.held)
14
+ return undefined;
15
+ if (0 === gap.claims)
16
+ return NO_CLAIM_HOOK;
17
+ const parts = [
18
+ 0 < gap.failed ? `${String(gap.failed)} failed` : undefined,
19
+ 0 < gap.undecided ? `${String(gap.undecided)} unknown` : undefined,
20
+ 0 < gap.nothingToProve ? `${String(gap.nothingToProve)} proved nothing` : undefined,
21
+ ].filter((part) => part !== undefined);
22
+ return `${HOOK_PREFIX} ${plural(gap.claims, 'claim')}: ${parts.join(', ')}`;
23
+ }
24
+ /** The whole gap: held of claimed first, then each kind of not-held with who can act on it. */
25
+ export function gapReportLines(gap) {
26
+ if (0 === gap.claims)
27
+ return [NO_CLAIM_REPORT];
28
+ const lines = [`${String(gap.held)} of ${plural(gap.claims, 'claim')} held`];
29
+ if (0 < gap.failed) {
30
+ const caught = 0 < gap.falseGreensCaught
31
+ ? `, ${String(gap.falseGreensCaught)} of them a green a channel contradicted`
32
+ : '';
33
+ lines.push(`${String(gap.failed)} failed${caught}`);
34
+ }
35
+ if (0 < gap.undecided) {
36
+ const owners = Object.entries(gap.undecidedBy)
37
+ .map(([owner, n]) => `${owner}: ${String(n)}`)
38
+ .join(', ');
39
+ lines.push(`${String(gap.undecided)} unknown${'' === owners ? '' : ` (${owners})`}`);
40
+ }
41
+ if (0 < gap.nothingToProve)
42
+ lines.push(`${String(gap.nothingToProve)} proved nothing`);
43
+ for (const failure of gap.failures)
44
+ lines.push(` ✗ ${failure.claim}`);
45
+ return lines;
46
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The gap, as a number the person who ran the session can see.
3
+ *
4
+ * Reticle's whole claim is that it measures the distance between what an agent believed happened
5
+ * and what did. It measures that on every verdict, writes the answer into the journal, and shows it
6
+ * to nobody: the counts exist, the reasons exist, the contradictions exist, and no surface anywhere
7
+ * says "you made 14 claims, 9 held, 2 were false, 3 could not be decided, and here is who owns each
8
+ * of those three". The value of the product is the one thing it never reports.
9
+ *
10
+ * A FOLD over the journal, never a second store — the rule `run-context.ts` next door states, for
11
+ * the same reason: two ways to compute one number is worse than one way, because the day they
12
+ * disagree the disagreement lands inside a verdict and a user finds it before we do.
13
+ *
14
+ * Nothing here sends anything anywhere, and that is deliberate rather than incidental. This is the
15
+ * LOCAL half. Whether a fleet-wide version of the same number is worth collecting is a separate
16
+ * decision with a separate owner, and building the local one first is what makes that decision
17
+ * answerable from something other than a guess.
18
+ */
19
+ import { VerifiedReason, VerdictAttribution, type JournalAction } from '@reticlehq/core';
20
+ /** One claim that did not hold, in the words it was made and the engine's reason for refusing it. */
21
+ export interface GapFailure {
22
+ claim: string;
23
+ reason?: VerifiedReason;
24
+ }
25
+ export interface GapSummary {
26
+ /** Actions that asserted something. An action that only drove the app is not a claim. */
27
+ claims: number;
28
+ held: number;
29
+ failed: number;
30
+ /** `unknown`: Reticle could not tell. Never folded into held or failed — that is the false green. */
31
+ undecided: number;
32
+ /**
33
+ * `no-fault`: nothing was declared, so nothing was checked.
34
+ *
35
+ * The fourth answer, kept apart from the other three. Folding it anywhere is how a session that
36
+ * proved nothing reads as a session that proved something.
37
+ */
38
+ nothingToProve: number;
39
+ /**
40
+ * WHO can act on each undecided verdict, counted per owner.
41
+ *
42
+ * The only part of an `unknown` a reader can do anything with: the four owners imply four
43
+ * different next moves — wait, fix the app, fix the harness, look again. Empty when nothing was
44
+ * undecided, and a verdict that recorded no reason contributes to no owner rather than to a
45
+ * guessed one.
46
+ */
47
+ undecidedBy: Partial<Record<VerdictAttribution, number>>;
48
+ /**
49
+ * Failures where a CHANNEL disagreed with what the app showed.
50
+ *
51
+ * Counted apart from an ordinary failure because they mean different things to the reader: an
52
+ * assertion that failed is a check doing its job, and a contradiction is a green that would have
53
+ * been believed. This number is the product's own headline, measured on the reader's own session.
54
+ */
55
+ falseGreensCaught: number;
56
+ failures: GapFailure[];
57
+ }
58
+ export declare function gapSummary(actions: readonly JournalAction[]): GapSummary;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The gap, as a number the person who ran the session can see.
3
+ *
4
+ * Reticle's whole claim is that it measures the distance between what an agent believed happened
5
+ * and what did. It measures that on every verdict, writes the answer into the journal, and shows it
6
+ * to nobody: the counts exist, the reasons exist, the contradictions exist, and no surface anywhere
7
+ * says "you made 14 claims, 9 held, 2 were false, 3 could not be decided, and here is who owns each
8
+ * of those three". The value of the product is the one thing it never reports.
9
+ *
10
+ * A FOLD over the journal, never a second store — the rule `run-context.ts` next door states, for
11
+ * the same reason: two ways to compute one number is worse than one way, because the day they
12
+ * disagree the disagreement lands inside a verdict and a user finds it before we do.
13
+ *
14
+ * Nothing here sends anything anywhere, and that is deliberate rather than incidental. This is the
15
+ * LOCAL half. Whether a fleet-wide version of the same number is worth collecting is a separate
16
+ * decision with a separate owner, and building the local one first is what makes that decision
17
+ * answerable from something other than a guess.
18
+ */
19
+ import { JournalVerdictEffectSchema, Verified, VerifiedReason, VerdictAttribution, verdictAttributionOf, } from '@reticlehq/core';
20
+ /**
21
+ * The reason, as the engine's own enum, or undefined.
22
+ *
23
+ * The journal stores `reason` as a free string rather than the enum — deliberately, because the
24
+ * ledger has to keep reading records written by an older engine whose vocabulary it cannot know.
25
+ * So a reason this build does not recognise is treated exactly like an absent one: no owner. The
26
+ * alternative is attributing an unknown word to one of four buckets, which is inventing an
27
+ * attribution nobody recorded.
28
+ */
29
+ function knownReason(reason) {
30
+ if (reason === undefined)
31
+ return undefined;
32
+ const known = Object.values(VerifiedReason);
33
+ return known.includes(reason) ? reason : undefined;
34
+ }
35
+ /** The verdict a verification tool recorded on this action, if it recorded one. */
36
+ function verdictOf(action) {
37
+ const parsed = JournalVerdictEffectSchema.safeParse(action.effect);
38
+ return parsed.success ? parsed.data : undefined;
39
+ }
40
+ export function gapSummary(actions) {
41
+ const summary = {
42
+ claims: 0,
43
+ held: 0,
44
+ failed: 0,
45
+ undecided: 0,
46
+ nothingToProve: 0,
47
+ undecidedBy: {},
48
+ falseGreensCaught: 0,
49
+ failures: [],
50
+ };
51
+ for (const action of actions) {
52
+ const verdict = verdictOf(action);
53
+ if (verdict === undefined)
54
+ continue;
55
+ summary.claims += 1;
56
+ switch (verdict.verified) {
57
+ case Verified.YES:
58
+ summary.held += 1;
59
+ break;
60
+ case Verified.NO: {
61
+ summary.failed += 1;
62
+ if (VerifiedReason.CONTRADICTED === verdict.reason)
63
+ summary.falseGreensCaught += 1;
64
+ const reason = knownReason(verdict.reason);
65
+ summary.failures.push(reason === undefined ? { claim: verdict.claim } : { claim: verdict.claim, reason });
66
+ break;
67
+ }
68
+ case Verified.UNKNOWN: {
69
+ summary.undecided += 1;
70
+ // A record written before `reason` existed has none, and must not be read as though it did:
71
+ // counting it under an owner would invent an attribution nobody recorded.
72
+ const reason = knownReason(verdict.reason);
73
+ const owner = reason === undefined ? undefined : verdictAttributionOf(reason);
74
+ if (owner !== undefined && VerdictAttribution.NONE !== owner) {
75
+ summary.undecidedBy[owner] = (summary.undecidedBy[owner] ?? 0) + 1;
76
+ }
77
+ break;
78
+ }
79
+ case Verified.NO_FAULT:
80
+ summary.nothingToProve += 1;
81
+ break;
82
+ }
83
+ }
84
+ return summary;
85
+ }
@@ -35,7 +35,10 @@ const CHECK_GLYPH = {
35
35
  [Verified.NO_FAULT]: '–',
36
36
  };
37
37
  function checkLine(c) {
38
- return ` ${CHECK_GLYPH[c.status]} ${c.kind}: ${c.predicate}`;
38
+ // No kind, no prefix. A check folded from a journal that never recorded one has nothing to name
39
+ // here, and printing a placeholder would read as a kind.
40
+ const read = c.kind === undefined ? '' : `${c.kind}: `;
41
+ return ` ${CHECK_GLYPH[c.status]} ${read}${c.predicate}`;
39
42
  }
40
43
  function riskLine(r) {
41
44
  return ` ⚠ ${r.surface} (${r.severity})${r.gated ? ' [GATED]' : ''} — ${r.detail}`;
@@ -22,8 +22,13 @@ export declare const OVP_ARTIFACT_VERSION = 1;
22
22
  export interface ExportedCheck {
23
23
  /** What was claimed, as the person or agent wrote it. */
24
24
  readonly claim: string;
25
- /** Which source of truth answering it required. */
26
- readonly reads: PredicateKind;
25
+ /**
26
+ * Which source of truth answering it required.
27
+ *
28
+ * Absent when the run did not record one -- which is the same rule the rest of this file follows:
29
+ * a kind nobody wrote down is left out, never filled in with a plausible one.
30
+ */
31
+ readonly reads?: PredicateKind;
27
32
  /**
28
33
  * Was the claim written down before the action, or after it?
29
34
  *
@@ -31,7 +31,7 @@ function exportedChecks(run) {
31
31
  for (const check of run.checks) {
32
32
  decided.push({
33
33
  claim: check.predicate,
34
- reads: check.kind,
34
+ ...(check.kind === undefined ? {} : { reads: check.kind }),
35
35
  verdict: check.status,
36
36
  // Spread, so a value the run did not record leaves the key out rather than writing a
37
37
  // comfortable default. `declaredBeforeActing: false` says the claim came afterwards; absent
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import {} from '@reticlehq/core';
3
3
  import { runContextFor } from './artifact/run-context.js';
4
+ import { gapSummary } from './artifact/gap-summary.js';
4
5
  import { openSessionIntents } from '../../memory/intent/open-intents.js';
5
6
  import { ReticleTool } from '@reticlehq/core';
6
7
  import { sessionIdShape } from '../../surface/tools/tool-kit.js';
@@ -27,9 +28,13 @@ const CONTEXT_OUTPUT_SCHEMA = {
27
28
  proven: z
28
29
  .array(z.unknown())
29
30
  .describe('Claims a verdict already settled, each `{ claim, verified, source?, doc?, epoch? }`. Read this before re-driving something: re-proving it is slow, and assuming it is a false green.'),
31
+ gap: z
32
+ .object({})
33
+ .passthrough()
34
+ .describe('The distance between what was claimed and what held, over THIS session: `{ claims, held, failed, undecided, nothingToProve, undecidedBy, falseGreensCaught, failures }`. `undecidedBy` says WHO can act on each unknown — environment (wait), code (fix the app), harness (fix Reticle or the call), could-not-see (look again). Folded from the same journal as everything else here, so it cannot disagree with it.'),
30
35
  remaining: z
31
36
  .array(z.string())
32
- .describe('The intents from .reticle/intent.json that no verdict has discharged. Derived from the ledger, never a guess at what you meant to do next.'),
37
+ .describe('The intents from .reticle/intent/ that no verdict has discharged. Derived from the ledger, never a guess at what you meant to do next.'),
33
38
  };
34
39
  /** The ledger and the live page state this run is folded out of. `[]`/undefined when nothing is connected. */
35
40
  async function evidenceFor(deps, sessionId) {
@@ -57,14 +62,27 @@ async function evidenceFor(deps, sessionId) {
57
62
  export const CONTEXT_TOOLS = [
58
63
  {
59
64
  name: ReticleTool.CONTEXT,
60
- description: 'What THIS run has already established, so you do not rediscover it. Call it when your own copy is gone: right after a compaction, at the top of a fresh sub-agent, or at the start of a turn you did not begin. Returns `established` (what Reticle OBSERVED, with the source file:line where one was reported), `proven` (claims a verdict already settled, so you neither re-prove them nor assume them) and `remaining` (the intents from .reticle/intent.json that nothing has discharged). It is a FOLD over the journal, never a second store, so it cannot disagree with the ledger. Bounded and superseding rather than accumulating, and anything observed under a replaced document or before your last source edit is already dropped rather than presented as current. Only what Reticle observed goes in: it never reports what you intended or believed.',
65
+ description: 'What THIS run has already established, so you do not rediscover it. Call it when your own copy is gone: right after a compaction, at the top of a fresh sub-agent, or at the start of a turn you did not begin. Returns `established` (what Reticle OBSERVED, with the source file:line where one was reported), `proven` (claims a verdict already settled, so you neither re-prove them nor assume them) and `remaining` (the intents from .reticle/intent/ that nothing has discharged). It is a FOLD over the journal, never a second store, so it cannot disagree with the ledger. Bounded and superseding rather than accumulating, and anything observed under a replaced document or before your last source edit is already dropped rather than presented as current. Only what Reticle observed goes in: it never reports what you intended or believed.',
61
66
  example: {},
62
67
  inputSchema: { ...sessionIdShape },
63
68
  outputSchema: CONTEXT_OUTPUT_SCHEMA,
64
69
  handler: async (deps, args) => {
65
70
  const sessionId = asString(args['sessionId']);
66
71
  const evidence = await evidenceFor(deps, sessionId);
67
- return runContextFor({ ...evidence, intents: await openSessionIntents(deps, sessionId) });
72
+ const context = runContextFor({
73
+ ...evidence,
74
+ intents: await openSessionIntents(deps, sessionId),
75
+ });
76
+ /*
77
+ * The gap rides on `reticle_context` rather than on a tool of its own.
78
+ *
79
+ * This is the one call an agent makes when it needs to know what the run has already settled,
80
+ * which is exactly the moment "and what did NOT settle, and whose problem each of those is"
81
+ * is worth reading. It is folded from the same journal the rest of this response is folded
82
+ * from, so it costs one more pass over an array already in hand and cannot disagree with the
83
+ * `proven` list beside it.
84
+ */
85
+ return { ...context, gap: gapSummary(evidence.actions) };
68
86
  },
69
87
  },
70
88
  ];