@reticlehq/server 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (290) hide show
  1. package/SKILL.md +73 -11
  2. package/dist/bridge/bridge.d.ts +0 -13
  3. package/dist/bridge/bridge.js +1 -1
  4. package/dist/bridge/bridge.test-harness.d.ts +2 -0
  5. package/dist/bridge/bridge.test-harness.js +8 -1
  6. package/dist/capsule/capsule-store.d.ts +2 -1
  7. package/dist/capsule/capsule-store.js +1 -1
  8. package/dist/capsule/causal-summary.d.ts +2 -2
  9. package/dist/chromium-launch-options.d.ts +28 -0
  10. package/dist/chromium-launch-options.js +29 -0
  11. package/dist/cli/cli-doctor.js +8 -0
  12. package/dist/cli/cli-flow-commands.d.ts +3 -1
  13. package/dist/cli/cli-flow-commands.js +34 -4
  14. package/dist/cli/cli-kill.d.ts +2 -1
  15. package/dist/cli/cli-parse.d.ts +3 -1
  16. package/dist/cli/cli-parse.js +29 -7
  17. package/dist/cli/cli-update-commands.js +20 -1
  18. package/dist/cli/cli-verify.d.ts +1 -1
  19. package/dist/cli/cli-verify.js +2 -2
  20. package/dist/cli/cloud-cli.js +12 -6
  21. package/dist/cli/config-discovery.d.ts +2 -1
  22. package/dist/cli/doctor-rows.d.ts +3 -1
  23. package/dist/cli/doctor-rows.js +3 -1
  24. package/dist/cli/gate-exit.d.ts +21 -0
  25. package/dist/cli/gate-exit.js +21 -0
  26. package/dist/cli/gate-hook-message.d.ts +33 -0
  27. package/dist/cli/gate-hook-message.js +46 -0
  28. package/dist/cli/mcp-command.d.ts +25 -0
  29. package/dist/cli/mcp-command.js +143 -0
  30. package/dist/cli/sibling-ports.d.ts +37 -0
  31. package/dist/cli/sibling-ports.js +56 -0
  32. package/dist/cli.js +37 -110
  33. package/dist/cloud/cloud-config.d.ts +2 -1
  34. package/dist/cloud/cloud-sync.d.ts +11 -0
  35. package/dist/cloud/cloud-sync.js +32 -0
  36. package/dist/crawl/crawl.d.ts +9 -13
  37. package/dist/crawl/crawl.js +20 -3
  38. package/dist/daemon/daemon-resolve.d.ts +79 -0
  39. package/dist/daemon/daemon-resolve.js +118 -0
  40. package/dist/daemon/free-port.d.ts +29 -0
  41. package/dist/daemon/free-port.js +55 -0
  42. package/dist/daemon/heartbeat.d.ts +0 -4
  43. package/dist/daemon/heartbeat.js +2 -2
  44. package/dist/dev/stale-issue-guard.d.ts +41 -0
  45. package/dist/dev/stale-issue-guard.js +75 -0
  46. package/dist/domain/domain-tools.js +3 -2
  47. package/dist/events/contradictions.d.ts +41 -0
  48. package/dist/events/contradictions.js +125 -12
  49. package/dist/events/declared.d.ts +14 -0
  50. package/dist/events/declared.js +33 -0
  51. package/dist/events/echo-mismatch.js +14 -0
  52. package/dist/events/observed-in-window.d.ts +12 -0
  53. package/dist/events/observed-in-window.js +17 -0
  54. package/dist/events/predicate-asks.d.ts +30 -0
  55. package/dist/events/predicate-asks.js +48 -0
  56. package/dist/events/predicate-eval.js +254 -25
  57. package/dist/events/predicate-parse.d.ts +0 -6
  58. package/dist/events/predicate-parse.js +51 -5
  59. package/dist/events/predicate-schema.d.ts +62 -0
  60. package/dist/events/predicate-schema.js +162 -4
  61. package/dist/events/predicate.d.ts +15 -0
  62. package/dist/events/predicate.js +41 -2
  63. package/dist/flows/coverage.d.ts +0 -8
  64. package/dist/flows/coverage.js +1 -1
  65. package/dist/flows/decision.d.ts +5 -1
  66. package/dist/flows/decision.js +33 -7
  67. package/dist/flows/flake.d.ts +0 -2
  68. package/dist/flows/flake.js +1 -1
  69. package/dist/flows/flow-intent.d.ts +52 -0
  70. package/dist/flows/flow-intent.js +123 -0
  71. package/dist/flows/flow-replay-run.js +27 -4
  72. package/dist/flows/flow-success.js +15 -3
  73. package/dist/flows/flow-tools.js +65 -8
  74. package/dist/flows/flows.d.ts +6 -1
  75. package/dist/flows/flows.js +34 -22
  76. package/dist/flows/predicate-to-expect.js +14 -3
  77. package/dist/flows/server-verify.js +16 -2
  78. package/dist/flows/verify-change-tools.js +9 -2
  79. package/dist/honesty/blind-spots.d.ts +8 -16
  80. package/dist/honesty/blind-spots.js +18 -5
  81. package/dist/honesty/feature-capture.d.ts +150 -0
  82. package/dist/honesty/feature-capture.js +233 -0
  83. package/dist/honesty/gap-ledger.d.ts +52 -0
  84. package/dist/honesty/gap-ledger.js +63 -0
  85. package/dist/honesty/instrumentation-gaps.d.ts +79 -0
  86. package/dist/honesty/instrumentation-gaps.js +77 -0
  87. package/dist/honesty/observability.d.ts +37 -0
  88. package/dist/honesty/observability.js +40 -0
  89. package/dist/honesty/tool-hit-rate.d.ts +86 -0
  90. package/dist/honesty/tool-hit-rate.js +90 -0
  91. package/dist/honesty/undeclared-change.d.ts +51 -0
  92. package/dist/honesty/undeclared-change.js +56 -0
  93. package/dist/honesty/verified.d.ts +15 -0
  94. package/dist/honesty/verified.js +68 -4
  95. package/dist/http-server.d.ts +0 -1
  96. package/dist/http-server.js +1 -1
  97. package/dist/hunt/hunt-report.d.ts +0 -2
  98. package/dist/hunt/hunt-report.js +1 -1
  99. package/dist/impact/impact-store.d.ts +0 -5
  100. package/dist/impact/impact-store.js +1 -1
  101. package/dist/index.d.ts +4 -2
  102. package/dist/index.js +41 -0
  103. package/dist/init/agent-rules.d.ts +17 -0
  104. package/dist/init/agent-rules.js +31 -1
  105. package/dist/init/capabilities.d.ts +0 -2
  106. package/dist/init/capabilities.js +1 -1
  107. package/dist/init/closing-hint.d.ts +36 -0
  108. package/dist/init/closing-hint.js +120 -0
  109. package/dist/init/confirm.d.ts +0 -25
  110. package/dist/init/confirm.js +3 -3
  111. package/dist/init/cra.d.ts +14 -1
  112. package/dist/init/cra.js +43 -12
  113. package/dist/init/dev-script.d.ts +48 -0
  114. package/dist/init/dev-script.js +63 -0
  115. package/dist/init/mcp.d.ts +16 -5
  116. package/dist/init/mcp.js +26 -10
  117. package/dist/init/node-io.js +3 -0
  118. package/dist/init/plan-framework.d.ts +0 -5
  119. package/dist/init/plan-framework.js +83 -9
  120. package/dist/init/plan.d.ts +10 -30
  121. package/dist/init/plan.js +30 -15
  122. package/dist/init/refresh-rules.d.ts +39 -0
  123. package/dist/init/refresh-rules.js +51 -0
  124. package/dist/init/run.d.ts +2 -10
  125. package/dist/init/run.js +26 -68
  126. package/dist/init/slash-command.d.ts +1 -1
  127. package/dist/init/slash-command.js +33 -10
  128. package/dist/init/snippets.js +53 -10
  129. package/dist/input/real-input.d.ts +2 -0
  130. package/dist/input/real-input.js +3 -2
  131. package/dist/input/scroll-find.d.ts +7 -4
  132. package/dist/input/scroll-find.js +63 -25
  133. package/dist/intent/inline-intent.d.ts +46 -0
  134. package/dist/intent/inline-intent.js +103 -0
  135. package/dist/intent/intent-store.d.ts +37 -0
  136. package/dist/intent/intent-store.js +112 -0
  137. package/dist/intent/intent-tools.d.ts +2 -0
  138. package/dist/intent/intent-tools.js +80 -0
  139. package/dist/intent/open-intents.d.ts +21 -0
  140. package/dist/intent/open-intents.js +25 -0
  141. package/dist/journal/envelope.d.ts +2 -2
  142. package/dist/journal/journal-recorder.d.ts +23 -0
  143. package/dist/journal/journal-recorder.js +35 -0
  144. package/dist/license/license.d.ts +8 -2
  145. package/dist/license/license.js +10 -15
  146. package/dist/mcp/attach-memory.d.ts +67 -0
  147. package/dist/mcp/attach-memory.js +142 -0
  148. package/dist/mcp/mcp-proxy.d.ts +0 -7
  149. package/dist/mcp/mcp-proxy.js +14 -2
  150. package/dist/mcp/mcp.d.ts +2 -0
  151. package/dist/mcp/mcp.js +45 -23
  152. package/dist/mcp/server-instructions.js +45 -3
  153. package/dist/mcp/tool-catalog-cache.d.ts +8 -0
  154. package/dist/mcp/tool-catalog-cache.js +10 -0
  155. package/dist/oracles/flow-instrument-gaps.d.ts +4 -4
  156. package/dist/oracles/flow-instrument-gaps.js +4 -5
  157. package/dist/oracles/self-instrument.d.ts +9 -9
  158. package/dist/oracles/self-instrument.js +19 -13
  159. package/dist/pool/browser-pool.d.ts +12 -1
  160. package/dist/pool/browser-pool.js +31 -1
  161. package/dist/pool/playwright-launcher.js +3 -1
  162. package/dist/project/artifact-root.d.ts +83 -0
  163. package/dist/project/artifact-root.js +112 -0
  164. package/dist/project/fs-port.d.ts +3 -0
  165. package/dist/project/fs-port.js +3 -2
  166. package/dist/project/memory-fs.d.ts +9 -0
  167. package/dist/project/memory-fs.js +93 -0
  168. package/dist/project/project-store.d.ts +17 -0
  169. package/dist/project/project-store.js +33 -0
  170. package/dist/project/project-tools.js +3 -32
  171. package/dist/project/remember-project.d.ts +24 -0
  172. package/dist/project/remember-project.js +40 -0
  173. package/dist/project/reticle-dir.d.ts +2 -0
  174. package/dist/project/reticle-dir.js +1 -0
  175. package/dist/project/session-root.d.ts +15 -0
  176. package/dist/project/session-root.js +31 -0
  177. package/dist/runs/context-tools.d.ts +2 -0
  178. package/dist/runs/context-tools.js +89 -0
  179. package/dist/runs/run-context.d.ts +40 -0
  180. package/dist/runs/run-context.js +158 -0
  181. package/dist/runs/run-diff.d.ts +2 -1
  182. package/dist/runs/run-tools.js +2 -1
  183. package/dist/runs/verification-sync.js +2 -2
  184. package/dist/session/dev-server-probe.d.ts +0 -8
  185. package/dist/session/dev-server-probe.js +1 -1
  186. package/dist/session/last-act.d.ts +7 -1
  187. package/dist/session/last-act.js +2 -2
  188. package/dist/session/lease-availability.d.ts +40 -0
  189. package/dist/session/lease-availability.js +43 -0
  190. package/dist/session/live-control-tools.js +1 -42
  191. package/dist/session/no-session-diagnosis.d.ts +20 -0
  192. package/dist/session/no-session-diagnosis.js +41 -12
  193. package/dist/session/no-session-next-action.d.ts +5 -0
  194. package/dist/session/no-session-next-action.js +35 -3
  195. package/dist/session/no-session-watch.d.ts +8 -0
  196. package/dist/session/no-session-watch.js +83 -65
  197. package/dist/session/output-budget.js +4 -1
  198. package/dist/session/session-health.d.ts +9 -0
  199. package/dist/session/session-health.js +23 -0
  200. package/dist/session/session-manager.js +50 -1
  201. package/dist/session/session-successor.d.ts +37 -0
  202. package/dist/session/session-successor.js +78 -0
  203. package/dist/session/session.d.ts +40 -1
  204. package/dist/session/session.js +84 -2
  205. package/dist/session/stall-clock.d.ts +18 -0
  206. package/dist/session/stall-clock.js +48 -0
  207. package/dist/telemetry/automation-hint.d.ts +2 -1
  208. package/dist/telemetry/daemon-telemetry.js +12 -23
  209. package/dist/telemetry/feedback-outbox.d.ts +2 -1
  210. package/dist/telemetry/project-profile.d.ts +0 -8
  211. package/dist/telemetry/project-profile.js +1 -1
  212. package/dist/telemetry/run-telemetry.d.ts +0 -2
  213. package/dist/telemetry/run-telemetry.js +1 -1
  214. package/dist/telemetry/session-metrics.d.ts +1 -1
  215. package/dist/telemetry/session-metrics.js +1 -1
  216. package/dist/telemetry/telemetry.d.ts +12 -3
  217. package/dist/telemetry/telemetry.js +25 -5
  218. package/dist/telemetry/verification-of.js +2 -2
  219. package/dist/tools/act-tools.js +159 -24
  220. package/dist/tools/assert-source.d.ts +22 -0
  221. package/dist/tools/assert-source.js +53 -0
  222. package/dist/tools/assert-verdict.d.ts +21 -2
  223. package/dist/tools/assert-verdict.js +59 -4
  224. package/dist/tools/browser-tools.js +0 -20
  225. package/dist/tools/complete-snapshot.d.ts +48 -0
  226. package/dist/tools/complete-snapshot.js +75 -0
  227. package/dist/tools/contract-tools.js +5 -3
  228. package/dist/tools/coverage-tools.d.ts +10 -1
  229. package/dist/tools/coverage-tools.js +124 -37
  230. package/dist/tools/dynamic-tools.js +29 -8
  231. package/dist/tools/error-recovery.d.ts +24 -14
  232. package/dist/tools/error-recovery.js +70 -9
  233. package/dist/tools/feedback-tools.d.ts +1 -1
  234. package/dist/tools/feedback-tools.js +2 -2
  235. package/dist/tools/invoke-tool.js +62 -2
  236. package/dist/tools/lease-hint.d.ts +10 -0
  237. package/dist/tools/lease-hint.js +53 -4
  238. package/dist/tools/lease-tools.d.ts +5 -1
  239. package/dist/tools/lease-tools.js +22 -8
  240. package/dist/tools/navigate-arrival.d.ts +0 -2
  241. package/dist/tools/navigate-arrival.js +1 -1
  242. package/dist/tools/observe-tools.js +70 -27
  243. package/dist/tools/read-tools.js +5 -3
  244. package/dist/tools/real-input-attempt.d.ts +22 -0
  245. package/dist/tools/real-input-attempt.js +153 -1
  246. package/dist/tools/reconcile-tools.js +18 -4
  247. package/dist/tools/resolve-target.d.ts +0 -7
  248. package/dist/tools/sdk-marker-probe.d.ts +29 -0
  249. package/dist/tools/sdk-marker-probe.js +57 -0
  250. package/dist/tools/snapshot-delta.d.ts +1 -1
  251. package/dist/tools/tool-kit.d.ts +28 -1
  252. package/dist/tools/tool-kit.js +16 -0
  253. package/dist/tools/tool-names.d.ts +3 -0
  254. package/dist/tools/tool-names.js +3 -0
  255. package/dist/tools/tool-surface.d.ts +85 -6
  256. package/dist/tools/tool-surface.js +122 -2
  257. package/dist/tools/tools.js +22 -4
  258. package/dist/tools/unadvertised-help.js +4 -4
  259. package/dist/tools/verify-next-baton.d.ts +53 -0
  260. package/dist/tools/verify-next-baton.js +52 -0
  261. package/dist/update/update-nudge.d.ts +7 -10
  262. package/dist/update/update-nudge.js +12 -3
  263. package/dist/update/updater.d.ts +1 -1
  264. package/docs/README.md +3 -1
  265. package/docs/cli/cloud.mdx +1 -1
  266. package/docs/cli/doctor.mdx +1 -0
  267. package/docs/cli/verify.mdx +4 -5
  268. package/docs/desktop.mdx +1 -1
  269. package/docs/docs.json +3 -3
  270. package/docs/enterprise.md +3 -1
  271. package/docs/gates.md +1 -1
  272. package/docs/install-manual.mdx +1 -1
  273. package/docs/license-activation.mdx +145 -0
  274. package/docs/packages/browser.mdx +2 -2
  275. package/docs/packages/server.mdx +1 -1
  276. package/docs/packages.mdx +8 -6
  277. package/docs/platform-integration.md +17 -0
  278. package/docs/predicates.mdx +8 -0
  279. package/docs/reticle-manual.md +106 -0
  280. package/docs/telemetry-contract.md +1 -12
  281. package/docs/telemetry-events.mdx +1 -34
  282. package/docs/telemetry.md +0 -1
  283. package/docs/tools-tools-and-run.mdx +2 -4
  284. package/docs/troubleshooting.mdx +9 -0
  285. package/docs/usage.md +3 -2
  286. package/package.json +2 -2
  287. package/dist/session/session-readiness.d.ts +0 -34
  288. package/dist/session/session-readiness.js +0 -36
  289. package/dist/telemetry/instrumentation-stall.d.ts +0 -57
  290. package/dist/telemetry/instrumentation-stall.js +0 -103
@@ -8,7 +8,7 @@ import { resolveTargetRef } from './resolve-target.js';
8
8
  import { z } from 'zod';
9
9
  import { aliasParam } from './alias-args.js';
10
10
  import { captureAct, compileSequenceStep } from '../flows/replay.js';
11
- import { ActionType, ActionWarning, CaptureLoss, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, ReticleCommand, Verified, PredicateKind, } from '@reticlehq/core';
11
+ import { ActionType, ActionWarning, CaptureLoss, DEFAULT_ASSERT_TIMEOUT_MS, InputMode, ReticleCommand, Verified, VerifiedReason, PredicateKind, } from '@reticlehq/core';
12
12
  import { assertNativeInputSupported } from './act-danger.js';
13
13
  import { leanActResult, mutatedWithin } from './act-view.js';
14
14
  import { ReticleTool } from './tool-names.js';
@@ -16,26 +16,52 @@ import { buildReactionReport, summarizeReaction } from '../events/reaction.js';
16
16
  import { parsePredicate } from '../events/predicate-parse.js';
17
17
  import { causalSummary } from '../capsule/causal-summary.js';
18
18
  import { findContradictions } from '../events/contradictions.js';
19
+ import { gapsForAction } from '../honesty/instrumentation-gaps.js';
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';
24
+ import { declaresState } from '../events/predicate-asks.js';
25
+ import { isStateUnwatched } from '../honesty/blind-spots.js';
19
26
  import { inFlightRequestLabels, repeatedRequestLabels, waitForInFlight, } from './settle-in-flight.js';
20
27
  import { waitForReaction } from './react-grace.js';
21
28
  import { decideVerified } from '../honesty/verified.js';
22
- import { declaredExpectations } from '../events/declared.js';
29
+ import { declaredExpectations, declaresBodyIndependentChannel } from '../events/declared.js';
23
30
  import { readsDomState } from '../honesty/already-true.js';
24
31
  import { describeWaitTarget } from '../honesty/unsettled.js';
25
32
  import { saveFailedAssertCapsule } from './act-capsule.js';
26
33
  import { buildDivergenceCapsule } from '../capsule/capsule.js';
27
34
  import { predicateToExpectedLinks } from '../capsule/predicate-to-links.js';
28
35
  import { buildHonestyBlock } from '../honesty/honesty.js';
29
- import { BlindSpotKind, buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage, impeachesCapture, } from '../honesty/blind-spots.js';
36
+ import { buildCoverageStatement, blindSpotsFromState, transportGapNote, Coverage, impeachesCapture, } from '../honesty/blind-spots.js';
30
37
  import { hasAcceptedWrite } from '../honesty/accepted-write.js';
31
38
  import { unreadWriteLabels } from '../honesty/unread-outcome.js';
32
39
  import { evaluatePredicate, waitForPredicate, provenExpectedLinks, PredicateSchema, } from '../events/predicate.js';
40
+ import { awaitDocumentSuccessor } from '../session/session-successor.js';
33
41
  import { healthEnvelope, refuseIfThrottled } from '../session/session-health.js';
34
42
  import { pausedShortCircuit, pausedOutputShape, withControl, PAUSED_NO_VERDICT, } from '../session/control-envelope.js';
35
43
  import { asString, asNumber, asRecord, sourceOf } from './tools-helpers.js';
36
- import { sessionIdShape } from './tool-kit.js';
44
+ import { intentArg, sessionIdShape } from './tool-kit.js';
37
45
  import { asActionType, gradeOf } from './act-helpers.js';
38
- 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
+ }
39
65
  /**
40
66
  * Narrow the wire's `action` to a real ActionType, or undefined.
41
67
  *
@@ -109,7 +135,7 @@ export const ACT_TOOLS = [
109
135
  args: z
110
136
  .record(z.unknown())
111
137
  .optional()
112
- .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.'),
113
139
  refuseWhenThrottled: z
114
140
  .boolean()
115
141
  .optional()
@@ -175,7 +201,7 @@ export const ACT_TOOLS = [
175
201
  settledOutcome = real.settled ?? undefined;
176
202
  // Native input reports no synthetic effect block, so nothing measured in-target: undefined
177
203
  // (the weaker empty-window test), never a fabricated zero.
178
- session.lastAct.markActed(since, action, undefined);
204
+ session.lastAct.markActed(since, action, undefined, asString(args['ref']));
179
205
  return withControl(session, {
180
206
  since,
181
207
  inputMode: InputMode.REAL,
@@ -186,7 +212,9 @@ export const ACT_TOOLS = [
186
212
  ...healthEnvelope(session),
187
213
  });
188
214
  }
189
- 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, {
190
218
  ref: targetRef.ref,
191
219
  action: args['action'],
192
220
  args: args['args'] ?? {},
@@ -201,7 +229,7 @@ export const ACT_TOOLS = [
201
229
  // Keep what only this call measured, so the observe that judges this window can ask whether
202
230
  // anything happened INSIDE the target — the one fact that separates a dead control from a
203
231
  // page that was merely busy with something else.
204
- session.lastAct.markActed(since, action, mutatedWithin(r));
232
+ session.lastAct.markActed(since, action, mutatedWithin(r), asString(args['ref']));
205
233
  return withControl(session, {
206
234
  since,
207
235
  inputMode: InputMode.SYNTHETIC,
@@ -276,7 +304,7 @@ export const ACT_TOOLS = [
276
304
  for (let i = 0; i < inputSteps.length; i++) {
277
305
  const step = asRecord(inputSteps[i]);
278
306
  try {
279
- 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);
280
308
  if (!result.ok) {
281
309
  stalledAt = i;
282
310
  stepResults.push({
@@ -368,7 +396,7 @@ export const ACT_TOOLS = [
368
396
  args: z
369
397
  .record(z.unknown())
370
398
  .optional()
371
- .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.'),
372
400
  predicate: PredicateSchema.optional().describe('Alias for `until` (the name reticle_assert / reticle_wait_for use).'),
373
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" }] }.'),
374
402
  timeout_ms: z
@@ -379,6 +407,7 @@ export const ACT_TOOLS = [
379
407
  .boolean()
380
408
  .optional()
381
409
  .describe('Throw if the tab is throttled. Default: false.'),
410
+ intent: intentArg,
382
411
  ...sessionIdShape,
383
412
  },
384
413
  outputSchema: {
@@ -432,12 +461,20 @@ export const ACT_TOOLS = [
432
461
  .array(z.unknown())
433
462
  .optional()
434
463
  .describe('Channels that DISAGREE about this action — the UI advanced while its write failed, a success signal fired over a failed request, a response changed nothing, a duplicate fired, a request never settled. OMITTED when clean. Treat any entry as a finding even when the verdict is green: a passing assertion and a contradicted channel is exactly the false green this exists to catch.'),
464
+ instrumentationGaps: z
465
+ .array(z.unknown())
466
+ .optional()
467
+ .describe('What the APP did not tell Reticle, and the one change that would fix it — each entry is { kind, missing, cost, fix, source?, ref? }. Reported ONLY where an absence made THIS verdict weaker (a red that cannot name a file:line, a state assertion with no registered store, a DOM change no signal announced, a route change nothing signalled); never a survey of the page, so an entry is always work worth doing now. OMITTED when the app told Reticle everything it needed. Applying `fix` makes every later verdict on this app stronger, not just this one.'),
435
468
  honesty: z
436
469
  .unknown()
437
470
  .describe('The verdict trust block { grade, attribution, coverage, integrity, envelope? } — a green never looks stronger than this. Gate on grade ≥ net AND integrity.clean. Fields that were not measured are OMITTED rather than reported as zero, so treat an absent `envelope` as "not sampled", never as a failure.'),
438
471
  since: z
439
472
  .number()
440
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.'),
441
478
  session: z
442
479
  .object({ lastSeenMs: z.number(), throttled: z.boolean(), focused: z.boolean() })
443
480
  .optional(),
@@ -445,7 +482,9 @@ export const ACT_TOOLS = [
445
482
  ...pausedOutputShape,
446
483
  },
447
484
  handler: async (deps, args) => {
448
- 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;
449
488
  // Live-control: refuse to drive the page (no act, no predicate eval) while paused.
450
489
  //
451
490
  // A VERDICT rides out with the refusal. This is the one tool here that promises `verified`,
@@ -464,6 +503,10 @@ export const ACT_TOOLS = [
464
503
  ? parsePredicate(withUntil['until'])
465
504
  : { kind: PredicateKind.SETTLED };
466
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);
467
510
  // Before anything is driven: this path cannot honour a native-input request, and taking the
468
511
  // argument and ignoring it told the agent its trusted click had happened. See act-danger.
469
512
  assertNativeInputSupported(asRecord(args['args']));
@@ -472,12 +515,17 @@ export const ACT_TOOLS = [
472
515
  const resolved = await resolveActTarget(session, args);
473
516
  if ('error' === resolved.kind)
474
517
  throw new Error(resolved.message);
475
- const since = session.elapsed();
518
+ let since = session.elapsed();
519
+ const actedSince = since;
476
520
  // The cursor + effect are marked after the act dispatches (below) — a refused act leaves none.
477
521
  // The attribution window stays open across the settle wait below, so post-dispatch async events
478
522
  // (the whole point of act_and_wait) attribute to this action. finishAction fires after the wait.
479
523
  session.beginAction(ReticleTool.ACT_AND_WAIT, asRecord(args));
480
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;
481
529
  // Was the declared consequence ALREADY TRUE? Only asked for predicates that read live DOM
482
530
  // state — event-based ones are floored at this act's cursor and cannot be satisfied by the
483
531
  // past, so they need no pre-check and pay nothing. One extra query, on the path where a green
@@ -486,7 +534,8 @@ export const ACT_TOOLS = [
486
534
  ? (await evaluatePredicate(session, until, since, false)).pass
487
535
  : false;
488
536
  try {
489
- 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, {
490
539
  ref: resolved.ref,
491
540
  action: args['action'],
492
541
  args: args['args'] ?? {},
@@ -496,12 +545,31 @@ export const ACT_TOOLS = [
496
545
  captureAct(deps.recordings, args, actResult.result);
497
546
  // Dispatched — now this act owns the cursor and the effect. Marking its OWN measurement also
498
547
  // stops the spread below from inheriting an earlier reticle_act's action and mutation count.
499
- 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']));
500
549
  // Honesty: floor the predicate at this act's cursor so a stale buffered event can't satisfy it.
501
550
  const predicateStarted = session.elapsed();
502
- const verdict = timeout > 0
551
+ let verdict = timeout > 0
503
552
  ? await waitForPredicate(session, until, timeout, since)
504
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
+ }
505
573
  // The predicate resolves the INSTANT it holds, which on an optimistically-navigating app is
506
574
  // while the write is still in flight — so the verdict was taken over a window the app had not
507
575
  // finished, and came back `unknown / unsettled` asking the CALLER to re-check. A login that
@@ -539,7 +607,8 @@ export const ACT_TOOLS = [
539
607
  const actedSource = sourceOf(r['source']);
540
608
  // Remembered on the session so a LATER assertion can name a file even when its failure has no
541
609
  // element to point at — a signal that never fired, a request that was never made.
542
- 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);
543
612
  const windowEvents = session.eventsSince(since);
544
613
  const trace = summarizeReaction(buildReactionReport(windowEvents, session.elapsed() - since));
545
614
  // The bounded Tier-1 causal summary — net/console/state/storage diffs the trace's counts miss.
@@ -561,7 +630,7 @@ export const ACT_TOOLS = [
561
630
  const coverage = buildCoverageStatement(spots);
562
631
  // Nothing subscribed ⇒ the state channel is dark, and the summary must say so rather than
563
632
  // report an empty diff list that reads like a fact about the app. See CausalSummary.
564
- const stateUnwatched = spots.some((s) => s.kind === BlindSpotKind.UNWATCHED_STATE && s.count > 0);
633
+ const stateUnwatched = isStateUnwatched(spots);
565
634
  // Only a spot that IMPEACHES the capture belongs in integrity — see impeachesCapture. A
566
635
  // structural boundary (virtualized rows, a cross-origin frame) is reported as coverage and
567
636
  // must not downgrade a verdict about what WAS observed.
@@ -582,8 +651,11 @@ export const ACT_TOOLS = [
582
651
  ...(gapNote === undefined ? [] : [CaptureLoss.TRANSPORT_GAP]),
583
652
  ...(impeaching.note === undefined ? [] : [CaptureLoss.BLIND_SPOT]),
584
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);
585
657
  const honesty = buildHonestyBlock({
586
- grade: gradeOf(gradedLinks),
658
+ grade,
587
659
  attribution: 'window',
588
660
  // Did the buffer lose scarce evidence FROM THIS WINDOW — not "did it evict anything while
589
661
  // the action ran", which was the previous rule and which is true on essentially every live
@@ -636,6 +708,8 @@ export const ACT_TOOLS = [
636
708
  // not evidence the destination rendered either — `alreadyTrue` decides that, once.
637
709
  renderProved: verdict.pass && !alreadyTrue && declared.rendersContent,
638
710
  ...session.lastAct.effect(),
711
+ currentDocumentId: session.currentDocumentId,
712
+ currentEditEpoch: session.currentEditEpoch,
639
713
  });
640
714
  // The single field an agent reads. Everything below it is the evidence it was derived from;
641
715
  // this is the only one that has to be interpreted, and now it interprets itself.
@@ -648,6 +722,9 @@ export const ACT_TOOLS = [
648
722
  // overriding it — see `declaredConsequence`. An explicit `{ kind: "settled" }` is not a
649
723
  // declaration about the app's behaviour, it IS the idle wait, so it does not count.
650
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 } : {}),
651
728
  ...(alreadyTrue ? { alreadyTrue } : {}),
652
729
  // An assertion nobody could evaluate must not be reported as one the app failed.
653
730
  ...(verdict.inconclusive === undefined ? {} : { inconclusive: verdict.inconclusive }),
@@ -672,6 +749,62 @@ export const ACT_TOOLS = [
672
749
  repeated: repeatedRequestLabels(windowEvents),
673
750
  },
674
751
  });
752
+ // Computed once: the verdict block reports it, and the instrumentation gaps are a second
753
+ // reading of the same evidence rather than a new observation.
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));
762
+ const gaps = gapsForAction({
763
+ pass: verdict.pass,
764
+ proved: decision.verifiedReason === VerifiedReason.PROVED,
765
+ changeUndeclared,
766
+ source: actedSourceLabel,
767
+ ref: asString(args['ref']),
768
+ stateAsked: declaresState(until),
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,
782
+ domMutated: (session.lastAct.effect().mutatedWithin ?? 0) > 0,
783
+ signalsFired: actionSummary.signals.length,
784
+ routeChanged: actionSummary.route !== undefined,
785
+ routeSignalFired: actionSummary.signals.some((name) => name.startsWith('route')),
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
+ };
804
+ // Recorded on the session, so a later "am I done?" can answer with what is STILL missing
805
+ // rather than with everything that was ever missing. An empty list closes a gap, which is
806
+ // why it is noted rather than skipped.
807
+ noteSessionGaps(session, gaps);
675
808
  return withControl(session, {
676
809
  ...decision,
677
810
  effect: leanActResult(actResult.result),
@@ -679,14 +812,15 @@ export const ACT_TOOLS = [
679
812
  // Promoted out of `effect` on red only. On green nobody needs it and it is noise; on red it
680
813
  // is the first thing the agent wants, and burying a file:line inside the effect block is
681
814
  // most of the way to not reporting it at all.
682
- ...(verdict.pass || actedSource === undefined
683
- ? {}
684
- : { source: `${actedSource.file}:${String(actedSource.line)}` }),
815
+ ...(verdict.pass || actedSourceLabel === undefined ? {} : { source: actedSourceLabel }),
685
816
  trace,
686
817
  ...(capsuleSaved === undefined ? {} : { capsuleSaved }),
687
818
  // The window cannot say whether anything was WATCHING state — that is a level fact the
688
819
  // session holds. Without it an empty `stateDiffs` reads as "the app changed nothing".
689
- summary: causalSummary(windowEvents, { stateUnwatched }),
820
+ summary: actionSummary,
821
+ // What the APP did not tell Reticle, and the one change that would fix it. Reported only
822
+ // where an absence made this very verdict weaker — never as a survey of the page.
823
+ ...(gaps.length > 0 ? { instrumentationGaps: gaps } : {}),
690
824
  // Cross-channel disagreement, reported WITH the action that caused it.
691
825
  //
692
826
  // This is the one finding here a human structurally cannot make — they watch one channel,
@@ -700,11 +834,12 @@ export const ACT_TOOLS = [
700
834
  honesty,
701
835
  ...(capsule === undefined ? {} : { capsule }),
702
836
  since,
837
+ ...(session.id === actedSessionId ? {} : { sessionId: session.id }),
703
838
  ...healthEnvelope(session),
704
839
  });
705
840
  }
706
841
  finally {
707
- session.finishAction(undefined, settledOutcome, true === settledOutcome ? session.elapsed() - since : undefined);
842
+ acted.finishAction(verdictEffect, settledOutcome, true === settledOutcome ? acted.elapsed() - actedSince : undefined);
708
843
  }
709
844
  },
710
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,3 +1,4 @@
1
+ import type { InstrumentationGap, JournalVerdictEffect } from '@reticlehq/core';
1
2
  import type { Predicate } from '../events/predicate.js';
2
3
  import type { Session } from '../session/session.js';
3
4
  import { type Contradiction } from '../events/contradictions.js';
@@ -14,7 +15,12 @@ import { type Contradiction } from '../events/contradictions.js';
14
15
  * consult it. Asserting a moment earlier — while the POST was still open — is the same story with a
15
16
  * different name: `request-never-settled`, also unreported.
16
17
  */
17
- 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,
18
24
  /** Set when the assertion was never evaluated — see honesty/verified.ts. */
19
25
  inconclusive?: string,
20
26
  /**
@@ -22,8 +28,21 @@ inconclusive?: string,
22
28
  * `reticle_wait_for` reach the same disconnect path as the act tools, and a verdict that blames
23
29
  * the app for a lost connection is no more honest on this route than on that one.
24
30
  */
25
- 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<{
26
38
  decision: Record<string, unknown>;
27
39
  contradictions: Contradiction[];
28
40
  coverage: Record<string, unknown>;
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;
29
48
  }>;
@@ -1,6 +1,10 @@
1
1
  import { BlindSpotKind, CaptureLoss, PredicateKind } from '@reticlehq/core';
2
+ import { gapsForAction } from '../honesty/instrumentation-gaps.js';
3
+ import { noteSessionGaps } from '../honesty/gap-ledger.js';
4
+ import { declaresState } from '../events/predicate-asks.js';
5
+ import { isStateUnwatched } from '../honesty/blind-spots.js';
2
6
  import { findContradictions } from '../events/contradictions.js';
3
- import { declaredExpectations } from '../events/declared.js';
7
+ import { declaredExpectations, declaresBodyIndependentChannel } from '../events/declared.js';
4
8
  import { blindSpotsFromState, buildCoverageStatement, Coverage, impeachesCapture, transportGapNote, } from '../honesty/blind-spots.js';
5
9
  import { buildHonestyBlock } from '../honesty/honesty.js';
6
10
  import { hasAcceptedWrite } from '../honesty/accepted-write.js';
@@ -9,6 +13,7 @@ import { decideVerified } from '../honesty/verified.js';
9
13
  import { describeWaitTarget } from '../honesty/unsettled.js';
10
14
  import { inFlightRequestLabels, repeatedRequestLabels } from './settle-in-flight.js';
11
15
  import { gradeOfPredicate } from './assert-grade.js';
16
+ import { assertSource } from './assert-source.js';
12
17
  function absenceBlindSpotNote(predicate, spots) {
13
18
  if (predicate.kind !== 'element' || predicate.absent !== true)
14
19
  return undefined;
@@ -33,7 +38,12 @@ function absenceBlindSpotNote(predicate, spots) {
33
38
  * consult it. Asserting a moment earlier — while the POST was still open — is the same story with a
34
39
  * different name: `request-never-settled`, also unreported.
35
40
  */
36
- 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,
37
47
  /** Set when the assertion was never evaluated — see honesty/verified.ts. */
38
48
  inconclusive,
39
49
  /**
@@ -41,7 +51,13 @@ inconclusive,
41
51
  * `reticle_wait_for` reach the same disconnect path as the act tools, and a verdict that blames
42
52
  * the app for a lost connection is no more honest on this route than on that one.
43
53
  */
44
- 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) {
45
61
  // Scope caveat, stated because it is a real limitation and not a bug: blind spots are tracked per
46
62
  // SESSION, not per assertion window, so a cross-origin iframe seen once marks every later verdict
47
63
  // in that session partial — including ones about a region it cannot affect. That errs toward
@@ -76,6 +92,8 @@ observationLost) {
76
92
  const declared = declaredExpectations(predicate);
77
93
  const contradictions = findContradictions(windowEvents, {
78
94
  prior,
95
+ currentDocumentId: session.currentDocumentId,
96
+ currentEditEpoch: session.currentEditEpoch,
79
97
  expectedFailures: declared.netFailures,
80
98
  renderProved: pass && declared.rendersContent,
81
99
  ...(actCursor !== undefined && actCursor >= since ? { actionSince: actCursor } : {}),
@@ -96,6 +114,7 @@ observationLost) {
96
114
  // not override it. A fix that lived on one half of the verdict surface would leave the other
97
115
  // half broken, and this is the tool agents call most.
98
116
  declaredConsequence: predicate.kind !== PredicateKind.SETTLED,
117
+ ...(declaresBodyIndependentChannel(predicate) ? { independentOfBody: true } : {}),
99
118
  ...(inconclusive === undefined ? {} : { inconclusive }),
100
119
  ...(true === observationLost ? { observationLost: true } : {}),
101
120
  ...(absenceBlindSpot === undefined ? {} : { absenceBlindSpot }),
@@ -122,5 +141,41 @@ observationLost) {
122
141
  repeated: repeatedRequestLabels(windowEvents),
123
142
  },
124
143
  });
125
- return { decision: decision, contradictions, coverage };
144
+ // The same rule the act path uses, fed by what THIS path knows. An assertion drives nothing, so
145
+ // only two of the four gates can fire here: a red with no remembered source to point at, and a
146
+ // state assertion against an app that registers no store.
147
+ const gaps = gapsForAction({
148
+ pass,
149
+ ...(changeUndeclared === undefined ? {} : { changeUndeclared }),
150
+ source: session.lastAct.source(),
151
+ stateAsked: declaresState(predicate),
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,
155
+ domMutated: false,
156
+ signalsFired: 0,
157
+ routeChanged: false,
158
+ routeSignalFired: false,
159
+ });
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
+ });
170
+ return {
171
+ decision: decision,
172
+ contradictions,
173
+ coverage,
174
+ gaps,
175
+ verdictEffect: {
176
+ claim: describeWaitTarget(predicate),
177
+ verified: decision.verified,
178
+ ...(source === undefined ? {} : { source }),
179
+ },
180
+ };
126
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
  ];