@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
@@ -18,12 +18,15 @@ import { causalSummary } from '../capsule/causal-summary.js';
18
18
  import { findContradictions } from '../events/contradictions.js';
19
19
  import { gapsForAction } from '../honesty/instrumentation-gaps.js';
20
20
  import { noteSessionGaps } from '../honesty/gap-ledger.js';
21
+ import { isChangeUndeclared } from '../honesty/undeclared-change.js';
22
+ import { openSessionIntents } from '../intent/open-intents.js';
23
+ import { dischargeInlineIntent, inlineVerdictId, linkInlineIntent, } from '../intent/inline-intent.js';
21
24
  import { declaresState } from '../events/predicate-asks.js';
22
25
  import { isStateUnwatched } from '../honesty/blind-spots.js';
23
26
  import { inFlightRequestLabels, repeatedRequestLabels, waitForInFlight, } from './settle-in-flight.js';
24
27
  import { waitForReaction } from './react-grace.js';
25
28
  import { decideVerified } from '../honesty/verified.js';
26
- import { declaredExpectations } from '../events/declared.js';
29
+ import { declaredExpectations, declaresBodyIndependentChannel } from '../events/declared.js';
27
30
  import { readsDomState } from '../honesty/already-true.js';
28
31
  import { describeWaitTarget } from '../honesty/unsettled.js';
29
32
  import { saveFailedAssertCapsule } from './act-capsule.js';
@@ -34,12 +37,31 @@ import { buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage
34
37
  import { hasAcceptedWrite } from '../honesty/accepted-write.js';
35
38
  import { unreadWriteLabels } from '../honesty/unread-outcome.js';
36
39
  import { evaluatePredicate, waitForPredicate, provenExpectedLinks, PredicateSchema, } from '../events/predicate.js';
40
+ import { awaitDocumentSuccessor } from '../session/session-successor.js';
37
41
  import { healthEnvelope, refuseIfThrottled } from '../session/session-health.js';
38
42
  import { pausedShortCircuit, pausedOutputShape, withControl, PAUSED_NO_VERDICT, } from '../session/control-envelope.js';
39
43
  import { asString, asNumber, asRecord, sourceOf } from './tools-helpers.js';
40
- import { sessionIdShape } from './tool-kit.js';
44
+ import { intentArg, sessionIdShape } from './tool-kit.js';
41
45
  import { asActionType, gradeOf } from './act-helpers.js';
42
- import { tryRealInput } from './real-input-attempt.js';
46
+ import { tryRealInput, rewriteUploadArgs } from './real-input-attempt.js';
47
+ /**
48
+ * Single dispatch point for every ACT and ACT_SEQUENCE command.
49
+ *
50
+ * This is the seam the reviewer asked for: instead of wiring rewriteUploadArgs at three separate
51
+ * call sites (reticle_act, reticle_act_and_wait, reticle_act_sequence) we intercept once here,
52
+ * which also covers flow-replay, crawl, and any future dispatch site that goes through this helper.
53
+ *
54
+ * captureAct is called AFTER this, so the flow-recording captures the pre-rewrite args (the path
55
+ * the agent actually wrote) rather than the base64 blob — replay would otherwise send 750 KiB of
56
+ * base64 to the browser as if it were an inline content call.
57
+ */
58
+ async function actCommand(deps, session, actArgs, timeoutMs) {
59
+ const rewritten = await rewriteUploadArgs(deps, 'string' === typeof actArgs['action'] ? actArgs['action'] : '', asRecord(actArgs['args']));
60
+ const bridgeArgs = { ...actArgs, args: rewritten };
61
+ return timeoutMs !== undefined
62
+ ? session.command(ReticleCommand.ACT, bridgeArgs, timeoutMs)
63
+ : session.command(ReticleCommand.ACT, bridgeArgs);
64
+ }
43
65
  /**
44
66
  * Narrow the wire's `action` to a real ActionType, or undefined.
45
67
  *
@@ -113,7 +135,7 @@ export const ACT_TOOLS = [
113
135
  args: z
114
136
  .record(z.unknown())
115
137
  .optional()
116
- .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration.'),
138
+ .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt — a Cmd+K), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { native: true } to force a trusted native click, { holdMs: N } to keep the pointer DOWN for N ms (hold-to-confirm controls; effect.heldMs reports what was achieved), { confirmDangerous: true } to allow a potentially destructive control — a permission gate, NOT a duration. For upload: { path } is a path on disk (absolute or relative to the project root; the daemon reads the file and delivers real bytes to the file picker — this is the way to verify document-ingestion flows); or { name, content?, type? } to supply inline bytes directly.'),
117
139
  refuseWhenThrottled: z
118
140
  .boolean()
119
141
  .optional()
@@ -179,7 +201,7 @@ export const ACT_TOOLS = [
179
201
  settledOutcome = real.settled ?? undefined;
180
202
  // Native input reports no synthetic effect block, so nothing measured in-target: undefined
181
203
  // (the weaker empty-window test), never a fabricated zero.
182
- session.lastAct.markActed(since, action, undefined);
204
+ session.lastAct.markActed(since, action, undefined, asString(args['ref']));
183
205
  return withControl(session, {
184
206
  since,
185
207
  inputMode: InputMode.REAL,
@@ -190,7 +212,9 @@ export const ACT_TOOLS = [
190
212
  ...healthEnvelope(session),
191
213
  });
192
214
  }
193
- const result = await session.command(ReticleCommand.ACT, {
215
+ // actCommand is the single interception point: it rewrites upload+path args to real bytes
216
+ // before any ACT command crosses the bridge, covering this call site and all others.
217
+ const result = await actCommand(deps, session, {
194
218
  ref: targetRef.ref,
195
219
  action: args['action'],
196
220
  args: args['args'] ?? {},
@@ -205,7 +229,7 @@ export const ACT_TOOLS = [
205
229
  // Keep what only this call measured, so the observe that judges this window can ask whether
206
230
  // anything happened INSIDE the target — the one fact that separates a dead control from a
207
231
  // page that was merely busy with something else.
208
- session.lastAct.markActed(since, action, mutatedWithin(r));
232
+ session.lastAct.markActed(since, action, mutatedWithin(r), asString(args['ref']));
209
233
  return withControl(session, {
210
234
  since,
211
235
  inputMode: InputMode.SYNTHETIC,
@@ -280,7 +304,7 @@ export const ACT_TOOLS = [
280
304
  for (let i = 0; i < inputSteps.length; i++) {
281
305
  const step = asRecord(inputSteps[i]);
282
306
  try {
283
- const result = await session.command(ReticleCommand.ACT, { ref: step['ref'], action: step['action'], args: step['args'] ?? {} }, perStepTimeout);
307
+ const result = await actCommand(deps, session, { ref: step['ref'], action: step['action'], args: step['args'] ?? {} }, perStepTimeout);
284
308
  if (!result.ok) {
285
309
  stalledAt = i;
286
310
  stepResults.push({
@@ -372,7 +396,7 @@ export const ACT_TOOLS = [
372
396
  args: z
373
397
  .record(z.unknown())
374
398
  .optional()
375
- .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control.'),
399
+ .describe('Action-specific arguments: { value } for fill/select, { text } for type/press (the key NAME, e.g. Escape or Tab), { modifiers: ["Meta","Shift"] } for a press shortcut (Meta/Control/Shift/Alt), { toRef } for drag (the ref to drop ON — without it the drag lands nowhere), { confirmDangerous: true } for a potentially destructive control. For upload: { path } is a path on disk (absolute or relative to project root; daemon reads real bytes) or { name, content?, type? } for inline bytes.'),
376
400
  predicate: PredicateSchema.optional().describe('Alias for `until` (the name reticle_assert / reticle_wait_for use).'),
377
401
  until: PredicateSchema.optional().describe('Predicate to wait for after the action completes (same shape as reticle_assert). OMIT to wait for the page to SETTLE — network + DOM idle — the deterministic default instead of a sleep. To assert a consequence AND settle, allOf them: { kind: "allOf", predicates: [<your predicate>, { kind: "settled" }] }.'),
378
402
  timeout_ms: z
@@ -383,6 +407,7 @@ export const ACT_TOOLS = [
383
407
  .boolean()
384
408
  .optional()
385
409
  .describe('Throw if the tab is throttled. Default: false.'),
410
+ intent: intentArg,
386
411
  ...sessionIdShape,
387
412
  },
388
413
  outputSchema: {
@@ -446,6 +471,10 @@ export const ACT_TOOLS = [
446
471
  since: z
447
472
  .number()
448
473
  .describe('Cursor for this act — pass to reticle_observe/reticle_assert for the full timeline.'),
474
+ sessionId: z
475
+ .string()
476
+ .optional()
477
+ .describe('The session that answered, when a full-document navigation replaced the one that was acted on. Absent when the original session survived.'),
449
478
  session: z
450
479
  .object({ lastSeenMs: z.number(), throttled: z.boolean(), focused: z.boolean() })
451
480
  .optional(),
@@ -453,7 +482,9 @@ export const ACT_TOOLS = [
453
482
  ...pausedOutputShape,
454
483
  },
455
484
  handler: async (deps, args) => {
456
- const session = deps.sessions.resolve(asString(args['sessionId']));
485
+ let session = deps.sessions.resolve(asString(args['sessionId']));
486
+ const acted = session;
487
+ const actedSessionId = session.id;
457
488
  // Live-control: refuse to drive the page (no act, no predicate eval) while paused.
458
489
  //
459
490
  // A VERDICT rides out with the refusal. This is the one tool here that promises `verified`,
@@ -472,6 +503,10 @@ export const ACT_TOOLS = [
472
503
  ? parsePredicate(withUntil['until'])
473
504
  : { kind: PredicateKind.SETTLED };
474
505
  const timeout = asNumber(args['timeout_ms']) ?? DEFAULT_ASSERT_TIMEOUT_MS;
506
+ // An intent declared here lands in the ledger BEFORE the verdict is drawn, which is what makes
507
+ // the undeclared-change gap silent on THIS verdict rather than the next one: it reads the
508
+ // ledger below and finds something open. The discharge comes after that read.
509
+ const intentId = await linkInlineIntent(deps, asString(args['sessionId']), asString(args['intent']), PredicateKind.SETTLED === until.kind ? undefined : until);
475
510
  // Before anything is driven: this path cannot honour a native-input request, and taking the
476
511
  // argument and ignoring it told the agent its trusted click had happened. See act-danger.
477
512
  assertNativeInputSupported(asRecord(args['args']));
@@ -480,12 +515,17 @@ export const ACT_TOOLS = [
480
515
  const resolved = await resolveActTarget(session, args);
481
516
  if ('error' === resolved.kind)
482
517
  throw new Error(resolved.message);
483
- const since = session.elapsed();
518
+ let since = session.elapsed();
519
+ const actedSince = since;
484
520
  // The cursor + effect are marked after the act dispatches (below) — a refused act leaves none.
485
521
  // The attribution window stays open across the settle wait below, so post-dispatch async events
486
522
  // (the whole point of act_and_wait) attribute to this action. finishAction fires after the wait.
487
523
  session.beginAction(ReticleTool.ACT_AND_WAIT, asRecord(args));
488
524
  let settledOutcome;
525
+ // The verdict, written into the action record so a LATER turn can read what this one proved.
526
+ // A verdict that lives only in the response lives only in the agent's context window, which is
527
+ // exactly the copy a compaction destroys — see runs/run-context.ts.
528
+ let verdictEffect;
489
529
  // Was the declared consequence ALREADY TRUE? Only asked for predicates that read live DOM
490
530
  // state — event-based ones are floored at this act's cursor and cannot be satisfied by the
491
531
  // past, so they need no pre-check and pay nothing. One extra query, on the path where a green
@@ -494,7 +534,8 @@ export const ACT_TOOLS = [
494
534
  ? (await evaluatePredicate(session, until, since, false)).pass
495
535
  : false;
496
536
  try {
497
- const actResult = await session.command(ReticleCommand.ACT, {
537
+ // actCommand is the single interception point for upload+path rewrite.
538
+ const actResult = await actCommand(deps, session, {
498
539
  ref: resolved.ref,
499
540
  action: args['action'],
500
541
  args: args['args'] ?? {},
@@ -504,12 +545,31 @@ export const ACT_TOOLS = [
504
545
  captureAct(deps.recordings, args, actResult.result);
505
546
  // Dispatched — now this act owns the cursor and the effect. Marking its OWN measurement also
506
547
  // stops the spread below from inheriting an earlier reticle_act's action and mutation count.
507
- session.lastAct.markActed(since, asString(args['action']), mutatedWithin(asRecord(actResult.result)));
548
+ session.lastAct.markActed(since, asString(args['action']), mutatedWithin(asRecord(actResult.result)), asString(args['ref']));
508
549
  // Honesty: floor the predicate at this act's cursor so a stale buffered event can't satisfy it.
509
550
  const predicateStarted = session.elapsed();
510
- const verdict = timeout > 0
551
+ let verdict = timeout > 0
511
552
  ? await waitForPredicate(session, until, timeout, since)
512
553
  : await evaluatePredicate(session, until, since);
554
+ // A full-document navigation tears the SDK down mid-wait. `navigate` already waits for the
555
+ // HELLO and returns the new id; this path used to grade `observation_lost` and leave the
556
+ // agent holding a dead id, so the next assert failed even though the new page had loaded.
557
+ // Follow the unique same-origin successor (the awaitArrival idea, without a known URL) and
558
+ // evaluate there. Two live tabs at that origin is still a guess — we do not follow then.
559
+ if (true === verdict.observationLost && timeout > 0) {
560
+ const remaining = timeout - (session.elapsed() - predicateStarted);
561
+ const next = remaining > 0 ? await awaitDocumentSuccessor(deps.sessions, session, remaining) : null;
562
+ if (next !== null) {
563
+ // Compute leftover on the departed session before we point `session` at the successor —
564
+ // the new session's elapsed() starts at 0 and would refund the whole budget.
565
+ const leftover = timeout - (session.elapsed() - predicateStarted);
566
+ session = next;
567
+ since = 0;
568
+ if (leftover > 0) {
569
+ verdict = await waitForPredicate(session, until, leftover, since);
570
+ }
571
+ }
572
+ }
513
573
  // The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is
514
574
  // while the write is still in flight — so the verdict was taken over a window the app had not
515
575
  // finished, and came back `unknown / unsettled` asking the CALLER to re-check. A login that
@@ -547,7 +607,8 @@ export const ACT_TOOLS = [
547
607
  const actedSource = sourceOf(r['source']);
548
608
  // Remembered on the session so a LATER assertion can name a file even when its failure has no
549
609
  // element to point at — a signal that never fired, a request that was never made.
550
- session.lastAct.markSource(actedSource === undefined ? undefined : `${actedSource.file}:${String(actedSource.line)}`);
610
+ const actedSourceLabel = actedSource === undefined ? undefined : `${actedSource.file}:${String(actedSource.line)}`;
611
+ session.lastAct.markSource(actedSourceLabel);
551
612
  const windowEvents = session.eventsSince(since);
552
613
  const trace = summarizeReaction(buildReactionReport(windowEvents, session.elapsed() - since));
553
614
  // The bounded Tier-1 causal summary — net/console/state/storage diffs the trace's counts miss.
@@ -590,8 +651,11 @@ export const ACT_TOOLS = [
590
651
  ...(gapNote === undefined ? [] : [CaptureLoss.TRANSPORT_GAP]),
591
652
  ...(impeaching.note === undefined ? [] : [CaptureLoss.BLIND_SPOT]),
592
653
  ];
654
+ // Named because the discharge below records the same grade the verdict reports, rather than a
655
+ // second reading of the same evidence that could drift from it.
656
+ const grade = gradeOf(gradedLinks);
593
657
  const honesty = buildHonestyBlock({
594
- grade: gradeOf(gradedLinks),
658
+ grade,
595
659
  attribution: 'window',
596
660
  // Did the buffer lose scarce evidence FROM THIS WINDOW — not "did it evict anything while
597
661
  // the action ran", which was the previous rule and which is true on essentially every live
@@ -644,6 +708,8 @@ export const ACT_TOOLS = [
644
708
  // not evidence the destination rendered either — `alreadyTrue` decides that, once.
645
709
  renderProved: verdict.pass && !alreadyTrue && declared.rendersContent,
646
710
  ...session.lastAct.effect(),
711
+ currentDocumentId: session.currentDocumentId,
712
+ currentEditEpoch: session.currentEditEpoch,
647
713
  });
648
714
  // The single field an agent reads. Everything below it is the evidence it was derived from;
649
715
  // this is the only one that has to be interpreted, and now it interprets itself.
@@ -656,6 +722,9 @@ export const ACT_TOOLS = [
656
722
  // overriding it — see `declaredConsequence`. An explicit `{ kind: "settled" }` is not a
657
723
  // declaration about the app's behaviour, it IS the idle wait, so it does not count.
658
724
  declaredConsequence: until.kind !== PredicateKind.SETTLED,
725
+ // A body-independent declaration that held is a channel the unread payload does not own.
726
+ // Omit when false: a net-only `until` must still hit `outcome_unread`.
727
+ ...(declaresBodyIndependentChannel(until) ? { independentOfBody: true } : {}),
659
728
  ...(alreadyTrue ? { alreadyTrue } : {}),
660
729
  // An assertion nobody could evaluate must not be reported as one the app failed.
661
730
  ...(verdict.inconclusive === undefined ? {} : { inconclusive: verdict.inconclusive }),
@@ -683,18 +752,55 @@ export const ACT_TOOLS = [
683
752
  // Computed once: the verdict block reports it, and the instrumentation gaps are a second
684
753
  // reading of the same evidence rather than a new observation.
685
754
  const actionSummary = causalSummary(windowEvents, { stateUnwatched });
755
+ // Asked of every verdict drawn after an observed edit, not once per edit — see
756
+ // isChangeUndeclared for why repeating it is disclosure rather than nagging.
757
+ // Read ONCE and used twice: `changeUndeclared` asks whether the ledger is empty, and the
758
+ // undischarged-intent gap asks how much it still holds. Two reads would be two chances for
759
+ // them to disagree about the same file.
760
+ const openIntents = await openSessionIntents(deps, asString(args['sessionId']));
761
+ const changeUndeclared = await isChangeUndeclared(session.currentEditEpoch, () => Promise.resolve(openIntents));
686
762
  const gaps = gapsForAction({
687
763
  pass: verdict.pass,
688
764
  proved: decision.verifiedReason === VerifiedReason.PROVED,
689
- sourceKnown: actedSource !== undefined,
765
+ changeUndeclared,
766
+ source: actedSourceLabel,
690
767
  ref: asString(args['ref']),
691
768
  stateAsked: declaresState(until),
692
769
  stateUnwatched,
770
+ // What the app DECLARED, so an under-instrumented one is told without having to be asked.
771
+ hasCapabilities: session.hasCapabilities,
772
+ // What the run still owes. A green that leaves this above zero is not the same as done.
773
+ // MINUS the one this verdict is about to discharge.
774
+ //
775
+ // The discharge happens below, after the gaps are built, so a straight `openIntents.length`
776
+ // counts the intent this very call proves. Measured live on the first drive: an inline
777
+ // intent was declared, asserted and PROVED by the same verdict, and the result still said
778
+ // "1 declared intent(s) are still unproved" while the ledger recorded it `proved`. A gap
779
+ // that fires on the one path doing everything right is noise, and noise is what gets
780
+ // filtered out — taking the honest gaps with it.
781
+ openIntentCount: openIntents.filter((i) => !(intentId !== undefined && Verified.YES === decision.verified && i.id === intentId)).length,
693
782
  domMutated: (session.lastAct.effect().mutatedWithin ?? 0) > 0,
694
783
  signalsFired: actionSummary.signals.length,
695
784
  routeChanged: actionSummary.route !== undefined,
696
785
  routeSignalFired: actionSummary.signals.some((name) => name.startsWith('route')),
697
786
  });
787
+ // The verdict IS the proof attempt, so a green one discharges the intent it was drawn for.
788
+ // Only a green: a red proved nothing, and `dischargeIntent` refuses an unbound intent anyway,
789
+ // so a bare settle leaves it open rather than collecting a proof nothing earned. The id is
790
+ // checked here rather than only inside the helper so a caller that declared no intent touches
791
+ // nothing at all, not even the clock.
792
+ if (intentId !== undefined && Verified.YES === decision.verified) {
793
+ await dischargeInlineIntent(deps, asString(args['sessionId']), intentId, {
794
+ verdictId: inlineVerdictId(ReticleTool.ACT_AND_WAIT, deps.now()),
795
+ grade,
796
+ at: deps.now(),
797
+ });
798
+ }
799
+ verdictEffect = {
800
+ claim: describeWaitTarget(until),
801
+ verified: decision.verified,
802
+ ...(actedSourceLabel === undefined ? {} : { source: actedSourceLabel }),
803
+ };
698
804
  // Recorded on the session, so a later "am I done?" can answer with what is STILL missing
699
805
  // rather than with everything that was ever missing. An empty list closes a gap, which is
700
806
  // why it is noted rather than skipped.
@@ -706,9 +812,7 @@ export const ACT_TOOLS = [
706
812
  // Promoted out of `effect` on red only. On green nobody needs it and it is noise; on red it
707
813
  // is the first thing the agent wants, and burying a file:line inside the effect block is
708
814
  // most of the way to not reporting it at all.
709
- ...(verdict.pass || actedSource === undefined
710
- ? {}
711
- : { source: `${actedSource.file}:${String(actedSource.line)}` }),
815
+ ...(verdict.pass || actedSourceLabel === undefined ? {} : { source: actedSourceLabel }),
712
816
  trace,
713
817
  ...(capsuleSaved === undefined ? {} : { capsuleSaved }),
714
818
  // The window cannot say whether anything was WATCHING state — that is a level fact the
@@ -730,11 +834,12 @@ export const ACT_TOOLS = [
730
834
  honesty,
731
835
  ...(capsule === undefined ? {} : { capsule }),
732
836
  since,
837
+ ...(session.id === actedSessionId ? {} : { sessionId: session.id }),
733
838
  ...healthEnvelope(session),
734
839
  });
735
840
  }
736
841
  finally {
737
- session.finishAction(undefined, settledOutcome, true === settledOutcome ? session.elapsed() - since : undefined);
842
+ acted.finishAction(verdictEffect, settledOutcome, true === settledOutcome ? acted.elapsed() - actedSince : undefined);
738
843
  }
739
844
  },
740
845
  },
@@ -0,0 +1,22 @@
1
+ import type { Predicate } from '../events/predicate-schema.js';
2
+ /**
3
+ * The `file:line` an ASSERTION is entitled to report — its own evidence, or nothing.
4
+ *
5
+ * `reticle_assert` and `reticle_wait_for` drive nothing, so the last act's source is whatever
6
+ * unrelated action ran before them. Journaling it made an assert on one component claim it had
7
+ * proven something about another, and an assert that matched nothing claim a location at all — a
8
+ * wrong file:line in front of an agent, persisted, and read back by `reticle_context` long after
9
+ * the turn that produced it.
10
+ *
11
+ * The one borrow that survives is the documented one, and only on a failure: a predicate with no DOM
12
+ * clause has no element to point at, and the handler that should have fired the missing signal or
13
+ * made the missing request lives with the control that was last driven. That is what the tools'
14
+ * output schemas describe, and it is a pointer to the code under test rather than a guess about
15
+ * which code this verdict was about.
16
+ */
17
+ export declare function assertSource(facts: {
18
+ predicate: Predicate;
19
+ evidence: unknown;
20
+ pass: boolean;
21
+ lastActSource: string | undefined;
22
+ }): string | undefined;
@@ -0,0 +1,53 @@
1
+ import { declaresDom } from '../events/predicate-asks.js';
2
+ /**
3
+ * Where an assertion's OWN evidence is written, as `file:line`.
4
+ *
5
+ * The element descriptors an oracle returns already carry the nearest `data-reticle-source` the build
6
+ * plugin stamped, so a predicate that matched an element knows exactly where that element lives. It
7
+ * is only reported when every matched descriptor agrees: two loci and no way to choose is the same
8
+ * problem as no locus at all, and a guessed pointer costs the agent the trip AND leaves it further
9
+ * from the code.
10
+ *
11
+ * Near-miss evidence is deliberately NOT read. A near miss is a DIFFERENT element than the one the
12
+ * caller asked about, so its line is a suggestion dressed as a fact.
13
+ */
14
+ function evidenceSource(evidence) {
15
+ if (!Array.isArray(evidence) || 0 === evidence.length)
16
+ return undefined;
17
+ let agreed;
18
+ for (const entry of evidence) {
19
+ if ('object' !== typeof entry || null === entry)
20
+ return undefined;
21
+ const found = entry.source;
22
+ if ('string' !== typeof found || 0 === found.length)
23
+ return undefined;
24
+ if (agreed === undefined)
25
+ agreed = found;
26
+ else if (agreed !== found)
27
+ return undefined;
28
+ }
29
+ return agreed;
30
+ }
31
+ /**
32
+ * The `file:line` an ASSERTION is entitled to report — its own evidence, or nothing.
33
+ *
34
+ * `reticle_assert` and `reticle_wait_for` drive nothing, so the last act's source is whatever
35
+ * unrelated action ran before them. Journaling it made an assert on one component claim it had
36
+ * proven something about another, and an assert that matched nothing claim a location at all — a
37
+ * wrong file:line in front of an agent, persisted, and read back by `reticle_context` long after
38
+ * the turn that produced it.
39
+ *
40
+ * The one borrow that survives is the documented one, and only on a failure: a predicate with no DOM
41
+ * clause has no element to point at, and the handler that should have fired the missing signal or
42
+ * made the missing request lives with the control that was last driven. That is what the tools'
43
+ * output schemas describe, and it is a pointer to the code under test rather than a guess about
44
+ * which code this verdict was about.
45
+ */
46
+ export function assertSource(facts) {
47
+ const own = evidenceSource(facts.evidence);
48
+ if (own !== undefined)
49
+ return own;
50
+ if (facts.pass || declaresDom(facts.predicate))
51
+ return undefined;
52
+ return facts.lastActSource;
53
+ }
@@ -1,4 +1,4 @@
1
- import type { InstrumentationGap } from '@reticlehq/core';
1
+ import type { InstrumentationGap, JournalVerdictEffect } from '@reticlehq/core';
2
2
  import type { Predicate } from '../events/predicate.js';
3
3
  import type { Session } from '../session/session.js';
4
4
  import { type Contradiction } from '../events/contradictions.js';
@@ -15,7 +15,12 @@ import { type Contradiction } from '../events/contradictions.js';
15
15
  * consult it. Asserting a moment earlier — while the POST was still open — is the same story with a
16
16
  * different name: `request-never-settled`, also unreported.
17
17
  */
18
- export declare function assertVerdict(session: Session, predicate: Predicate, pass: boolean, since: number,
18
+ export declare function assertVerdict(session: Session, predicate: Predicate, pass: boolean,
19
+ /**
20
+ * What the oracle actually looked at. The only thing this verdict is entitled to point at — see
21
+ * `assertSource`, which reads the matched element descriptors' own `file:line`.
22
+ */
23
+ evidence: unknown, since: number,
19
24
  /** Set when the assertion was never evaluated — see honesty/verified.ts. */
20
25
  inconclusive?: string,
21
26
  /**
@@ -23,9 +28,21 @@ inconclusive?: string,
23
28
  * `reticle_wait_for` reach the same disconnect path as the act tools, and a verdict that blames
24
29
  * the app for a lost connection is no more honest on this route than on that one.
25
30
  */
26
- observationLost?: boolean): Promise<{
31
+ observationLost?: boolean,
32
+ /**
33
+ * Set when code changed since the last verdict with nothing declaring what it was for. Decided by
34
+ * `isChangeUndeclared` at the call site, which is the only place that can reach the intent ledger —
35
+ * this path takes a session and no `deps`.
36
+ */
37
+ changeUndeclared?: boolean): Promise<{
27
38
  decision: Record<string, unknown>;
28
39
  contradictions: Contradiction[];
29
40
  coverage: Record<string, unknown>;
30
41
  gaps: InstrumentationGap[];
42
+ /**
43
+ * The bounded verdict, in the SAME shape `act_and_wait` writes into its journal action — so the
44
+ * journal holds one verdict shape rather than two and the run fold that reads it needs no second
45
+ * case. Built here because this is the one place that holds the typed decision and the predicate.
46
+ */
47
+ verdictEffect: JournalVerdictEffect;
31
48
  }>;
@@ -4,7 +4,7 @@ import { noteSessionGaps } from '../honesty/gap-ledger.js';
4
4
  import { declaresState } from '../events/predicate-asks.js';
5
5
  import { isStateUnwatched } from '../honesty/blind-spots.js';
6
6
  import { findContradictions } from '../events/contradictions.js';
7
- import { declaredExpectations } from '../events/declared.js';
7
+ import { declaredExpectations, declaresBodyIndependentChannel } from '../events/declared.js';
8
8
  import { blindSpotsFromState, buildCoverageStatement, Coverage, impeachesCapture, transportGapNote, } from '../honesty/blind-spots.js';
9
9
  import { buildHonestyBlock } from '../honesty/honesty.js';
10
10
  import { hasAcceptedWrite } from '../honesty/accepted-write.js';
@@ -13,6 +13,7 @@ import { decideVerified } from '../honesty/verified.js';
13
13
  import { describeWaitTarget } from '../honesty/unsettled.js';
14
14
  import { inFlightRequestLabels, repeatedRequestLabels } from './settle-in-flight.js';
15
15
  import { gradeOfPredicate } from './assert-grade.js';
16
+ import { assertSource } from './assert-source.js';
16
17
  function absenceBlindSpotNote(predicate, spots) {
17
18
  if (predicate.kind !== 'element' || predicate.absent !== true)
18
19
  return undefined;
@@ -37,7 +38,12 @@ function absenceBlindSpotNote(predicate, spots) {
37
38
  * consult it. Asserting a moment earlier — while the POST was still open — is the same story with a
38
39
  * different name: `request-never-settled`, also unreported.
39
40
  */
40
- export async function assertVerdict(session, predicate, pass, since,
41
+ export async function assertVerdict(session, predicate, pass,
42
+ /**
43
+ * What the oracle actually looked at. The only thing this verdict is entitled to point at — see
44
+ * `assertSource`, which reads the matched element descriptors' own `file:line`.
45
+ */
46
+ evidence, since,
41
47
  /** Set when the assertion was never evaluated — see honesty/verified.ts. */
42
48
  inconclusive,
43
49
  /**
@@ -45,7 +51,13 @@ inconclusive,
45
51
  * `reticle_wait_for` reach the same disconnect path as the act tools, and a verdict that blames
46
52
  * the app for a lost connection is no more honest on this route than on that one.
47
53
  */
48
- observationLost) {
54
+ observationLost,
55
+ /**
56
+ * Set when code changed since the last verdict with nothing declaring what it was for. Decided by
57
+ * `isChangeUndeclared` at the call site, which is the only place that can reach the intent ledger —
58
+ * this path takes a session and no `deps`.
59
+ */
60
+ changeUndeclared) {
49
61
  // Scope caveat, stated because it is a real limitation and not a bug: blind spots are tracked per
50
62
  // SESSION, not per assertion window, so a cross-origin iframe seen once marks every later verdict
51
63
  // in that session partial — including ones about a region it cannot affect. That errs toward
@@ -80,6 +92,8 @@ observationLost) {
80
92
  const declared = declaredExpectations(predicate);
81
93
  const contradictions = findContradictions(windowEvents, {
82
94
  prior,
95
+ currentDocumentId: session.currentDocumentId,
96
+ currentEditEpoch: session.currentEditEpoch,
83
97
  expectedFailures: declared.netFailures,
84
98
  renderProved: pass && declared.rendersContent,
85
99
  ...(actCursor !== undefined && actCursor >= since ? { actionSince: actCursor } : {}),
@@ -100,6 +114,7 @@ observationLost) {
100
114
  // not override it. A fix that lived on one half of the verdict surface would leave the other
101
115
  // half broken, and this is the tool agents call most.
102
116
  declaredConsequence: predicate.kind !== PredicateKind.SETTLED,
117
+ ...(declaresBodyIndependentChannel(predicate) ? { independentOfBody: true } : {}),
103
118
  ...(inconclusive === undefined ? {} : { inconclusive }),
104
119
  ...(true === observationLost ? { observationLost: true } : {}),
105
120
  ...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
@@ -131,19 +146,36 @@ observationLost) {
131
146
  // state assertion against an app that registers no store.
132
147
  const gaps = gapsForAction({
133
148
  pass,
134
- sourceKnown: session.lastAct.source() !== undefined,
149
+ ...(changeUndeclared === undefined ? {} : { changeUndeclared }),
150
+ source: session.lastAct.source(),
135
151
  stateAsked: declaresState(predicate),
136
152
  stateUnwatched: isStateUnwatched(spots),
153
+ // What the app DECLARED, so an under-instrumented one is told without having to be asked.
154
+ hasCapabilities: session.hasCapabilities,
137
155
  domMutated: false,
138
156
  signalsFired: 0,
139
157
  routeChanged: false,
140
158
  routeSignalFired: false,
141
159
  });
142
160
  noteSessionGaps(session, gaps);
161
+ // The one file:line this verdict is entitled to: its own evidence, or — for a failure with no DOM
162
+ // clause to point at — the control last driven. Returned so the RESPONSE can echo the same value
163
+ // the journal keeps; one verdict must never carry two different pointers.
164
+ const source = assertSource({
165
+ predicate,
166
+ evidence,
167
+ pass,
168
+ lastActSource: session.lastAct.source(),
169
+ });
143
170
  return {
144
171
  decision: decision,
145
172
  contradictions,
146
173
  coverage,
147
174
  gaps,
175
+ verdictEffect: {
176
+ claim: describeWaitTarget(predicate),
177
+ verified: decision.verified,
178
+ ...(source === undefined ? {} : { source }),
179
+ },
148
180
  };
149
181
  }
@@ -88,24 +88,4 @@ export const BROWSER_TOOLS = [
88
88
  }
89
89
  },
90
90
  },
91
- {
92
- name: ReticleTool.REFRESH,
93
- description: 'Reload the connected browser tab. Pass { hard: true } to bypass the browser cache (equivalent to Cmd+Shift+R). The SDK reconnects automatically after the reload.',
94
- inputSchema: {
95
- hard: z
96
- .boolean()
97
- .optional()
98
- .describe('Set true to bypass the browser cache. Default: false (normal reload).'),
99
- ...sessionIdShape,
100
- },
101
- outputSchema: {
102
- ok: z.boolean(),
103
- },
104
- handler: async (deps, args) => {
105
- await commandOrThrow(deps, asString(args['sessionId']), ReticleCommand.REFRESH, {
106
- hard: true === args['hard'],
107
- });
108
- return { ok: true };
109
- },
110
- },
111
91
  ];
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Finish a truncated snapshot, or say it could not be finished.
3
+ *
4
+ * The snapshot walk stops at its node cap and returns a document-order PREFIX with `truncated:true`.
5
+ * That flag has always been honest and has never been actionable: it says the read stopped, not
6
+ * where, so a caller could receive half a page and draw a NEGATIVE conclusion from it — "no error is
7
+ * shown", "the row is gone" — when the thing was simply past the cut. A read that under-reports is a
8
+ * verification result that is quietly wrong.
9
+ *
10
+ * The walk now names its own frontier (`unread`: refs of the subtrees it never entered), so the cut
11
+ * is recoverable: re-read each branch at its own path and the union is the whole tree. This is that
12
+ * loop, plus the half that matters — when the union CANNOT be assembled it says so, and the caller
13
+ * must not turn an empty result over it into an absence.
14
+ *
15
+ * The assembled tree is a UNION of subtrees, not a re-render: each appended chunk is indented from
16
+ * its own root. Callers scan it line by line (that is what every consumer of `tree` does), so the
17
+ * indentation restart costs nothing and re-walking the whole page from scratch would cost a lot.
18
+ */
19
+ /**
20
+ * How many snapshot round-trips one completion may spend.
21
+ *
22
+ * Each is a real command to the page, so this is the cost ceiling of a completed read, not a safety
23
+ * net. Twelve covers a page whose cut frontier is a handful of branches a couple of levels deep —
24
+ * the shape a node cap actually produces — while keeping the worst case near a dozen round-trips
25
+ * rather than a walk of the DOM one subtree at a time.
26
+ */
27
+ export declare const MAX_COMPLETION_READS = 12;
28
+ /**
29
+ * How many times the completion may follow a frontier into a new one.
30
+ *
31
+ * A branch re-read can itself truncate and name further branches, so this recursion needs a floor
32
+ * under it independent of the read budget: a hostile or pathologically deep DOM can hand back a new
33
+ * frontier every round forever. Four rounds (the first read plus three levels of re-reads) is deeper
34
+ * than a node cap reaches on a real page, and terminates on one that is not real.
35
+ */
36
+ export declare const MAX_COMPLETION_DEPTH = 4;
37
+ /** Reads one snapshot — whole page when `scope` is undefined, that branch when it is not. */
38
+ export type SnapshotReader = (scope: string | undefined) => Promise<unknown>;
39
+ export interface CompletedRead {
40
+ /** The union of every subtree that was read. Never a lie about what it contains — only about how much. */
41
+ tree: string;
42
+ /** True only when every branch the page cut was read back. A negative conclusion needs this. */
43
+ complete: boolean;
44
+ reads: number;
45
+ /** Why completion stopped short, for the caller to quote. Present ONLY when `complete` is false. */
46
+ incompleteBecause?: string;
47
+ }
48
+ export declare function readCompleteTree(read: SnapshotReader): Promise<CompletedRead>;