@reticlehq/server 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/SKILL.md +16 -13
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/cli-doctor.js +25 -5
  7. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  8. package/dist/command/cli/cli-parse-grammar.js +7 -0
  9. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  10. package/dist/command/cli/cli-parse-verify.js +86 -13
  11. package/dist/command/cli/cli-parse.d.ts +7 -1
  12. package/dist/command/cli/cli-parse.js +33 -5
  13. package/dist/command/cli/cli-verify.d.ts +59 -0
  14. package/dist/command/cli/cli-verify.js +129 -17
  15. package/dist/command/cli/cloud-kit.d.ts +0 -2
  16. package/dist/command/cli/cloud-kit.js +1 -1
  17. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  18. package/dist/command/cli/launch/cli-launch.d.ts +1 -29
  19. package/dist/command/cli/launch/cli-launch.js +6 -40
  20. package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
  21. package/dist/command/cli/ports/resolve/cli-port.js +45 -1
  22. package/dist/command/cli/report-command.d.ts +16 -0
  23. package/dist/command/cli/report-command.js +97 -0
  24. package/dist/command/cli.js +5 -0
  25. package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
  26. package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
  27. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  28. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  29. package/dist/command/setup/init/init-host.js +8 -0
  30. package/dist/features/crawl/crawl.d.ts +2 -0
  31. package/dist/features/crawl/crawl.js +1 -0
  32. package/dist/features/harness/jev-driver.js +6 -9
  33. package/dist/features/harness/platform-config.d.ts +10 -0
  34. package/dist/features/harness/platform-config.js +8 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +40 -95
  37. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  38. package/dist/judgement/capsule/capsule-store.js +58 -6
  39. package/dist/judgement/capsule/capsule.js +5 -0
  40. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  41. package/dist/judgement/capsule/causal-summary.js +7 -0
  42. package/dist/judgement/domain/domain-model.js +38 -9
  43. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  44. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  45. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  46. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  47. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  48. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  49. package/dist/judgement/runs/artifact/render-report.js +4 -1
  50. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  51. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  52. package/dist/judgement/runs/context-tools.js +21 -3
  53. package/dist/judgement/runs/drive-run.js +7 -2
  54. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  55. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  56. package/dist/language/flows/flow-anchor.d.ts +12 -0
  57. package/dist/language/flows/flow-anchor.js +20 -0
  58. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  59. package/dist/language/flows/flow-expect-grammar.js +92 -11
  60. package/dist/language/flows/flow-intent.js +1 -1
  61. package/dist/language/flows/flow-learning.d.ts +3 -0
  62. package/dist/language/flows/flow-learning.js +39 -8
  63. package/dist/language/flows/flow-replay-run.d.ts +31 -9
  64. package/dist/language/flows/flow-replay-run.js +146 -33
  65. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  66. package/dist/language/flows/flow-replay.d.ts +12 -11
  67. package/dist/language/flows/flow-replay.js +79 -109
  68. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  69. package/dist/language/flows/flow-step-runners.js +20 -4
  70. package/dist/language/flows/flow-success.d.ts +21 -6
  71. package/dist/language/flows/flow-success.js +50 -138
  72. package/dist/language/flows/flows.d.ts +8 -4
  73. package/dist/language/flows/flows.js +11 -2
  74. package/dist/language/flows/heal.d.ts +9 -1
  75. package/dist/language/flows/heal.js +24 -2
  76. package/dist/language/flows/learned-sources.d.ts +21 -0
  77. package/dist/language/flows/learned-sources.js +84 -0
  78. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  79. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  80. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  81. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  82. package/dist/language/flows/replay.js +12 -8
  83. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  84. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  85. package/dist/language/flows/stores/flake-store.js +2 -3
  86. package/dist/language/flows/suite/coverage.d.ts +1 -1
  87. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  88. package/dist/language/flows/verify-change-tools.js +14 -0
  89. package/dist/memory/cloud/cloud-sync.d.ts +0 -2
  90. package/dist/memory/cloud/cloud-sync.js +1 -1
  91. package/dist/memory/cloud/harness-config.d.ts +2 -0
  92. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  93. package/dist/memory/cloud/sync-cycle.js +29 -4
  94. package/dist/memory/cloud/sync-daemon.d.ts +13 -0
  95. package/dist/memory/cloud/sync-daemon.js +21 -0
  96. package/dist/memory/cloud/sync-disk.js +11 -6
  97. package/dist/memory/impact/impact-store.js +6 -0
  98. package/dist/memory/intent/inline-intent.js +8 -3
  99. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  100. package/dist/memory/intent/intent-shard-store.js +252 -140
  101. package/dist/memory/intent/intent-shard.d.ts +10 -11
  102. package/dist/memory/intent/intent-shard.js +1 -1
  103. package/dist/memory/intent/intent-store.d.ts +13 -8
  104. package/dist/memory/intent/intent-store.js +46 -90
  105. package/dist/memory/intent/intent-tools.js +34 -23
  106. package/dist/memory/journal/ambient-store.js +2 -3
  107. package/dist/memory/journal/deviation-report.js +4 -1
  108. package/dist/memory/journal/deviation-service.js +10 -2
  109. package/dist/memory/journal/drive-flow.d.ts +2 -2
  110. package/dist/memory/journal/drive-flow.js +21 -12
  111. package/dist/memory/journal/envelope-store.js +2 -3
  112. package/dist/memory/journal/journal-recorder.d.ts +21 -0
  113. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  114. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  115. package/dist/memory/journal/on-disk/retention.d.ts +14 -10
  116. package/dist/memory/journal/on-disk/retention.js +13 -12
  117. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  118. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  119. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
  120. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
  121. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  122. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  123. package/dist/memory/journal/route-template.d.ts +22 -0
  124. package/dist/memory/journal/route-template.js +44 -0
  125. package/dist/memory/journal/session-end.d.ts +4 -1
  126. package/dist/memory/journal/session-end.js +64 -6
  127. package/dist/memory/journal/session-journal.d.ts +8 -1
  128. package/dist/memory/journal/session-journal.js +10 -0
  129. package/dist/memory/project/baselines.d.ts +1 -1
  130. package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
  131. package/dist/memory/project/fs/fs-port.js +44 -5
  132. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  133. package/dist/memory/project/fs/write-atomic.js +48 -0
  134. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  135. package/dist/memory/recall/prior/connection-memory.js +8 -0
  136. package/dist/memory/recall/registered-projects.d.ts +2 -1
  137. package/dist/portal/bridge/auth-failure-reason.js +5 -2
  138. package/dist/portal/input/real-input.js +2 -1
  139. package/dist/portal/pool/browser-pool.d.ts +10 -0
  140. package/dist/portal/pool/browser-pool.js +10 -0
  141. package/dist/portal/pool/playwright-launcher.js +1 -0
  142. package/dist/portal/pool/pool-contract.d.ts +2 -0
  143. package/dist/portal/pool/storage-seed.d.ts +2 -1
  144. package/dist/portal/pool/zero-install.d.ts +27 -0
  145. package/dist/portal/pool/zero-install.js +54 -0
  146. package/dist/portal/realm/web-realm.js +6 -0
  147. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  148. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  149. package/dist/portal/session/dev-server/served-document.js +57 -0
  150. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  151. package/dist/portal/session/facts/background-traffic.js +25 -0
  152. package/dist/portal/session/live-control-tools.js +48 -3
  153. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  154. package/dist/portal/session/session-health.d.ts +61 -0
  155. package/dist/portal/session/session-health.js +40 -1
  156. package/dist/portal/session/session-info.d.ts +6 -0
  157. package/dist/portal/session/session-manager.d.ts +9 -0
  158. package/dist/portal/session/session-manager.js +23 -2
  159. package/dist/portal/session/session-successor.d.ts +11 -0
  160. package/dist/portal/session/session-successor.js +14 -1
  161. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  162. package/dist/portal/session/session-verdict-facts.js +15 -0
  163. package/dist/portal/session/session.d.ts +5 -0
  164. package/dist/portal/session/session.js +19 -7
  165. package/dist/surface/mcp/mcp-proxy.js +3 -3
  166. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  167. package/dist/surface/mcp/server-instructions.js +12 -1
  168. package/dist/surface/tools/act/already-true.d.ts +40 -0
  169. package/dist/surface/tools/act/already-true.js +36 -0
  170. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  171. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  172. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  173. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  174. package/dist/surface/tools/act-capsule.js +22 -7
  175. package/dist/surface/tools/act-tools.js +25 -24
  176. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  177. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  178. package/dist/surface/tools/assert/assert-grade.js +14 -2
  179. package/dist/surface/tools/assert/assert-verdict.js +30 -4
  180. package/dist/surface/tools/browser-tools.js +4 -3
  181. package/dist/surface/tools/dynamic-tools.js +6 -0
  182. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  183. package/dist/surface/tools/feedback-tools.js +1 -1
  184. package/dist/surface/tools/invoke-tool.js +17 -4
  185. package/dist/surface/tools/lease-hint.d.ts +8 -0
  186. package/dist/surface/tools/lease-hint.js +10 -0
  187. package/dist/surface/tools/lease-tools.d.ts +16 -28
  188. package/dist/surface/tools/lease-tools.js +53 -5
  189. package/dist/surface/tools/lineage-tools.js +1 -1
  190. package/dist/surface/tools/observe-tools.js +14 -6
  191. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  192. package/dist/surface/tools/real-input-attempt.js +1 -1
  193. package/dist/surface/tools/tool-kit.d.ts +5 -0
  194. package/dist/surface/tools/tool-kit.js +6 -1
  195. package/dist/surface/tools/tool-surface.js +17 -5
  196. package/dist/surface/tools/tools.js +64 -29
  197. package/dist/telemetry/feedback-local.d.ts +1 -0
  198. package/dist/telemetry/feedback-local.js +1 -3
  199. package/dist/telemetry/feedback.d.ts +1 -1
  200. package/dist/telemetry/hud-metrics.d.ts +32 -0
  201. package/dist/telemetry/hud-metrics.js +95 -0
  202. package/dist/telemetry/session-metrics.d.ts +5 -0
  203. package/dist/telemetry/session-metrics.js +15 -1
  204. package/dist/wire-journal.d.ts +31 -0
  205. package/dist/wire-journal.js +100 -0
  206. package/docs/README.md +1 -0
  207. package/docs/cli/cloud.mdx +9 -7
  208. package/docs/cli/init.mdx +1 -0
  209. package/docs/cli/report.mdx +44 -0
  210. package/docs/desktop.mdx +1 -1
  211. package/docs/flows.md +11 -0
  212. package/docs/frameworks.mdx +113 -1
  213. package/docs/install-agentic.mdx +36 -0
  214. package/docs/install-manual.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages.mdx +1 -1
  217. package/docs/predicates.mdx +128 -4
  218. package/docs/tools/query.mdx +10 -10
  219. package/docs/usage.md +1 -1
  220. package/docs/what-is-recorded.md +62 -0
  221. package/package.json +6 -6
  222. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  223. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
@@ -0,0 +1,97 @@
1
+ /**
2
+ * `reticle report [--session <id>] [--hook]` — what this project's latest session claimed, and what held.
3
+ *
4
+ * Read from disk, with no daemon: the journal is on disk, and a Stop hook has to answer in the second
5
+ * after the agent stops, when there may be nothing running to ask. The fold is `gapSummary`, the same
6
+ * one `reticle_context` serves, so the terminal and the agent cannot be told two different stories.
7
+ */
8
+ import { execFileSync } from 'node:child_process';
9
+ import { existsSync, readdirSync, statSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { DiscoveryInvite, ReticleDir } from '@reticlehq/core';
12
+ import { gapSummary } from '../../judgement/runs/artifact/gap-summary.js';
13
+ import { SessionJournal } from '../../memory/journal/session-journal.js';
14
+ import { createNodeFileSystem } from '../../memory/project/fs/fs-port.js';
15
+ import { gapHookLine, gapReportLines } from '../../judgement/runs/artifact/gap-report.js';
16
+ /**
17
+ * A session last written longer ago than this is not the turn a hook is asking about.
18
+ *
19
+ * ponytail: a fixed window, not a turn boundary — the Stop hook is not told when the turn began. A
20
+ * drive older than an hour reads as "no claim this session", which errs toward saying so.
21
+ */
22
+ const HOOK_WINDOW_MS = 60 * 60 * 1000;
23
+ /** The session whose action ledger was written most recently, if any. */
24
+ function newestSession(root) {
25
+ const sessionsDir = join(root, ReticleDir.SESSIONS_SUBDIR);
26
+ if (!existsSync(sessionsDir))
27
+ return undefined;
28
+ let newest;
29
+ for (const id of readdirSync(sessionsDir)) {
30
+ const ledger = join(sessionsDir, id, ReticleDir.JOURNAL_ACTIONS_FILE);
31
+ if (!existsSync(ledger))
32
+ continue;
33
+ const writtenAt = statSync(ledger).mtimeMs;
34
+ if (newest === undefined || writtenAt > newest.writtenAt)
35
+ newest = { id, writtenAt };
36
+ }
37
+ return newest;
38
+ }
39
+ export async function reportFor(input) {
40
+ if (true === input.hook && !input.treeChanged)
41
+ return { lines: [] };
42
+ const root = join(input.cwd, ReticleDir.ROOT);
43
+ const found = input.session === undefined ? newestSession(root) : { id: input.session, writtenAt: input.now };
44
+ const current = found !== undefined && (true !== input.hook || input.now - found.writtenAt <= HOOK_WINDOW_MS)
45
+ ? found
46
+ : undefined;
47
+ const actions = current === undefined
48
+ ? []
49
+ : await new SessionJournal(createNodeFileSystem(), root, current.id).readActions();
50
+ const gap = gapSummary(actions);
51
+ if (true === input.hook) {
52
+ const line = gapHookLine(gap);
53
+ // A finish with nothing proved is the moment Reticle let somebody down: say who to tell.
54
+ return { lines: line === undefined ? [] : [line, DiscoveryInvite.HUMAN] };
55
+ }
56
+ const header = current === undefined ? [] : [`session ${current.id}`];
57
+ return { lines: [...header, ...gapReportLines(gap), DiscoveryInvite.HUMAN] };
58
+ }
59
+ /** Uncommitted changes in `cwd`. Outside a git repository nothing can say, so it answers yes. */
60
+ function treeChanged(cwd) {
61
+ try {
62
+ return '' !== execFileSync('git', ['status', '--porcelain'], { cwd, encoding: 'utf8' }).trim();
63
+ }
64
+ catch {
65
+ return true;
66
+ }
67
+ }
68
+ /**
69
+ * The command. As a hook it prints Claude Code's `systemMessage` shape and always exits 0: it is
70
+ * print-only by design, so the human sees the gap and the agent is never blocked by it.
71
+ */
72
+ export async function handleReport(session, hook) {
73
+ const cwd = process.cwd();
74
+ let lines;
75
+ try {
76
+ ({ lines } = await reportFor({
77
+ cwd,
78
+ now: Date.now(),
79
+ treeChanged: hook ? treeChanged(cwd) : true,
80
+ ...(session === undefined ? {} : { session }),
81
+ hook,
82
+ }));
83
+ }
84
+ catch (error) {
85
+ // A hook that throws turns the agent's stop into an error the user did not cause; it says nothing
86
+ // instead. Run by hand, the reason is the answer (an id that is not a session, a journal it
87
+ // could not read), so it is printed and the exit is non-zero.
88
+ if (hook)
89
+ return;
90
+ process.stderr.write(`reticle report: ${error instanceof Error ? error.message : String(error)}\n`);
91
+ process.exitCode = 1;
92
+ return;
93
+ }
94
+ if (0 === lines.length)
95
+ return;
96
+ process.stdout.write(hook ? `${JSON.stringify({ systemMessage: lines.join('\n') })}\n` : `${lines.join('\n')}\n`);
97
+ }
@@ -14,6 +14,7 @@ import { attachStatusFields } from '../surface/mcp/attach-memory.js';
14
14
  import { splitBrainFields, withNextAction } from './cli/status-fields.js';
15
15
  import { reticleStateHome } from './daemon/daemon.js';
16
16
  import { handleMcp } from './cli/mcp-command.js';
17
+ import { handleReport } from './cli/report-command.js';
17
18
  import { resolveDaemonForProject } from './daemon/daemon-resolve.js';
18
19
  import { daemonStartOptions } from './cli/daemon-start-options.js';
19
20
  import { handleWatch, handleCapsules, handleGate, loadNamedFlows, resolveChangedFiles, } from './cli/cli-flow-commands.js';
@@ -69,6 +70,7 @@ function handleInit(parsed) {
69
70
  // past a missing package manager, and could not honour that while never being told about it.
70
71
  ...(parsed.url === undefined ? {} : { url: parsed.url }),
71
72
  captureBodies: true === parsed.captureBodies,
73
+ hooks: true === parsed.hooks,
72
74
  // The outcome is reported by confirmInstall instead, once it knows whether an app connected —
73
75
  // `init` writing files was never the same thing as `init` working (#269).
74
76
  deferOutcome: true,
@@ -669,6 +671,9 @@ export function main() {
669
671
  case 'gate':
670
672
  void handleGate(parsed.files, parsed.since, parsed.hook);
671
673
  break;
674
+ case 'report':
675
+ void handleReport(parsed.session, parsed.hook);
676
+ break;
672
677
  case 'watch':
673
678
  handleWatch();
674
679
  break;
@@ -1,2 +1,32 @@
1
1
  /** GET the daemon's /status JSON. Resolves to the parsed body, or undefined on any failure. */
2
2
  export declare function fetchStatus(port: number): Promise<unknown>;
3
+ /** One connected tab as `reticle status` reports it — the at-a-glance health line. */
4
+ interface StatusSession {
5
+ sessionId: string;
6
+ url: string;
7
+ projectId?: string;
8
+ throttled: boolean;
9
+ /**
10
+ * The tab is backgrounded. Reported by the daemon on every session; `false` on one too old to say,
11
+ * which reads as visible — the conservative answer, since it keeps the reuse this command has
12
+ * always done rather than opening a tab on a guess.
13
+ */
14
+ hidden: boolean;
15
+ stale: boolean;
16
+ pendingMarks: number;
17
+ /** The tab is attached and answering nothing — see Session.unresponsive. Absent on an older daemon. */
18
+ unresponsive?: true;
19
+ }
20
+ /**
21
+ * Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
22
+ * untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
23
+ * degrades to "running, 0 sessions" instead of throwing.
24
+ */
25
+ export declare function summarizeStatus(payload: unknown): {
26
+ sessionCount: number;
27
+ sessions: StatusSession[];
28
+ why?: string;
29
+ /** The same diagnosis without the differential, for a surface a person reads. */
30
+ whyLead?: string;
31
+ };
32
+ export {};
@@ -46,3 +46,48 @@ export function fetchStatus(port) {
46
46
  });
47
47
  });
48
48
  }
49
+ /**
50
+ * Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
51
+ * untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
52
+ * degrades to "running, 0 sessions" instead of throwing.
53
+ */
54
+ export function summarizeStatus(payload) {
55
+ if (typeof payload !== 'object' || null === payload)
56
+ return { sessionCount: 0, sessions: [] };
57
+ const obj = payload;
58
+ // Carried through to the printed line: with no sessions this is the whole answer, and dropping it
59
+ // here would silently undo the reason it is on the wire.
60
+ const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
61
+ // Absent on a daemon older than this field, which is why every reader falls back to `why`.
62
+ const whyLead = 'string' === typeof obj['whyLead'] ? obj['whyLead'] : undefined;
63
+ const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
64
+ const sessions = raw
65
+ .map((s) => {
66
+ if (typeof s !== 'object' || null === s)
67
+ return null;
68
+ const r = s;
69
+ const sessionId = 'string' === typeof r['sessionId'] ? r['sessionId'] : '';
70
+ if ('' === sessionId)
71
+ return null;
72
+ return {
73
+ sessionId,
74
+ url: 'string' === typeof r['url'] ? r['url'] : '',
75
+ ...('string' === typeof r['projectId'] && 0 < r['projectId'].length
76
+ ? { projectId: r['projectId'] }
77
+ : {}),
78
+ throttled: true === r['throttled'],
79
+ hidden: true === r['hidden'],
80
+ stale: true === r['stale'],
81
+ pendingMarks: 'number' === typeof r['pendingMarks'] ? r['pendingMarks'] : 0,
82
+ ...(true === r['unresponsive'] ? { unresponsive: true } : {}),
83
+ };
84
+ })
85
+ .filter((s) => s !== null);
86
+ const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
87
+ return {
88
+ sessionCount,
89
+ sessions,
90
+ ...(why === undefined ? {} : { why }),
91
+ ...(whyLead === undefined ? {} : { whyLead }),
92
+ };
93
+ }
@@ -65,7 +65,7 @@ export declare function probePresence(port: number, probes: {
65
65
  * these lets the sentence tell them apart when — and only when — the recorded pid for the port
66
66
  * matches the process actually holding it.
67
67
  */
68
- export interface PortHolder {
68
+ interface PortHolder {
69
69
  /** The pid we recorded for this port, from the daemon pid file. `null` when there is none. */
70
70
  ourPid: number | null;
71
71
  /** The pid actually holding the port, when the caller could determine it. */
@@ -1,4 +1,4 @@
1
- export interface IdleShutdownOptions {
1
+ interface IdleShutdownOptions {
2
2
  /** True when nothing is using the daemon: no agent connected, no browser session, no pool lease. */
3
3
  isIdle: () => boolean;
4
4
  /** Clean teardown + process exit. Called at most once. */
@@ -52,3 +52,4 @@ export declare function resolveIdleCheckMs(raw: string | undefined): number;
52
52
  * broken.
53
53
  */
54
54
  export declare function resolveIdleShutdownMs(raw: string | undefined): number;
55
+ export {};
@@ -3,6 +3,9 @@ import { reportInitOutcome } from '../../../telemetry/init-telemetry.js';
3
3
  import { reportOnboardingStep } from '../../../telemetry/onboarding-funnel.js';
4
4
  import { defaultPairingTokenDir, readOrCreatePairingTokenSync, } from '../../../portal/bridge/pairing-token.js';
5
5
  import { declaredInstallSource } from '../../../telemetry/install-source.js';
6
+ /** The variable Claude Code sets on every process it spawns, and the value it sets it to. */
7
+ const CLAUDE_CODE_MARKER_ENV = 'CLAUDECODE';
8
+ const CLAUDE_CODE_MARKER_VALUE = '1';
6
9
  export function serverInitHost() {
7
10
  return {
8
11
  span(name, fields, fn) {
@@ -33,5 +36,10 @@ export function serverInitHost() {
33
36
  installSource() {
34
37
  return declaredInstallSource();
35
38
  },
39
+ // Claude Code marks every process it spawns. If the marker ever changes, init falls back to the
40
+ // notice it printed before, which is the safe direction.
41
+ insideClaudeCode() {
42
+ return CLAUDE_CODE_MARKER_VALUE === process.env[CLAUDE_CODE_MARKER_ENV];
43
+ },
36
44
  };
37
45
  }
@@ -16,6 +16,8 @@ export interface CrawlSession {
16
16
  /** The page under test, so a third-party beacon cannot be reported against a control. Optional
17
17
  * for the same reason `currentDocumentId` is. */
18
18
  readonly url?: string | undefined;
19
+ /** Same-origin endpoints the project declared as background. Optional like `url`. */
20
+ readonly background?: readonly string[] | undefined;
19
21
  /**
20
22
  * Is the page BACKGROUNDED? A hidden tab has its rAF clamped and the DOM observer flushes on rAF,
21
23
  * so real renders emit nothing and every absence-derived contradiction reads that silence as a
@@ -347,6 +347,7 @@ export async function crawl(session, opts, sleep) {
347
347
  currentDocumentId: session.currentDocumentId,
348
348
  currentEditEpoch: session.currentEditEpoch,
349
349
  appOrigin: session.url,
350
+ background: session.background,
350
351
  // A hidden tab cannot be observed, so its silence is not evidence. Stated from the session
351
352
  // rather than left to a heartbeat landing inside a 300ms window.
352
353
  pageHidden: session.throttled?.(),
@@ -496,12 +496,10 @@ export function jevDriver(options) {
496
496
  const unsaved = drive.unsaved[0];
497
497
  if (unsaved !== undefined)
498
498
  return only(
499
- // `intent` is not decoration. A flow saved without one still replays, but when it goes
500
- // red the report can only name the step that broke, not the thing that stopped being true.
501
- request(Tool.FLOW_SAVE, {
502
- flowName: unsaved,
503
- intent: `Autonomous coverage drive of ${slugAfter(unsaved)}: ${String(drive.acted.length)} actions.`,
504
- }), `save ${unsaved}`);
499
+ // No intent. A drive has no business rule of its own to state, and what it used to send
500
+ // ("Autonomous coverage drive: N actions") was a count of what it did, filed in the ledger
501
+ // as a rule and re-amended every time the count changed.
502
+ request(Tool.FLOW_SAVE, { flowName: unsaved }), `save ${unsaved}`);
505
503
  /*
506
504
  * THE PLAN'S CHEAP HALF, and it runs before the model is asked anything.
507
505
  *
@@ -640,9 +638,8 @@ export function jevDriver(options) {
640
638
  ref: picked.ref,
641
639
  action,
642
640
  ...(wasRefused ? { args: { [DANGEROUS_ARG]: true } } : {}),
643
- // The element's own description, so the drive reads back as a journey rather than as refs.
644
- // A ref is a handle that expired when the page changed; this survives being read tomorrow.
645
- intent: `${action} ${picked.desc}`,
641
+ // No `intent`: "click button \"Save\"" describes a step, not a rule, and the ledger refuses it.
642
+ // The recorded step keeps the element's role and name, which is what reads back as a journey.
646
643
  ...(consequence === undefined ? {} : { until: consequence.predicate }),
647
644
  };
648
645
  if ('fill' === action) {
@@ -30,6 +30,16 @@ export interface PlatformModelConfig {
30
30
  * report "you turned this off" to somebody whose free months quietly ran out.
31
31
  */
32
32
  harnessEntitled: boolean;
33
+ /**
34
+ * Whether the platform actually holds a key for the provider this project is pointed at.
35
+ *
36
+ * Entitlement and readiness are different facts and the HUD needs both. A workspace can be fully
37
+ * entitled -- claimed period, `harnessEnabled` on -- against a deployment that has no provider key
38
+ * configured, and every signal then reads healthy while a drive cannot run. Absent reads as READY,
39
+ * on the same rule as the two booleans above: an older API that does not report availability must
40
+ * not have its silence treated as a refusal.
41
+ */
42
+ providerReady: boolean;
33
43
  }
34
44
  /** A GET, narrowed to what this file uses, so a test can answer it without a network. */
35
45
  export type ConfigFetch = (url: string, init: {
@@ -41,10 +41,18 @@ function parse(body) {
41
41
  // Same rule for both, for the same reason: an older API that reports neither must not have its
42
42
  // silence read as a refusal.
43
43
  const entitled = record['harnessEntitled'];
44
+ // `available` is keyed by provider and says which ones the platform holds a key for. Read only
45
+ // the entry for the provider this project uses: another provider being configured says nothing
46
+ // about whether THIS one can drive.
47
+ const available = record['available'];
48
+ const ready = 'object' === typeof available && null !== available
49
+ ? available[provider]
50
+ : undefined;
44
51
  return {
45
52
  provider,
46
53
  harnessEnabled: 'boolean' === typeof enabled ? enabled : true,
47
54
  harnessEntitled: 'boolean' === typeof entitled ? entitled : true,
55
+ providerReady: 'boolean' === typeof ready ? ready : true,
48
56
  };
49
57
  }
50
58
  catch {
package/dist/index.d.ts CHANGED
@@ -21,7 +21,7 @@ export type { RecordedStep, CompiledProgram } from './language/flows/recording/t
21
21
  export { FlowStore, recordedStepToFlowStep } from './language/flows/flows.js';
22
22
  export type { FlowResult } from './language/flows/flows.js';
23
23
  export type { Clock } from './machine/clock.js';
24
- export { assertSuccess, successToPredicate, dynamicTestids, successLabel, } from './language/flows/flow-success.js';
24
+ export { assertSuccess, dynamicTestids, successLabel, successToPredicate, } from './language/flows/flow-success.js';
25
25
  export { classifyFlowAssertions, FlowAssertionGrade } from './language/flows/flow-classify.js';
26
26
  export type { FlowAssertionClassification } from './language/flows/flow-classify.js';
27
27
  export { buildDomainModel } from './judgement/domain/domain-model.js';
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { PROJECT_REGISTRY_FILE, parseProjectRegistry, projectCandidates, } from
11
11
  import { discoverProjectConfigs } from './command/cli/config/config-discovery.js';
12
12
  import { artifactRootResolver } from './memory/project/artifact-root-resolver.js';
13
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
- import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
14
+ import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, bridgeWsUrl, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
15
15
  import { originOf } from './portal/session/session-manager.js';
16
16
  import { setBrowserMode, BrowserMode } from './telemetry/browser-mode.js';
17
17
  import { replayAndLearn } from './language/flows/flow-learning.js';
@@ -51,19 +51,16 @@ import { BrowserPool } from './portal/pool/browser-pool.js';
51
51
  import { AGENT_ALREADY_DRIVING_ELSEWHERE, shouldGreetWithLeaseNotice, } from './portal/session/lease-visibility.js';
52
52
  import { playwrightLauncher, resolveMaxContexts } from './portal/pool/playwright-launcher.js';
53
53
  import { LeaseReaper } from './portal/pool/lease-reaper.js';
54
- import { readJournalEnabled, readProjectId } from './command/cli/ports/resolve/cli-port.js';
55
- import { hasProjectConnectedBefore } from './memory/recall/prior/connection-memory.js';
54
+ import { findProjectConfig, projectIdsAt, readJournalEnabled, readProjectId, } from './command/cli/ports/resolve/cli-port.js';
55
+ import { hasAnyProjectConnectedBefore } from './memory/recall/prior/connection-memory.js';
56
56
  import { reticleStateHome } from './command/daemon/daemon.js';
57
57
  import { probeChromium } from './command/cli/doctor/browser/chromium-hint.js';
58
- import { makeJournalAttach } from './memory/journal/attach-journal.js';
59
- import { makeSessionEnd, recordDriveRun } from './memory/journal/session-end.js';
60
- import { attachDriveRunFlush } from './memory/journal/drive-run-flush.js';
58
+ import { attachJournal } from './wire-journal.js';
61
59
  import { reportOnboardingStep } from './telemetry/onboarding-funnel.js';
62
60
  import { AMBIENT_RECORDING } from './language/flows/recording/tape/recordings.js';
63
- import { AmbientStore } from './memory/journal/ambient-store.js';
64
- import { ensureWorkspaceGitignore } from './memory/journal/on-disk/workspace-gitignore.js';
65
- import { pruneFeedback, pruneSessions, pruneVisualDiffs, } from './memory/journal/on-disk/retention.js';
61
+ import { readRetainPolicy } from './memory/journal/on-disk/retain-policy.js';
66
62
  import { log } from './log.js';
63
+ import { readReaderBundle, zeroInstallScript as zeroInstallScriptFor, } from './portal/pool/zero-install.js';
67
64
  /** A human-facing one-liner for a panel replay verdict — ✓ passed / ⚠ drifted / ✗ errored. */
68
65
  function replayVerdictLine(result) {
69
66
  if (result.status === ReplayStatus.OK)
@@ -86,7 +83,7 @@ export { runTool, SESSION_BOUND_TOOLS, SESSION_EXEMPT_TOOLS } from './surface/to
86
83
  export { BaselineStore, normalizeLines, diffLines } from './memory/project/baselines.js';
87
84
  export { RecordingStore } from './language/flows/recording/tape/recordings.js';
88
85
  export { FlowStore, recordedStepToFlowStep } from './language/flows/flows.js';
89
- export { assertSuccess, successToPredicate, dynamicTestids, successLabel, } from './language/flows/flow-success.js';
86
+ export { assertSuccess, dynamicTestids, successLabel, successToPredicate, } from './language/flows/flow-success.js';
90
87
  export { classifyFlowAssertions, FlowAssertionGrade } from './language/flows/flow-classify.js';
91
88
  export { buildDomainModel } from './judgement/domain/domain-model.js';
92
89
  export { ProjectStore } from './memory/project/project-store.js';
@@ -108,12 +105,29 @@ export { resolveBridgeSecurity } from './portal/bridge/bridge-security.js';
108
105
  * Build the shared browser pool (one headless Chromium, N capped isolated leased contexts). Lazy —
109
106
  * no Chromium launches until the first lease — so creating it is free even when never used.
110
107
  */
111
- function createBrowserPool(headless) {
108
+ /** Where the zero-install reader connects: this daemon's own bridge, with its pairing token. */
109
+ function readerConnect(port, token) {
110
+ return { url: bridgeWsUrl(port), ...(token === undefined ? {} : { token }) };
111
+ }
112
+ function createBrowserPool(headless, reader) {
112
113
  const maxContexts = resolveMaxContexts(process.env[ReticleEnv.MAX_CONTEXTS], cpus().length);
113
114
  const genSessionId = () => `lease-${'function' === typeof globalThis.crypto?.randomUUID
114
115
  ? globalThis.crypto.randomUUID()
115
116
  : String(Date.now())}`;
116
- return new BrowserPool(playwrightLauncher({ headless }), { maxContexts, genSessionId });
117
+ // Built on first use and kept: the page that needs it is one whose app never connected.
118
+ let script;
119
+ const zeroInstallScript = () => {
120
+ if (script === undefined) {
121
+ const bundle = readReaderBundle();
122
+ script = bundle === undefined ? null : zeroInstallScriptFor(bundle, reader);
123
+ }
124
+ return script ?? undefined;
125
+ };
126
+ return new BrowserPool(playwrightLauncher({ headless }), {
127
+ maxContexts,
128
+ genSessionId,
129
+ zeroInstallScript,
130
+ });
117
131
  }
118
132
  /**
119
133
  * Route CDP-authoritative network detail onto the driven session's journal.
@@ -153,82 +167,6 @@ function makeNetworkDetailRouter(bridge, driveUrl) {
153
167
  }
154
168
  };
155
169
  }
156
- /**
157
- * Wire journal capture, ambient seeding and the journal-tail flush onto a bridge.
158
- *
159
- * Both entry points need all three, and both used to hand-roll them. `startDaemon` only ever wired the
160
- * first, so on the path every user actually takes (`reticle serve` / `reticle mcp`) the journal tail was
161
- * dropped at session end and the learned ambient map was never persisted OR seeded — meaning ambient
162
- * learning could not converge across sessions and the last events of every session were lost. The two
163
- * call sites had already drifted once before, which is why this is one function rather than a
164
- * copy-paste both are asked to keep in step.
165
- */
166
- function attachJournal(bridge, deps) {
167
- const journalAttach = makeJournalAttach(deps);
168
- const ambientStore = new AmbientStore(deps.fs, deps.reticleRoot);
169
- // Built once, not per session: the resolver walks config discovery and the user-level registry,
170
- // and neither changes between two tabs connecting a second apart.
171
- const resolveArtifactRoot = artifactRootResolver(deps.reticleRoot);
172
- bridge.attachSessionCreate((session) => {
173
- // Stamp the project's own `.reticle` before ANY counter fires for this session. Without it every
174
- // verdict is recorded against wherever the daemon was started, which is how one app's evidence
175
- // reached a different account's production dashboard.
176
- // The origin is passed for the case where the page never stamped a project id: it is the only
177
- // distinguishing fact left, and without it every such app shares one bucket.
178
- session.artifactRoot = resolveArtifactRoot(session.projectId, originOf(session.url)).root;
179
- // Here rather than on the start path, which was neither the moment we were about to write into a
180
- // repository nor the root we were about to write into: it created `.reticle/` — holding nothing
181
- // but the ignore file — wherever the daemon was launched, coming back every boot after the user
182
- // deleted it, while the journals this ignore protects landed in another tree, uncovered.
183
- if (deps.enabled)
184
- void ensureWorkspaceGitignore(deps.fs, session.artifactRoot);
185
- journalAttach(session);
186
- // Seed the learned ambient map so a fresh session starts knowing which regions churn, instead of
187
- // re-learning from zero. Best-effort + async: a late seed still helps, a failure is silent.
188
- if (deps.enabled) {
189
- void ambientStore
190
- .load()
191
- .then((counts) => session.seedAmbient(counts))
192
- .catch(() => undefined);
193
- }
194
- });
195
- // Teardown: flush the journal tail to disk + persist what this session learned.
196
- bridge.attachSessionEnd(makeSessionEnd({
197
- ...deps,
198
- // Retention runs from teardown, and it must not delete the journal of a session that is still
199
- // being written. The registry is the only thing that knows which those are.
200
- liveSessionIds: () => new Set(bridge.sessions.all().map((s) => s.id)),
201
- }));
202
- /*
203
- * And the same write, DURING the session rather than only at the end of it.
204
- *
205
- * Teardown was the only writer of a run artifact, so every verdict produced while a tab stayed
206
- * open was invisible to cloud sync — which can only push artifacts that exist. Reported as "sync
207
- * is not happening" and diagnosed as the agent forgetting to sync; there is no sync command to
208
- * forget. The evidence simply was not on disk yet.
209
- *
210
- * Safe to call repeatedly because the run id is derived from the session, so this rewrites one
211
- * artifact rather than accumulating them — a property `recordDriveRun` already had, for the
212
- * unrelated reason that a reconnecting tab must not publish two overlapping rows.
213
- */
214
- if (deps.enabled) {
215
- attachDriveRunFlush({
216
- resolve: (sessionId) => bridge.sessions.get(sessionId),
217
- write: (session) => recordDriveRun(deps, session),
218
- });
219
- }
220
- if (deps.enabled) {
221
- // Empty in the ordinary case (nothing has connected yet), but this path also runs on a daemon
222
- // that is already serving sessions.
223
- void pruneSessions(deps.fs, deps.reticleRoot, {
224
- live: new Set(bridge.sessions.all().map((s) => s.id)),
225
- });
226
- // The largest thing in the workspace, and until now the only one with no delete path at all.
227
- void pruneVisualDiffs(deps.fs, deps.reticleRoot);
228
- // Write-only local copies of reports the outbox already carries.
229
- void pruneFeedback(deps.fs, deps.reticleRoot);
230
- }
231
- }
232
170
  /**
233
171
  * Resolve the drive/real-input provider from options — shared by start and startDaemon so the
234
172
  * precedence (driveUrl launch+own → CDP attach → none) and the storageState/injectConnect plumbing
@@ -364,12 +302,16 @@ export async function start(options = {}) {
364
302
  const fs = createNodeFileSystem();
365
303
  const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
366
304
  const now = options.now ?? (() => Date.now());
367
- const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]);
305
+ const retain = readRetainPolicy(findProjectConfig(process.cwd()));
306
+ // `retain.sessions: 0` says keep no journals. Honoured at CAPTURE as well as at the sweep —
307
+ // writing a directory in order to delete it on the way out is work nobody asked for.
308
+ const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]) && 0 < retain.sessions;
368
309
  const flows = new FlowStore(fs, reticleRoot, { now });
369
310
  attachJournal(bridge, {
370
311
  fs,
371
312
  reticleRoot,
372
313
  enabled: journalEnabled,
314
+ retain,
373
315
  // Bound HERE, where the recorder is already in hand: teardown gets the tape, not the recorder.
374
316
  takeAmbientTape: () => recordings.stop(AMBIENT_RECORDING),
375
317
  reportStep: reportOnboardingStep,
@@ -378,7 +320,7 @@ export async function start(options = {}) {
378
320
  const project = new ProjectStore(fs, reticleRoot, { now });
379
321
  attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
380
322
  const annotations = new AnnotationStore();
381
- pool = createBrowserPool(options.headless ?? true);
323
+ pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token));
382
324
  leaseReaper = new LeaseReaper(pool);
383
325
  leaseReaper.start();
384
326
  /*
@@ -419,7 +361,7 @@ export async function start(options = {}) {
419
361
  ...(activeProjectId === undefined ? {} : { projectId: activeProjectId }),
420
362
  };
421
363
  const profile = resolveToolSurface(options.toolProfile);
422
- const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile, hasProjectConnectedBefore(reticleStateHome(), port, activeProjectId));
364
+ const server = createMcpServer(realInput !== undefined ? { ...deps, realInput } : deps, profile, hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd())));
423
365
  // When the agent (the MCP client) disconnects cleanly, end every active session at once so the
424
366
  // HUD doesn't linger. (If the agent instead KILLS this process, the WS dies and the browser
425
367
  // self-ends via SESSION_LIFECYCLE.BRIDGE_LOST_MS — see transport.ts.)
@@ -497,7 +439,9 @@ export async function startDaemon(options = {}) {
497
439
  const fs = createNodeFileSystem();
498
440
  const reticleRoot = options.reticleRoot ?? join(process.cwd(), ReticleDir.ROOT);
499
441
  const now = options.now ?? (() => Date.now());
500
- const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]);
442
+ const retain = readRetainPolicy(findProjectConfig(process.cwd()));
443
+ // See the sibling path: `retain.sessions: 0` stops the capture too, not only the sweep.
444
+ const journalEnabled = readJournalEnabled(process.cwd(), process.env[ReticleEnv.JOURNAL]) && 0 < retain.sessions;
501
445
  const flows = new FlowStore(fs, reticleRoot, { now });
502
446
  // Built here rather than inside `deps` below, so teardown can save what a drive recorded. Both
503
447
  // paths pass the same pair — `daemon-parity.test.ts` is what keeps them from drifting apart.
@@ -514,6 +458,7 @@ export async function startDaemon(options = {}) {
514
458
  fs,
515
459
  reticleRoot,
516
460
  enabled: journalEnabled,
461
+ retain,
517
462
  takeAmbientTape: () => recordings.stop(AMBIENT_RECORDING),
518
463
  reportStep: reportOnboardingStep,
519
464
  flows,
@@ -522,7 +467,7 @@ export async function startDaemon(options = {}) {
522
467
  const project = new ProjectStore(fs, reticleRoot, { now });
523
468
  attachRouteLearning(bridge, projectStoreResolver(fs, project, reticleRoot, now));
524
469
  const annotations = new AnnotationStore();
525
- const pool = createBrowserPool(options.headless ?? true);
470
+ const pool = createBrowserPool(options.headless ?? true, readerConnect(port, security.token));
526
471
  const leaseReaper = new LeaseReaper(pool);
527
472
  leaseReaper.start();
528
473
  /*
@@ -596,7 +541,7 @@ export async function startDaemon(options = {}) {
596
541
  const effectiveDeps = realInput !== undefined ? { ...deps, realInput } : deps;
597
542
  // Read per attach, not once: a project that gets wired while this daemon is alive should stop
598
543
  // being told to wire itself on the next agent that connects.
599
- shared.attachMcp(() => createMcpServer(effectiveDeps, profile, hasProjectConnectedBefore(reticleStateHome(), port, readProjectId(process.cwd()))));
544
+ shared.attachMcp(() => createMcpServer(effectiveDeps, profile, hasAnyProjectConnectedBefore(reticleStateHome(), port, projectIdsAt(process.cwd()))));
600
545
  // `reticle drive <url>` when this daemon already owns the port: it asks HERE instead of trying to
601
546
  // bind a port we are holding, and gets the same pooled context an agent's reticle_lease returns —
602
547
  // through runTool, so it is counted and reported like any other call rather than being a second,
@@ -698,7 +643,7 @@ export async function startDaemon(options = {}) {
698
643
  if (vh !== undefined)
699
644
  await new Promise((resolve) => vh.server.close(() => resolve()));
700
645
  leaseReaper.stop();
701
- cloudSync.stop();
646
+ await cloudSync.flush(); // not stop(): the last run written is the one nobody has yet
702
647
  await loopbackAlias.close?.();
703
648
  await pool.shutdown();
704
649
  await owned?.dispose();