@reticlehq/server 3.2.0 → 3.3.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 (223) hide show
  1. package/SKILL.md +16 -13
  2. package/dist/command/cli/adhoc-suite.d.ts +19 -0
  3. package/dist/command/cli/adhoc-suite.js +88 -0
  4. package/dist/command/cli/adhoc-verdict.d.ts +23 -2
  5. package/dist/command/cli/adhoc-verdict.js +18 -10
  6. package/dist/command/cli/cli-doctor.js +25 -5
  7. package/dist/command/cli/cli-parse-grammar.d.ts +7 -0
  8. package/dist/command/cli/cli-parse-grammar.js +7 -0
  9. package/dist/command/cli/cli-parse-verify.d.ts +2 -5
  10. package/dist/command/cli/cli-parse-verify.js +86 -13
  11. package/dist/command/cli/cli-parse.d.ts +7 -1
  12. package/dist/command/cli/cli-parse.js +33 -5
  13. package/dist/command/cli/cli-verify.d.ts +59 -0
  14. package/dist/command/cli/cli-verify.js +129 -17
  15. package/dist/command/cli/cloud-kit.d.ts +0 -2
  16. package/dist/command/cli/cloud-kit.js +1 -1
  17. package/dist/command/cli/doctor/doctor-sessions-line.d.ts +2 -1
  18. package/dist/command/cli/launch/cli-launch.d.ts +1 -29
  19. package/dist/command/cli/launch/cli-launch.js +6 -40
  20. package/dist/command/cli/ports/resolve/cli-port.d.ts +13 -0
  21. package/dist/command/cli/ports/resolve/cli-port.js +45 -1
  22. package/dist/command/cli/report-command.d.ts +16 -0
  23. package/dist/command/cli/report-command.js +97 -0
  24. package/dist/command/cli.js +5 -0
  25. package/dist/command/daemon/binding/daemon-status-probe.d.ts +30 -0
  26. package/dist/command/daemon/binding/daemon-status-probe.js +45 -0
  27. package/dist/command/daemon/binding/port-presence.d.ts +1 -1
  28. package/dist/command/daemon/idle-shutdown.d.ts +2 -1
  29. package/dist/command/setup/init/init-host.js +8 -0
  30. package/dist/features/crawl/crawl.d.ts +2 -0
  31. package/dist/features/crawl/crawl.js +1 -0
  32. package/dist/features/harness/jev-driver.js +6 -9
  33. package/dist/features/harness/platform-config.d.ts +10 -0
  34. package/dist/features/harness/platform-config.js +8 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.js +40 -95
  37. package/dist/judgement/capsule/capsule-store.d.ts +31 -2
  38. package/dist/judgement/capsule/capsule-store.js +58 -6
  39. package/dist/judgement/capsule/capsule.js +5 -0
  40. package/dist/judgement/capsule/causal-summary.d.ts +13 -1
  41. package/dist/judgement/capsule/causal-summary.js +7 -0
  42. package/dist/judgement/domain/domain-model.js +38 -9
  43. package/dist/judgement/outcome/assertion-integrity.d.ts +3 -3
  44. package/dist/judgement/phenomena/phenomena.d.ts +2 -1
  45. package/dist/judgement/runs/artifact/gap-report.d.ts +17 -0
  46. package/dist/judgement/runs/artifact/gap-report.js +46 -0
  47. package/dist/judgement/runs/artifact/gap-summary.d.ts +58 -0
  48. package/dist/judgement/runs/artifact/gap-summary.js +85 -0
  49. package/dist/judgement/runs/artifact/render-report.js +4 -1
  50. package/dist/judgement/runs/artifact/to-artifact.d.ts +7 -2
  51. package/dist/judgement/runs/artifact/to-artifact.js +1 -1
  52. package/dist/judgement/runs/context-tools.js +21 -3
  53. package/dist/judgement/runs/drive-run.js +7 -2
  54. package/dist/language/flows/annotate-notes/annotate.d.ts +0 -15
  55. package/dist/language/flows/annotate-notes/annotate.js +66 -18
  56. package/dist/language/flows/flow-anchor.d.ts +12 -0
  57. package/dist/language/flows/flow-anchor.js +20 -0
  58. package/dist/language/flows/flow-expect-grammar.d.ts +2 -2
  59. package/dist/language/flows/flow-expect-grammar.js +92 -11
  60. package/dist/language/flows/flow-intent.js +1 -1
  61. package/dist/language/flows/flow-learning.d.ts +3 -0
  62. package/dist/language/flows/flow-learning.js +39 -8
  63. package/dist/language/flows/flow-replay-run.d.ts +31 -9
  64. package/dist/language/flows/flow-replay-run.js +146 -33
  65. package/dist/language/flows/flow-replay-types.d.ts +9 -0
  66. package/dist/language/flows/flow-replay.d.ts +12 -11
  67. package/dist/language/flows/flow-replay.js +79 -109
  68. package/dist/language/flows/flow-step-runners.d.ts +26 -10
  69. package/dist/language/flows/flow-step-runners.js +20 -4
  70. package/dist/language/flows/flow-success.d.ts +21 -6
  71. package/dist/language/flows/flow-success.js +50 -138
  72. package/dist/language/flows/flows.d.ts +8 -4
  73. package/dist/language/flows/flows.js +11 -2
  74. package/dist/language/flows/heal.d.ts +9 -1
  75. package/dist/language/flows/heal.js +24 -2
  76. package/dist/language/flows/learned-sources.d.ts +21 -0
  77. package/dist/language/flows/learned-sources.js +84 -0
  78. package/dist/language/flows/recording/recording-backtrack.d.ts +0 -7
  79. package/dist/language/flows/recording/recording-backtrack.js +1 -1
  80. package/dist/language/flows/recording/replay-halt.d.ts +2 -1
  81. package/dist/language/flows/recording/tape/recordings.d.ts +2 -2
  82. package/dist/language/flows/replay.js +12 -8
  83. package/dist/language/flows/stores/annotation-store.d.ts +5 -5
  84. package/dist/language/flows/stores/assertion-tiers-store.js +2 -3
  85. package/dist/language/flows/stores/flake-store.js +2 -3
  86. package/dist/language/flows/suite/coverage.d.ts +1 -1
  87. package/dist/language/flows/suite/parallel-suite.d.ts +2 -1
  88. package/dist/language/flows/verify-change-tools.js +14 -0
  89. package/dist/memory/cloud/cloud-sync.d.ts +0 -2
  90. package/dist/memory/cloud/cloud-sync.js +1 -1
  91. package/dist/memory/cloud/harness-config.d.ts +2 -0
  92. package/dist/memory/cloud/sync-cycle.d.ts +12 -0
  93. package/dist/memory/cloud/sync-cycle.js +29 -4
  94. package/dist/memory/cloud/sync-daemon.d.ts +13 -0
  95. package/dist/memory/cloud/sync-daemon.js +21 -0
  96. package/dist/memory/cloud/sync-disk.js +11 -6
  97. package/dist/memory/impact/impact-store.js +6 -0
  98. package/dist/memory/intent/inline-intent.js +8 -3
  99. package/dist/memory/intent/intent-shard-store.d.ts +33 -47
  100. package/dist/memory/intent/intent-shard-store.js +252 -140
  101. package/dist/memory/intent/intent-shard.d.ts +10 -11
  102. package/dist/memory/intent/intent-shard.js +1 -1
  103. package/dist/memory/intent/intent-store.d.ts +13 -8
  104. package/dist/memory/intent/intent-store.js +46 -90
  105. package/dist/memory/intent/intent-tools.js +34 -23
  106. package/dist/memory/journal/ambient-store.js +2 -3
  107. package/dist/memory/journal/deviation-report.js +4 -1
  108. package/dist/memory/journal/deviation-service.js +10 -2
  109. package/dist/memory/journal/drive-flow.d.ts +2 -2
  110. package/dist/memory/journal/drive-flow.js +21 -12
  111. package/dist/memory/journal/envelope-store.js +2 -3
  112. package/dist/memory/journal/journal-recorder.d.ts +21 -0
  113. package/dist/memory/journal/on-disk/retain-policy.d.ts +34 -0
  114. package/dist/memory/journal/on-disk/retain-policy.js +44 -0
  115. package/dist/memory/journal/on-disk/retention.d.ts +14 -10
  116. package/dist/memory/journal/on-disk/retention.js +13 -12
  117. package/dist/memory/journal/on-disk/startup-maintenance.d.ts +42 -0
  118. package/dist/memory/journal/on-disk/startup-maintenance.js +14 -0
  119. package/dist/memory/journal/on-disk/workspace-gitignore.d.ts +0 -11
  120. package/dist/memory/journal/on-disk/workspace-gitignore.js +6 -45
  121. package/dist/memory/journal/on-disk/workspace-tiers.d.ts +52 -0
  122. package/dist/memory/journal/on-disk/workspace-tiers.js +88 -0
  123. package/dist/memory/journal/route-template.d.ts +22 -0
  124. package/dist/memory/journal/route-template.js +44 -0
  125. package/dist/memory/journal/session-end.d.ts +4 -1
  126. package/dist/memory/journal/session-end.js +64 -6
  127. package/dist/memory/journal/session-journal.d.ts +8 -1
  128. package/dist/memory/journal/session-journal.js +10 -0
  129. package/dist/memory/project/baselines.d.ts +1 -1
  130. package/dist/memory/project/dir/reticle-dir.d.ts +1 -1
  131. package/dist/memory/project/fs/fs-port.js +44 -5
  132. package/dist/memory/project/fs/write-atomic.d.ts +26 -0
  133. package/dist/memory/project/fs/write-atomic.js +48 -0
  134. package/dist/memory/recall/prior/connection-memory.d.ts +6 -0
  135. package/dist/memory/recall/prior/connection-memory.js +8 -0
  136. package/dist/memory/recall/registered-projects.d.ts +2 -1
  137. package/dist/portal/bridge/auth-failure-reason.js +5 -2
  138. package/dist/portal/input/real-input.js +2 -1
  139. package/dist/portal/pool/browser-pool.d.ts +10 -0
  140. package/dist/portal/pool/browser-pool.js +10 -0
  141. package/dist/portal/pool/playwright-launcher.js +1 -0
  142. package/dist/portal/pool/pool-contract.d.ts +2 -0
  143. package/dist/portal/pool/storage-seed.d.ts +2 -1
  144. package/dist/portal/pool/zero-install.d.ts +27 -0
  145. package/dist/portal/pool/zero-install.js +54 -0
  146. package/dist/portal/realm/web-realm.js +6 -0
  147. package/dist/portal/session/dev-server/dev-server-probe.d.ts +2 -1
  148. package/dist/portal/session/dev-server/served-document.d.ts +21 -0
  149. package/dist/portal/session/dev-server/served-document.js +57 -0
  150. package/dist/portal/session/facts/background-traffic.d.ts +9 -0
  151. package/dist/portal/session/facts/background-traffic.js +25 -0
  152. package/dist/portal/session/live-control-tools.js +48 -3
  153. package/dist/portal/session/presence/attachment-history.d.ts +13 -2
  154. package/dist/portal/session/session-health.d.ts +61 -0
  155. package/dist/portal/session/session-health.js +40 -1
  156. package/dist/portal/session/session-info.d.ts +6 -0
  157. package/dist/portal/session/session-manager.d.ts +9 -0
  158. package/dist/portal/session/session-manager.js +23 -2
  159. package/dist/portal/session/session-successor.d.ts +11 -0
  160. package/dist/portal/session/session-successor.js +14 -1
  161. package/dist/portal/session/session-verdict-facts.d.ts +28 -0
  162. package/dist/portal/session/session-verdict-facts.js +15 -0
  163. package/dist/portal/session/session.d.ts +5 -0
  164. package/dist/portal/session/session.js +19 -7
  165. package/dist/surface/mcp/mcp-proxy.js +3 -3
  166. package/dist/surface/mcp/proxy-idle-exit.d.ts +2 -1
  167. package/dist/surface/mcp/server-instructions.js +12 -1
  168. package/dist/surface/tools/act/already-true.d.ts +40 -0
  169. package/dist/surface/tools/act/already-true.js +36 -0
  170. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +6 -0
  171. package/dist/surface/tools/act/navigation/navigate-arrival.js +13 -0
  172. package/dist/surface/tools/act/settle-in-flight.d.ts +6 -2
  173. package/dist/surface/tools/act/settle-in-flight.js +29 -6
  174. package/dist/surface/tools/act-capsule.js +22 -7
  175. package/dist/surface/tools/act-tools.js +25 -24
  176. package/dist/surface/tools/args/numeric-bounds.d.ts +15 -2
  177. package/dist/surface/tools/args/numeric-bounds.js +30 -3
  178. package/dist/surface/tools/assert/assert-grade.js +14 -2
  179. package/dist/surface/tools/assert/assert-verdict.js +30 -4
  180. package/dist/surface/tools/browser-tools.js +4 -3
  181. package/dist/surface/tools/dynamic-tools.js +6 -0
  182. package/dist/surface/tools/feedback-tools.d.ts +2 -1
  183. package/dist/surface/tools/feedback-tools.js +1 -1
  184. package/dist/surface/tools/invoke-tool.js +17 -4
  185. package/dist/surface/tools/lease-hint.d.ts +8 -0
  186. package/dist/surface/tools/lease-hint.js +10 -0
  187. package/dist/surface/tools/lease-tools.d.ts +16 -28
  188. package/dist/surface/tools/lease-tools.js +53 -5
  189. package/dist/surface/tools/lineage-tools.js +1 -1
  190. package/dist/surface/tools/observe-tools.js +14 -6
  191. package/dist/surface/tools/real-input-attempt.d.ts +0 -5
  192. package/dist/surface/tools/real-input-attempt.js +1 -1
  193. package/dist/surface/tools/tool-kit.d.ts +5 -0
  194. package/dist/surface/tools/tool-kit.js +6 -1
  195. package/dist/surface/tools/tool-surface.js +17 -5
  196. package/dist/surface/tools/tools.js +64 -29
  197. package/dist/telemetry/feedback-local.d.ts +1 -0
  198. package/dist/telemetry/feedback-local.js +1 -3
  199. package/dist/telemetry/feedback.d.ts +1 -1
  200. package/dist/telemetry/hud-metrics.d.ts +32 -0
  201. package/dist/telemetry/hud-metrics.js +95 -0
  202. package/dist/telemetry/session-metrics.d.ts +5 -0
  203. package/dist/telemetry/session-metrics.js +15 -1
  204. package/dist/wire-journal.d.ts +31 -0
  205. package/dist/wire-journal.js +100 -0
  206. package/docs/README.md +1 -0
  207. package/docs/cli/cloud.mdx +9 -7
  208. package/docs/cli/init.mdx +1 -0
  209. package/docs/cli/report.mdx +44 -0
  210. package/docs/desktop.mdx +1 -1
  211. package/docs/flows.md +11 -0
  212. package/docs/frameworks.mdx +113 -1
  213. package/docs/install-agentic.mdx +36 -0
  214. package/docs/install-manual.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages.mdx +1 -1
  217. package/docs/predicates.mdx +128 -4
  218. package/docs/tools/query.mdx +10 -10
  219. package/docs/usage.md +1 -1
  220. package/docs/what-is-recorded.md +62 -0
  221. package/package.json +6 -6
  222. package/dist/judgement/outcome/predicate-to-expect.d.ts +0 -44
  223. package/dist/judgement/outcome/predicate-to-expect.js +0 -148
@@ -23,7 +23,7 @@ export { AGENT_FLAG, BUG_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from '.
23
23
  export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticle <command>\` once the bin is on your PATH)
24
24
 
25
25
  reticle init [--dry-run] [--port N] [--no-mcp] [--no-install] [--app <dir>]
26
- [--env KEY=VALUE]... [--files-only] (wire Reticle into the project in this directory)
26
+ [--env KEY=VALUE]... [--files-only] [--hooks] (wire Reticle into the project in this directory)
27
27
  init is ONBOARDING: it wires the project, boots the app and proves a session
28
28
  connected. It does not drive. The FIRST RUN is the stage that proves a flow:
29
29
  reticle verify <url> --explore --persona "<who does what>", or the same thing
@@ -56,12 +56,18 @@ export const CLI_USAGE = `usage: npx @reticlehq/server <command> (or \`reticl
56
56
  reticle verify <url> [--port N] [--headed] [--timeout N] [--storage-state <file>] [--session-id <id>] (one-shot: drive the URL, verify saved flows, exit 0=pass)
57
57
  [--explore] [--persona <who>] (no saved flows? let Reticle drive the app itself and record them)
58
58
  [--select <label>] (repeatable: verify only flows carrying these labels — no model, exit 0=pass)
59
- [--expect '<json predicate>'] (one verdict, no saved flows needed — asks
60
- the daemon that is already running, so nothing is bound and nothing is stopped. This
61
- is the path when your client never loaded the reticle_* tools. exit 0 ONLY on
62
- verified:"yes" — "unknown" is not a pass)
59
+ [--expect '<json predicate>' | --expect-file <path>] (one verdict, no saved
60
+ flows needed. --expect-file is the form no shell can mangle: cmd.exe keeps the
61
+ single quotes the docs show, and PowerShell strips the inner double quotes, and
62
+ a predicate that does not parse produces no verdict at all — asks the daemon that
63
+ is already running, so nothing is bound and nothing is stopped. It NEEDS that
64
+ daemon: with none on the port it refuses and names the flag, rather than reporting
65
+ on your saved flows instead. Cannot be combined with --storage-state, which this
66
+ path has nowhere to load. This is the path when your client never loaded the
67
+ reticle_* tools. exit 0 ONLY on verified:"yes" — "unknown" is not a pass)
63
68
  reticle affected [--since <ref>] [file...] (which saved flows must re-verify for the changed files)
64
69
  reticle gate [--since <ref>] [file...] (exit non-zero unless passing artifacts cover the affected flows)
70
+ reticle report [--session <id>] [--hook] (what the latest session claimed, and what held)
65
71
  reticle watch [url] (on save, report which saved flows must re-verify)
66
72
  reticle drive <url> [--headless] (foreground mode, for debugging)
67
73
  reticle mcp [--port N] [--drive <url>] [--headless] (MCP stdio proxy; auto-starts daemon if needed)
@@ -102,6 +108,9 @@ const CAPSULES_COMMAND = 'capsules';
102
108
  const GATE_COMMAND = 'gate';
103
109
  /** Hook mode: prose a human can read, and silence when there was simply nothing to check. */
104
110
  const HOOK_FLAG = '--hook';
111
+ /** `reticle report [--session <id>] [--hook]` — what the latest session claimed, and what held. */
112
+ const REPORT_COMMAND = 'report';
113
+ const SESSION_FLAG = '--session';
105
114
  const WATCH_COMMAND = 'watch';
106
115
  const UPDATE_COMMAND = 'update';
107
116
  const ROLLBACK_COMMAND = 'rollback';
@@ -146,6 +155,7 @@ const KNOWN_COMMANDS = new Set([
146
155
  HUNT_COMMAND,
147
156
  CAPSULES_COMMAND,
148
157
  GATE_COMMAND,
158
+ REPORT_COMMAND,
149
159
  WATCH_COMMAND,
150
160
  UPDATE_COMMAND,
151
161
  ROLLBACK_COMMAND,
@@ -228,6 +238,8 @@ const FILES_ONLY_FLAG = '--files-only';
228
238
  * and `init` runs unattended. Whoever passes this has decided; nothing decides it for them.
229
239
  */
230
240
  const CAPTURE_BODIES_FLAG = '--capture-bodies';
241
+ /** `--hooks`: also install the print-only Claude Code Stop hook (`reticle report --hook`). */
242
+ const HOOKS_FLAG = '--hooks';
231
243
  /**
232
244
  * The rest of the runtime surface.
233
245
  *
@@ -410,6 +422,7 @@ function parseInitFlags(args) {
410
422
  const env = [];
411
423
  let filesOnly = false;
412
424
  let captureBodies = false;
425
+ let hooks = false;
413
426
  let json = false;
414
427
  let open = true;
415
428
  let relaunch = false;
@@ -457,6 +470,9 @@ function parseInitFlags(args) {
457
470
  else if (arg === CAPTURE_BODIES_FLAG) {
458
471
  captureBodies = true;
459
472
  }
473
+ else if (arg === HOOKS_FLAG) {
474
+ hooks = true;
475
+ }
460
476
  else if (arg === JSON_FLAG) {
461
477
  json = true;
462
478
  }
@@ -520,6 +536,7 @@ function parseInitFlags(args) {
520
536
  env,
521
537
  filesOnly,
522
538
  captureBodies,
539
+ hooks,
523
540
  json,
524
541
  open,
525
542
  relaunch,
@@ -606,6 +623,7 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
606
623
  env: r.env,
607
624
  filesOnly: r.filesOnly,
608
625
  captureBodies: r.captureBodies,
626
+ hooks: r.hooks,
609
627
  json: r.json,
610
628
  open: r.open,
611
629
  relaunch: r.relaunch,
@@ -772,6 +790,7 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
772
790
  ...(r.storageState !== undefined ? { storageState: r.storageState } : {}),
773
791
  ...(r.sessionId !== undefined ? { sessionId: r.sessionId } : {}),
774
792
  ...(r.expect !== undefined ? { expect: r.expect } : {}),
793
+ ...(r.expectFile !== undefined ? { expectFile: r.expectFile } : {}),
775
794
  ...(true === r.explore ? { explore: true } : {}),
776
795
  ...(r.persona !== undefined ? { persona: r.persona } : {}),
777
796
  ...(r.select !== undefined ? { select: r.select } : {}),
@@ -802,6 +821,15 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
802
821
  ...(hook ? { hook: true } : {}),
803
822
  };
804
823
  }
824
+ case REPORT_COMMAND: {
825
+ const at = rest.indexOf(SESSION_FLAG);
826
+ const session = -1 === at ? undefined : rest[at + 1];
827
+ return {
828
+ kind: 'report',
829
+ hook: rest.includes(HOOK_FLAG),
830
+ ...(session === undefined ? {} : { session }),
831
+ };
832
+ }
805
833
  case WATCH_COMMAND: {
806
834
  // `reticle watch [url]` — on file save, report which saved flows must re-verify.
807
835
  const url = rest.find((arg) => !arg.startsWith('-'));
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import { type ReticleVerificationRun } from '@reticlehq/core';
13
13
  import { type RunningServer } from '../../index.js';
14
+ import { PortPresence } from '../daemon/binding/port-presence.js';
14
15
  import { type VerifyProgressListener } from '../../judgement/runs/reticle-runner.js';
15
16
  import type { ToolDeps } from '../../surface/tools/tool-kit.js';
16
17
  /** The live capabilities runVerify needs — faked in tests so the logic runs without a browser. */
@@ -104,10 +105,68 @@ export declare function urlParts(url: string): {
104
105
  * first bullet — not this change.
105
106
  */
106
107
  export declare function portBusyMessage(port: number): string;
108
+ /**
109
+ * What `--expect` is answered with when the port is not serving a daemon.
110
+ *
111
+ * It used to be answered with nothing at all. The predicate was read only inside the daemon branch,
112
+ * so on a free or foreign port the flag was dropped without a word and the command carried on into
113
+ * the saved-flows path — where it reported, accurately and about something else entirely, that there
114
+ * were no saved flows to verify. Byte-identical output to the same command with no `--expect` on it.
115
+ *
116
+ * That is the documented recovery for a client that never loaded the `reticle_*` tools, so it failed
117
+ * for the people with no other route to a verdict, and it failed in the one way they could not
118
+ * diagnose: the refusal they got was true.
119
+ */
120
+ export declare function expectNeedsDaemonMessage(port: number, presence: PortPresence): string;
121
+ /** What `verify` does next, once the options and the state of the port are both known. */
122
+ export declare const VerifyRoute: {
123
+ /** Ask the daemon that already owns the port for a one-shot verdict on the predicate. */
124
+ readonly ADHOC: "adhoc";
125
+ /** Ask the daemon that already owns the port to replay the saved flows. See runAdhocSuite. */
126
+ readonly ADHOC_SUITE: "adhoc-suite";
127
+ /** Boot our own daemon, drive the url, replay the flows saved on disk. */
128
+ readonly FLOWS: "flows";
129
+ /** Answer the caller, and run nothing. */
130
+ readonly REFUSE: "refuse";
131
+ };
132
+ export type VerifyRoute = (typeof VerifyRoute)[keyof typeof VerifyRoute];
133
+ export type VerifyPlan = {
134
+ route: typeof VerifyRoute.ADHOC;
135
+ } | {
136
+ route: typeof VerifyRoute.ADHOC_SUITE;
137
+ } | {
138
+ route: typeof VerifyRoute.FLOWS;
139
+ } | {
140
+ route: typeof VerifyRoute.REFUSE;
141
+ message: string;
142
+ };
143
+ /**
144
+ * All four routes this command has, in one pure function.
145
+ *
146
+ * They used to be a pair of nested `if`s that consulted `--expect` only after the port had already
147
+ * answered as a daemon, which is how one of the four went missing: a predicate on any other port
148
+ * state fell through to a path that cannot see it. Stating the cases together is what makes an
149
+ * unhandled one visible, and it lets every one of them be driven with no port, no daemon and no
150
+ * browser — which is where an argument-level decision belongs.
151
+ */
152
+ export declare function routeVerify(args: {
153
+ /** True when the caller supplied a predicate, by `--expect` or `--expect-file`. */
154
+ hasPredicate: boolean;
155
+ /**
156
+ * True when the caller asked for something only a browser of OUR OWN can do: `--explore` (a model
157
+ * driving a fresh page), `--headed`, or `--storage-state`. The running daemon's tab can honour
158
+ * none of them, so replaying there would silently ignore the request.
159
+ */
160
+ wantsOwnBrowser?: boolean;
161
+ presence: PortPresence;
162
+ port: number;
163
+ }): VerifyPlan;
107
164
  export declare function handleVerify(parsed: {
108
165
  url: string;
109
166
  /** A parsed predicate for a flow-free, one-shot verdict against the running daemon. */
110
167
  expect?: unknown;
168
+ /** The same predicate in a file, which is the form no shell can mangle. Resolved below. */
169
+ expectFile?: string;
111
170
  headless: boolean;
112
171
  timeoutMs?: number;
113
172
  storageState?: string;
@@ -10,14 +10,17 @@
10
10
  * — is unit-tested without launching a real browser.
11
11
  */
12
12
  import { join, basename } from 'node:path';
13
+ import { readFileSync } from 'node:fs';
13
14
  import { runAdhocVerdict } from './adhoc-verdict.js';
15
+ import { runAdhocSuite } from './adhoc-suite.js';
14
16
  import { exploreApp, harnessAvailable, MSG_NO_HARNESS_KEY, } from '../../surface/tools/harness-explore.js';
15
17
  import { resolveSuiteSelection } from '../../language/flows/suite-selection.js';
16
18
  import { readOrCreatePairingTokenSync, defaultPairingTokenDir, } from '../../portal/bridge/pairing-token.js';
17
19
  import { randomUUID } from 'node:crypto';
18
20
  import { RETICLE_DEFAULT_PORT, ReticleEnv, bridgeWsUrl, isLoopbackHostname, ReticleDir, RunAgentKind, RunFramework, RunProfile, RunTrigger, VerdictStatus, VerifyPhase, } from '@reticlehq/core';
19
21
  import { start } from '../../index.js';
20
- import { probePresence, PortPresence } from '../daemon/binding/port-presence.js';
22
+ import { probePresence, describePresence, PortPresence, } from '../daemon/binding/port-presence.js';
23
+ import { EXPECT_FLAG } from './cli-parse-grammar.js';
21
24
  import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
22
25
  import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
23
26
  import { cloudFetch, createProgressReporter, resolveCloudConfig, syncProgressToCloud, syncRunToCloud, SyncOutcome, } from '../../memory/cloud/cloud-sync.js';
@@ -365,8 +368,92 @@ export function portBusyMessage(port) {
365
368
  "your agent's MCP link and the proxy usually respawns a daemon into the gap before verify can " +
366
369
  'bind — prefer one of the above.');
367
370
  }
371
+ /**
372
+ * What `--expect` is answered with when the port is not serving a daemon.
373
+ *
374
+ * It used to be answered with nothing at all. The predicate was read only inside the daemon branch,
375
+ * so on a free or foreign port the flag was dropped without a word and the command carried on into
376
+ * the saved-flows path — where it reported, accurately and about something else entirely, that there
377
+ * were no saved flows to verify. Byte-identical output to the same command with no `--expect` on it.
378
+ *
379
+ * That is the documented recovery for a client that never loaded the `reticle_*` tools, so it failed
380
+ * for the people with no other route to a verdict, and it failed in the one way they could not
381
+ * diagnose: the refusal they got was true.
382
+ */
383
+ export function expectNeedsDaemonMessage(port, presence) {
384
+ return (`${EXPECT_FLAG} asks the Reticle daemon that already owns port ${String(port)} for the ` +
385
+ 'verdict — it binds nothing and launches nothing, which is what lets it answer while the port ' +
386
+ `is busy. Right now ${describePresence(presence, port)}.\n\n` +
387
+ ' The predicate did not run, and nothing was proved.\n\n' +
388
+ ' • Start a daemon on that port, pointed at the app, then re-run this command: ' +
389
+ `npx @reticlehq/server serve --port ${String(port)} --drive <url>\n` +
390
+ ` • Or drop ${EXPECT_FLAG}, and verify drives the url itself and replays your saved flows — ` +
391
+ 'a different question, and it proves nothing about your predicate.');
392
+ }
393
+ /** What `verify` does next, once the options and the state of the port are both known. */
394
+ export const VerifyRoute = {
395
+ /** Ask the daemon that already owns the port for a one-shot verdict on the predicate. */
396
+ ADHOC: 'adhoc',
397
+ /** Ask the daemon that already owns the port to replay the saved flows. See runAdhocSuite. */
398
+ ADHOC_SUITE: 'adhoc-suite',
399
+ /** Boot our own daemon, drive the url, replay the flows saved on disk. */
400
+ FLOWS: 'flows',
401
+ /** Answer the caller, and run nothing. */
402
+ REFUSE: 'refuse',
403
+ };
404
+ /**
405
+ * All four routes this command has, in one pure function.
406
+ *
407
+ * They used to be a pair of nested `if`s that consulted `--expect` only after the port had already
408
+ * answered as a daemon, which is how one of the four went missing: a predicate on any other port
409
+ * state fell through to a path that cannot see it. Stating the cases together is what makes an
410
+ * unhandled one visible, and it lets every one of them be driven with no port, no daemon and no
411
+ * browser — which is where an argument-level decision belongs.
412
+ */
413
+ export function routeVerify(args) {
414
+ if (args.hasPredicate) {
415
+ return args.presence === PortPresence.DAEMON
416
+ ? { route: VerifyRoute.ADHOC }
417
+ : { route: VerifyRoute.REFUSE, message: expectNeedsDaemonMessage(args.port, args.presence) };
418
+ }
419
+ if (args.presence !== PortPresence.DAEMON)
420
+ return { route: VerifyRoute.FLOWS };
421
+ // No predicate, but there may be SAVED FLOWS, and a daemon already owns the port: replay them
422
+ // there rather than refusing, unless the request needs a browser that path never opens.
423
+ return true === args.wantsOwnBrowser
424
+ ? { route: VerifyRoute.REFUSE, message: portBusyMessage(args.port) }
425
+ : { route: VerifyRoute.ADHOC_SUITE };
426
+ }
368
427
  export function handleVerify(parsed) {
369
428
  const now = () => Date.now();
429
+ /*
430
+ * `--expect-file` is `--expect` with the shell taken out of the loop.
431
+ *
432
+ * Resolved here rather than in the parser because the parser is pure and reads no disk, and
433
+ * because the two failures a reader can hit — the file is not there, and what is in it is not
434
+ * JSON — are worth saying apart. Either one ends the run before anything binds: a predicate that
435
+ * could not be read produces NO verdict, and exiting 1 with the reason is the only honest answer.
436
+ */
437
+ let expectation = parsed.expect;
438
+ if (parsed.expectFile !== undefined) {
439
+ let text;
440
+ try {
441
+ text = readFileSync(parsed.expectFile, 'utf8');
442
+ }
443
+ catch {
444
+ process.stderr.write(`--expect-file: cannot read ${parsed.expectFile}\n`);
445
+ process.exit(1);
446
+ return;
447
+ }
448
+ try {
449
+ expectation = JSON.parse(text);
450
+ }
451
+ catch {
452
+ process.stderr.write(`--expect-file: ${parsed.expectFile} is not valid JSON, so no predicate was read\n`);
453
+ process.exit(1);
454
+ return;
455
+ }
456
+ }
370
457
  const reticleRoot = join(process.cwd(), ReticleDir.ROOT);
371
458
  const projectName = basename(process.cwd()) || DEFAULT_PROJECT_NAME;
372
459
  const ports = {
@@ -389,20 +476,32 @@ export function handleVerify(parsed) {
389
476
  // this used to reach the user as a raw node stack rather than as an answer.
390
477
  void (async () => {
391
478
  const port = parsed.port ?? RETICLE_DEFAULT_PORT;
392
- if ((await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus })) ===
393
- PortPresence.DAEMON) {
479
+ const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
480
+ const plan = routeVerify({
481
+ hasPredicate: expectation !== undefined,
482
+ wantsOwnBrowser: true === parsed.explore || !parsed.headless || parsed.storageState !== undefined,
483
+ presence,
484
+ port,
485
+ });
486
+ switch (plan.route) {
487
+ // Either the busy port, or a predicate this state cannot answer. Both are refusals that name
488
+ // what was asked for — never a report about a check nobody requested.
489
+ case VerifyRoute.REFUSE:
490
+ ports.fail(plan.message);
491
+ ports.exit(EXIT_FAIL);
492
+ return;
394
493
  // A daemon owning the port is the NORMAL state after a working install, and it used to be the
395
494
  // end of the road: `verify` refused, the other verdict paths need saved flows a first-install
396
495
  // project does not have, and stopping the daemon cuts the agent's own MCP link. An agent could
397
496
  // hold a live, correctly-wired app and have no way to reach a verdict at all.
398
497
  //
399
- // With `--expect` there is now a way, and it does not need the port: ask the daemon that
400
- // already owns it. See runAdhocVerdict.
401
- if (parsed.expect !== undefined) {
498
+ // With `--expect` there is a way, and it does not need the port: ask the daemon that already
499
+ // owns it. See runAdhocVerdict.
500
+ case VerifyRoute.ADHOC: {
402
501
  const verdict = await runAdhocVerdict({
403
502
  port,
404
503
  ...(parsed.url !== undefined && '' !== parsed.url ? { url: parsed.url } : {}),
405
- predicate: parsed.expect,
504
+ predicate: expectation,
406
505
  ...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
407
506
  ...((t) => (t === undefined || 0 === t.length ? {} : { token: t }))(readOrCreatePairingTokenSync(defaultPairingTokenDir())),
408
507
  });
@@ -411,16 +510,29 @@ export function handleVerify(parsed) {
411
510
  ports.exit(verdict.code);
412
511
  return;
413
512
  }
414
- ports.fail(portBusyMessage(port));
415
- ports.exit(1);
416
- return;
513
+ // No predicate, a daemon on the port, and saved flows to run: the other half of the same
514
+ // dead end. The whole promise of a saved suite is that it runs again without an agent.
515
+ case VerifyRoute.ADHOC_SUITE: {
516
+ const suite = await runAdhocSuite({
517
+ port,
518
+ ...(parsed.select === undefined ? {} : { select: parsed.select }),
519
+ ...(parsed.sessionId === undefined ? {} : { sessionId: parsed.sessionId }),
520
+ ...((t) => (t === undefined || 0 === t.length ? {} : { token: t }))(readOrCreatePairingTokenSync(defaultPairingTokenDir())),
521
+ });
522
+ for (const line of suite.lines)
523
+ ports.out(line);
524
+ ports.exit(suite.code);
525
+ return;
526
+ }
527
+ case VerifyRoute.FLOWS:
528
+ await runVerify({
529
+ url: parsed.url,
530
+ timeoutMs: parsed.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS,
531
+ ...(true === parsed.explore ? { explore: true } : {}),
532
+ ...(parsed.persona === undefined ? {} : { persona: parsed.persona }),
533
+ ...(parsed.select === undefined ? {} : { select: parsed.select }),
534
+ }, ports);
535
+ return;
417
536
  }
418
- await runVerify({
419
- url: parsed.url,
420
- timeoutMs: parsed.timeoutMs ?? DEFAULT_SESSION_TIMEOUT_MS,
421
- ...(true === parsed.explore ? { explore: true } : {}),
422
- ...(parsed.persona === undefined ? {} : { persona: parsed.persona }),
423
- ...(parsed.select === undefined ? {} : { select: parsed.select }),
424
- }, ports);
425
537
  })();
426
538
  }
@@ -1,6 +1,4 @@
1
1
  import { type Session } from './auth/cloud-session.js';
2
- /** Where `reticle login` dials when nothing says otherwise: the hosted service. */
3
- export declare const DEFAULT_URL = "https://app.reticle.sh";
4
2
  export declare const RETICLE_DIR = ".reticle";
5
3
  export declare const SESSION_FILE = "session.json";
6
4
  export declare const CREDENTIALS_FILE = "credentials.json";
@@ -16,7 +16,7 @@ import { z } from 'zod';
16
16
  import { cloudFetch } from '../../memory/cloud/cloud-sync.js';
17
17
  import { normalizeUrl, readSessionFrom, resolveSessionFor, sessionPath, } from './auth/cloud-session.js';
18
18
  /** Where `reticle login` dials when nothing says otherwise: the hosted service. */
19
- export const DEFAULT_URL = 'https://app.reticle.sh';
19
+ const DEFAULT_URL = 'https://app.reticle.sh';
20
20
  export const RETICLE_DIR = '.reticle';
21
21
  export const SESSION_FILE = 'session.json';
22
22
  export const CREDENTIALS_FILE = 'credentials.json';
@@ -1,5 +1,5 @@
1
1
  /** What this reads off `/status`. Everything is optional — an older daemon may send none of it. */
2
- export interface SessionFacts {
2
+ interface SessionFacts {
3
3
  sessionCount?: unknown;
4
4
  why?: unknown;
5
5
  }
@@ -10,3 +10,4 @@ export interface SessionsLine {
10
10
  why?: string;
11
11
  }
12
12
  export declare function sessionsLine(facts: SessionFacts): SessionsLine;
13
+ export {};
@@ -3,34 +3,7 @@
3
3
  * "show me the app" flow (`reticle open`). Split out of cli.ts so that file stays under the size cap.
4
4
  * The decision logic is pure (unit-tested); the IO (fetch, OS browser launch) is injected/isolated.
5
5
  */
6
- /** One connected tab as `reticle status` reports it — the at-a-glance health line. */
7
- interface StatusSession {
8
- sessionId: string;
9
- url: string;
10
- throttled: boolean;
11
- /**
12
- * The tab is backgrounded. Reported by the daemon on every session; `false` on one too old to say,
13
- * which reads as visible — the conservative answer, since it keeps the reuse this command has
14
- * always done rather than opening a tab on a guess.
15
- */
16
- hidden: boolean;
17
- stale: boolean;
18
- pendingMarks: number;
19
- /** The tab is attached and answering nothing — see Session.unresponsive. Absent on an older daemon. */
20
- unresponsive?: true;
21
- }
22
- /**
23
- * Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
24
- * untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
25
- * degrades to "running, 0 sessions" instead of throwing.
26
- */
27
- export declare function summarizeStatus(payload: unknown): {
28
- sessionCount: number;
29
- sessions: StatusSession[];
30
- why?: string;
31
- /** The same diagnosis without the differential, for a surface a person reads. */
32
- whyLead?: string;
33
- };
6
+ export { summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
34
7
  /**
35
8
  * Warn when the daemon already on this port is a different version than this CLI, and attach anyway.
36
9
  *
@@ -97,4 +70,3 @@ export declare function openInBrowser(url: string, platform?: NodeJS.Platform, r
97
70
  * by spawning a real launcher on a developer machine.
98
71
  */
99
72
  export declare function launcherFailure(code: number | null, signal: NodeJS.Signals | null): string | null;
100
- export {};
@@ -7,47 +7,13 @@ import { SERVER_VERSION } from '../../version/identity/server-version.js';
7
7
  import { log } from '../../../log.js';
8
8
  import { fetchStatus } from '../../daemon/binding/daemon-status-probe.js';
9
9
  /**
10
- * Reduce the daemon's /status JSON to the compact view `reticle status` prints. Pure: narrows the
11
- * untrusted wire payload (never `any`) and tolerates a missing/partial body so a malformed response
12
- * degrades to "running, 0 sessions" instead of throwing.
10
+ * CLI launch + status helpers — the daemon-introspection (`reticle status`) and the one-command
11
+ * "show me the app" flow (`reticle open`). Split out of cli.ts so that file stays under the size cap.
12
+ * The decision logic is pure (unit-tested); the IO (fetch, OS browser launch) is injected/isolated.
13
13
  */
14
- export function summarizeStatus(payload) {
15
- if (typeof payload !== 'object' || null === payload)
16
- return { sessionCount: 0, sessions: [] };
17
- const obj = payload;
18
- // Carried through to the printed line: with no sessions this is the whole answer, and dropping it
19
- // here would silently undo the reason it is on the wire.
20
- const why = 'string' === typeof obj['why'] ? obj['why'] : undefined;
21
- // Absent on a daemon older than this field, which is why every reader falls back to `why`.
22
- const whyLead = 'string' === typeof obj['whyLead'] ? obj['whyLead'] : undefined;
23
- const raw = Array.isArray(obj['sessions']) ? obj['sessions'] : [];
24
- const sessions = raw
25
- .map((s) => {
26
- if (typeof s !== 'object' || null === s)
27
- return null;
28
- const r = s;
29
- const sessionId = 'string' === typeof r['sessionId'] ? r['sessionId'] : '';
30
- if ('' === sessionId)
31
- return null;
32
- return {
33
- sessionId,
34
- url: 'string' === typeof r['url'] ? r['url'] : '',
35
- throttled: true === r['throttled'],
36
- hidden: true === r['hidden'],
37
- stale: true === r['stale'],
38
- pendingMarks: 'number' === typeof r['pendingMarks'] ? r['pendingMarks'] : 0,
39
- ...(true === r['unresponsive'] ? { unresponsive: true } : {}),
40
- };
41
- })
42
- .filter((s) => s !== null);
43
- const sessionCount = 'number' === typeof obj['sessionCount'] ? obj['sessionCount'] : sessions.length;
44
- return {
45
- sessionCount,
46
- sessions,
47
- ...(why === undefined ? {} : { why }),
48
- ...(whyLead === undefined ? {} : { whyLead }),
49
- };
50
- }
14
+ // Parsing the daemon's /status payload lives beside the probe that fetches it; re-exported here for
15
+ // the callers that already import it from this module.
16
+ export { summarizeStatus } from '../../daemon/binding/daemon-status-probe.js';
51
17
  /** A string field off the /status body, or undefined on a daemon too old to report it. */
52
18
  function statusField(payload, key) {
53
19
  if (typeof payload !== 'object' || null === payload)
@@ -41,6 +41,19 @@ export declare function readProjectPort(cwd: string): number | undefined;
41
41
  * undefined if the file is absent/unreadable or has no non-empty string projectId.
42
42
  */
43
43
  export declare function readProjectId(cwd: string): ProjectId | undefined;
44
+ /**
45
+ * Every project id this directory can speak for, for "has THIS project ever connected".
46
+ *
47
+ * `readProjectId` knows only what `init` wrote. The build plugins derive the same id from the package
48
+ * name and the app root WITHOUT writing `.reticle.json`, so a monorepo root (the app lives in
49
+ * `apps/web`) and an app wired by the plugin alone both read `undefined`, and the handshake told a
50
+ * working project nothing had ever connected. Deriving the plugin's own id cannot reopen the opposite
51
+ * lie: an id only counts once an app with exactly that id has dialled in.
52
+ *
53
+ * Workspace discovery is init's `findWorkspaceApps`, reused so the two cannot disagree about which
54
+ * directories are apps. Best-effort: a directory it cannot read contributes nothing.
55
+ */
56
+ export declare function projectIdsAt(cwd: string): ProjectId[];
44
57
  /**
45
58
  * The framework `reticle init` stamped into `.reticle.json`, when there is one.
46
59
  *
@@ -8,7 +8,8 @@
8
8
  * 4. RETICLE_DEFAULT_PORT (4400)
9
9
  */
10
10
  import { readFileSync } from 'node:fs';
11
- import { dirname, resolve } from 'node:path';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { SILENT_HOST, buildNodeIo, deriveProjectId, findWorkspaceApps, packageName, } from '@reticlehq/init';
12
13
  import { asProjectId } from '@reticlehq/core';
13
14
  /** The project config `reticle init` writes. */
14
15
  export const RETICLE_CONFIG_BASENAME = '.reticle.json';
@@ -84,6 +85,49 @@ export function readProjectId(cwd) {
84
85
  return asProjectId(id);
85
86
  return undefined;
86
87
  }
88
+ const PACKAGE_JSON = 'package.json';
89
+ /** The package name in `dir/package.json`, or undefined when there is no readable one. */
90
+ function packageNameAt(dir) {
91
+ try {
92
+ return packageName(JSON.parse(readFileSync(join(dir, PACKAGE_JSON), 'utf8')));
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ }
98
+ /** The id an app at `dir` carries: the one `init` recorded, else the one the build plugin derives. */
99
+ function projectIdFor(dir) {
100
+ return readProjectId(dir) ?? asProjectId(deriveProjectId(packageNameAt(dir), dir));
101
+ }
102
+ /**
103
+ * Every project id this directory can speak for, for "has THIS project ever connected".
104
+ *
105
+ * `readProjectId` knows only what `init` wrote. The build plugins derive the same id from the package
106
+ * name and the app root WITHOUT writing `.reticle.json`, so a monorepo root (the app lives in
107
+ * `apps/web`) and an app wired by the plugin alone both read `undefined`, and the handshake told a
108
+ * working project nothing had ever connected. Deriving the plugin's own id cannot reopen the opposite
109
+ * lie: an id only counts once an app with exactly that id has dialled in.
110
+ *
111
+ * Workspace discovery is init's `findWorkspaceApps`, reused so the two cannot disagree about which
112
+ * directories are apps. Best-effort: a directory it cannot read contributes nothing.
113
+ */
114
+ export function projectIdsAt(cwd) {
115
+ const recorded = readProjectId(cwd);
116
+ if (recorded !== undefined)
117
+ return [recorded];
118
+ const ids = [];
119
+ if (packageNameAt(cwd) !== undefined)
120
+ ids.push(projectIdFor(cwd));
121
+ try {
122
+ for (const app of findWorkspaceApps(buildNodeIo(cwd, SILENT_HOST))) {
123
+ ids.push(projectIdFor(join(cwd, app)));
124
+ }
125
+ }
126
+ catch {
127
+ // Discovery touches the filesystem; an unreadable tree is simply no further evidence.
128
+ }
129
+ return [...new Set(ids)];
130
+ }
87
131
  /**
88
132
  * The framework `reticle init` stamped into `.reticle.json`, when there is one.
89
133
  *
@@ -0,0 +1,16 @@
1
+ export interface ReportInput {
2
+ cwd: string;
3
+ now: number;
4
+ /** Did the working tree change? The hook stays silent on a turn that edited nothing. */
5
+ treeChanged: boolean;
6
+ session?: string;
7
+ hook?: boolean;
8
+ }
9
+ export declare function reportFor(input: ReportInput): Promise<{
10
+ lines: string[];
11
+ }>;
12
+ /**
13
+ * The command. As a hook it prints Claude Code's `systemMessage` shape and always exits 0: it is
14
+ * print-only by design, so the human sees the gap and the agent is never blocked by it.
15
+ */
16
+ export declare function handleReport(session: string | undefined, hook: boolean): Promise<void>;