@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,37 @@
1
+ import type { InstrumentationGap } from '@reticlehq/core';
2
+ /**
3
+ * Of the controls this session drove, how many could Reticle fully observe?
4
+ *
5
+ * The first honest denominator this product has ever had. `reticle_verify{action:"coverage"}`
6
+ * already answers "which controls did you not drive"; this answers the question underneath it —
7
+ * *of the ones you did drive, how many could the app actually answer for?* A high untouched-count is
8
+ * work remaining for the agent. A low observability is work remaining in the APP, and no amount of
9
+ * driving closes it.
10
+ *
11
+ * ## Two things it deliberately refuses to do
12
+ *
13
+ * **It does not report a percentage for an empty run.** 0/0 rendered as 100% is the most flattering
14
+ * possible reading of no evidence at all, and a coverage number that starts perfect is one nobody
15
+ * believes the first time it drops.
16
+ *
17
+ * **It does not count app-level gaps against a control.** An unregistered store or an unsignalled
18
+ * route is missing from the application, not from a button. Attributing it to whichever control
19
+ * happened to be driven would put the number on the wrong thing and make it move for reasons the
20
+ * agent cannot act on. Those gaps are reported alongside, not divided into.
21
+ */
22
+ export interface Observability {
23
+ /** Distinct controls driven this session. */
24
+ driven: number;
25
+ /** How many of them Reticle could fully observe. */
26
+ observable: number;
27
+ /** Rounded percentage. OMITTED when nothing was driven — see above. */
28
+ percent?: number;
29
+ }
30
+ export declare function observabilityOf(drivenRefs: Iterable<string>, gaps: readonly InstrumentationGap[]): Observability;
31
+ export interface CoverageRegression {
32
+ was: number;
33
+ now: number;
34
+ }
35
+ export declare function coverageRegressed(best: {
36
+ percent: number;
37
+ } | undefined, current: Observability): CoverageRegression | undefined;
@@ -0,0 +1,40 @@
1
+ export function observabilityOf(
2
+ // Iterable, not an array: the session holds these as a Set, and making the caller spread it would
3
+ // be a conversion that exists only to satisfy a signature.
4
+ drivenRefs, gaps) {
5
+ const driven = new Set(drivenRefs);
6
+ const impaired = new Set();
7
+ for (const gap of gaps) {
8
+ // App-level gaps carry no ref, and a gap on a control this session never drove is about some
9
+ // earlier route — neither belongs in a ratio over what WAS driven.
10
+ if (gap.ref !== undefined && driven.has(gap.ref))
11
+ impaired.add(gap.ref);
12
+ }
13
+ const observable = driven.size - impaired.size;
14
+ if (0 === driven.size)
15
+ return { driven: 0, observable: 0 };
16
+ return { driven: driven.size, observable, percent: Math.round((observable / driven.size) * 100) };
17
+ }
18
+ /**
19
+ * The floor under the number, shipped with it rather than after it.
20
+ *
21
+ * A coverage figure with no downgrade check is a figure that gets gamed, and we would be the ones
22
+ * teaching agents to game it: the cheapest way to stop a gap firing is to stop asserting the thing
23
+ * that revealed it. The same reasoning already runs for flow assertions in `assertion-tiers-store`,
24
+ * where a flow that passes more weakly than it used to is a finding rather than a pass.
25
+ *
26
+ * Returns undefined — not a false — when there is nothing honest to compare. A first run has no
27
+ * best to fall from, and a run that drove almost nothing is evidence of a short session rather than
28
+ * of a regression.
29
+ */
30
+ /** Below this, a run is too small for its ratio to mean anything. */
31
+ const MIN_DRIVEN_TO_COMPARE = 3;
32
+ export function coverageRegressed(best, current) {
33
+ if (best === undefined)
34
+ return undefined;
35
+ if (current.percent === undefined)
36
+ return undefined;
37
+ if (current.driven < MIN_DRIVEN_TO_COMPARE)
38
+ return undefined;
39
+ return current.percent < best.percent ? { was: best.percent, now: current.percent } : undefined;
40
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Which tools this session called, and how reachable each one was.
3
+ *
4
+ * The generalisation of the capture ledger: it recorded a short list of read tools to answer one
5
+ * feature's disproof, and the question that followed is the same question asked of the whole table —
6
+ * most of the surface has never been observed being called at all, and a decision about what to cut
7
+ * or consolidate is about to be made. This is the instrument for that decision, and nothing else.
8
+ *
9
+ * ## Why the count alone is not enough
10
+ *
11
+ * A tool that goes uncalled because nobody was told it exists and a tool that goes uncalled because
12
+ * nobody wants it look identical in a call count. They are opposite conclusions. So every count is
13
+ * reported beside the tool's REACHABILITY — advertised on the default surface, advertised only on
14
+ * the extended one, or reachable solely by naming it to `reticle_run` — and the interesting cell is
15
+ * the one where an agent found a run-only tool anyway.
16
+ *
17
+ * ## The tiers are derived, never restated
18
+ *
19
+ * `tierOfTool` reads `CORE_TOOL_NAMES` and `EXTENDED_TOOL_NAMES` directly. A tool promoted or
20
+ * demoted between surfaces changes tier here with nothing edited. A local copy of either list would
21
+ * go stale on the first move and would then be wrong about the exact axis this exists to measure —
22
+ * and it would be wrong silently, in a report used to justify deleting things.
23
+ *
24
+ * ## What these numbers do NOT establish
25
+ *
26
+ * - **One session is not a hit rate.** This is evidence about ONE run by ONE agent against ONE app.
27
+ * A hit rate is a distribution over many sessions; a single session quoted as one is an anecdote
28
+ * with a denominator drawn on it.
29
+ * - **Never-called is not useless.** A tool one team reaches for once a month in an emergency and a
30
+ * tool nobody has ever called are the same zero here. Frequency alone cannot separate them, so a
31
+ * cut decided on frequency alone will delete the wrong things.
32
+ * - **Absent is not zero.** A session with no ledger reports `observed: false`. It has not
33
+ * established that no tools were called; it has established that nothing was watching.
34
+ * - The tools that ran BEFORE this instrument existed in the process, or against a session this
35
+ * daemon adopted after the fact, are simply missing rather than counted low.
36
+ */
37
+ /** How an agent could have reached a tool. Derived from the surface sets — see above. */
38
+ export declare const ToolTier: {
39
+ /** Advertised directly on the surface every user gets. */
40
+ readonly DEFAULT: "default";
41
+ /** Advertised only when the full schema-carrying surface is switched on. */
42
+ readonly EXTENDED: "extended";
43
+ /** Advertised nowhere: reachable only by naming it to `reticle_run`. */
44
+ readonly RUN_ONLY: "run-only";
45
+ };
46
+ export type ToolTier = (typeof ToolTier)[keyof typeof ToolTier];
47
+ /** Which surface would have put this tool in front of an agent, read off that surface's own set. */
48
+ export declare function tierOfTool(tool: string): ToolTier;
49
+ /** How many tools fell in each tier on one side of the called/never-called split. */
50
+ export interface TierCounts {
51
+ default: number;
52
+ extended: number;
53
+ runOnly: number;
54
+ }
55
+ export interface ToolHitRate {
56
+ /** False when this session carries no ledger — "not watched", never "nothing was called". */
57
+ observed: boolean;
58
+ /** The four-plus-two cells: every tier, called and never-called. Omitted when not observed. */
59
+ cells?: {
60
+ called: TierCounts;
61
+ neverCalled: TierCounts;
62
+ };
63
+ /** Each tool called this session, most-called first. */
64
+ called?: readonly {
65
+ tool: string;
66
+ tier: ToolTier;
67
+ calls: number;
68
+ }[];
69
+ /** Each tool in the table this session never called, by name. */
70
+ neverCalled?: readonly {
71
+ tool: string;
72
+ tier: ToolTier;
73
+ }[];
74
+ }
75
+ /**
76
+ * Cross one session's per-tool counts with the tool table.
77
+ *
78
+ * `allTools` is the DISPATCHABLE table — the names `reticle_run` will accept — passed in rather than
79
+ * imported, because the table is assembled in the module that owns this tool and importing it back
80
+ * would close a cycle. A tool counted but absent from the table still appears, so a name the table
81
+ * lost is visible rather than dropped.
82
+ */
83
+ export declare function foldToolHitRate(input: {
84
+ calls: ReadonlyMap<string, number> | undefined;
85
+ allTools: readonly string[];
86
+ }): ToolHitRate;
@@ -0,0 +1,90 @@
1
+ import { CORE_TOOL_NAMES, EXTENDED_TOOL_NAMES } from '../tools/tool-surface.js';
2
+ /**
3
+ * Which tools this session called, and how reachable each one was.
4
+ *
5
+ * The generalisation of the capture ledger: it recorded a short list of read tools to answer one
6
+ * feature's disproof, and the question that followed is the same question asked of the whole table —
7
+ * most of the surface has never been observed being called at all, and a decision about what to cut
8
+ * or consolidate is about to be made. This is the instrument for that decision, and nothing else.
9
+ *
10
+ * ## Why the count alone is not enough
11
+ *
12
+ * A tool that goes uncalled because nobody was told it exists and a tool that goes uncalled because
13
+ * nobody wants it look identical in a call count. They are opposite conclusions. So every count is
14
+ * reported beside the tool's REACHABILITY — advertised on the default surface, advertised only on
15
+ * the extended one, or reachable solely by naming it to `reticle_run` — and the interesting cell is
16
+ * the one where an agent found a run-only tool anyway.
17
+ *
18
+ * ## The tiers are derived, never restated
19
+ *
20
+ * `tierOfTool` reads `CORE_TOOL_NAMES` and `EXTENDED_TOOL_NAMES` directly. A tool promoted or
21
+ * demoted between surfaces changes tier here with nothing edited. A local copy of either list would
22
+ * go stale on the first move and would then be wrong about the exact axis this exists to measure —
23
+ * and it would be wrong silently, in a report used to justify deleting things.
24
+ *
25
+ * ## What these numbers do NOT establish
26
+ *
27
+ * - **One session is not a hit rate.** This is evidence about ONE run by ONE agent against ONE app.
28
+ * A hit rate is a distribution over many sessions; a single session quoted as one is an anecdote
29
+ * with a denominator drawn on it.
30
+ * - **Never-called is not useless.** A tool one team reaches for once a month in an emergency and a
31
+ * tool nobody has ever called are the same zero here. Frequency alone cannot separate them, so a
32
+ * cut decided on frequency alone will delete the wrong things.
33
+ * - **Absent is not zero.** A session with no ledger reports `observed: false`. It has not
34
+ * established that no tools were called; it has established that nothing was watching.
35
+ * - The tools that ran BEFORE this instrument existed in the process, or against a session this
36
+ * daemon adopted after the fact, are simply missing rather than counted low.
37
+ */
38
+ /** How an agent could have reached a tool. Derived from the surface sets — see above. */
39
+ export const ToolTier = {
40
+ /** Advertised directly on the surface every user gets. */
41
+ DEFAULT: 'default',
42
+ /** Advertised only when the full schema-carrying surface is switched on. */
43
+ EXTENDED: 'extended',
44
+ /** Advertised nowhere: reachable only by naming it to `reticle_run`. */
45
+ RUN_ONLY: 'run-only',
46
+ };
47
+ /** Which surface would have put this tool in front of an agent, read off that surface's own set. */
48
+ export function tierOfTool(tool) {
49
+ if (CORE_TOOL_NAMES.has(tool))
50
+ return ToolTier.DEFAULT;
51
+ if (EXTENDED_TOOL_NAMES.has(tool))
52
+ return ToolTier.EXTENDED;
53
+ return ToolTier.RUN_ONLY;
54
+ }
55
+ function tally(tools) {
56
+ return {
57
+ default: tools.filter((tier) => ToolTier.DEFAULT === tier).length,
58
+ extended: tools.filter((tier) => ToolTier.EXTENDED === tier).length,
59
+ runOnly: tools.filter((tier) => ToolTier.RUN_ONLY === tier).length,
60
+ };
61
+ }
62
+ /**
63
+ * Cross one session's per-tool counts with the tool table.
64
+ *
65
+ * `allTools` is the DISPATCHABLE table — the names `reticle_run` will accept — passed in rather than
66
+ * imported, because the table is assembled in the module that owns this tool and importing it back
67
+ * would close a cycle. A tool counted but absent from the table still appears, so a name the table
68
+ * lost is visible rather than dropped.
69
+ */
70
+ export function foldToolHitRate(input) {
71
+ const { calls } = input;
72
+ if (calls === undefined)
73
+ return { observed: false };
74
+ const called = [...calls]
75
+ .map(([tool, count]) => ({ tool, tier: tierOfTool(tool), calls: count }))
76
+ .sort((a, b) => b.calls - a.calls || a.tool.localeCompare(b.tool));
77
+ const neverCalled = input.allTools
78
+ .filter((tool) => !calls.has(tool))
79
+ .map((tool) => ({ tool, tier: tierOfTool(tool) }))
80
+ .sort((a, b) => a.tool.localeCompare(b.tool));
81
+ return {
82
+ observed: true,
83
+ cells: {
84
+ called: tally(called.map((entry) => entry.tier)),
85
+ neverCalled: tally(neverCalled.map((entry) => entry.tier)),
86
+ },
87
+ called,
88
+ neverCalled,
89
+ };
90
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Is this verdict being drawn over code nobody said anything about?
3
+ *
4
+ * Declaring intent is opt-in, and nothing has ever noticed that a change landed and nobody said what
5
+ * it was supposed to make true. That is the moment it matters: a change with no declared intent can
6
+ * only be verified against itself, which is the definition of a false green.
7
+ *
8
+ * ## Why this fires on EVERY verdict, not once per change
9
+ *
10
+ * The house rule is that a gap nobody hit is a backlog, and a backlog reported as a finding is how
11
+ * an agent learns to stop reading findings. This clears that bar on the same test every other
12
+ * instrumentation gap clears it on: **did the absence make THIS verdict weaker?** An unwatched store
13
+ * weakens every state assertion made over it, not merely the first, so it is reported on every one.
14
+ * An undeclared change is the same shape — while nothing says what the edit was for, each verdict
15
+ * drawn after it is checked against nothing but itself, and that is true of the fifth one exactly as
16
+ * it was of the first. Reporting it once and then falling silent would leave four verdicts carrying
17
+ * a weakness they no longer disclose, which is the failure mode this whole surface exists to prevent.
18
+ *
19
+ * It is also not a backlog, because it is not a survey of the project: it is answered from what this
20
+ * session observed (an epoch that moved) about the verdict being returned right now, and it is
21
+ * closed by one call the agent can make immediately. A finding that stays open only because the
22
+ * agent has not made that call is a finding about the current verdict, not a chore.
23
+ *
24
+ * ## The silences, and why each one is required
25
+ *
26
+ * - **An edit was actually OBSERVED.** The epoch degrades to `NO_EDITS_OBSERVED` outside Vite, and
27
+ * that means "no edits observed", never "no edits happened" — a finding built on an absence
28
+ * Reticle cannot see would be a fabrication.
29
+ * - **The ledger holds nothing undischarged.** An open intent IS the declaration; silence is the
30
+ * correct response to it, and declaring one is what turns this off from the next verdict on.
31
+ *
32
+ * ## Why "covers the work" is answered as "anything open at all"
33
+ *
34
+ * The tempting answer is to match the intent's `surface` against the files that changed. Reticle
35
+ * cannot do that honestly: it observes that an epoch advanced, not WHAT advanced — the hot-update
36
+ * channel reports a counter, not a diff — so any file-level matching would be inference dressed as a
37
+ * fact, and it would fire on an agent that had declared its intent perfectly well. Over-reporting on
38
+ * a correct agent is the direction of error that trains it to stop reading findings.
39
+ *
40
+ * So the question asked is the coarse one, and it is asked honestly: has the agent declared anything
41
+ * that is still outstanding? If it has, it is working to a stated purpose and this stays quiet.
42
+ */
43
+ /**
44
+ * `openIntents` is a thunk rather than a list because reading the ledger touches disk, and the epoch
45
+ * check answers without it for every page that never reported a hot update — which is most of them.
46
+ * Once an edit HAS been observed the read happens per verdict, and that is the intended cost: the
47
+ * ledger is the only thing that can end it, so it has to be re-asked each time.
48
+ */
49
+ export declare function isChangeUndeclared(
50
+ /** The epoch the session is under now. Undefined when the page never stamped one. */
51
+ currentEpoch: number | undefined, openIntents: () => Promise<readonly unknown[]>): Promise<boolean>;
@@ -0,0 +1,56 @@
1
+ import { NO_EDITS_OBSERVED } from '@reticlehq/core';
2
+ /**
3
+ * Is this verdict being drawn over code nobody said anything about?
4
+ *
5
+ * Declaring intent is opt-in, and nothing has ever noticed that a change landed and nobody said what
6
+ * it was supposed to make true. That is the moment it matters: a change with no declared intent can
7
+ * only be verified against itself, which is the definition of a false green.
8
+ *
9
+ * ## Why this fires on EVERY verdict, not once per change
10
+ *
11
+ * The house rule is that a gap nobody hit is a backlog, and a backlog reported as a finding is how
12
+ * an agent learns to stop reading findings. This clears that bar on the same test every other
13
+ * instrumentation gap clears it on: **did the absence make THIS verdict weaker?** An unwatched store
14
+ * weakens every state assertion made over it, not merely the first, so it is reported on every one.
15
+ * An undeclared change is the same shape — while nothing says what the edit was for, each verdict
16
+ * drawn after it is checked against nothing but itself, and that is true of the fifth one exactly as
17
+ * it was of the first. Reporting it once and then falling silent would leave four verdicts carrying
18
+ * a weakness they no longer disclose, which is the failure mode this whole surface exists to prevent.
19
+ *
20
+ * It is also not a backlog, because it is not a survey of the project: it is answered from what this
21
+ * session observed (an epoch that moved) about the verdict being returned right now, and it is
22
+ * closed by one call the agent can make immediately. A finding that stays open only because the
23
+ * agent has not made that call is a finding about the current verdict, not a chore.
24
+ *
25
+ * ## The silences, and why each one is required
26
+ *
27
+ * - **An edit was actually OBSERVED.** The epoch degrades to `NO_EDITS_OBSERVED` outside Vite, and
28
+ * that means "no edits observed", never "no edits happened" — a finding built on an absence
29
+ * Reticle cannot see would be a fabrication.
30
+ * - **The ledger holds nothing undischarged.** An open intent IS the declaration; silence is the
31
+ * correct response to it, and declaring one is what turns this off from the next verdict on.
32
+ *
33
+ * ## Why "covers the work" is answered as "anything open at all"
34
+ *
35
+ * The tempting answer is to match the intent's `surface` against the files that changed. Reticle
36
+ * cannot do that honestly: it observes that an epoch advanced, not WHAT advanced — the hot-update
37
+ * channel reports a counter, not a diff — so any file-level matching would be inference dressed as a
38
+ * fact, and it would fire on an agent that had declared its intent perfectly well. Over-reporting on
39
+ * a correct agent is the direction of error that trains it to stop reading findings.
40
+ *
41
+ * So the question asked is the coarse one, and it is asked honestly: has the agent declared anything
42
+ * that is still outstanding? If it has, it is working to a stated purpose and this stays quiet.
43
+ */
44
+ /**
45
+ * `openIntents` is a thunk rather than a list because reading the ledger touches disk, and the epoch
46
+ * check answers without it for every page that never reported a hot update — which is most of them.
47
+ * Once an edit HAS been observed the read happens per verdict, and that is the intended cost: the
48
+ * ledger is the only thing that can end it, so it has to be re-asked each time.
49
+ */
50
+ export async function isChangeUndeclared(
51
+ /** The epoch the session is under now. Undefined when the page never stamped one. */
52
+ currentEpoch, openIntents) {
53
+ if (undefined === currentEpoch || NO_EDITS_OBSERVED === currentEpoch)
54
+ return false;
55
+ return 0 === (await openIntents()).length;
56
+ }
@@ -76,6 +76,21 @@ interface VerifiedInputs {
76
76
  * Empty means nothing went unread.
77
77
  */
78
78
  outcomeUnread?: readonly string[];
79
+ /**
80
+ * The declared consequence that held does not depend on the response body — an exact string
81
+ * rendered, a store path, a signal. See `declaresBodyIndependentChannel`.
82
+ *
83
+ * The unread-body clause exists for the case where the body is the ONLY channel that could have
84
+ * contradicted the screen (a 200 with batch/GraphQL errors inside). When a body-independent
85
+ * declaration held, that is no longer true, and grading `unknown` costs a real verdict. The unread
86
+ * labels still ride out in `because`, the way absence-derived contradictions still ride out in
87
+ * `contradictions`.
88
+ *
89
+ * Narrow: it does not soften a 202, an observed contradiction, a dirty capture, or a net-only
90
+ * declaration. `declaredConsequence` alone is not enough — a `{ kind: "net", status: 200 }` is a
91
+ * declaration whose remaining channel IS the body.
92
+ */
93
+ independentOfBody?: boolean;
79
94
  /**
80
95
  * What the wait was for and what the window held when it ended — read ONLY by the two clauses that
81
96
  * answer UNSETTLED, which is the commonest reason a verdict comes back `unknown` and was also the
@@ -31,6 +31,24 @@ export function decideVerified(inputs) {
31
31
  'to repeat',
32
32
  };
33
33
  }
34
+ // ABOVE the failure clause, for the third time and the same reason: absence is not evidence.
35
+ //
36
+ // Omitting `until` waits for the page to SETTLE. Nobody declared a consequence, so a settle that
37
+ // times out says "the page was still busy when I stopped looking" — the definition of an
38
+ // absence-derived finding. The failure clause below took it at face value and returned
39
+ // `verified: "no"` with `because: 'the declared consequence did not hold'`, naming a consequence
40
+ // that does not exist. Measured on the hard fixture, whose push updates mean it never goes idle:
41
+ // a healthy pagination click was graded NO. Any app with polling or a live feed gets accused of a
42
+ // defect for being alive, which is the false NEGATIVE the ABSENCE_DERIVED doctrine exists to stop.
43
+ if (false === pass && false === inputs.declaredConsequence) {
44
+ return {
45
+ verified: Verified.UNKNOWN,
46
+ verifiedReason: VerifiedReason.UNSETTLED,
47
+ because: 'no consequence was declared, so this only waited for the page to go idle and it never ' +
48
+ 'did — that is a statement about when Reticle stopped looking, not about the app. Pass ' +
49
+ '`until` naming what the action should cause (a signal, a request, a route, or store state)',
50
+ };
51
+ }
34
52
  // A failed assertion is the most actionable fact there is; it leads — including over
35
53
  // `alreadyTrue`, because a condition that held before AND fails now is a real regression.
36
54
  if (false === pass) {
@@ -82,6 +100,27 @@ export function decideVerified(inputs) {
82
100
  inputs.unsettled !== undefined &&
83
101
  !openWrite &&
84
102
  contradictions.every((c) => c.kind === ContradictionKind.REQUEST_NEVER_SETTLED);
103
+ // `signal-without-consequence` gets its own sentence, because the generic one is FALSE for it.
104
+ //
105
+ // That sentence says the window "closed before the app finished" and then explains that a poll or
106
+ // a timer kept the page busy. Nothing kept this page busy — nothing happened at all, which is the
107
+ // entire finding. Measured on the bench fixture: the correct UNKNOWN arrived wearing an
108
+ // explanation that told the agent to wait longer or assert something else, when the fact to act on
109
+ // was that the app announced a consequence it did not deliver. A right verdict with a wrong reason
110
+ // sends an agent to the wrong place, which costs as much as the wrong verdict did.
111
+ const signalOnly = contradictions.length > 0 &&
112
+ contradictions.every((c) => c.kind === ContradictionKind.SIGNAL_WITHOUT_CONSEQUENCE);
113
+ if (signalOnly) {
114
+ return {
115
+ verified: Verified.UNKNOWN,
116
+ verifiedReason: VerifiedReason.EVIDENCE_INCOMPLETE,
117
+ because: 'the app fired the signal you waited for and NOTHING else moved — no DOM, no store, no ' +
118
+ 'route, no request — so the only evidence is the app saying so. A signal emitted from the ' +
119
+ 'value the app was ASKED for, rather than the one it committed, looks exactly like this. ' +
120
+ 'Check the consequence directly (reticle_snapshot for what rendered, reticle_state for what ' +
121
+ 'the store holds) before trusting it',
122
+ };
123
+ }
85
124
  if (contradictions.length > 0 && !settlementOnly) {
86
125
  const kinds = contradictions.map((c) => c.kind).join(', ');
87
126
  return {
@@ -142,7 +181,21 @@ export function decideVerified(inputs) {
142
181
  // Gated on `true === settled` rather than on a caller's say-so. A call that returned before the app
143
182
  // stopped moving has not earned this, whatever it declared — which is what stops `no-fault` from
144
183
  // becoming the green-forever button an always-available "nothing was wrong" would be.
145
- if (honesty.grade === HonestyGrade.NONE) {
184
+ // Two ways to have proved nothing, and only one of them used to be caught. Grade NONE is the
185
+ // engine's own reading — no signal, no request, no state change, nothing pinned. An explicit
186
+ // `declaredConsequence: false` is the CALLER saying it asserted nothing, which `act_and_wait`
187
+ // reports whenever `until` is omitted (an omitted `until` becomes `{kind:"settled"}` — a
188
+ // sleep-replacement, not an assertion).
189
+ //
190
+ // Grading alone missed the second case, because a click that merely moves the DOM lifts the grade
191
+ // to PRESENCE and walks past this branch into a full green. Measured live: clicking a submit
192
+ // button, and an inert text input, with no `until` both returned `verified:"yes"` /
193
+ // `verifiedReason:"proved"`. The claim it printed — "assertion held at presence grade" — was true
194
+ // about a grade and false about an assertion, because there was no assertion.
195
+ //
196
+ // It compounds: `reticle_context` persists proven claims, so an undeclared green becomes
197
+ // established fact for the agent that re-enters after a compaction.
198
+ if (honesty.grade === HonestyGrade.NONE || false === inputs.declaredConsequence) {
146
199
  if (true === settled) {
147
200
  return {
148
201
  verified: Verified.NO_FAULT,
@@ -178,7 +231,15 @@ export function decideVerified(inputs) {
178
231
  //
179
232
  // UNKNOWN, not NO: nothing is known to have failed. The remedy is in the sentence, because an
180
233
  // agent that cannot act on a caveat will learn to skip it.
181
- if (outcomeUnread !== undefined && outcomeUnread.length > 0) {
234
+ //
235
+ // Skipped when the caller declared a consequence that does not depend on the body and it held.
236
+ // The clause was written for the body being the ONLY remaining channel; a unique row on screen, a
237
+ // store path, or a signal that held is a different channel, and grading unknown there cost a real
238
+ // verdict (a 201 plus the exact message text, agent went to enable capture instead of finishing).
239
+ // The unread write still rides out in `because`. A net-only declaration does not skip: then the
240
+ // body is still the remaining channel.
241
+ const unreadHeldIndependently = declaredHeld && true === inputs.independentOfBody;
242
+ if (outcomeUnread !== undefined && outcomeUnread.length > 0 && !unreadHeldIndependently) {
182
243
  return {
183
244
  verified: Verified.UNKNOWN,
184
245
  verifiedReason: VerifiedReason.OUTCOME_UNREAD,
@@ -214,11 +275,14 @@ export function decideVerified(inputs) {
214
275
  const notIdle = false === settled
215
276
  ? ', though the page never went idle — this rests on the consequence you declared, not on the page going quiet'
216
277
  : '';
278
+ const unreadCaveat = unreadHeldIndependently && outcomeUnread !== undefined && outcomeUnread.length > 0
279
+ ? `, though a write returned 2xx with a response body that was never recorded (${outcomeUnread.join('; ')}) — a 200 describes the transport, not the result`
280
+ : '';
217
281
  return {
218
282
  verified: Verified.YES,
219
283
  verifiedReason: VerifiedReason.PROVED,
220
284
  because: true === honesty.coverage?.partial
221
- ? `assertion held at ${honesty.grade} grade with no channel disagreeing, but coverage was PARTIAL — see \`coverage\` for what went unobserved${notIdle}`
222
- : `assertion held at ${honesty.grade} grade over a clean capture with no channel disagreeing${notIdle}`,
285
+ ? `assertion held at ${honesty.grade} grade with no channel disagreeing, but coverage was PARTIAL — see \`coverage\` for what went unobserved${notIdle}${unreadCaveat}`
286
+ : `assertion held at ${honesty.grade} grade over a clean capture with no channel disagreeing${notIdle}${unreadCaveat}`,
223
287
  };
224
288
  }
@@ -50,4 +50,3 @@ export interface SharedServer {
50
50
  export declare function createSharedServer(options?: {
51
51
  token?: string;
52
52
  }): SharedServer;
53
- export declare function driveSchemeRefusal(url: string): string | undefined;
@@ -330,7 +330,7 @@ function schemeOf(url) {
330
330
  return undefined;
331
331
  }
332
332
  }
333
- export function driveSchemeRefusal(url) {
333
+ function driveSchemeRefusal(url) {
334
334
  const scheme = schemeOf(url);
335
335
  if (scheme !== undefined && DRIVEABLE_SCHEMES.includes(scheme))
336
336
  return undefined;
@@ -18,8 +18,6 @@
18
18
  * Pure functions over already-collected reports — the driving is `reticle_crawl`'s job, and running
19
19
  * it across a commit range is a shell loop. This is only the part that must not be done by eye.
20
20
  */
21
- /** The contradiction kinds — cross-channel disagreement, as opposed to a single-channel fault. */
22
- export declare const CONTRADICTION_KINDS: readonly string[];
23
21
  export interface HuntAnomaly {
24
22
  kind: string;
25
23
  ref?: string;
@@ -19,7 +19,7 @@
19
19
  * it across a commit range is a shell loop. This is only the part that must not be done by eye.
20
20
  */
21
21
  /** The contradiction kinds — cross-channel disagreement, as opposed to a single-channel fault. */
22
- export const CONTRADICTION_KINDS = [
22
+ const CONTRADICTION_KINDS = [
23
23
  'ui-advanced-request-failed',
24
24
  'signal-contradicted',
25
25
  'response-ignored',
@@ -3,11 +3,6 @@ export interface ImpactPaths {
3
3
  project: string;
4
4
  global: string;
5
5
  }
6
- /**
7
- * `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
8
- * the global scope always lives beside the daemon's own state in `~/.reticle`.
9
- */
10
- export declare function impactPaths(reticleRoot: string): ImpactPaths;
11
6
  /** Read a scope, tolerating absence and corruption: a broken file starts over rather than throwing. */
12
7
  export declare function readScope(path: string, now: number): ImpactScope;
13
8
  /** YYYY-MM-DD in local time - the day boundary a person recognises, not UTC's. */
@@ -18,7 +18,7 @@ const WRITE_DEBOUNCE_MS = 800;
18
18
  * `reticleRoot` is the project's own `.reticle` directory (what every other store here is handed);
19
19
  * the global scope always lives beside the daemon's own state in `~/.reticle`.
20
20
  */
21
- export function impactPaths(reticleRoot) {
21
+ function impactPaths(reticleRoot) {
22
22
  return {
23
23
  project: join(reticleRoot, ReticleDir.IMPACT_FILE),
24
24
  global: join(homedir(), ReticleDir.ROOT, ReticleDir.IMPACT_FILE),
package/dist/index.d.ts CHANGED
@@ -117,7 +117,10 @@ export interface StartOptions {
117
117
  pairingTokenDir?: string;
118
118
  /** injectable clock for contract.json's generatedAt stamp. Defaults to Date.now. */
119
119
  now?: () => number;
120
- /** 'core' exposes the lean tool surface. Defaults to env RETICLE_TOOL_PROFILE, else 'full'. */
120
+ /**
121
+ * Retired profile name (`core`/`full`/…). Old values still map. The live switch is
122
+ * RETICLE_ADVERTISE_ALL_TOOLS=1; the default is the lean surface.
123
+ */
121
124
  toolProfile?: string;
122
125
  /** Start the OEM/CI verify HTTP endpoint alongside the daemon (`reticle serve --http`). */
123
126
  httpVerify?: boolean;
@@ -151,7 +154,6 @@ export interface RunningServer {
151
154
  close: () => Promise<void>;
152
155
  }
153
156
  export { resolveBridgeSecurity } from './bridge/bridge-security.js';
154
- /** Start the Reticle bridge (browser WS endpoint) and, by default, the MCP stdio server. */
155
157
  export declare function start(options?: StartOptions): Promise<RunningServer>;
156
158
  /**
157
159
  * Start the Reticle bridge in daemon mode: a single HTTP server handles both the WebSocket
package/dist/index.js CHANGED
@@ -1,4 +1,9 @@
1
1
  import { join } from 'node:path';
2
+ import { existsSync, readFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { PROJECT_REGISTRY_FILE, emptyProjectRegistry, parseProjectRegistry } from '@reticlehq/core';
5
+ import { discoverProjectConfigs } from './cli/config-discovery.js';
6
+ import { projectCandidatesFrom, resolveArtifactRoot, } from './project/artifact-root.js';
2
7
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
8
  import { AGENT_STOPPED_NOTICE, RETICLE_DEFAULT_PORT, ReticleCommand, ReticleDir, ReticleEnv, LOOPBACK_HOST, ReplayStatus, EventType, } from '@reticlehq/core';
4
9
  import { originOf } from './session/session-manager.js';
@@ -251,6 +256,41 @@ async function resolveRealInput(options, onNavigateError, onNetworkDetail) {
251
256
  return {};
252
257
  }
253
258
  /** Start the Reticle bridge (browser WS endpoint) and, by default, the MCP stdio server. */
259
+ /**
260
+ * Resolve a session's artifact root, from everything this machine knows about where projects live.
261
+ *
262
+ * Built once per daemon and closed over by every tool call. The two sources are read lazily and
263
+ * cheaply on each call rather than snapshotted at startup: `init` can run in another terminal while
264
+ * this daemon is up, and a resolution that used a startup snapshot would keep answering with a map
265
+ * from before the project the agent is now driving existed.
266
+ */
267
+ function artifactRootResolver(daemonRoot) {
268
+ return (projectId) => {
269
+ let registry = emptyProjectRegistry();
270
+ try {
271
+ const path = join(homedir(), ReticleDir.ROOT, PROJECT_REGISTRY_FILE);
272
+ registry = existsSync(path)
273
+ ? parseProjectRegistry(JSON.parse(readFileSync(path, 'utf8')))
274
+ : registry;
275
+ }
276
+ catch {
277
+ // A cache that cannot be read is an empty cache, never an error: the daemon still resolves
278
+ // through discovery, and falls back to its own root exactly as it did before this existed.
279
+ }
280
+ let discovery = { found: [], searched: [] };
281
+ try {
282
+ discovery = discoverProjectConfigs(process.cwd());
283
+ }
284
+ catch {
285
+ // Same reasoning: a diagnostic search that throws must not take a tool call with it.
286
+ }
287
+ return resolveArtifactRoot({
288
+ projectId,
289
+ candidates: projectCandidatesFrom(discovery, registry),
290
+ daemonRoot,
291
+ });
292
+ };
293
+ }
254
294
  export async function start(options = {}) {
255
295
  const port = options.port ?? RETICLE_DEFAULT_PORT;
256
296
  // Open the user's impact record before anything can connect. Not inside the MCP branch: a daemon
@@ -302,6 +342,7 @@ export async function start(options = {}) {
302
342
  project,
303
343
  fs,
304
344
  reticleRoot,
345
+ artifactRootFor: artifactRootResolver(reticleRoot),
305
346
  now,
306
347
  bridgePort: port,
307
348
  browserProbe: probeChromium,