@reticlehq/server 2.11.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 (252) hide show
  1. package/SKILL.md +26 -2
  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 +0 -2
  45. package/dist/dev/stale-issue-guard.js +1 -1
  46. package/dist/events/contradictions.d.ts +41 -0
  47. package/dist/events/contradictions.js +125 -12
  48. package/dist/events/declared.d.ts +14 -0
  49. package/dist/events/declared.js +33 -0
  50. package/dist/events/echo-mismatch.js +14 -0
  51. package/dist/events/observed-in-window.d.ts +12 -0
  52. package/dist/events/observed-in-window.js +17 -0
  53. package/dist/events/predicate-asks.d.ts +13 -0
  54. package/dist/events/predicate-asks.js +22 -0
  55. package/dist/events/predicate-eval.js +64 -22
  56. package/dist/events/predicate-parse.d.ts +0 -6
  57. package/dist/events/predicate-parse.js +43 -3
  58. package/dist/events/predicate-schema.d.ts +44 -0
  59. package/dist/events/predicate-schema.js +126 -23
  60. package/dist/events/predicate.d.ts +15 -0
  61. package/dist/events/predicate.js +41 -2
  62. package/dist/flows/coverage.d.ts +0 -8
  63. package/dist/flows/coverage.js +1 -1
  64. package/dist/flows/decision.d.ts +5 -1
  65. package/dist/flows/decision.js +33 -7
  66. package/dist/flows/flake.d.ts +0 -2
  67. package/dist/flows/flake.js +1 -1
  68. package/dist/flows/flow-intent.d.ts +52 -0
  69. package/dist/flows/flow-intent.js +123 -0
  70. package/dist/flows/flow-replay-run.js +27 -4
  71. package/dist/flows/flow-success.js +15 -3
  72. package/dist/flows/flow-tools.js +32 -4
  73. package/dist/flows/flows.d.ts +6 -1
  74. package/dist/flows/flows.js +34 -22
  75. package/dist/flows/predicate-to-expect.js +6 -3
  76. package/dist/flows/server-verify.js +16 -2
  77. package/dist/flows/verify-change-tools.js +4 -1
  78. package/dist/honesty/blind-spots.d.ts +0 -17
  79. package/dist/honesty/blind-spots.js +1 -1
  80. package/dist/honesty/feature-capture.d.ts +150 -0
  81. package/dist/honesty/feature-capture.js +233 -0
  82. package/dist/honesty/instrumentation-gaps.d.ts +30 -6
  83. package/dist/honesty/instrumentation-gaps.js +61 -5
  84. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  85. package/dist/honesty/tool-hit-rate.js +90 -0
  86. package/dist/honesty/undeclared-change.d.ts +51 -0
  87. package/dist/honesty/undeclared-change.js +56 -0
  88. package/dist/honesty/verified.d.ts +15 -0
  89. package/dist/honesty/verified.js +68 -4
  90. package/dist/http-server.d.ts +0 -1
  91. package/dist/http-server.js +1 -1
  92. package/dist/hunt/hunt-report.d.ts +0 -2
  93. package/dist/hunt/hunt-report.js +1 -1
  94. package/dist/impact/impact-store.d.ts +0 -5
  95. package/dist/impact/impact-store.js +1 -1
  96. package/dist/index.d.ts +4 -1
  97. package/dist/init/agent-rules.d.ts +17 -0
  98. package/dist/init/agent-rules.js +31 -1
  99. package/dist/init/capabilities.d.ts +0 -2
  100. package/dist/init/capabilities.js +1 -1
  101. package/dist/init/closing-hint.d.ts +36 -0
  102. package/dist/init/closing-hint.js +120 -0
  103. package/dist/init/confirm.d.ts +0 -25
  104. package/dist/init/confirm.js +3 -3
  105. package/dist/init/cra.d.ts +14 -1
  106. package/dist/init/cra.js +43 -12
  107. package/dist/init/dev-script.d.ts +48 -0
  108. package/dist/init/dev-script.js +63 -0
  109. package/dist/init/mcp.d.ts +16 -5
  110. package/dist/init/mcp.js +26 -10
  111. package/dist/init/plan-framework.d.ts +0 -5
  112. package/dist/init/plan-framework.js +83 -9
  113. package/dist/init/plan.d.ts +10 -30
  114. package/dist/init/plan.js +30 -15
  115. package/dist/init/refresh-rules.d.ts +39 -0
  116. package/dist/init/refresh-rules.js +51 -0
  117. package/dist/init/run.d.ts +0 -26
  118. package/dist/init/run.js +14 -87
  119. package/dist/init/snippets.js +53 -10
  120. package/dist/input/real-input.d.ts +2 -0
  121. package/dist/input/real-input.js +3 -2
  122. package/dist/input/scroll-find.d.ts +7 -4
  123. package/dist/input/scroll-find.js +63 -25
  124. package/dist/intent/inline-intent.d.ts +46 -0
  125. package/dist/intent/inline-intent.js +103 -0
  126. package/dist/intent/intent-store.d.ts +37 -0
  127. package/dist/intent/intent-store.js +112 -0
  128. package/dist/intent/intent-tools.d.ts +2 -0
  129. package/dist/intent/intent-tools.js +80 -0
  130. package/dist/intent/open-intents.d.ts +21 -0
  131. package/dist/intent/open-intents.js +25 -0
  132. package/dist/journal/envelope.d.ts +26 -26
  133. package/dist/journal/journal-recorder.d.ts +23 -0
  134. package/dist/journal/journal-recorder.js +35 -0
  135. package/dist/mcp/attach-memory.d.ts +67 -0
  136. package/dist/mcp/attach-memory.js +142 -0
  137. package/dist/mcp/mcp-proxy.d.ts +0 -7
  138. package/dist/mcp/mcp-proxy.js +14 -2
  139. package/dist/mcp/mcp.d.ts +2 -0
  140. package/dist/mcp/mcp.js +45 -23
  141. package/dist/mcp/server-instructions.js +45 -3
  142. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  143. package/dist/mcp/tool-catalog-cache.js +10 -0
  144. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  145. package/dist/oracles/flow-instrument-gaps.js +4 -5
  146. package/dist/oracles/self-instrument.d.ts +9 -9
  147. package/dist/oracles/self-instrument.js +19 -13
  148. package/dist/pool/browser-pool.d.ts +12 -1
  149. package/dist/pool/browser-pool.js +31 -1
  150. package/dist/pool/playwright-launcher.js +3 -1
  151. package/dist/project/fs-port.d.ts +3 -0
  152. package/dist/project/fs-port.js +3 -2
  153. package/dist/project/memory-fs.js +6 -1
  154. package/dist/project/project-tools.js +3 -32
  155. package/dist/project/reticle-dir.d.ts +2 -0
  156. package/dist/project/reticle-dir.js +1 -0
  157. package/dist/runs/context-tools.d.ts +2 -0
  158. package/dist/runs/context-tools.js +89 -0
  159. package/dist/runs/run-context.d.ts +40 -0
  160. package/dist/runs/run-context.js +158 -0
  161. package/dist/runs/run-diff.d.ts +2 -1
  162. package/dist/runs/verification-sync.js +2 -2
  163. package/dist/session/dev-server-probe.d.ts +0 -8
  164. package/dist/session/dev-server-probe.js +1 -1
  165. package/dist/session/last-act.d.ts +7 -1
  166. package/dist/session/last-act.js +2 -2
  167. package/dist/session/live-control-tools.js +1 -42
  168. package/dist/session/no-session-diagnosis.d.ts +10 -0
  169. package/dist/session/no-session-diagnosis.js +24 -7
  170. package/dist/session/no-session-next-action.d.ts +5 -0
  171. package/dist/session/no-session-next-action.js +35 -3
  172. package/dist/session/no-session-watch.d.ts +8 -0
  173. package/dist/session/no-session-watch.js +82 -64
  174. package/dist/session/output-budget.js +4 -1
  175. package/dist/session/session-health.d.ts +9 -0
  176. package/dist/session/session-health.js +23 -0
  177. package/dist/session/session-manager.js +50 -1
  178. package/dist/session/session-successor.d.ts +37 -0
  179. package/dist/session/session-successor.js +78 -0
  180. package/dist/session/session.d.ts +31 -1
  181. package/dist/session/session.js +75 -2
  182. package/dist/telemetry/automation-hint.d.ts +2 -1
  183. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  184. package/dist/telemetry/project-profile.d.ts +0 -8
  185. package/dist/telemetry/project-profile.js +1 -1
  186. package/dist/telemetry/run-telemetry.d.ts +0 -2
  187. package/dist/telemetry/run-telemetry.js +1 -1
  188. package/dist/telemetry/verification-of.js +2 -2
  189. package/dist/tools/act-tools.js +126 -21
  190. package/dist/tools/assert-source.d.ts +22 -0
  191. package/dist/tools/assert-source.js +53 -0
  192. package/dist/tools/assert-verdict.d.ts +20 -3
  193. package/dist/tools/assert-verdict.js +36 -4
  194. package/dist/tools/browser-tools.js +0 -20
  195. package/dist/tools/complete-snapshot.d.ts +48 -0
  196. package/dist/tools/complete-snapshot.js +75 -0
  197. package/dist/tools/coverage-tools.d.ts +10 -1
  198. package/dist/tools/coverage-tools.js +123 -69
  199. package/dist/tools/dynamic-tools.js +29 -8
  200. package/dist/tools/error-recovery.d.ts +24 -14
  201. package/dist/tools/error-recovery.js +70 -9
  202. package/dist/tools/feedback-tools.d.ts +1 -1
  203. package/dist/tools/feedback-tools.js +2 -2
  204. package/dist/tools/invoke-tool.js +62 -2
  205. package/dist/tools/lease-hint.d.ts +10 -0
  206. package/dist/tools/lease-hint.js +53 -4
  207. package/dist/tools/lease-tools.d.ts +5 -1
  208. package/dist/tools/lease-tools.js +22 -8
  209. package/dist/tools/navigate-arrival.d.ts +0 -2
  210. package/dist/tools/navigate-arrival.js +1 -1
  211. package/dist/tools/observe-tools.js +63 -26
  212. package/dist/tools/read-tools.js +5 -3
  213. package/dist/tools/real-input-attempt.d.ts +22 -0
  214. package/dist/tools/real-input-attempt.js +153 -1
  215. package/dist/tools/reconcile-tools.js +18 -4
  216. package/dist/tools/resolve-target.d.ts +0 -7
  217. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  218. package/dist/tools/sdk-marker-probe.js +57 -0
  219. package/dist/tools/snapshot-delta.d.ts +1 -1
  220. package/dist/tools/tool-kit.d.ts +12 -0
  221. package/dist/tools/tool-kit.js +16 -0
  222. package/dist/tools/tool-names.d.ts +3 -0
  223. package/dist/tools/tool-names.js +3 -0
  224. package/dist/tools/tool-surface.d.ts +85 -6
  225. package/dist/tools/tool-surface.js +122 -2
  226. package/dist/tools/tools.js +22 -4
  227. package/dist/tools/unadvertised-help.js +4 -4
  228. package/dist/tools/verify-next-baton.d.ts +53 -0
  229. package/dist/tools/verify-next-baton.js +52 -0
  230. package/dist/update/update-nudge.d.ts +7 -10
  231. package/dist/update/update-nudge.js +12 -3
  232. package/dist/update/updater.d.ts +1 -1
  233. package/docs/README.md +2 -1
  234. package/docs/cli/cloud.mdx +1 -1
  235. package/docs/cli/doctor.mdx +1 -0
  236. package/docs/cli/verify.mdx +4 -5
  237. package/docs/desktop.mdx +1 -1
  238. package/docs/docs.json +2 -2
  239. package/docs/gates.md +1 -1
  240. package/docs/install-manual.mdx +1 -1
  241. package/docs/packages/browser.mdx +2 -2
  242. package/docs/packages/server.mdx +1 -1
  243. package/docs/packages.mdx +8 -6
  244. package/docs/platform-integration.md +17 -0
  245. package/docs/predicates.mdx +8 -0
  246. package/docs/reticle-manual.md +106 -0
  247. package/docs/tools-tools-and-run.mdx +2 -4
  248. package/docs/troubleshooting.mdx +9 -0
  249. package/docs/usage.md +3 -2
  250. package/package.json +2 -2
  251. package/dist/session/session-readiness.d.ts +0 -34
  252. package/dist/session/session-readiness.js +0 -36
@@ -1,4 +1,5 @@
1
1
  import { healthEnvelope } from '../session/session-health.js';
2
+ import { verifyNextBaton, SUPPRESS_VERIFY_NEXT_ENV } from './verify-next-baton.js';
2
3
  import { TelemetryActor, TelemetryEventKind, TRANSPORT_LIMITS, fingerprintFinding, } from '@reticlehq/core';
3
4
  import { getSessionMetrics } from '../telemetry/session-metrics.js';
4
5
  import { getTelemetry } from '../telemetry/telemetry.js';
@@ -16,6 +17,7 @@ import { ReticleTool } from './tool-names.js';
16
17
  import { takeFeedbackPrompt } from './feedback-tools.js';
17
18
  import { takeFeedbackUndelivered } from '../telemetry/feedback-delivery.js';
18
19
  import { noteRefsMinted, wrongTabRefusal } from '../session/ref-provenance.js';
20
+ import { CAPTURED_TOOLS, noteCapturedCall, noteToolDispatched, } from '../honesty/feature-capture.js';
19
21
  import { span } from '../trace.js';
20
22
  import { deltaForToolResult, impactSnapshot, initImpact, recordImpact, } from '../impact/impact-recorder.js';
21
23
  import { frictionOf, inviteFor } from './feedback-invite.js';
@@ -98,6 +100,8 @@ export const SESSION_EXEMPT_TOOLS = new Set([
98
100
  ReticleTool.FLOW_REPLAY, // returns its own FlowReplayResult contract (+ auto-records a run)
99
101
  ReticleTool.FLOW_SAVE_RECORDED, // reads the recording buffer, writes disk
100
102
  ReticleTool.FLOW_HEAL, // returns its own FlowHealResult contract
103
+ ReticleTool.INTENT, // reads/writes .reticle/intent.json; sessionId only picks the project
104
+ ReticleTool.CONTEXT, // folds the journal + intent ledger; must still answer when nothing is connected
101
105
  ReticleTool.PROJECT, // reads .reticle/project.json
102
106
  ReticleTool.RUN_EXPORT, // reads .reticle/runs/<id>.json (verification-run artifact)
103
107
  ReticleTool.SESSION, // merged lifecycle/human-channel family (tune/yield/end/resume/messages/review/narrate)
@@ -300,6 +304,43 @@ export async function runTool(tool, deps, args) {
300
304
  session = undefined;
301
305
  }
302
306
  }
307
+ // The read-only calls the journal never keeps, recorded HERE because this is the one dispatch
308
+ // point every call routes through — a second recording site is a second thing to forget, and this
309
+ // instrument exists precisely because nothing was recorded before. Recorded before the handler
310
+ // runs: a read dispatches no action, so the counter reads the same either side.
311
+ //
312
+ // `reticle_context` is session-EXEMPT, so `session` above is undefined for it and it needs its own
313
+ // lenient resolve. Failing that resolve is silent by design: a call made with nothing connected
314
+ // has no per-session ledger to land in, and an instrument must never be why a tool call fails.
315
+ //
316
+ // EVERY tool is counted here, not just the read set: which of the 70-odd names in the table an
317
+ // agent actually reaches for is the question a decision about cutting or consolidating the surface
318
+ // turns on, and it had no answer. `reticle_run` is the one exclusion, for the same reason
319
+ // `reportRefusal` and `bugsInResult` exclude it — its handler calls `runTool` on the real tool, so
320
+ // the inner call is already counted under the name that RAN, and counting the wrapper too would
321
+ // both double the total and hide the run-only tier behind a single wrapper name. That tier is
322
+ // precisely the one the decision hinges on. See honesty/tool-hit-rate.ts.
323
+ if (ReticleTool.RUN !== tool.name) {
324
+ let target = session;
325
+ if (target === undefined) {
326
+ try {
327
+ target = deps.sessions.resolve(rawSessionId);
328
+ }
329
+ catch {
330
+ target = undefined;
331
+ }
332
+ }
333
+ if (target !== undefined) {
334
+ noteToolDispatched(target, tool.name);
335
+ if (CAPTURED_TOOLS.has(tool.name)) {
336
+ const subject = asString(args['ref']) ?? asString(args['target']);
337
+ noteCapturedCall(target, {
338
+ tool: tool.name,
339
+ ...(subject === undefined ? {} : { subject }),
340
+ });
341
+ }
342
+ }
343
+ }
303
344
  // A ref that was handed out by a DIFFERENT tab, on a call that named no session. Refused here, at
304
345
  // the one point that knows both the ref and the tab resolution picked — the handler only learns
305
346
  // that the ref missed, and answers that with "the DOM re-rendered", which is a confident diagnosis
@@ -450,8 +491,27 @@ export async function runTool(tool, deps, args) {
450
491
  // verdict-less session in the field never called a verdict-producing tool ONCE;
451
492
  // the counter behind this already existed and was reported only to us. One-shot per abandoned
452
493
  // run, same discipline as the pool lease — a hint on every call is noise that gets tuned out.
494
+ // The A/B control, and the reason it exists as an env flag rather than a code edit.
495
+ //
496
+ // `verify_next` is described in this repo's own changelog as "the largest known lever on whether a
497
+ // session produces a verdict at all", and it has never been measured — it was once built, fired
498
+ // and silently dropped by schema-strict clients for a whole release, which is exactly what an
499
+ // unmeasured lever looks like from the inside. "The payload now arrives" and "the agent acts on
500
+ // it" are different claims and only the first was ever verified.
501
+ //
502
+ // Suppressing it for a control arm has to leave the ONLY difference being the baton: same build,
503
+ // same surface, same counters. So the nudge is still TAKEN (the counter still resets, so the
504
+ // one-shot cadence is identical) and only the envelope key is withheld.
505
+ const suppressBaton = '1' === process.env[SUPPRESS_VERIFY_NEXT_ENV];
453
506
  const unverified = getSessionMetrics().takeUnverifiedNudge();
454
- if (unverified !== undefined)
455
- envelope[EnvelopeKey.VERIFY_NEXT] = unverified;
507
+ // Carry the CALL, not just the sentence. The prose has to be translated back into arguments, and
508
+ // that translation is where agents were already going wrong — `until` omitted, action arguments
509
+ // flat instead of nested under `args`. `ref` and `action` come from the act that actually
510
+ // dispatched, so the suggestion is about the element the agent touched. See verify-next-baton.
511
+ if (unverified !== undefined && !suppressBaton)
512
+ // `lastAct` read defensively: this envelope is built on EVERY tool call, so a session shape
513
+ // without it would turn a missing field into a crash on the whole surface rather than a missing
514
+ // suggestion. The baton degrades to prose, which is what it carried before.
515
+ envelope[EnvelopeKey.VERIFY_NEXT] = verifyNextBaton(unverified, resolved.lastAct?.effect() ?? {});
456
516
  return Object.keys(envelope).length > 0 ? { ...result, ...envelope } : result;
457
517
  }
@@ -35,5 +35,15 @@ export interface LeaseEvidence {
35
35
  * reported as a marker that was not found.
36
36
  */
37
37
  sdkMarker?: boolean;
38
+ /**
39
+ * The websocket URL the leased page actually tried, read from its own console.
40
+ *
41
+ * The only per-page proof there is about where the dial WENT. `refusal` proves a dial arrived
42
+ * HERE; this proves where one was aimed, which is the strictly more useful fact when it was aimed
43
+ * somewhere else — the case in which no refusal exists to be recorded, because nothing arrived.
44
+ *
45
+ * Absent means the page said nothing, never that it dialled correctly.
46
+ */
47
+ dialledUrl?: string;
38
48
  }
39
49
  export declare function leaseNotConnectedHint(url: string, port: number, evidence?: LeaseEvidence): string;
@@ -45,9 +45,45 @@ function markerClause(sdkMarker) {
45
45
  'document, so a lazily-imported SDK can still be missed — but combined with the rest it ' +
46
46
  'points at a bundle that carries no Reticle at all, i.e. wiring that never took effect.';
47
47
  }
48
+ /**
49
+ * The port a websocket URL names, or undefined when it names none we can read.
50
+ *
51
+ * `ws://host/path` with no explicit port is deliberately undefined rather than defaulted to 80: a
52
+ * guessed port compared against the daemon's would manufacture a mismatch out of nothing, and this
53
+ * branch outranks every other cause. No answer is correct here; a plausible one is not.
54
+ */
55
+ function dialledPort(url) {
56
+ if (url === undefined)
57
+ return undefined;
58
+ try {
59
+ const port = new URL(url).port;
60
+ return '' === port ? undefined : Number(port);
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
48
66
  export function leaseNotConnectedHint(url, port, evidence = {}) {
49
67
  const opening = `the leased tab loaded ${url} but never dialled this daemon (port ${String(port)}).`;
50
68
  const marker = markerClause(evidence.sdkMarker);
69
+ // 0. The page told us where it dialled, and it was not here. Proof, and it outranks the refusal
70
+ // branch below: a refusal proves a dial reached THIS daemon, which is a fact about some page;
71
+ // this is a fact about the page in hand. When they disagree, the specific one wins.
72
+ //
73
+ // Neither side can reach this conclusion alone, which is why it went unsaid for so long. The
74
+ // page's own warning deliberately refuses to diagnose the daemon — from inside a browser a
75
+ // daemon that is absent and one that is unreachable are the same observation. The daemon cannot
76
+ // see a dial that never arrived. Put the address the page names beside the port the daemon is
77
+ // bound to and the ambiguity disappears.
78
+ const dialled = dialledPort(evidence.dialledUrl);
79
+ if (dialled !== undefined && dialled !== port) {
80
+ return (`${opening} The page dialled ${String(evidence.dialledUrl)} (port ${String(dialled)}) and ` +
81
+ `this daemon is on ${String(port)}, so the dial never reached it. Nothing about the app's ` +
82
+ `wiring is in question. Either start the daemon on ${String(dialled)}, or point the app at ` +
83
+ `${String(port)} — the app's port comes from its build config (\`.reticle.json\`, ` +
84
+ `RETICLE_PORT, or the build plugin's \`port\`), and \`VITE_RETICLE_WS_URL\` overrides it ` +
85
+ `outright.${marker} ${RELEASE}`);
86
+ }
51
87
  // 1. A recorded refusal. The only cause with proof behind it, so nothing outranks it.
52
88
  if (evidence.refusal !== undefined) {
53
89
  return `${opening} A dial DID arrive and was refused: ${evidence.refusal}${marker} ${RELEASE}`;
@@ -55,11 +91,24 @@ export function leaseNotConnectedHint(url, port, evidence = {}) {
55
91
  // 2. Nuxt, whose most likely cause differs from every other framework's and which `init` warns
56
92
  // about at install time — while this hint never mentioned it.
57
93
  const nuxt = evidence.framework?.toLowerCase() === NUXT ? ` ${NUXT_FIRST}` : '';
58
- // 3. Proven wiring. Blaming the port here is the self-contradiction this rewrite exists to end.
94
+ // 3. Proven PORT. Blaming the port here is the self-contradiction this rewrite exists to end —
95
+ // but "the wiring is correct" was a second claim, and it was not proven at all.
96
+ //
97
+ // `previouslyConnected` is scoped to project + port, never to the app. In a monorepo, or for
98
+ // anyone adding a second app, the thing that connected yesterday is a DIFFERENT app. Measured:
99
+ // an uninstrumented app driven in exactly that situation was told its wiring was correct, and
100
+ // handed four causes — a dev-mode guard, a stale dev server, a missing peer dependency, a
101
+ // non-localhost page — every one of which presupposes the SDK is already installed. It was
102
+ // not. That is the commonest failure there is, and `reticle init` appeared nowhere in the
103
+ // answer, because the only branch that mentions it is the one reached when nothing is known.
59
104
  if (true === evidence.previouslyConnected) {
60
- return (`${opening} An app for this project HAS connected on this port before, so the port and the ` +
61
- `wiring are both correct — what is failing is the SDK reaching initialise on the page.${nuxt}` +
62
- `${marker} ${REAL_CAUSES} ${RELEASE}`);
105
+ const notThisApp = true === evidence.sdkMarker
106
+ ? ''
107
+ : ' That may have been a DIFFERENT app, though: this one may carry no Reticle SDK at all, ' +
108
+ 'in which case run `reticle init` in ITS directory first — every cause below assumes the ' +
109
+ 'SDK is already installed.';
110
+ return (`${opening} An app for this project HAS connected on this port before, so the port is ` +
111
+ `proven.${notThisApp}${nuxt}${marker} ${REAL_CAUSES} ${RELEASE}`);
63
112
  }
64
113
  // 4. Wired, but never seen connect. Rank the real causes; keep the port differential last, where
65
114
  // the evidence for it actually sits.
@@ -58,6 +58,11 @@ export declare function acquireLeasedSession(pool: {
58
58
  sessionId: string;
59
59
  release: () => Promise<void>;
60
60
  }>;
61
+ /**
62
+ * The address this lease's page said it could not reach. Optional so a test double need not
63
+ * implement it; the real pool always does.
64
+ */
65
+ dialFailureUrl?: (sessionId: string) => string | undefined;
61
66
  /** Optional so a test double need not implement it; the real pool always does. */
62
67
  alias?: (registeredId: string, leaseId: string) => void;
63
68
  }, sessions: {
@@ -81,5 +86,4 @@ export declare function waitForLeasedSession(isConnected: () => boolean, sleeper
81
86
  * any other call instead of being a second, invisible dispatch path.
82
87
  */
83
88
  export declare const LEASE_ACQUIRE_TOOL: ToolDef;
84
- export declare const LEASE_RELEASE_TOOL: ToolDef;
85
89
  export declare const LEASE_TOOLS: ToolDef[];
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { z } from 'zod';
12
12
  import { leaseNotConnectedHint } from './lease-hint.js';
13
+ import { probeSdkMarker } from './sdk-marker-probe.js';
13
14
  import { readProjectFramework, readProjectId } from '../cli/cli-port.js';
14
15
  import { hasConnectedBefore } from '../session/connection-memory.js';
15
16
  import { reticleStateHome } from '../daemon/daemon.js';
@@ -24,14 +25,23 @@ import { chromiumHint } from '../cli/chromium-hint.js';
24
25
  * printed a static differential over the top of it — telling one agent in `reticle_sessions` that
25
26
  * the wiring was proven correct and telling it seconds later that the port was probably wrong.
26
27
  */
27
- function leaseEvidence(deps, port) {
28
+ /**
29
+ * Async only for the marker probe, which is the piece that decides whether this app ships an SDK at
30
+ * all — the difference between the diagnosis a new user needs and four causes that all assume the
31
+ * install already happened. It is one localhost GET on a path that has already failed and is about
32
+ * to print a paragraph, and it can only ever add a sentence: the probe returns `undefined` on any
33
+ * doubt and the hint then says nothing about markers.
34
+ */
35
+ async function leaseEvidence(deps, port, url) {
28
36
  const cwd = process.cwd();
29
37
  const projectId = readProjectId(cwd);
30
38
  const refusal = deps.sessions.lastClosure()?.reason;
31
39
  const framework = readProjectFramework(cwd);
40
+ const sdkMarker = await probeSdkMarker(url);
32
41
  return {
33
42
  ...(refusal === undefined ? {} : { refusal }),
34
43
  ...(framework === undefined ? {} : { framework }),
44
+ ...(sdkMarker === undefined ? {} : { sdkMarker }),
35
45
  previouslyConnected: hasConnectedBefore(reticleStateHome(), port, projectId),
36
46
  initialized: projectId !== undefined,
37
47
  };
@@ -239,16 +249,20 @@ export const LEASE_ACQUIRE_TOOL = {
239
249
  queued: pool.queuedCount(),
240
250
  ...(ready
241
251
  ? {}
242
- : (() => {
243
- const bridgePort = deps.bridgePort ?? RETICLE_DEFAULT_PORT;
244
- return {
245
- hint: leaseNotConnectedHint(url, bridgePort, leaseEvidence(deps, bridgePort)),
246
- };
247
- })()),
252
+ : { hint: await notConnectedHint(deps, url, pool.dialFailureUrl?.(lease.sessionId)) }),
248
253
  };
249
254
  },
250
255
  };
251
- export const LEASE_RELEASE_TOOL = {
256
+ /** The whole not-connected diagnosis, gathered and worded. Split out so the acquire path stays flat. */
257
+ async function notConnectedHint(deps, url, dialledUrl) {
258
+ const bridgePort = deps.bridgePort ?? RETICLE_DEFAULT_PORT;
259
+ const evidence = await leaseEvidence(deps, bridgePort, url);
260
+ return leaseNotConnectedHint(url, bridgePort, {
261
+ ...evidence,
262
+ ...(dialledUrl === undefined ? {} : { dialledUrl }),
263
+ });
264
+ }
265
+ const LEASE_RELEASE_TOOL = {
252
266
  name: ReticleTool.LEASE_RELEASE,
253
267
  description: 'Release a leased browser context by sessionId, closing it and freeing the pool slot for a queued acquire. Call this when a flow finishes so the pool stays within its concurrency cap.',
254
268
  inputSchema: {
@@ -13,8 +13,6 @@
13
13
  */
14
14
  import type { SessionManager } from '../session/session-manager.js';
15
15
  import type { NavigateArrival } from './navigate-result.js';
16
- /** Long enough for a dev server to serve a page and the SDK to dial back; short enough to not hang. */
17
- export declare const ARRIVAL_TIMEOUT_MS = 5000;
18
16
  export interface ArrivalClock {
19
17
  now: () => number;
20
18
  sleep: (ms: number) => Promise<void>;
@@ -12,7 +12,7 @@
12
12
  * the sleep are injected so this is testable without waiting on a real one.
13
13
  */
14
14
  /** Long enough for a dev server to serve a page and the SDK to dial back; short enough to not hang. */
15
- export const ARRIVAL_TIMEOUT_MS = 5_000;
15
+ const ARRIVAL_TIMEOUT_MS = 5_000;
16
16
  const POLL_MS = 100;
17
17
  /**
18
18
  * Compare only origin + pathname.
@@ -5,7 +5,7 @@
5
5
  import { noteEmptyRead } from './observed-nothing.js';
6
6
  import { z } from 'zod';
7
7
  import { aliasParam } from './alias-args.js';
8
- import { CONSOLE_LEVELS, ReticleCommand, DEFAULT_ASSERT_TIMEOUT_MS, PredicateKind, } from '@reticlehq/core';
8
+ import { CONSOLE_LEVELS, ReticleCommand, DEFAULT_ASSERT_TIMEOUT_MS, PredicateKind, Verified, } from '@reticlehq/core';
9
9
  import { ReticleTool } from './tool-names.js';
10
10
  import { buildReactionReport } from '../events/reaction.js';
11
11
  import { findContradictions } from '../events/contradictions.js';
@@ -13,13 +13,18 @@ import { evaluatePredicate, waitForPredicate, PredicateSchema } from '../events/
13
13
  import { parsePredicate } from '../events/predicate-parse.js';
14
14
  import { matchNet, matchConsole, isConsoleEvent, eventMatchesFilters, netEmptyHint, consoleEmptyHint, reconcileNet, projectNetCall, projectConsoleLog, } from '../events/event-filters.js';
15
15
  import { applyEventBudget, costHint, withSizeCost, DEFAULT_QUERY_LIMIT, } from '../session/output-budget.js';
16
- import { healthEnvelope, bufferEnvelope } from '../session/session-health.js';
16
+ import { annotateStarvedFailure, healthEnvelope, bufferEnvelope, } from '../session/session-health.js';
17
17
  import { assertsDerivedIpcStatus, DERIVED_IPC_STATUS_ADVICE, isPresenceOnlyAssertion, PRESENCE_ONLY_ADVICE, } from './assert-grade.js';
18
18
  import { assertVerdict } from './assert-verdict.js';
19
+ import { assertSource } from './assert-source.js';
20
+ import { isChangeUndeclared } from '../honesty/undeclared-change.js';
21
+ import { openSessionIntents } from '../intent/open-intents.js';
22
+ import { dischargeInlineIntent, inlineVerdictId, linkInlineIntent, } from '../intent/inline-intent.js';
19
23
  import { bodiesNotCaptured } from '../honesty/uncaptured-bodies.js';
20
24
  import { withControl } from '../session/control-envelope.js';
21
- import { asString, asNumber } from './tools-helpers.js';
22
- import { sessionIdShape, commandOrThrow } from './tool-kit.js';
25
+ import { asString, asNumber, asRecord } from './tools-helpers.js';
26
+ import { intentArg, sessionIdShape, commandOrThrow } from './tool-kit.js';
27
+ import { gradeOfPredicate } from './assert-grade.js';
23
28
  /**
24
29
  * Evidence-completeness block: present on observe/network/console only when the ring buffer has
25
30
  * evicted events, so a "no such event" answer is distinguishable from "I dropped it" (issue #27).
@@ -31,20 +36,20 @@ const bufferOutputShape = {
31
36
  .describe('Present only when the event buffer evicted events — a negative result may then be a false negative.'),
32
37
  };
33
38
  /**
34
- * Where to look when a failure has no element.
39
+ * The file:line an assertion may report — see `assertSource`.
35
40
  *
36
- * "the signal never fired", "the request was never made", "the store did not change" have no DOM node
37
- * to map to a component, so the file:line that covers element failures leaves exactly the failures
38
- * that most need explaining with nowhere to send the agent. The handler that should have fired the
39
- * signal lives with the control that was clicked, and the act path captured that control's source —
40
- * so a failing assertion can point there instead of at nothing.
41
- *
42
- * RED only: on a pass this is noise on the path the agent walks most.
41
+ * Neither `reticle_assert` nor `reticle_wait_for` drives anything, so the last act's source is about
42
+ * some earlier action and not about this verdict. The pointer comes from the assertion's own matched
43
+ * evidence; the last driven control is borrowed only for a RED whose predicate has no DOM clause at
44
+ * all, which is the failure that genuinely has no element to point at.
43
45
  */
44
- function lastActSourceOnFailure(session, pass) {
45
- if (pass)
46
- return {};
47
- const source = session.lastAct.source();
46
+ function assertionSource(session, predicate, verdict) {
47
+ const source = assertSource({
48
+ predicate,
49
+ evidence: verdict.evidence,
50
+ pass: verdict.pass,
51
+ lastActSource: session.lastAct.source(),
52
+ });
48
53
  return source === undefined ? {} : { source };
49
54
  }
50
55
  /**
@@ -179,7 +184,11 @@ export const OBSERVE_TOOLS = [
179
184
  // never asked about.
180
185
  const actCursor = session.lastAct.cursor();
181
186
  const judgingTheAct = actCursor !== undefined && actCursor >= since;
182
- const contradictions = findContradictions(filtered, judgingTheAct ? { ...session.lastAct.effect(), actionSince: actCursor } : {});
187
+ const contradictions = findContradictions(filtered, {
188
+ currentDocumentId: session.currentDocumentId,
189
+ currentEditEpoch: session.currentEditEpoch,
190
+ ...(judgingTheAct ? { ...session.lastAct.effect(), actionSince: actCursor } : {}),
191
+ });
183
192
  // carry session health — a throttled tab means the observed timeline may be incomplete.
184
193
  return withControl(session, {
185
194
  ...report,
@@ -233,7 +242,7 @@ export const OBSERVE_TOOLS = [
233
242
  source: z
234
243
  .string()
235
244
  .optional()
236
- .describe('On a FAILING assertion, the `file:line` of the control last acted on — where the code that should have produced the missing signal/request/state change lives. Present only when an act preceded this assertion.'),
245
+ .describe('Where the code behind this verdict lives, as `file:line`. For an element/text assertion it is the matched element itself; for a FAILING signal/net/state assertion — which has no element to point at — it is the control last acted on, where the handler that should have fired lives. OMITTED when neither is known: this tool never borrows an unrelated location.'),
237
246
  },
238
247
  handler: async (deps, args) => {
239
248
  const session = deps.sessions.resolve(asString(args['sessionId']));
@@ -245,8 +254,11 @@ export const OBSERVE_TOOLS = [
245
254
  // match reticle_assert — wrap with control + session health (throttle matters most while blocking)
246
255
  // and the buffer envelope, so a verdict reached over an evicted window says so.
247
256
  return withControl(session, {
248
- ...verdict,
249
- ...lastActSourceOnFailure(session, verdict.pass),
257
+ // #537's starved-wait note wraps the verdict (it RETURNS the verdict), so it stands where
258
+ // `...verdict` did. The source line is #533's `assertionSource`, which superseded
259
+ // `lastActSourceOnFailure` — an assert used to be blamed on the previous act's file:line.
260
+ ...annotateStarvedFailure(session, verdict),
261
+ ...assertionSource(session, predicate, verdict),
250
262
  ...healthEnvelope(session),
251
263
  ...bufferEnvelope(session),
252
264
  });
@@ -255,14 +267,15 @@ export const OBSERVE_TOOLS = [
255
267
  {
256
268
  name: ReticleTool.ASSERT,
257
269
  example: { predicate: { kind: PredicateKind.SIGNAL, name: 'todos:loaded' } },
258
- description: 'Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. Prefer a { signal } or { net } consequence over { element }/{ text } presence — a passing presence-only assertion returns `advice` because a wrong/healed element can fake it. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.',
270
+ description: 'Evaluate a predicate (optionally waiting up to timeout_ms). Returns { pass, evidence, failureReason? }. The end of every verify loop. CHECKING SEVERAL THINGS? Put them in ONE call with { kind: "allOf", predicates: [...] } — it returns one verdict naming whichever member failed, and one call costs one round trip where N calls cost N. Prefer a { signal } or { net } consequence over { element }/{ text } presence — a passing presence-only assertion returns `advice` because a wrong/healed element can fake it. By default it only counts events since your last act, so a stale buffered signal can never fake a pass; pass `since` (an observe/act cursor) to set the window explicitly.',
259
271
  inputSchema: {
260
272
  predicate: PredicateSchema.optional().describe(
261
273
  // Every kind, each with the field that carries its argument. Five of the nine were
262
274
  // undocumented here, including `route` — and "did submitting the login form navigate away"
263
275
  // is the most common thing an agent wants to assert. A field report reached us from an agent
264
276
  // that guessed `urlContains` on route (net's spelling) and got unrecognized_keys.
265
- 'Predicate to evaluate. Kinds: { signal, name } { net, urlContains|method|status|count|bodyContains } ' +
277
+ 'Predicate to evaluate. Kinds: { signal, name|dataMatches|count } ' +
278
+ '{ net, urlContains|method|status|count|bodyContains } ' +
266
279
  '{ state, path|equals } { route, pathname (exact) | contains (path+query+hash) } ' +
267
280
  '{ element, testid|role|text } { text } { console, level|contains|absent } { animation, name } ' +
268
281
  '{ settled } — combine with { allOf | anyOf | not }. Prefer a signal/net/state consequence ' +
@@ -276,6 +289,7 @@ export const OBSERVE_TOOLS = [
276
289
  .number()
277
290
  .optional()
278
291
  .describe('Cursor from a prior reticle_act — scopes the assertion to events after that act.'),
292
+ intent: intentArg,
279
293
  ...sessionIdShape,
280
294
  },
281
295
  outputSchema: {
@@ -309,7 +323,7 @@ export const OBSERVE_TOOLS = [
309
323
  source: z
310
324
  .string()
311
325
  .optional()
312
- .describe('On a FAILING assertion, the `file:line` of the control last acted on — where the code that should have produced the missing signal/request/state change lives. Present only when an act preceded this assertion.'),
326
+ .describe('Where the code behind this verdict lives, as `file:line`. For an element/text assertion it is the matched element itself; for a FAILING signal/net/state assertion — which has no element to point at — it is the control last acted on, where the handler that should have fired lives. OMITTED when neither is known: this tool never borrows an unrelated location.'),
313
327
  coverage: z
314
328
  .string()
315
329
  .optional()
@@ -330,6 +344,9 @@ export const OBSERVE_TOOLS = [
330
344
  const timeout = asNumber(args['timeout_ms']) ?? 0;
331
345
  // Honesty: explicit since wins; else default to the last act's cursor; else the whole buffer.
332
346
  const since = asNumber(args['since']) ?? session.lastAct.cursor() ?? 0;
347
+ // Declared BEFORE the verdict, so the undeclared-change read below finds it open and stays
348
+ // quiet on THIS verdict rather than on the next one. Discharged after that read.
349
+ const intentId = await linkInlineIntent(deps, asString(args['sessionId']), asString(args['intent']), PredicateKind.SETTLED === predicate.kind ? undefined : predicate);
333
350
  const verdict = timeout > 0
334
351
  ? await waitForPredicate(session, predicate, timeout, since)
335
352
  : await evaluatePredicate(session, predicate, since);
@@ -345,16 +362,36 @@ export const OBSERVE_TOOLS = [
345
362
  : assertsDerivedIpcStatus(predicate)
346
363
  ? { advice: DERIVED_IPC_STATUS_ADVICE }
347
364
  : {};
348
- const { decision, contradictions, coverage, gaps } = await assertVerdict(session, predicate, verdict.pass, since, verdict.inconclusive, verdict.observationLost);
365
+ // Asked of every verdict drawn after an observed edit, not once per edit — see
366
+ // isChangeUndeclared for why repeating it is disclosure rather than nagging.
367
+ const changeUndeclared = await isChangeUndeclared(session.currentEditEpoch, () => openSessionIntents(deps, asString(args['sessionId'])));
368
+ const { decision, contradictions, coverage, gaps, verdictEffect } = await assertVerdict(session, predicate, verdict.pass, verdict.evidence, since, verdict.inconclusive, verdict.observationLost, changeUndeclared);
369
+ // The assertion IS the proof attempt, so a green one discharges the intent it was drawn for. A
370
+ // red proved nothing, and an unbound intent refuses discharge anyway — see inline-intent.ts.
371
+ // The id is checked HERE rather than only inside the helper so a caller that declared no intent
372
+ // touches nothing at all, not even the clock.
373
+ if (intentId !== undefined && Verified.YES === decision['verified']) {
374
+ await dischargeInlineIntent(deps, asString(args['sessionId']), intentId, {
375
+ verdictId: inlineVerdictId(ReticleTool.ASSERT, deps.now()),
376
+ grade: gradeOfPredicate(predicate),
377
+ at: deps.now(),
378
+ });
379
+ }
380
+ // Journal the verdict so a LATER turn can read what this one proved. A verdict that lives only
381
+ // in the response lives only in the agent's context window, which is the copy a compaction
382
+ // destroys — see runs/run-context.ts. Recorded WITHOUT an attribution window: this tool drives
383
+ // nothing, so no event it observed was caused by it.
384
+ session.recordAction(ReticleTool.ASSERT, asRecord(args), verdictEffect);
349
385
  return withControl(session, {
350
386
  ...decision,
351
- ...verdict,
387
+ ...annotateStarvedFailure(session, verdict),
352
388
  ...(contradictions.length > 0 ? { contradictions } : {}),
353
389
  // What the app did not tell Reticle, on the same rule the act path uses.
354
390
  ...(gaps.length > 0 ? { instrumentationGaps: gaps } : {}),
355
391
  ...advice,
356
392
  ...coverage,
357
- ...lastActSourceOnFailure(session, verdict.pass),
393
+ // The SAME pointer the journal keeps, not a second lookup — one verdict, one file:line.
394
+ ...(verdictEffect.source === undefined ? {} : { source: verdictEffect.source }),
358
395
  ...healthEnvelope(session),
359
396
  ...bufferEnvelope(session),
360
397
  });
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { resolveAnnotateTarget } from '../flows/annotate-target.js';
6
6
  import { z } from 'zod';
7
- import { EventType, ReticleCommand, REPLAY_PROGRAM_VERSION, SnapshotMode } from '@reticlehq/core';
7
+ import { EventType, ReticleCommand, REPLAY_PROGRAM_VERSION, SnapshotMode, StorageArea, } from '@reticlehq/core';
8
8
  import { ReticleTool } from './tool-names.js';
9
9
  import { proposeConsequences } from '../oracles/propose-consequences.js';
10
10
  import { replayProgram } from '../flows/replay.js';
@@ -320,8 +320,10 @@ export const READ_TOOLS = [
320
320
  .describe("Dot-path into the store (e.g. 'captionCache.v3'). Numeric array indices are supported."),
321
321
  depth: z
322
322
  .number()
323
+ .int()
324
+ .positive()
323
325
  .optional()
324
- .describe('Collapse anything deeper than N levels to a size marker — avoids huge outputs for large stores.'),
326
+ .describe('Collapse anything deeper than N levels to a size marker — avoids huge outputs for large stores. A whole number of levels, 1 or more.'),
325
327
  ...sessionIdShape,
326
328
  },
327
329
  outputSchema: {
@@ -513,7 +515,7 @@ export const READ_TOOLS = [
513
515
  'diagnosable.',
514
516
  inputSchema: {
515
517
  area: z
516
- .enum(['local', 'session', 'cookies'])
518
+ .nativeEnum(StorageArea)
517
519
  .optional()
518
520
  .describe('Scope to one storage area. Omit to read all three.'),
519
521
  key: z
@@ -5,10 +5,32 @@
5
5
  * Split out of act-tools.ts along its natural seam: the act tools care only about the outcome
6
6
  * (`result` defined = it went native), never about provider availability, box resolution,
7
7
  * drag-target inspection or the reason taxonomy.
8
+ *
9
+ * Also hosts `rewriteUploadArgs` — the daemon-side path that lets an agent name a file on disk
10
+ * and have its REAL bytes reach the browser's `<input type="file">`. The browser SDK cannot read
11
+ * the filesystem; the daemon can. The rewrite happens here, before the ACT command crosses the
12
+ * bridge, so the browser side sees a normal `{ content, name, type }` call and `assertUploadArgs`
13
+ * keeps its invariant (no fabricated bytes, no silently-dropped keys).
14
+ *
15
+ * Trust boundary: scoped to the project root (one level above `deps.reticleRoot`), resolved
16
+ * through `realpath` so symlinks cannot escape it. Sensitive files (.env*, .git/, *.pem, id_*,
17
+ * .npmrc) are denied with a message that says why. The cap is derived from the bridge's own
18
+ * MAX_MESSAGE_BYTES with the base64 4/3 inflation factor applied, so the encoded payload always
19
+ * fits in one WebSocket frame.
8
20
  */
9
21
  import { ActionType, InputModeReason } from '@reticlehq/core';
10
22
  import type { Session } from '../session/session.js';
11
23
  import { type ToolDeps } from './tool-kit.js';
24
+ /**
25
+ * If the action is `upload` AND the inner args carry `path`, read the file from disk and rewrite
26
+ * the args to `{ content, name, type, __base64: true }` before the ACT command reaches the browser.
27
+ *
28
+ * Returns the (possibly rewritten) args object. All other actions are returned unchanged.
29
+ *
30
+ * This is the SINGLE interception point — called from session.command() so flow replay, crawl,
31
+ * and every other ACT dispatch site are covered without per-site wiring.
32
+ */
33
+ export declare function rewriteUploadArgs(deps: Pick<ToolDeps, 'fs' | 'reticleRoot'>, action: string, innerArgs: Record<string, unknown>): Promise<Record<string, unknown>>;
12
34
  interface RealActResult {
13
35
  /** Defined only on a successful native action; `undefined` means the synthetic path runs. */
14
36
  result: unknown;