@reticlehq/server 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/SKILL.md +73 -11
  2. package/dist/bridge/bridge.d.ts +0 -13
  3. package/dist/bridge/bridge.js +1 -1
  4. package/dist/bridge/bridge.test-harness.d.ts +2 -0
  5. package/dist/bridge/bridge.test-harness.js +8 -1
  6. package/dist/capsule/capsule-store.d.ts +2 -1
  7. package/dist/capsule/capsule-store.js +1 -1
  8. package/dist/capsule/causal-summary.d.ts +2 -2
  9. package/dist/chromium-launch-options.d.ts +28 -0
  10. package/dist/chromium-launch-options.js +29 -0
  11. package/dist/cli/cli-doctor.js +8 -0
  12. package/dist/cli/cli-flow-commands.d.ts +3 -1
  13. package/dist/cli/cli-flow-commands.js +34 -4
  14. package/dist/cli/cli-kill.d.ts +2 -1
  15. package/dist/cli/cli-parse.d.ts +3 -1
  16. package/dist/cli/cli-parse.js +29 -7
  17. package/dist/cli/cli-update-commands.js +20 -1
  18. package/dist/cli/cli-verify.d.ts +1 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/cloud-cli.js +12 -6
  21. package/dist/cli/config-discovery.d.ts +2 -1
  22. package/dist/cli/doctor-rows.d.ts +3 -1
  23. package/dist/cli/doctor-rows.js +3 -1
  24. package/dist/cli/gate-exit.d.ts +21 -0
  25. package/dist/cli/gate-exit.js +21 -0
  26. package/dist/cli/gate-hook-message.d.ts +33 -0
  27. package/dist/cli/gate-hook-message.js +46 -0
  28. package/dist/cli/mcp-command.d.ts +25 -0
  29. package/dist/cli/mcp-command.js +143 -0
  30. package/dist/cli/sibling-ports.d.ts +37 -0
  31. package/dist/cli/sibling-ports.js +56 -0
  32. package/dist/cli.js +37 -110
  33. package/dist/cloud/cloud-config.d.ts +2 -1
  34. package/dist/cloud/cloud-sync.d.ts +11 -0
  35. package/dist/cloud/cloud-sync.js +32 -0
  36. package/dist/crawl/crawl.d.ts +9 -13
  37. package/dist/crawl/crawl.js +20 -3
  38. package/dist/daemon/daemon-resolve.d.ts +79 -0
  39. package/dist/daemon/daemon-resolve.js +118 -0
  40. package/dist/daemon/free-port.d.ts +29 -0
  41. package/dist/daemon/free-port.js +55 -0
  42. package/dist/daemon/heartbeat.d.ts +0 -4
  43. package/dist/daemon/heartbeat.js +2 -2
  44. package/dist/dev/stale-issue-guard.d.ts +41 -0
  45. package/dist/dev/stale-issue-guard.js +75 -0
  46. package/dist/domain/domain-tools.js +3 -2
  47. package/dist/events/contradictions.d.ts +41 -0
  48. package/dist/events/contradictions.js +125 -12
  49. package/dist/events/declared.d.ts +14 -0
  50. package/dist/events/declared.js +33 -0
  51. package/dist/events/echo-mismatch.js +14 -0
  52. package/dist/events/observed-in-window.d.ts +12 -0
  53. package/dist/events/observed-in-window.js +17 -0
  54. package/dist/events/predicate-asks.d.ts +30 -0
  55. package/dist/events/predicate-asks.js +48 -0
  56. package/dist/events/predicate-eval.js +254 -25
  57. package/dist/events/predicate-parse.d.ts +0 -6
  58. package/dist/events/predicate-parse.js +51 -5
  59. package/dist/events/predicate-schema.d.ts +62 -0
  60. package/dist/events/predicate-schema.js +162 -4
  61. package/dist/events/predicate.d.ts +15 -0
  62. package/dist/events/predicate.js +41 -2
  63. package/dist/flows/coverage.d.ts +0 -8
  64. package/dist/flows/coverage.js +1 -1
  65. package/dist/flows/decision.d.ts +5 -1
  66. package/dist/flows/decision.js +33 -7
  67. package/dist/flows/flake.d.ts +0 -2
  68. package/dist/flows/flake.js +1 -1
  69. package/dist/flows/flow-intent.d.ts +52 -0
  70. package/dist/flows/flow-intent.js +123 -0
  71. package/dist/flows/flow-replay-run.js +27 -4
  72. package/dist/flows/flow-success.js +15 -3
  73. package/dist/flows/flow-tools.js +65 -8
  74. package/dist/flows/flows.d.ts +6 -1
  75. package/dist/flows/flows.js +34 -22
  76. package/dist/flows/predicate-to-expect.js +14 -3
  77. package/dist/flows/server-verify.js +16 -2
  78. package/dist/flows/verify-change-tools.js +9 -2
  79. package/dist/honesty/blind-spots.d.ts +8 -16
  80. package/dist/honesty/blind-spots.js +18 -5
  81. package/dist/honesty/feature-capture.d.ts +150 -0
  82. package/dist/honesty/feature-capture.js +233 -0
  83. package/dist/honesty/gap-ledger.d.ts +52 -0
  84. package/dist/honesty/gap-ledger.js +63 -0
  85. package/dist/honesty/instrumentation-gaps.d.ts +79 -0
  86. package/dist/honesty/instrumentation-gaps.js +77 -0
  87. package/dist/honesty/observability.d.ts +37 -0
  88. package/dist/honesty/observability.js +40 -0
  89. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  90. package/dist/honesty/tool-hit-rate.js +90 -0
  91. package/dist/honesty/undeclared-change.d.ts +51 -0
  92. package/dist/honesty/undeclared-change.js +56 -0
  93. package/dist/honesty/verified.d.ts +15 -0
  94. package/dist/honesty/verified.js +68 -4
  95. package/dist/http-server.d.ts +0 -1
  96. package/dist/http-server.js +1 -1
  97. package/dist/hunt/hunt-report.d.ts +0 -2
  98. package/dist/hunt/hunt-report.js +1 -1
  99. package/dist/impact/impact-store.d.ts +0 -5
  100. package/dist/impact/impact-store.js +1 -1
  101. package/dist/index.d.ts +4 -2
  102. package/dist/index.js +41 -0
  103. package/dist/init/agent-rules.d.ts +17 -0
  104. package/dist/init/agent-rules.js +31 -1
  105. package/dist/init/capabilities.d.ts +0 -2
  106. package/dist/init/capabilities.js +1 -1
  107. package/dist/init/closing-hint.d.ts +36 -0
  108. package/dist/init/closing-hint.js +120 -0
  109. package/dist/init/confirm.d.ts +0 -25
  110. package/dist/init/confirm.js +3 -3
  111. package/dist/init/cra.d.ts +14 -1
  112. package/dist/init/cra.js +43 -12
  113. package/dist/init/dev-script.d.ts +48 -0
  114. package/dist/init/dev-script.js +63 -0
  115. package/dist/init/mcp.d.ts +16 -5
  116. package/dist/init/mcp.js +26 -10
  117. package/dist/init/node-io.js +3 -0
  118. package/dist/init/plan-framework.d.ts +0 -5
  119. package/dist/init/plan-framework.js +83 -9
  120. package/dist/init/plan.d.ts +10 -30
  121. package/dist/init/plan.js +30 -15
  122. package/dist/init/refresh-rules.d.ts +39 -0
  123. package/dist/init/refresh-rules.js +51 -0
  124. package/dist/init/run.d.ts +2 -10
  125. package/dist/init/run.js +26 -68
  126. package/dist/init/slash-command.d.ts +1 -1
  127. package/dist/init/slash-command.js +33 -10
  128. package/dist/init/snippets.js +53 -10
  129. package/dist/input/real-input.d.ts +2 -0
  130. package/dist/input/real-input.js +3 -2
  131. package/dist/input/scroll-find.d.ts +7 -4
  132. package/dist/input/scroll-find.js +63 -25
  133. package/dist/intent/inline-intent.d.ts +46 -0
  134. package/dist/intent/inline-intent.js +103 -0
  135. package/dist/intent/intent-store.d.ts +37 -0
  136. package/dist/intent/intent-store.js +112 -0
  137. package/dist/intent/intent-tools.d.ts +2 -0
  138. package/dist/intent/intent-tools.js +80 -0
  139. package/dist/intent/open-intents.d.ts +21 -0
  140. package/dist/intent/open-intents.js +25 -0
  141. package/dist/journal/envelope.d.ts +2 -2
  142. package/dist/journal/journal-recorder.d.ts +23 -0
  143. package/dist/journal/journal-recorder.js +35 -0
  144. package/dist/license/license.d.ts +8 -2
  145. package/dist/license/license.js +10 -15
  146. package/dist/mcp/attach-memory.d.ts +67 -0
  147. package/dist/mcp/attach-memory.js +142 -0
  148. package/dist/mcp/mcp-proxy.d.ts +0 -7
  149. package/dist/mcp/mcp-proxy.js +14 -2
  150. package/dist/mcp/mcp.d.ts +2 -0
  151. package/dist/mcp/mcp.js +45 -23
  152. package/dist/mcp/server-instructions.js +45 -3
  153. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  154. package/dist/mcp/tool-catalog-cache.js +10 -0
  155. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  156. package/dist/oracles/flow-instrument-gaps.js +4 -5
  157. package/dist/oracles/self-instrument.d.ts +9 -9
  158. package/dist/oracles/self-instrument.js +19 -13
  159. package/dist/pool/browser-pool.d.ts +12 -1
  160. package/dist/pool/browser-pool.js +31 -1
  161. package/dist/pool/playwright-launcher.js +3 -1
  162. package/dist/project/artifact-root.d.ts +83 -0
  163. package/dist/project/artifact-root.js +112 -0
  164. package/dist/project/fs-port.d.ts +3 -0
  165. package/dist/project/fs-port.js +3 -2
  166. package/dist/project/memory-fs.d.ts +9 -0
  167. package/dist/project/memory-fs.js +93 -0
  168. package/dist/project/project-store.d.ts +17 -0
  169. package/dist/project/project-store.js +33 -0
  170. package/dist/project/project-tools.js +3 -32
  171. package/dist/project/remember-project.d.ts +24 -0
  172. package/dist/project/remember-project.js +40 -0
  173. package/dist/project/reticle-dir.d.ts +2 -0
  174. package/dist/project/reticle-dir.js +1 -0
  175. package/dist/project/session-root.d.ts +15 -0
  176. package/dist/project/session-root.js +31 -0
  177. package/dist/runs/context-tools.d.ts +2 -0
  178. package/dist/runs/context-tools.js +89 -0
  179. package/dist/runs/run-context.d.ts +40 -0
  180. package/dist/runs/run-context.js +158 -0
  181. package/dist/runs/run-diff.d.ts +2 -1
  182. package/dist/runs/run-tools.js +2 -1
  183. package/dist/runs/verification-sync.js +2 -2
  184. package/dist/session/dev-server-probe.d.ts +0 -8
  185. package/dist/session/dev-server-probe.js +1 -1
  186. package/dist/session/last-act.d.ts +7 -1
  187. package/dist/session/last-act.js +2 -2
  188. package/dist/session/lease-availability.d.ts +40 -0
  189. package/dist/session/lease-availability.js +43 -0
  190. package/dist/session/live-control-tools.js +1 -42
  191. package/dist/session/no-session-diagnosis.d.ts +20 -0
  192. package/dist/session/no-session-diagnosis.js +41 -12
  193. package/dist/session/no-session-next-action.d.ts +5 -0
  194. package/dist/session/no-session-next-action.js +35 -3
  195. package/dist/session/no-session-watch.d.ts +8 -0
  196. package/dist/session/no-session-watch.js +83 -65
  197. package/dist/session/output-budget.js +4 -1
  198. package/dist/session/session-health.d.ts +9 -0
  199. package/dist/session/session-health.js +23 -0
  200. package/dist/session/session-manager.js +50 -1
  201. package/dist/session/session-successor.d.ts +37 -0
  202. package/dist/session/session-successor.js +78 -0
  203. package/dist/session/session.d.ts +40 -1
  204. package/dist/session/session.js +84 -2
  205. package/dist/session/stall-clock.d.ts +18 -0
  206. package/dist/session/stall-clock.js +48 -0
  207. package/dist/telemetry/automation-hint.d.ts +2 -1
  208. package/dist/telemetry/daemon-telemetry.js +12 -23
  209. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  210. package/dist/telemetry/project-profile.d.ts +0 -8
  211. package/dist/telemetry/project-profile.js +1 -1
  212. package/dist/telemetry/run-telemetry.d.ts +0 -2
  213. package/dist/telemetry/run-telemetry.js +1 -1
  214. package/dist/telemetry/session-metrics.d.ts +1 -1
  215. package/dist/telemetry/session-metrics.js +1 -1
  216. package/dist/telemetry/telemetry.d.ts +12 -3
  217. package/dist/telemetry/telemetry.js +25 -5
  218. package/dist/telemetry/verification-of.js +2 -2
  219. package/dist/tools/act-tools.js +159 -24
  220. package/dist/tools/assert-source.d.ts +22 -0
  221. package/dist/tools/assert-source.js +53 -0
  222. package/dist/tools/assert-verdict.d.ts +21 -2
  223. package/dist/tools/assert-verdict.js +59 -4
  224. package/dist/tools/browser-tools.js +0 -20
  225. package/dist/tools/complete-snapshot.d.ts +48 -0
  226. package/dist/tools/complete-snapshot.js +75 -0
  227. package/dist/tools/contract-tools.js +5 -3
  228. package/dist/tools/coverage-tools.d.ts +10 -1
  229. package/dist/tools/coverage-tools.js +124 -37
  230. package/dist/tools/dynamic-tools.js +29 -8
  231. package/dist/tools/error-recovery.d.ts +24 -14
  232. package/dist/tools/error-recovery.js +70 -9
  233. package/dist/tools/feedback-tools.d.ts +1 -1
  234. package/dist/tools/feedback-tools.js +2 -2
  235. package/dist/tools/invoke-tool.js +62 -2
  236. package/dist/tools/lease-hint.d.ts +10 -0
  237. package/dist/tools/lease-hint.js +53 -4
  238. package/dist/tools/lease-tools.d.ts +5 -1
  239. package/dist/tools/lease-tools.js +22 -8
  240. package/dist/tools/navigate-arrival.d.ts +0 -2
  241. package/dist/tools/navigate-arrival.js +1 -1
  242. package/dist/tools/observe-tools.js +70 -27
  243. package/dist/tools/read-tools.js +5 -3
  244. package/dist/tools/real-input-attempt.d.ts +22 -0
  245. package/dist/tools/real-input-attempt.js +153 -1
  246. package/dist/tools/reconcile-tools.js +18 -4
  247. package/dist/tools/resolve-target.d.ts +0 -7
  248. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  249. package/dist/tools/sdk-marker-probe.js +57 -0
  250. package/dist/tools/snapshot-delta.d.ts +1 -1
  251. package/dist/tools/tool-kit.d.ts +28 -1
  252. package/dist/tools/tool-kit.js +16 -0
  253. package/dist/tools/tool-names.d.ts +3 -0
  254. package/dist/tools/tool-names.js +3 -0
  255. package/dist/tools/tool-surface.d.ts +85 -6
  256. package/dist/tools/tool-surface.js +122 -2
  257. package/dist/tools/tools.js +22 -4
  258. package/dist/tools/unadvertised-help.js +4 -4
  259. package/dist/tools/verify-next-baton.d.ts +53 -0
  260. package/dist/tools/verify-next-baton.js +52 -0
  261. package/dist/update/update-nudge.d.ts +7 -10
  262. package/dist/update/update-nudge.js +12 -3
  263. package/dist/update/updater.d.ts +1 -1
  264. package/docs/README.md +3 -1
  265. package/docs/cli/cloud.mdx +1 -1
  266. package/docs/cli/doctor.mdx +1 -0
  267. package/docs/cli/verify.mdx +4 -5
  268. package/docs/desktop.mdx +1 -1
  269. package/docs/docs.json +3 -3
  270. package/docs/enterprise.md +3 -1
  271. package/docs/gates.md +1 -1
  272. package/docs/install-manual.mdx +1 -1
  273. package/docs/license-activation.mdx +145 -0
  274. package/docs/packages/browser.mdx +2 -2
  275. package/docs/packages/server.mdx +1 -1
  276. package/docs/packages.mdx +8 -6
  277. package/docs/platform-integration.md +17 -0
  278. package/docs/predicates.mdx +8 -0
  279. package/docs/reticle-manual.md +106 -0
  280. package/docs/telemetry-contract.md +1 -12
  281. package/docs/telemetry-events.mdx +1 -34
  282. package/docs/telemetry.md +0 -1
  283. package/docs/tools-tools-and-run.mdx +2 -4
  284. package/docs/troubleshooting.mdx +9 -0
  285. package/docs/usage.md +3 -2
  286. package/package.json +2 -2
  287. package/dist/session/session-readiness.d.ts +0 -34
  288. package/dist/session/session-readiness.js +0 -36
  289. package/dist/telemetry/instrumentation-stall.d.ts +0 -57
  290. package/dist/telemetry/instrumentation-stall.js +0 -103
@@ -0,0 +1,112 @@
1
+ import { join } from 'node:path';
2
+ import { ReticleDir, projectCandidates, } from '@reticlehq/core';
3
+ /**
4
+ * Which `.reticle/` a session's artifacts belong in.
5
+ *
6
+ * Everything Reticle persists — flows, the capability contract, baselines, capsules, the cross-run
7
+ * project memory — used to resolve against the DAEMON's own `process.cwd()`. That is a statement
8
+ * about where the daemon was launched and it was being read as a statement about which project is
9
+ * being verified. Those are not the same thing and are usually not even the same tree: a user-scoped
10
+ * MCP registration is the common case, and the editor that spawns it starts it wherever it likes.
11
+ *
12
+ * Three field shapes, one defect: `cwd=/` gave `ENOENT: mkdir '/.reticle'`; a daemon started in
13
+ * project A wrote project B's flow into A's checkout and reported success without naming the path;
14
+ * and `verify_change` could only answer "unknown" because no flow could be persisted for it to match
15
+ * against.
16
+ *
17
+ * ## Two sources, one rule
18
+ *
19
+ * Candidates arrive from config discovery (which walks out from the daemon's own directory) and from
20
+ * the user-level project registry (which `init` writes, and which reaches checkouts discovery cannot
21
+ * see). This function takes the merged list and does not know or care which source an entry came
22
+ * from — a privileged source would be a second rule, and two rules for one question is how the
23
+ * answers start disagreeing.
24
+ *
25
+ * ## Why matching on projectId, and not a new wire field
26
+ *
27
+ * HELLO already stamps `projectId`, and `.reticle.json` already declares the same id sitting next to
28
+ * the code it configures. The join is therefore available today, for every SDK already in the field.
29
+ * Adding a `root` to HELLO would move the wire contract, strand every older SDK on the fallback, and
30
+ * buy nothing this does not already give.
31
+ *
32
+ * ## Why it can decline to answer
33
+ *
34
+ * Every non-matching branch returns the daemon root and SAYS which branch it was. A wrong root is
35
+ * worse than no root — it writes a caller's evidence into a tree they never drove — so the ambiguous
36
+ * case refuses rather than picks, exactly as `discoverProjectConfigs` refuses to pick one config.
37
+ * The reason travels with the answer so callers can report the surprise instead of hiding it.
38
+ */
39
+ /** Why the root below is the root. Travels with the answer so a caller can say what happened. */
40
+ export const ArtifactRootReason = {
41
+ /** Exactly one discovered config declares this session's project. The good case. */
42
+ MATCHED_PROJECT: 'matched-project',
43
+ /** The session declared no projectId — a pre-2.0 SDK. Nothing to match on. */
44
+ NO_PROJECT_ID: 'no-project-id',
45
+ /** The search ran and nothing it found declares this project. */
46
+ NO_MATCH: 'no-match',
47
+ /** Two or more checkouts declare this project. Refused rather than guessed. */
48
+ AMBIGUOUS: 'ambiguous',
49
+ };
50
+ /**
51
+ * Resolve the artifact root for one session.
52
+ *
53
+ * Pure: no IO, no cwd, no clock. The filesystem walk belongs to `discoverProjectConfigs` and is
54
+ * passed in, which is what makes every branch below testable without a fixture tree.
55
+ */
56
+ export function resolveArtifactRoot(query) {
57
+ const { projectId, candidates, daemonRoot } = query;
58
+ if (projectId === undefined || 0 === projectId.length) {
59
+ return { root: daemonRoot, reason: ArtifactRootReason.NO_PROJECT_ID };
60
+ }
61
+ const matches = dedupeByDirectory(candidates.filter((c) => c.projectId === projectId));
62
+ if (0 === matches.length) {
63
+ return { root: daemonRoot, reason: ArtifactRootReason.NO_MATCH };
64
+ }
65
+ if (matches.length > 1) {
66
+ return {
67
+ root: daemonRoot,
68
+ reason: ArtifactRootReason.AMBIGUOUS,
69
+ candidates: matches.map((config) => config.directory),
70
+ };
71
+ }
72
+ const [only] = matches;
73
+ // `matches.length === 1` above, so this is defined — but the codebase forbids `!`, and a default
74
+ // that can never be taken is cheaper than the alternative spelling.
75
+ const directory = only?.directory ?? '';
76
+ if (0 === directory.length) {
77
+ return { root: daemonRoot, reason: ArtifactRootReason.NO_MATCH };
78
+ }
79
+ return { root: join(directory, ReticleDir.ROOT), reason: ArtifactRootReason.MATCHED_PROJECT };
80
+ }
81
+ /**
82
+ * One entry per directory.
83
+ *
84
+ * Discovery and the registry routinely name the SAME checkout — the registry remembers what `init`
85
+ * wrote, and discovery finds that same file whenever the daemon happens to be in the tree. Counting
86
+ * it twice would read as two competing checkouts and make the resolver refuse, which is the one
87
+ * outcome worse than either source alone.
88
+ */
89
+ function dedupeByDirectory(matches) {
90
+ const seen = new Set();
91
+ const out = [];
92
+ for (const candidate of matches) {
93
+ if (seen.has(candidate.directory))
94
+ continue;
95
+ seen.add(candidate.directory);
96
+ out.push(candidate);
97
+ }
98
+ return out;
99
+ }
100
+ /**
101
+ * Everything this machine knows about where projects live, from both sources.
102
+ *
103
+ * Discovery first, deliberately: it read a `.reticle.json` that exists RIGHT NOW, while the registry
104
+ * is a cache of something `init` saw once. Order only decides which duplicate survives dedupe, and
105
+ * the one confirmed a moment ago is the better survivor.
106
+ */
107
+ export function projectCandidatesFrom(discovery, registry) {
108
+ const discovered = discovery.found.flatMap((config) => config.projectId === undefined || 0 === config.projectId.length
109
+ ? []
110
+ : [{ projectId: config.projectId, directory: config.directory }]);
111
+ return [...discovered, ...projectCandidates(registry)];
112
+ }
@@ -36,7 +36,10 @@ export interface FileSystemPort {
36
36
  /** Modification time in epoch ms — for recency-based retention pruning. Rejects if absent. */
37
37
  stat(path: string): Promise<{
38
38
  mtimeMs: number;
39
+ size: number;
39
40
  }>;
41
+ /** Resolve symlinks to their real path — for upload trust-boundary checks. Rejects if absent. */
42
+ realpath(path: string): Promise<string>;
40
43
  /** ENOENT classifier — narrows unknown without `any`, so callers can distinguish missing-file. */
41
44
  isNotFound(error: unknown): boolean;
42
45
  }
@@ -1,4 +1,4 @@
1
- import { access, appendFile, mkdir, open, readdir, readFile, rename, rm, stat, writeFile, } from 'node:fs/promises';
1
+ import { access, appendFile, mkdir, open, readdir, readFile, realpath, rename, rm, stat, writeFile, } from 'node:fs/promises';
2
2
  /** Production adapter — the single import site of node:fs/promises in the server. */
3
3
  export function createNodeFileSystem() {
4
4
  return {
@@ -46,8 +46,9 @@ export function createNodeFileSystem() {
46
46
  },
47
47
  stat: async (path) => {
48
48
  const s = await stat(path);
49
- return { mtimeMs: s.mtimeMs };
49
+ return { mtimeMs: s.mtimeMs, size: s.size };
50
50
  },
51
+ realpath: (path) => realpath(path),
51
52
  isNotFound: (error) => 'ENOENT' === error?.code,
52
53
  };
53
54
  }
@@ -0,0 +1,9 @@
1
+ import type { FileSystemPort } from './fs-port.js';
2
+ export interface MemoryFs {
3
+ fs: FileSystemPort;
4
+ /** Every file written, keyed by normalised absolute path. */
5
+ written: Map<string, string>;
6
+ /** Every directory created. */
7
+ dirs: Set<string>;
8
+ }
9
+ export declare function createMemoryFs(): MemoryFs;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * An in-memory `FileSystemPort`, and the map it wrote into.
3
+ *
4
+ * Three specs had hand-rolled their own copy of this before it was extracted, each one slightly
5
+ * different and each one a fresh chance to reintroduce the same fixture bug: the code under test
6
+ * joins paths with the PLATFORM separator, the fixtures are written with POSIX ones, and a spec that
7
+ * forgets to normalise passes on macOS and fails on Windows. Normalising at the port — once, here —
8
+ * is what stops that recurring.
9
+ *
10
+ * Exposing `written` is the other reason this exists. A store's return value says what it believes it
11
+ * did; the map says where the bytes actually went, which is the only way to test a defect about
12
+ * writing to the wrong directory.
13
+ */
14
+ /** POSIX form, so a `\`-joined path and a `/`-joined one are the same key. */
15
+ const norm = (path) => path.split('\\').join('/');
16
+ export function createMemoryFs() {
17
+ const written = new Map();
18
+ const dirs = new Set();
19
+ const notFound = () => {
20
+ const err = new Error('ENOENT: no such file or directory');
21
+ err.code = 'ENOENT';
22
+ return err;
23
+ };
24
+ const fs = {
25
+ readFile(path) {
26
+ const value = written.get(norm(path));
27
+ return value === undefined ? Promise.reject(notFound()) : Promise.resolve(value);
28
+ },
29
+ writeFile(path, data) {
30
+ written.set(norm(path), data);
31
+ return Promise.resolve();
32
+ },
33
+ appendFile(path, data) {
34
+ written.set(norm(path), (written.get(norm(path)) ?? '') + data);
35
+ return Promise.resolve();
36
+ },
37
+ readFileBytes(path) {
38
+ const value = written.get(norm(path));
39
+ return value === undefined
40
+ ? Promise.reject(notFound())
41
+ : Promise.resolve(new TextEncoder().encode(value));
42
+ },
43
+ writeFileBytes(path, data) {
44
+ written.set(norm(path), new TextDecoder().decode(data));
45
+ return Promise.resolve();
46
+ },
47
+ mkdir(path) {
48
+ dirs.add(norm(path));
49
+ return Promise.resolve();
50
+ },
51
+ exists(path) {
52
+ const key = norm(path);
53
+ return Promise.resolve(written.has(key) || dirs.has(key));
54
+ },
55
+ readdir(path) {
56
+ const prefix = `${norm(path)}/`;
57
+ const names = new Set();
58
+ for (const key of written.keys()) {
59
+ if (key.startsWith(prefix)) {
60
+ const rest = key.slice(prefix.length).split('/')[0];
61
+ if (rest !== undefined && rest.length > 0)
62
+ names.add(rest);
63
+ }
64
+ }
65
+ return Promise.resolve([...names]);
66
+ },
67
+ rename(from, to) {
68
+ const value = written.get(norm(from));
69
+ if (value === undefined)
70
+ return Promise.reject(notFound());
71
+ written.set(norm(to), value);
72
+ written.delete(norm(from));
73
+ return Promise.resolve();
74
+ },
75
+ rm(path) {
76
+ written.delete(norm(path));
77
+ dirs.delete(norm(path));
78
+ return Promise.resolve();
79
+ },
80
+ stat(path) {
81
+ return written.has(norm(path))
82
+ ? Promise.resolve({ mtimeMs: 0, size: 0 })
83
+ : Promise.reject(notFound());
84
+ },
85
+ realpath(path) {
86
+ return written.has(norm(path)) ? Promise.resolve(norm(path)) : Promise.reject(notFound());
87
+ },
88
+ isNotFound(error) {
89
+ return 'ENOENT' === error?.code;
90
+ },
91
+ };
92
+ return { fs, written, dirs };
93
+ }
@@ -32,6 +32,23 @@ export declare class ProjectStore {
32
32
  * concurrent browser sessions accumulate rather than overwriting one another.
33
33
  */
34
34
  recordRoutes(routes: readonly string[]): Promise<void>;
35
+ /**
36
+ * The best observability this project has reached, and the raiser of that bar.
37
+ *
38
+ * Read returns undefined on a first run or an unreadable file, which is the honest "no best to
39
+ * fall from" — never a zero, which would make every first run look like a recovery.
40
+ */
41
+ bestObservability(): Promise<{
42
+ percent: number;
43
+ } | undefined>;
44
+ /**
45
+ * Raise the bar, if this run cleared it. Lowering is deliberately impossible.
46
+ *
47
+ * A best that could fall would let a weakened run quietly redefine what good looks like, which is
48
+ * precisely the gaming the figure exists to resist. Serialized per file so two sessions racing
49
+ * cannot lose the higher of the two.
50
+ */
51
+ raiseObservability(percent: number): Promise<void>;
35
52
  /** The most-recent run for `name` (undefined on missing/malformed/none). Powers diff-vs-last. */
36
53
  lastRun(name: string): Promise<RunRecord | undefined>;
37
54
  }
@@ -101,6 +101,39 @@ export class ProjectStore {
101
101
  await this.#fs.writeFile(path, this.#serialize(next));
102
102
  });
103
103
  }
104
+ /**
105
+ * The best observability this project has reached, and the raiser of that bar.
106
+ *
107
+ * Read returns undefined on a first run or an unreadable file, which is the honest "no best to
108
+ * fall from" — never a zero, which would make every first run look like a recovery.
109
+ */
110
+ async bestObservability() {
111
+ const existing = await this.read();
112
+ return existing.ok ? existing.file.learned?.bestObservability : undefined;
113
+ }
114
+ /**
115
+ * Raise the bar, if this run cleared it. Lowering is deliberately impossible.
116
+ *
117
+ * A best that could fall would let a weakened run quietly redefine what good looks like, which is
118
+ * precisely the gaming the figure exists to resist. Serialized per file so two sessions racing
119
+ * cannot lose the higher of the two.
120
+ */
121
+ async raiseObservability(percent) {
122
+ const path = reticleDirPaths(this.#root).project;
123
+ await withFileLock(path, async () => {
124
+ const existing = await this.read();
125
+ const base = existing.ok ? existing.file : EMPTY_PROJECT;
126
+ const current = base.learned?.bestObservability?.percent;
127
+ if (current !== undefined && percent <= current)
128
+ return;
129
+ const learned = {
130
+ ...base.learned,
131
+ bestObservability: { percent, at: this.#clock.now() },
132
+ };
133
+ await this.#fs.mkdir(reticleDirPaths(this.#root).root);
134
+ await this.#fs.writeFile(path, this.#serialize({ ...base, learned }));
135
+ });
136
+ }
104
137
  /** The most-recent run for `name` (undefined on missing/malformed/none). Powers diff-vs-last. */
105
138
  async lastRun(name) {
106
139
  const read = await this.read();
@@ -1,9 +1,9 @@
1
1
  import { z } from 'zod';
2
- import { ProjectReadError, RunKind, RunStatus } from '@reticlehq/core';
2
+ import { ProjectReadError, RunStatus } from '@reticlehq/core';
3
3
  import { ReticleTool } from '../tools/tool-names.js';
4
4
  import { sessionIdShape } from '../tools/tool-kit.js';
5
5
  import { asNumber, asString } from '../tools/tools-helpers.js';
6
- import { fetchProjectRegressionFromCloud, resolveCloudConfig } from '../cloud/cloud-sync.js';
6
+ import { cloudFetch, fetchProjectRegressionFromCloud, resolveCloudConfig, } from '../cloud/cloud-sync.js';
7
7
  import { RunStore } from '../runs/run-store.js';
8
8
  import { diffRuns as diffVerificationRuns, } from '../runs/run-diff.js';
9
9
  const REGRESSION_STATUSES = new Set([
@@ -45,7 +45,7 @@ async function cloudRegression(deps, sessionId) {
45
45
  catch {
46
46
  projectId = undefined;
47
47
  }
48
- const report = await fetchProjectRegressionFromCloud(config, projectId, (url, init) => fetch(url, init));
48
+ const report = await fetchProjectRegressionFromCloud(config, projectId, cloudFetch);
49
49
  return report ?? undefined;
50
50
  }
51
51
  /** The two most-recent runs for `name`, oldest-first, or undefined if there are fewer than two. */
@@ -152,33 +152,4 @@ export const PROJECT_TOOLS = [
152
152
  });
153
153
  },
154
154
  },
155
- {
156
- name: ReticleTool.RUN_RECORD,
157
- description: 'Explicitly record a run outcome into .reticle/project.json (the manual companion to the auto-record on reticle_flow_replay). Use it to log the result of an assertion sequence or a manual journey so future runs can diff against it. Returns { recorded:true, name, status }.',
158
- inputSchema: {
159
- name: z.string().describe('Run name for grouping in reticle_project history.'),
160
- status: z.nativeEnum(RunStatus).describe('Outcome: pass | fail | drift | error'),
161
- kind: z.nativeEnum(RunKind).optional(),
162
- summary: z.string().optional().describe('One-line human summary of what this run covered.'),
163
- ...sessionIdShape,
164
- },
165
- outputSchema: {
166
- recorded: z.boolean(),
167
- runName: z.string(),
168
- status: z.string(),
169
- },
170
- handler: async (deps, args) => {
171
- const name = asString(args['name']) ?? '';
172
- const status = args['status'];
173
- const kindArg = args['kind'];
174
- const summary = asString(args['summary']);
175
- await deps.project.recordRun({
176
- kind: 'string' === typeof kindArg ? kindArg : RunKind.MANUAL,
177
- name,
178
- status,
179
- ...(summary !== undefined ? { summary } : {}),
180
- });
181
- return { recorded: true, runName: name, status };
182
- },
183
- },
184
155
  ];
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Record where this project lives, so a daemon started somewhere else can still find it.
3
+ *
4
+ * Called at the end of `init`, which is the one moment both halves are known for certain: the
5
+ * directory being initialised, and the projectId that was just written into its `.reticle.json`.
6
+ *
7
+ * Deliberately runs on EVERY init, not only the first. Most init runs report "already wired", and a
8
+ * re-run in a re-cloned or moved checkout is exactly when the remembered path has gone stale — so
9
+ * the run that would have been a no-op is the one that repairs the entry.
10
+ *
11
+ * Every failure is swallowed. This is a cache that makes a later resolution better; a read-only home
12
+ * directory, a full disk or a hand-mangled file must not turn a successful init into a failed one.
13
+ * The user's project is wired either way, and `init`'s report is the first thing a new user reads.
14
+ */
15
+ /** The sync filesystem seam `init` already carries. Narrowed to what this needs. */
16
+ export interface RegistryIo {
17
+ readFile(path: string): string | null;
18
+ writeFile(path: string, content: string): void;
19
+ exists(path: string): boolean;
20
+ homeDir(): string;
21
+ }
22
+ /** The `projectId` a `.reticle.json` declares, or undefined when it declares none we can use. */
23
+ export declare function projectIdOf(configSource: string | null): string | undefined;
24
+ export declare function rememberProjectOnDisk(io: RegistryIo, projectId: string, directory: string, now: number): boolean;
@@ -0,0 +1,40 @@
1
+ import { join } from 'node:path';
2
+ import { PROJECT_REGISTRY_FILE, ReticleDir, emptyProjectRegistry, parseProjectRegistry, rememberProject, } from '@reticlehq/core';
3
+ /** The `projectId` a `.reticle.json` declares, or undefined when it declares none we can use. */
4
+ export function projectIdOf(configSource) {
5
+ if (null === configSource || 0 === configSource.length)
6
+ return undefined;
7
+ try {
8
+ const parsed = JSON.parse(configSource);
9
+ if ('object' !== typeof parsed || null === parsed || Array.isArray(parsed))
10
+ return undefined;
11
+ const id = parsed['projectId'];
12
+ return 'string' === typeof id && id.length > 0 ? id : undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ }
18
+ export function rememberProjectOnDisk(io, projectId, directory, now) {
19
+ if (0 === projectId.length || 0 === directory.length)
20
+ return false;
21
+ try {
22
+ const path = join(io.homeDir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
23
+ const existing = io.exists(path) ? io.readFile(path) : null;
24
+ const registry = null === existing ? emptyProjectRegistry() : parseProjectRegistry(safeJson(existing));
25
+ const next = rememberProject(registry, projectId, directory, now);
26
+ io.writeFile(path, `${JSON.stringify(next, null, 2)}\n`);
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ function safeJson(raw) {
34
+ try {
35
+ return JSON.parse(raw);
36
+ }
37
+ catch {
38
+ return undefined;
39
+ }
40
+ }
@@ -15,6 +15,8 @@ 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) */
19
+ intent: string;
18
20
  /**.../.reticle/visual (PNG baselines + diffs) */
19
21
  visual: string;
20
22
  /**.../.reticle/runs (verification-run artifacts) */
@@ -8,6 +8,7 @@ export function reticleDirPaths(root) {
8
8
  baselines: join(root, ReticleDir.BASELINES_SUBDIR),
9
9
  project: join(root, ReticleDir.PROJECT_FILE),
10
10
  impact: join(root, ReticleDir.IMPACT_FILE),
11
+ intent: join(root, ReticleDir.INTENT_FILE),
11
12
  visual: join(root, ReticleDir.VISUAL_SUBDIR),
12
13
  runs: join(root, ReticleDir.RUNS_SUBDIR),
13
14
  sessions: join(root, ReticleDir.SESSIONS_SUBDIR),
@@ -0,0 +1,15 @@
1
+ import type { ToolDeps } from '../tools/tool-kit.js';
2
+ /**
3
+ * The `.reticle` directory a tool call's artifacts belong in.
4
+ *
5
+ * Every call site that reads or writes a project artifact goes through here, and that is the whole
6
+ * point of the function existing rather than each site asking `deps.artifactRootFor` itself. The
7
+ * write half and the read half MUST agree: a `flow_save` that resolves to the project while
8
+ * `verify_change` still reads the daemon's own directory would be a worse defect than the one being
9
+ * fixed — the flow would save successfully and then be invisible to the tool that exists to replay
10
+ * it, which reads as "no flows covered this change" rather than as an error.
11
+ *
12
+ * Falls back to `deps.reticleRoot` whenever resolution cannot name a project, which is exactly the
13
+ * behaviour every call site had before the resolver existed.
14
+ */
15
+ export declare function sessionRoot(deps: ToolDeps, sessionId: string | undefined): string;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The `.reticle` directory a tool call's artifacts belong in.
3
+ *
4
+ * Every call site that reads or writes a project artifact goes through here, and that is the whole
5
+ * point of the function existing rather than each site asking `deps.artifactRootFor` itself. The
6
+ * write half and the read half MUST agree: a `flow_save` that resolves to the project while
7
+ * `verify_change` still reads the daemon's own directory would be a worse defect than the one being
8
+ * fixed — the flow would save successfully and then be invisible to the tool that exists to replay
9
+ * it, which reads as "no flows covered this change" rather than as an error.
10
+ *
11
+ * Falls back to `deps.reticleRoot` whenever resolution cannot name a project, which is exactly the
12
+ * behaviour every call site had before the resolver existed.
13
+ */
14
+ export function sessionRoot(deps, sessionId) {
15
+ return deps.artifactRootFor?.(sessionProjectIdOf(deps, sessionId)).root ?? deps.reticleRoot;
16
+ }
17
+ /**
18
+ * The connected session's projectId, or undefined when there is no session to ask.
19
+ *
20
+ * `sessions.resolve` throws when nothing is connected, when the id names no session, and when
21
+ * several are connected and none was named. All three mean the same thing here — we cannot tell
22
+ * which project this call is about — and none of them is a reason to fail the caller's tool.
23
+ */
24
+ function sessionProjectIdOf(deps, sessionId) {
25
+ try {
26
+ return deps.sessions.resolve(sessionId).projectId;
27
+ }
28
+ catch {
29
+ return undefined;
30
+ }
31
+ }
@@ -0,0 +1,2 @@
1
+ import type { ToolDef } from '../tools/tool-kit.js';
2
+ export declare const CONTEXT_TOOLS: ToolDef[];
@@ -0,0 +1,89 @@
1
+ import { z } from 'zod';
2
+ import {} from '@reticlehq/core';
3
+ import { runContextFor } from './run-context.js';
4
+ import { openSessionIntents } from '../intent/open-intents.js';
5
+ import { ReticleTool } from '../tools/tool-names.js';
6
+ import { sessionIdShape } from '../tools/tool-kit.js';
7
+ import { asString } from '../tools/tools-helpers.js';
8
+ /**
9
+ * What this run has established, asked for by the one party that knows when it is needed.
10
+ *
11
+ * Reticle holds the trajectory: the journal, the verdicts, the intent ledger. The agent holds a copy
12
+ * in its context window, and that copy is the one that disappears — at a compaction, at the end of a
13
+ * turn, at the handover to a sub-agent. Reticle's does not disappear at the same moment. So this is
14
+ * not a competing memory; it is the ground truth compaction destroyed, handed back on request.
15
+ *
16
+ * ## Why it is PULLED
17
+ *
18
+ * The same content was once pushed onto every session-bound tool response. It cost +136% on a
19
+ * verdict and was cut, because most of the time the agent still had the context and we were paying
20
+ * on every call to duplicate what it knew. Nobody but the agent can tell when that stops being true.
21
+ * So the agent asks, once, at the moment it knows its own memory is gone, and pays once.
22
+ *
23
+ * ## Why it is on the EXTENDED surface rather than the default
24
+ *
25
+ * The default surface is a hard COUNT — editors budget tools across every connected MCP server, so a
26
+ * slot taken here is taken from a tool that has already earned it, and nothing on that list has been
27
+ * demoted for this. The claim behind this tool is unmeasured, which is the same test `reticle_intent`
28
+ * and the `reticle_capabilities` demotion were held to, and it fails that test today.
29
+ *
30
+ * The reachability argument is also stronger here than for most of the cold tail. The caller is,
31
+ * by construction, an agent that has just lost its context and is re-reading the tool list it was
32
+ * handed this turn — so `reticle_tools` (advertised on every surface) is exactly the call it is
33
+ * already going to make, and this tool is one `reticle_run` hop behind it. A tool that is only useful
34
+ * after a discovery step, to a caller who is already performing a discovery step, is the cheapest
35
+ * possible thing to leave off the hot list. It moves to the default surface when a measurement says
36
+ * the pull happens often enough to be worth a permanent slot, and not before.
37
+ */
38
+ /** What the run has established, and where each part of it came from. */
39
+ const CONTEXT_OUTPUT_SCHEMA = {
40
+ step: z
41
+ .number()
42
+ .describe('How many actions this run has dispatched. Counted off the journal, not a counter of its own.'),
43
+ established: z
44
+ .array(z.unknown())
45
+ .describe('What Reticle OBSERVED, folded and capped, each `{ key, fact, source?, doc?, epoch? }`. `key` is the subject, so re-observing it supersedes rather than appends. Anything seen under a replaced document or a pre-edit epoch is already gone from this list.'),
46
+ proven: z
47
+ .array(z.unknown())
48
+ .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.'),
49
+ remaining: z
50
+ .array(z.string())
51
+ .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.'),
52
+ };
53
+ /** The ledger and the live page state this run is folded out of. `[]`/undefined when nothing is connected. */
54
+ async function evidenceFor(deps, sessionId) {
55
+ try {
56
+ const session = deps.sessions.resolve(sessionId);
57
+ return {
58
+ actions: await session.readJournalActions(),
59
+ events: await session.queryEvents({}),
60
+ currentDocumentId: session.currentDocumentId,
61
+ currentEditEpoch: session.currentEditEpoch,
62
+ };
63
+ }
64
+ catch {
65
+ // `resolve` throws when nothing is connected, when the id names no session, and when several are
66
+ // connected and none was named. All three mean the same thing here: there is no run to report,
67
+ // and an empty context is the honest answer rather than a reason to fail the call.
68
+ return {
69
+ actions: [],
70
+ events: [],
71
+ currentDocumentId: undefined,
72
+ currentEditEpoch: undefined,
73
+ };
74
+ }
75
+ }
76
+ export const CONTEXT_TOOLS = [
77
+ {
78
+ name: ReticleTool.CONTEXT,
79
+ 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.',
80
+ example: {},
81
+ inputSchema: { ...sessionIdShape },
82
+ outputSchema: CONTEXT_OUTPUT_SCHEMA,
83
+ handler: async (deps, args) => {
84
+ const sessionId = asString(args['sessionId']);
85
+ const evidence = await evidenceFor(deps, sessionId);
86
+ return runContextFor({ ...evidence, intents: await openSessionIntents(deps, sessionId) });
87
+ },
88
+ },
89
+ ];
@@ -0,0 +1,40 @@
1
+ import { type EstablishedFact, type Intent, type JournalAction, type ProvenClaim, type ReticleEvent, type RunContext } from '@reticlehq/core';
2
+ /**
3
+ * The SUBJECT a journal action was about: the ref it spent, else the target it resolved from.
4
+ *
5
+ * Exported so the feature-use instrument files a re-fetch under the same key this fold establishes
6
+ * it under. A second copy of this rule there would be a second definition of "the same subject".
7
+ */
8
+ export declare function subjectOf(action: JournalAction): string | undefined;
9
+ /**
10
+ * Fold the journal into candidate facts, newest last.
11
+ *
12
+ * Only the last `RUN_ESTABLISHED_CAP` actions are read: anything older cannot survive the fold, so
13
+ * parsing it would be work done to throw away. An action with no observed settle outcome states
14
+ * nothing and is skipped — "we drove it and cannot say what happened" is not an established fact,
15
+ * and writing it down as one is how a memory starts lying.
16
+ */
17
+ export declare function establishedFromJournal(actions: readonly JournalAction[], events: readonly ReticleEvent[]): EstablishedFact[];
18
+ /**
19
+ * What a verdict already settled, newest last.
20
+ *
21
+ * Both verdict tools write here. `reticle_act_and_wait` closes its attribution window with one;
22
+ * `reticle_assert` drives nothing, so it appends its record WITHOUT opening a window — same effect
23
+ * shape, so this fold reads one kind of verdict rather than two.
24
+ */
25
+ export declare function provenFromJournal(actions: readonly JournalAction[], events: readonly ReticleEvent[]): ProvenClaim[];
26
+ /**
27
+ * The context for one run.
28
+ *
29
+ * `step` is the journal's action count rather than a counter of its own, for the reason at the top
30
+ * of this file: a second counter is a second thing that can disagree with the ledger. The document
31
+ * and epoch are the session's own, so the staleness rule here is the same one every other verdict in
32
+ * this codebase is scoped by.
33
+ */
34
+ export declare function runContextFor(input: {
35
+ actions: readonly JournalAction[];
36
+ events: readonly ReticleEvent[];
37
+ intents: readonly Intent[];
38
+ currentDocumentId: string | undefined;
39
+ currentEditEpoch: number | undefined;
40
+ }): RunContext;