@reticlehq/server 3.8.2 → 3.9.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 (224) hide show
  1. package/LICENSE +201 -104
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/bin/launch.js +13 -0
  5. package/bin/package.json +3 -0
  6. package/bin/reticle.js +66 -8
  7. package/dist/chromium-launch-options.d.ts +25 -0
  8. package/dist/chromium-launch-options.js +21 -1
  9. package/dist/command/cli/adhoc-verdict.js +34 -7
  10. package/dist/command/cli/answers/gate-exit.d.ts +6 -2
  11. package/dist/command/cli/answers/gate-exit.js +12 -2
  12. package/dist/command/cli/ci-hint.d.ts +44 -0
  13. package/dist/command/cli/ci-hint.js +72 -0
  14. package/dist/command/cli/cli-doctor.d.ts +6 -1
  15. package/dist/command/cli/cli-doctor.js +45 -4
  16. package/dist/command/cli/cli-flow-commands.d.ts +7 -1
  17. package/dist/command/cli/cli-flow-commands.js +39 -17
  18. package/dist/command/cli/cli-kill.d.ts +21 -0
  19. package/dist/command/cli/cli-kill.js +46 -3
  20. package/dist/command/cli/cli-parse.d.ts +5 -2
  21. package/dist/command/cli/cli-parse.js +10 -5
  22. package/dist/command/cli/cli-usage.js +3 -2
  23. package/dist/command/cli/cli-verify.d.ts +6 -1
  24. package/dist/command/cli/cli-verify.js +32 -13
  25. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
  26. package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
  27. package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
  28. package/dist/command/cli/doctor/doctor-fix.js +89 -0
  29. package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
  30. package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
  31. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  32. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  33. package/dist/command/cli/doctor-repairs.d.ts +64 -0
  34. package/dist/command/cli/doctor-repairs.js +214 -0
  35. package/dist/command/cli/drive/drive-command.js +3 -7
  36. package/dist/command/cli/first-use-checks.d.ts +41 -0
  37. package/dist/command/cli/first-use-checks.js +127 -0
  38. package/dist/command/cli/first-verdict-node.d.ts +3 -0
  39. package/dist/command/cli/first-verdict-node.js +189 -0
  40. package/dist/command/cli/first-verdict.d.ts +126 -0
  41. package/dist/command/cli/first-verdict.js +267 -0
  42. package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
  43. package/dist/command/cli/launch/sdk-version-match.js +3 -2
  44. package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
  45. package/dist/command/cli/mcp-command.js +13 -4
  46. package/dist/command/cli/ports/port-holder.d.ts +2 -0
  47. package/dist/command/cli/ports/port-holder.js +11 -3
  48. package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
  49. package/dist/command/cli/setup-mcp-cli.js +98 -23
  50. package/dist/command/cli/status/status-lines.d.ts +1 -0
  51. package/dist/command/cli/status/status-lines.js +50 -17
  52. package/dist/command/cli/status-fields.js +17 -3
  53. package/dist/command/cli/try-command.d.ts +7 -0
  54. package/dist/command/cli/try-command.js +11 -3
  55. package/dist/command/cli.js +70 -33
  56. package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
  57. package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
  58. package/dist/command/daemon/daemon-argv.d.ts +9 -0
  59. package/dist/command/daemon/daemon-argv.js +11 -0
  60. package/dist/command/daemon/daemon.d.ts +10 -3
  61. package/dist/command/daemon/daemon.js +17 -6
  62. package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
  63. package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
  64. package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
  65. package/dist/command/daemon/lifetime/death-cause.js +130 -0
  66. package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
  67. package/dist/command/daemon/lifetime/heartbeat.js +4 -13
  68. package/dist/command/daemon/port-state.d.ts +14 -0
  69. package/dist/command/daemon/port-state.js +31 -0
  70. package/dist/command/setup/agent-configs.d.ts +2 -0
  71. package/dist/command/setup/agent-configs.js +42 -15
  72. package/dist/command/setup/agent-next-steps.d.ts +77 -0
  73. package/dist/command/setup/agent-next-steps.js +284 -0
  74. package/dist/command/setup/agent-writer.js +11 -6
  75. package/dist/command/setup/init/init-host.js +2 -0
  76. package/dist/command/setup/init/init-runtime.d.ts +6 -1
  77. package/dist/command/setup/init/init-runtime.js +57 -21
  78. package/dist/command/setup/mcp-launcher.d.ts +3 -0
  79. package/dist/command/setup/mcp-launcher.js +11 -0
  80. package/dist/command/setup/setup-command.js +30 -12
  81. package/dist/command/setup/setup-mcp.d.ts +8 -0
  82. package/dist/command/setup/setup-mcp.js +47 -20
  83. package/dist/command/setup/start-dev-server.d.ts +8 -0
  84. package/dist/command/setup/start-dev-server.js +43 -0
  85. package/dist/command/version/identity/server-version.d.ts +2 -0
  86. package/dist/command/version/identity/server-version.js +2 -0
  87. package/dist/command/version/version-skew.d.ts +7 -0
  88. package/dist/command/version/version-skew.js +13 -2
  89. package/dist/features/license/license.d.ts +11 -71
  90. package/dist/features/license/license.js +43 -136
  91. package/dist/features/visual/desktop-capture.d.ts +33 -0
  92. package/dist/features/visual/desktop-capture.js +94 -0
  93. package/dist/features/visual/step-shots.js +139 -35
  94. package/dist/features/visual/visual-tools.d.ts +0 -18
  95. package/dist/features/visual/visual-tools.js +2 -81
  96. package/dist/import-graph.js +5 -6
  97. package/dist/index.d.ts +3 -1
  98. package/dist/index.js +14 -5
  99. package/dist/judgement/runs/run-ledger.d.ts +7 -5
  100. package/dist/judgement/runs/run-ledger.js +98 -69
  101. package/dist/judgement/runs/verification-sync.d.ts +6 -3
  102. package/dist/judgement/runs/verification-sync.js +21 -10
  103. package/dist/language/flows/flow-store-for-session.js +7 -5
  104. package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
  105. package/dist/language/flows/recording/tape/recordings.js +41 -44
  106. package/dist/language/flows/replay.d.ts +17 -7
  107. package/dist/language/flows/replay.js +95 -44
  108. package/dist/launch-chromium.d.ts +1 -0
  109. package/dist/launch-chromium.js +49 -12
  110. package/dist/memory/cloud/sync-screenshots.js +6 -1
  111. package/dist/portal/bridge/bridge.d.ts +13 -4
  112. package/dist/portal/bridge/bridge.js +10 -9
  113. package/dist/portal/input/network-mock.js +3 -2
  114. package/dist/portal/input/real-input.d.ts +15 -0
  115. package/dist/portal/input/real-input.js +59 -3
  116. package/dist/portal/input/viewport-tools.js +6 -3
  117. package/dist/portal/pool/browser-pool.d.ts +5 -1
  118. package/dist/portal/pool/browser-pool.js +32 -38
  119. package/dist/portal/pool/context-permissions.d.ts +20 -17
  120. package/dist/portal/pool/context-permissions.js +63 -1
  121. package/dist/portal/pool/csp-widen.d.ts +62 -0
  122. package/dist/portal/pool/csp-widen.js +132 -0
  123. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  124. package/dist/portal/pool/lease-page-error.js +18 -0
  125. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  126. package/dist/portal/pool/page-error-journal.js +181 -0
  127. package/dist/portal/pool/playwright-launcher.js +21 -8
  128. package/dist/portal/pool/pool-contract.d.ts +22 -0
  129. package/dist/portal/pool/pool-lease.d.ts +35 -2
  130. package/dist/portal/pool/pool-lease.js +23 -0
  131. package/dist/portal/pool/zero-install.js +69 -1
  132. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  133. package/dist/portal/session/dev-server/dev-command.js +56 -8
  134. package/dist/portal/session/no-session-diagnosis.js +10 -3
  135. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  136. package/dist/portal/session/no-session-next-action.js +44 -4
  137. package/dist/portal/session/no-session-watch.js +3 -1
  138. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  139. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  140. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  141. package/dist/portal/session/recorded-gaps.js +5 -2
  142. package/dist/portal/session/session-manager.js +40 -21
  143. package/dist/portal/session/session.js +4 -3
  144. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  145. package/dist/portal/session/zero-install-first-call.js +34 -2
  146. package/dist/start-wiring.d.ts +29 -2
  147. package/dist/start-wiring.js +73 -9
  148. package/dist/surface/mcp/mcp-proxy.js +10 -2
  149. package/dist/surface/mcp/mcp.js +5 -3
  150. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  151. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  152. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  153. package/dist/surface/tools/act/act-preflight.js +37 -17
  154. package/dist/surface/tools/act/act-target.js +3 -1
  155. package/dist/surface/tools/act/after-match.d.ts +7 -0
  156. package/dist/surface/tools/act/after-match.js +33 -0
  157. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  158. package/dist/surface/tools/act/resolve-target.js +35 -8
  159. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  160. package/dist/surface/tools/act/sequence-grade.js +8 -1
  161. package/dist/surface/tools/act-sequence-tool.js +3 -0
  162. package/dist/surface/tools/act-tools.js +10 -10
  163. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  164. package/dist/surface/tools/browser-tools.js +75 -7
  165. package/dist/surface/tools/dynamic-tools.js +2 -1
  166. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  167. package/dist/surface/tools/empty-session-list.js +4 -2
  168. package/dist/surface/tools/error-recovery.js +43 -22
  169. package/dist/surface/tools/invoke-tool.js +27 -20
  170. package/dist/surface/tools/lease-hint.d.ts +7 -0
  171. package/dist/surface/tools/lease-hint.js +10 -3
  172. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  173. package/dist/surface/tools/lease-readiness.js +47 -0
  174. package/dist/surface/tools/lease-seed.d.ts +2 -0
  175. package/dist/surface/tools/lease-seed.js +2 -1
  176. package/dist/surface/tools/lease-tools.js +43 -15
  177. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  178. package/dist/surface/tools/repeat-hint.js +171 -0
  179. package/dist/surface/tools/run-report.d.ts +2 -2
  180. package/dist/surface/tools/run-report.js +27 -15
  181. package/dist/surface/tools/tool-kit.d.ts +12 -0
  182. package/dist/surface/tools/tool-kit.js +7 -0
  183. package/dist/surface/tools/tools.js +26 -13
  184. package/dist/telemetry/argument-shape.js +16 -7
  185. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  186. package/dist/telemetry/cli-telemetry.js +15 -0
  187. package/dist/telemetry/init-telemetry.d.ts +2 -1
  188. package/dist/telemetry/init-telemetry.js +47 -2
  189. package/dist/telemetry/install-trace.js +23 -4
  190. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  191. package/dist/telemetry/onboarding-funnel.js +25 -3
  192. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  193. package/dist/telemetry/proxy-telemetry.js +2 -2
  194. package/dist/telemetry/session-metrics.d.ts +1 -1
  195. package/dist/telemetry/session-metrics.js +67 -27
  196. package/dist/telemetry/telemetry.d.ts +3 -1
  197. package/dist/telemetry/telemetry.js +60 -28
  198. package/dist/telemetry/tool-refused.d.ts +2 -2
  199. package/dist/telemetry/tool-refused.js +9 -2
  200. package/docs/architecture.md +3 -6
  201. package/docs/cli/doctor.mdx +41 -2
  202. package/docs/cli/help.mdx +4 -2
  203. package/docs/cli/license.mdx +4 -5
  204. package/docs/cli/setup.mdx +13 -2
  205. package/docs/cli/status.mdx +4 -0
  206. package/docs/cli.mdx +1 -1
  207. package/docs/deploy-checks.md +25 -0
  208. package/docs/desktop.mdx +1 -1
  209. package/docs/enterprise.md +15 -20
  210. package/docs/environment.mdx +1 -0
  211. package/docs/first-drive.md +1 -1
  212. package/docs/install-manual.mdx +16 -1
  213. package/docs/packages/engine.mdx +1 -1
  214. package/docs/packages/server.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages/test.mdx +1 -1
  217. package/docs/packages.mdx +1 -1
  218. package/docs/platform-integration.md +1 -1
  219. package/docs/skill.md +1 -1
  220. package/docs/telemetry.md +4 -2
  221. package/package.json +13 -9
  222. package/LICENSE-ENTERPRISE +0 -37
  223. package/dist/features/ee/audit-log.d.ts +0 -14
  224. package/dist/features/ee/audit-log.js +0 -11
@@ -1,142 +1,171 @@
1
1
  /**
2
- * The run ledger: what one agent run checked, saved and read, kept until its report is taken.
2
+ * The run ledger: what each agent run checked, saved and read, kept until its report is taken.
3
3
  *
4
- * A run is the agent's calls from its first Reticle call to the call that ends it (a session end, a
4
+ * A run is one agent's calls from its first Reticle call to the call that ends it (a session end, a
5
5
  * yield, a released tab). The verdicts come from the journal records the agent's own calls wrote;
6
- * the Harness's are never counted. Taking the report empties the ledger, so the next run starts at
7
- * zero.
6
+ * the Harness's are never counted. Taking the report ends that run, so the agent's next run starts
7
+ * at zero.
8
8
  *
9
- * A call from another agent (another MCP attach id) starts a new run. An agent that stops without a
10
- * call that ends its run then does not leak its claims into the next agent's report.
9
+ * The ledger keeps one run for each MCP attach id. Two agents on one daemon (two worktrees of one
10
+ * project) call in turn. When the ledger kept one run, each call restarted the other agent's run,
11
+ * and a released lease reported the other agent's claims and none of its own. A record goes to the
12
+ * run of the connection that wrote it, and a call that ends a run takes the caller's run (#1900).
11
13
  *
12
- * ponytail: one ledger for each daemon, not for each agent. Two agents that call at the same time
13
- * restart each other's run. Key the ledger by attach id when that happens.
14
+ * A record from no connection (the HUD, the CLI, an agent record that ends outside its call
15
+ * context) goes to the run of the agent that called last. An agent that stops without a call that
16
+ * ends its run leaves its run here. That run is dropped when it is idle for `RUN_IDLE_LIMIT_MS`.
14
17
  */
15
18
  import { supersededChecks } from '@reticlehq/engine/evidence/superseded-checks.js';
16
19
  import { JournalVerdictEffectSchema, ReticleTool, Verified, } from '@reticlehq/core';
20
+ import { callingAttachId } from '../../hooks/coding-agents.js';
17
21
  const SAW = 'saw ';
18
22
  const FLOW = 'flow ';
23
+ /** A run with no agent call for this long is dropped. A working agent calls much more often. */
24
+ export const RUN_IDLE_LIMIT_MS = 6 * 60 * 60 * 1000;
19
25
  /** One piece of evidence for a claim: what the check saw, else the source line it drove. */
20
26
  function evidenceOf(effect) {
21
- if (effect.observed !== undefined)
27
+ if (effect.observed !== undefined) {
22
28
  return `${SAW}${effect.observed}`;
29
+ }
23
30
  return effect.source;
24
31
  }
25
32
  /** `no` when anything failed, `yes` only when something held and nothing was undecided. */
26
33
  function runVerified(all) {
27
- if (all.includes(Verified.NO))
34
+ if (all.includes(Verified.NO)) {
28
35
  return Verified.NO;
29
- if (0 === all.length || all.some((v) => Verified.YES !== v))
36
+ }
37
+ if (0 === all.length || all.some((v) => Verified.YES !== v)) {
30
38
  return Verified.UNKNOWN;
39
+ }
31
40
  return Verified.YES;
32
41
  }
42
+ const openRun = (now) => ({
43
+ startedAt: now,
44
+ lastAt: now,
45
+ firstVerdictAt: undefined,
46
+ verdicts: [],
47
+ saved: new Set(),
48
+ replayed: new Map(),
49
+ written: 0,
50
+ read: new Set(),
51
+ });
33
52
  export class RunLedger {
34
- #owner;
35
- #startedAt;
36
- #firstVerdictAt;
37
- #verdicts = [];
38
- #saved = new Set();
39
- #replayed = new Map();
40
- #written = 0;
41
- #read = new Set();
42
- /** An agent call starts. The first one starts the run, and a call from another agent starts a new one. */
53
+ #runs = new Map();
54
+ #last;
55
+ /** An agent call starts. The agent's first call starts its run. A run idle too long is dropped. */
43
56
  begin(now, owner) {
44
- if (this.#owner !== owner)
45
- this.#reset();
46
- this.#owner = owner;
47
- this.#startedAt ??= now;
57
+ for (const [id, run] of this.#runs) {
58
+ if (now - run.lastAt > RUN_IDLE_LIMIT_MS) {
59
+ this.#runs.delete(id);
60
+ }
61
+ }
62
+ const run = this.#runs.get(owner) ?? openRun(now);
63
+ run.lastAt = now;
64
+ this.#runs.set(owner, run);
65
+ this.#last = owner;
48
66
  }
49
67
  /** An agent call ended. The first one that left a verdict sets the time to the first verdict. */
50
- after(now) {
51
- if (0 < this.#verdicts.length || 0 < this.#replayed.size)
52
- this.#firstVerdictAt ??= now;
68
+ after(now, owner) {
69
+ const run = this.#run(owner);
70
+ if (run === undefined) {
71
+ return;
72
+ }
73
+ run.lastAt = Math.max(run.lastAt, now);
74
+ if (0 < run.verdicts.length || 0 < run.replayed.size) {
75
+ run.firstVerdictAt ??= now;
76
+ }
53
77
  }
54
78
  /** A journal record from the agent's own call. A record with no verdict is not a claim. */
55
79
  verdict(action) {
56
- if (this.#idle() || action.drivenBy !== undefined)
80
+ const run = this.#run();
81
+ if (run === undefined || action.drivenBy !== undefined) {
57
82
  return;
83
+ }
58
84
  // A replay is counted once, as a flow, decided as its run decides it (`replayed`). Its journal
59
85
  // record said `unknown` where the run said `no`, and one report gave both answers.
60
- if (ReticleTool.FLOW_REPLAY === action.tool)
86
+ if (ReticleTool.FLOW_REPLAY === action.tool) {
61
87
  return;
88
+ }
62
89
  const parsed = JournalVerdictEffectSchema.safeParse(action.effect);
63
- if (parsed.success)
64
- this.#verdicts.push(parsed.data);
90
+ if (parsed.success) {
91
+ run.verdicts.push(parsed.data);
92
+ }
65
93
  }
66
94
  flowSaved(name) {
67
- if (!this.#idle())
68
- this.#saved.add(name);
95
+ this.#run()?.saved.add(name);
69
96
  }
70
97
  /** The last replay of a flow in this run is its verdict. */
71
98
  replayed(name, verified) {
72
- if (!this.#idle())
73
- this.#replayed.set(name, verified);
99
+ this.#run()?.replayed.set(name, verified);
74
100
  }
75
101
  memoryWritten(count = 1) {
76
- if (!this.#idle())
77
- this.#written += count;
102
+ const run = this.#run();
103
+ if (run !== undefined) {
104
+ run.written += count;
105
+ }
78
106
  }
79
107
  /** Notes given to the agent. Each one counts once, however often it is shown. */
80
108
  memoryRead(notes) {
81
- if (this.#idle())
109
+ const run = this.#run();
110
+ if (run === undefined) {
82
111
  return;
83
- for (const note of notes)
84
- this.#read.add(note);
112
+ }
113
+ for (const note of notes) {
114
+ run.read.add(note);
115
+ }
116
+ }
117
+ isEmpty(owner) {
118
+ return this.#run(owner) === undefined;
85
119
  }
86
- isEmpty() {
87
- return this.#idle();
120
+ /** The named owner, else the calling connection, else the agent that called last. */
121
+ #owner(owner) {
122
+ return owner ?? callingAttachId() ?? this.#last;
88
123
  }
89
124
  /**
90
- * No agent run is open. A CLI `reticle verify` replays through the same code, and what it did is
91
- * not the next agent's run, so nothing is kept until an agent call begins one.
125
+ * The run a record belongs to. None while no agent call has begun one: a CLI `reticle verify`
126
+ * replays through the same code, and what it did is not the next agent's run.
92
127
  */
93
- #idle() {
94
- return this.#startedAt === undefined;
128
+ #run(owner) {
129
+ const id = this.#owner(owner);
130
+ return id === undefined ? undefined : this.#runs.get(id);
95
131
  }
96
- /** The report, without the cloud line (the caller knows the project). Empties the ledger. */
97
- take(now) {
98
- const started = this.#startedAt ?? now;
132
+ /** The owner's report, without the cloud line (the caller knows the project). Ends that run. */
133
+ take(now, owner) {
134
+ const id = this.#owner(owner);
135
+ const run = this.#run(owner) ?? openRun(now);
136
+ if (id !== undefined) {
137
+ this.#runs.delete(id);
138
+ }
139
+ const started = run.startedAt;
99
140
  // A passive check that read too early is not counted: the rule every fold applies.
100
- const superseded = supersededChecks(this.#verdicts.map((v) => ({
141
+ const superseded = supersededChecks(run.verdicts.map((v) => ({
101
142
  key: v.claim,
102
143
  passive: false === v.declaredBeforeActing,
103
144
  held: Verified.YES === v.verified,
104
145
  failed: Verified.NO === v.verified,
105
146
  })));
106
- const counted = this.#verdicts.filter((_, at) => !superseded.has(at));
147
+ const counted = run.verdicts.filter((_, at) => !superseded.has(at));
107
148
  const claims = counted.map((v) => {
108
149
  const evidence = evidenceOf(v);
109
150
  return evidence === undefined
110
151
  ? { claim: v.claim, verified: v.verified }
111
152
  : { claim: v.claim, verified: v.verified, evidence };
112
153
  });
113
- const replayed = [...this.#replayed].map(([name, verified]) => ({ name, verified }));
114
- const report = {
154
+ const replayed = [...run.replayed].map(([name, verified]) => ({ name, verified }));
155
+ return {
115
156
  verified: runVerified([...claims.map((c) => c.verified), ...replayed.map((r) => r.verified)]),
116
157
  claims,
117
158
  caught: [
118
159
  ...claims.filter((c) => Verified.NO === c.verified).map((c) => c.claim),
119
160
  ...replayed.filter((r) => Verified.NO === r.verified).map((r) => `${FLOW}${r.name}`),
120
161
  ],
121
- flows: { saved: [...this.#saved], replayed },
122
- memory: { written: this.#written, read: this.#read.size },
162
+ flows: { saved: [...run.saved], replayed },
163
+ memory: { written: run.written, read: run.read.size },
123
164
  wallMs: Math.max(0, now - started),
124
- ...(this.#firstVerdictAt === undefined
165
+ ...(run.firstVerdictAt === undefined
125
166
  ? {}
126
- : { firstVerdictMs: Math.max(0, this.#firstVerdictAt - started) }),
167
+ : { firstVerdictMs: Math.max(0, run.firstVerdictAt - started) }),
127
168
  };
128
- this.#reset();
129
- return report;
130
- }
131
- #reset() {
132
- this.#owner = undefined;
133
- this.#startedAt = undefined;
134
- this.#firstVerdictAt = undefined;
135
- this.#verdicts = [];
136
- this.#saved = new Set();
137
- this.#replayed = new Map();
138
- this.#written = 0;
139
- this.#read = new Set();
140
169
  }
141
170
  }
142
171
  /** The daemon's ledger. */
@@ -1,5 +1,6 @@
1
1
  import { type FlowFile, type FlowReplayResult } from '@reticlehq/core';
2
2
  import { type VerificationRunInput } from './artifact/build-verification-run.js';
3
+ import { AutomationHint } from '@reticlehq/core/telemetry';
3
4
  import { type ProjectTarget } from '../../memory/project/session-root.js';
4
5
  import type { ToolDeps } from '../../surface/tools/tool-kit.js';
5
6
  /** A replay plus the wall-clock time it took — the shape the verify handler already collects. */
@@ -12,8 +13,10 @@ export interface TimedReplay {
12
13
  /** The MCP client name `reticle verify` (the command line, CI's path) connects to a daemon as. */
13
14
  export declare const CLI_CLIENT_NAME = "reticle-cli";
14
15
  /**
15
- * Who ran the command line, and why. On a CI runner it is the pipeline; anywhere else it is the
16
- * person who holds the key, so the cloud credits them and the run does not read as CI.
16
+ * Who ran the command line, and why. Off CI it is the person who holds the key, so the cloud
17
+ * credits them. On CI the `automation` hint says which kind: a CI vendor runner is the pipeline,
18
+ * trigger `ci`; a coding-agent sandbox is a coding agent. A container, or `CI` with no vendor,
19
+ * keeps trigger `oem`, because the cloud has no value for either yet.
17
20
  */
18
- export declare function cliRunAuthor(env: NodeJS.ProcessEnv): Pick<VerificationRunInput, 'agent' | 'trigger'>;
21
+ export declare function cliRunAuthor(env: NodeJS.ProcessEnv, automation?: AutomationHint | undefined): Pick<VerificationRunInput, 'agent' | 'trigger'>;
19
22
  export declare function persistAndSyncVerificationRun(deps: ToolDeps, timed: TimedReplay[], target: ProjectTarget): Promise<string | undefined>;
@@ -21,26 +21,37 @@ import { RunStore } from './artifact/run-store.js';
21
21
  import { cloudFetch, syncRunToCloud, SyncOutcome } from '../../memory/cloud/cloud-sync.js';
22
22
  import { resolveProjectCloud } from '../../memory/cloud/cloud-config.js';
23
23
  import { log } from '../../log.js';
24
- import { isCiEnv } from '../../telemetry/automation-hint.js';
24
+ import { currentAutomationHint, isCiEnv } from '../../telemetry/automation-hint.js';
25
+ import { AutomationHint } from '@reticlehq/core/telemetry';
25
26
  import { rootForTarget } from '../../memory/project/session-root.js';
26
27
  /** The author of record when no MCP peer introduced itself — a CLI run, or a client that skipped the handshake. */
27
28
  const RETICLE_MCP_AGENT_ID = 'reticle-mcp';
28
29
  /** The MCP client name `reticle verify` (the command line, CI's path) connects to a daemon as. */
29
30
  export const CLI_CLIENT_NAME = 'reticle-cli';
30
31
  /**
31
- * Who ran the command line, and why. On a CI runner it is the pipeline; anywhere else it is the
32
- * person who holds the key, so the cloud credits them and the run does not read as CI.
32
+ * Who ran the command line, and why. Off CI it is the person who holds the key, so the cloud
33
+ * credits them. On CI the `automation` hint says which kind: a CI vendor runner is the pipeline,
34
+ * trigger `ci`; a coding-agent sandbox is a coding agent. A container, or `CI` with no vendor,
35
+ * keeps trigger `oem`, because the cloud has no value for either yet.
33
36
  */
34
- export function cliRunAuthor(env) {
35
- return isCiEnv(env)
36
- ? {
37
- agent: { id: CLI_CLIENT_NAME, kind: RunAgentKind.OEM_PIPELINE },
38
- trigger: { kind: RunTrigger.OEM },
39
- }
40
- : {
37
+ export function cliRunAuthor(env, automation = currentAutomationHint(env)) {
38
+ if (!isCiEnv(env)) {
39
+ return {
41
40
  agent: { id: CLI_CLIENT_NAME, kind: RunAgentKind.HUMAN },
42
41
  trigger: { kind: RunTrigger.MANUAL },
43
42
  };
43
+ }
44
+ if (AutomationHint.AGENT_SANDBOX === automation) {
45
+ return {
46
+ agent: { id: CLI_CLIENT_NAME, kind: RunAgentKind.CODING_AGENT },
47
+ trigger: { kind: RunTrigger.MANUAL },
48
+ };
49
+ }
50
+ const trigger = AutomationHint.CI_RUNNER === automation ? RunTrigger.CI : RunTrigger.OEM;
51
+ return {
52
+ agent: { id: CLI_CLIENT_NAME, kind: RunAgentKind.OEM_PIPELINE },
53
+ trigger: { kind: trigger },
54
+ };
44
55
  }
45
56
  /** Who ran a suite, and why: the command line by `cliRunAuthor`, any other client as a coding agent. */
46
57
  function agentAndTrigger(client) {
@@ -24,16 +24,18 @@
24
24
  * returned unchanged.
25
25
  */
26
26
  import { FlowStore } from './flows.js';
27
- import { sessionTarget } from '../../memory/project/session-root.js';
27
+ import { rootForTarget, sessionTarget } from '../../memory/project/session-root.js';
28
28
  export function flowsForSession(deps, target) {
29
- const resolved = deps.artifactRootFor?.(target.projectId, target.origin);
30
- if (resolved === undefined || resolved.root === deps.reticleRoot) {
29
+ // Through `rootForTarget`, so the root a lease claimed wins here as it does for runs. Without
30
+ // it a lease from one folder replayed the flows of the folder the daemon was started in.
31
+ const root = rootForTarget(deps, target);
32
+ if (root === deps.reticleRoot) {
31
33
  return { flows: deps.flows, root: deps.reticleRoot };
32
34
  }
33
35
  const onWrote = deps.onRunPersisted;
34
36
  return {
35
- flows: new FlowStore(deps.fs, resolved.root, { now: deps.now }, onWrote === undefined ? {} : { onWrote }),
36
- root: resolved.root,
37
+ flows: new FlowStore(deps.fs, root, { now: deps.now }, onWrote === undefined ? {} : { onWrote }),
38
+ root,
37
39
  };
38
40
  }
39
41
  /** `flowsForSession` for a session id, with the project id to scope the store by. Same refusals. */
@@ -160,8 +160,8 @@ export declare class RecordingStore {
160
160
  markMocked(session: string | undefined, active: boolean): void;
161
161
  /**
162
162
  * `forAmbient` is the step as the ambient tape keeps it, when that differs: a journey's refuted
163
- * claims come off the ambient tape (see `unassertLast`), and a recording somebody opened keeps
164
- * every claim they declared, held or not.
163
+ * claims stay off the ambient tape, and a recording somebody opened keeps every claim they
164
+ * declared, held or not. See captureVerdictedAct.
165
165
  */
166
166
  capture(step: RecordedStep, route?: string, session?: string, forAmbient?: RecordedStep): void;
167
167
  /**
@@ -178,17 +178,6 @@ export declare class RecordingStore {
178
178
  * step, replay would ask it right after the click, of a page it was never true on.
179
179
  */
180
180
  markNavigated(): void;
181
- /**
182
- * Take the expectation off the ambient tape's last step, because the verdict on it came back no.
183
- *
184
- * A step is captured at dispatch, before anything is judged, so a claim that turned out false is
185
- * already on the tape as that step's `expect`. Cut into a journey it becomes a flow that is red for
186
- * ever, on an expectation nobody but the agent's wrong guess made. The failure is not lost: it is
187
- * filed as a capsule, the artifact built for "this should hold and does not". The ACTION stays,
188
- * since the steps after it ran on the page it produced. A recording somebody opened deliberately
189
- * is left as they drove it.
190
- */
191
- unassertLast(): void;
192
181
  /**
193
182
  * The page the step just captured ended on, once its action settled.
194
183
  *
@@ -40,8 +40,6 @@ export class RecordingStore {
40
40
  byHarness;
41
41
  #active = new Map();
42
42
  #compiled = new Map();
43
- /** The ambient tape the last step went on: the one a refuted claim is taken off. */
44
- #lastAmbient;
45
43
  /** A navigation came after the last captured step — see markNavigated. */
46
44
  #navigatedSinceStep = false;
47
45
  /** Tabs with network mocks installed now, keyed by session ('' for none named). See markMocked. */
@@ -52,8 +50,9 @@ export class RecordingStore {
52
50
  }
53
51
  start(name, cursor, startPath, session, document) {
54
52
  const openedOver = new Map();
55
- for (const [outer, rec] of this.#targets(session))
53
+ for (const [outer, rec] of this.#targets(session)) {
56
54
  openedOver.set(outer, rec.steps.length);
55
+ }
57
56
  this.#active.set(name, {
58
57
  cursor,
59
58
  steps: [],
@@ -96,15 +95,17 @@ export class RecordingStore {
96
95
  * they are, so the journey it belongs to is not auto-saved as a flow (#1459).
97
96
  */
98
97
  markMocked(session, active) {
99
- if (active)
98
+ if (active) {
100
99
  this.#mocked.add(session ?? '');
101
- else
100
+ }
101
+ else {
102
102
  this.#mocked.delete(session ?? '');
103
+ }
103
104
  }
104
105
  /**
105
106
  * `forAmbient` is the step as the ambient tape keeps it, when that differs: a journey's refuted
106
- * claims come off the ambient tape (see `unassertLast`), and a recording somebody opened keeps
107
- * every claim they declared, held or not.
107
+ * claims stay off the ambient tape, and a recording somebody opened keeps every claim they
108
+ * declared, held or not. See captureVerdictedAct.
108
109
  */
109
110
  capture(step, route, session, forAmbient) {
110
111
  this.#navigatedSinceStep = false;
@@ -118,23 +119,25 @@ export class RecordingStore {
118
119
  ...(session === undefined ? {} : { session }),
119
120
  });
120
121
  }
121
- this.#lastAmbient = ambient;
122
122
  for (const [name, rec] of this.#targets(session)) {
123
123
  // Another tab's tape is that tab's journey, whatever else this step belongs to.
124
- if (isAmbient(name) && name !== ambient)
124
+ if (isAmbient(name) && name !== ambient) {
125
125
  continue;
126
+ }
126
127
  // The ambient tape is opened by the system and closed by nobody, so it is the one recording
127
128
  // with no human deciding when it has seen enough. Bounded here rather than left to grow for
128
129
  // the length of a daemon's life. Appending STOPS at the cap instead of dropping the oldest: a
129
130
  // flow replays from its FIRST step, so a tape missing its beginning is not a shorter journey,
130
131
  // it is a different one that starts in a state nothing established. A recording somebody
131
132
  // opened on purpose is not capped — they said when it starts and they say when it stops.
132
- if (isAmbient(name) && rec.steps.length >= AMBIENT_STEP_CAP)
133
+ if (isAmbient(name) && rec.steps.length >= AMBIENT_STEP_CAP) {
133
134
  continue;
135
+ }
134
136
  // The Harness saves its own recordings. Taping its steps too saved every drive twice: once as
135
137
  // the Harness's flow and again, at session end, as a `drive-*` copy with a guessed name.
136
- if (isAmbient(name) && this.byHarness())
138
+ if (isAmbient(name) && this.byHarness()) {
137
139
  continue;
140
+ }
138
141
  // A step nobody marked ended where the next one began.
139
142
  const previous = rec.steps.at(-1);
140
143
  if (previous !== undefined && previous.endPage === undefined && step.page !== undefined) {
@@ -161,26 +164,33 @@ export class RecordingStore {
161
164
  * the proof. What the step already declared is kept; the new check joins it under `allOf`.
162
165
  */
163
166
  attachExpect(expect, session) {
164
- if (this.#navigatedSinceStep)
167
+ if (this.#navigatedSinceStep) {
165
168
  return;
169
+ }
166
170
  const mocked = this.#mocked.has(session ?? '');
167
171
  for (const [name, rec] of this.#targets(session)) {
168
- if (isAmbient(name) && name !== ambientRecordingOf(session))
172
+ if (isAmbient(name) && name !== ambientRecordingOf(session)) {
169
173
  continue;
174
+ }
170
175
  const last = rec.steps.at(-1);
171
- if (last === undefined)
176
+ if (last === undefined) {
172
177
  continue;
178
+ }
173
179
  // A proof read while mocks are active rests on mocked responses even when the act before it
174
180
  // ran unmocked, so the ambient step it joins is marked too (#1459).
175
- if (mocked && isAmbient(name))
181
+ if (mocked && isAmbient(name)) {
176
182
  last.mocked = true;
183
+ }
177
184
  const held = last.expect;
178
- last.expect =
179
- held === undefined
180
- ? expect
181
- : PredicateKind.ALL_OF === held.kind
182
- ? { ...held, predicates: [...held.predicates, expect] }
183
- : { kind: PredicateKind.ALL_OF, predicates: [held, expect] };
185
+ if (held === undefined) {
186
+ last.expect = expect;
187
+ }
188
+ else if (PredicateKind.ALL_OF === held.kind) {
189
+ last.expect = { ...held, predicates: [...held.predicates, expect] };
190
+ }
191
+ else {
192
+ last.expect = { kind: PredicateKind.ALL_OF, predicates: [held, expect] };
193
+ }
184
194
  }
185
195
  }
186
196
  /**
@@ -191,24 +201,6 @@ export class RecordingStore {
191
201
  markNavigated() {
192
202
  this.#navigatedSinceStep = true;
193
203
  }
194
- /**
195
- * Take the expectation off the ambient tape's last step, because the verdict on it came back no.
196
- *
197
- * A step is captured at dispatch, before anything is judged, so a claim that turned out false is
198
- * already on the tape as that step's `expect`. Cut into a journey it becomes a flow that is red for
199
- * ever, on an expectation nobody but the agent's wrong guess made. The failure is not lost: it is
200
- * filed as a capsule, the artifact built for "this should hold and does not". The ACTION stays,
201
- * since the steps after it ran on the page it produced. A recording somebody opened deliberately
202
- * is left as they drove it.
203
- */
204
- unassertLast() {
205
- const tape = this.#lastAmbient === undefined ? undefined : this.#active.get(this.#lastAmbient);
206
- const last = tape?.steps.at(-1);
207
- if (tape === undefined || last === undefined || last.expect === undefined)
208
- return;
209
- const { expect: _dropped, ...kept } = last;
210
- tape.steps[tape.steps.length - 1] = kept;
211
- }
212
204
  /**
213
205
  * The page the step just captured ended on, once its action settled.
214
206
  *
@@ -216,14 +208,17 @@ export class RecordingStore {
216
208
  * previous step's own answer alone.
217
209
  */
218
210
  markEnded(page, session) {
219
- if (page === undefined)
211
+ if (page === undefined) {
220
212
  return;
213
+ }
221
214
  for (const [name, rec] of this.#targets(session)) {
222
- if (isAmbient(name) && name !== ambientRecordingOf(session))
215
+ if (isAmbient(name) && name !== ambientRecordingOf(session)) {
223
216
  continue;
217
+ }
224
218
  const last = rec.steps.at(-1);
225
- if (last !== undefined && last.endPage === undefined)
219
+ if (last !== undefined && last.endPage === undefined) {
226
220
  last.endPage = page;
221
+ }
227
222
  }
228
223
  }
229
224
  /**
@@ -241,14 +236,16 @@ export class RecordingStore {
241
236
  stop(name) {
242
237
  const rec = this.#active.get(name);
243
238
  this.#active.delete(name);
244
- if (rec === undefined)
239
+ if (rec === undefined) {
245
240
  return undefined;
241
+ }
246
242
  for (const [outer, mark] of rec.openedOver) {
247
243
  const parent = this.#active.get(outer);
248
244
  // Gone already, or somehow shorter than when we started: leave it exactly as it is rather
249
245
  // than splice a range that no longer means what it meant.
250
- if (parent === undefined || parent.steps.length < mark)
246
+ if (parent === undefined || parent.steps.length < mark) {
251
247
  continue;
248
+ }
252
249
  parent.steps.splice(mark, parent.steps.length - mark, {
253
250
  tool: INVOKE_TOOL,
254
251
  args: { flow: name },
@@ -55,21 +55,31 @@ export declare const UNKEPT_EXPECT: string;
55
55
  */
56
56
  export declare function captureAct(recordings: {
57
57
  active: () => string[];
58
- capture: (step: RecordedStep, route?: string, session?: string) => void;
58
+ capture: (step: RecordedStep, route?: string, session?: string, forAmbient?: RecordedStep) => void;
59
59
  }, args: Record<string, unknown>, res: unknown,
60
60
  /** Where this step ran, so the ambient tape can be cut into journeys. See RecordedStep.route. */
61
61
  route?: string,
62
62
  /** The session that acted: only recordings it started take the step. */
63
63
  session?: string,
64
64
  /** The act's window sent a write request; see `wroteIn`. */
65
- wrote?: boolean): void;
65
+ wrote?: boolean,
66
+ /** The act as the ambient tape keeps it, when that differs. See RecordingStore.capture. */
67
+ ambientArgs?: Record<string, unknown>): void;
66
68
  /**
67
- * Capture an act_and_wait once its verdict is known, keeping `until` as the step's expectation only
68
- * on a yes. Recorded before the verdict, a step kept an `until` that came back `no` or `no-fault` —
69
- * a regression test asserting something never once observed to hold because of the action. The
70
- * action itself is always recorded: it happened, and replay needs it to reach the next step.
69
+ * Capture an act_and_wait once its verdict is known.
70
+ *
71
+ * The rule act_sequence keeps (#988). A recording somebody opened keeps every claim the agent
72
+ * declared, held or not: dropping a refuted one saved a flow that replayed green on the defect it
73
+ * had caught. The ambient tape keeps a claim only if the action made it hold (`held`), so a flow cut
74
+ * from it never replays a claim that was never once true because of the action. The action is
75
+ * always recorded: replay needs it to reach the next step.
76
+ *
77
+ * Keyed on `held`, not on `verified: yes`. A claim the action made hold under an `unknown` or a
78
+ * contradicted verdict is still what the agent proved about the page, and dropping it left the
79
+ * replay with no check at all. One function decides both tapes: a second rule that took the claim off the tape
80
+ * after the fact ran before this capture, so it took the PREVIOUS step's claim, which had passed.
71
81
  */
72
- export declare function captureVerdictedAct(recordings: Parameters<typeof captureAct>[0], args: Record<string, unknown>, res: unknown, route: string | undefined, verified: string | undefined, session?: string,
82
+ export declare function captureVerdictedAct(recordings: Parameters<typeof captureAct>[0], args: Record<string, unknown>, res: unknown, route: string | undefined, held: boolean, session?: string,
73
83
  /** The act's window sent a write request; see `wroteIn`. */
74
84
  wrote?: boolean): void;
75
85
  /** Fold a PASSING standalone assertion into the step it proved. See RecordingStore.attachExpect. */