@reticlehq/server 3.5.0 → 3.6.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 (328) hide show
  1. package/README.md +1 -1
  2. package/SKILL.md +16 -8
  3. package/dist/chromium-launch-options.d.ts +3 -1
  4. package/dist/chromium-launch-options.js +2 -1
  5. package/dist/command/cli/adhoc-verdict.d.ts +1 -0
  6. package/dist/command/cli/adhoc-verdict.js +6 -3
  7. package/dist/command/cli/answers/open-note.d.ts +2 -0
  8. package/dist/command/cli/answers/open-note.js +22 -0
  9. package/dist/command/cli/cli-flow-commands.js +4 -8
  10. package/dist/command/cli/cli-parse.d.ts +1 -1
  11. package/dist/command/cli/cli-parse.js +10 -10
  12. package/dist/command/cli/cli-verify.d.ts +1 -1
  13. package/dist/command/cli/cli-verify.js +35 -25
  14. package/dist/command/cli/cloud-cli.js +51 -12
  15. package/dist/command/cli/cloud-login.d.ts +8 -0
  16. package/dist/command/cli/cloud-login.js +36 -2
  17. package/dist/command/cli/demo/demo-page.js +11 -3
  18. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
  19. package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
  20. package/dist/command/cli/drive/drive-attach.d.ts +27 -2
  21. package/dist/command/cli/drive/drive-attach.js +71 -4
  22. package/dist/command/cli/drive/drive-command.d.ts +2 -1
  23. package/dist/command/cli/drive/drive-command.js +13 -7
  24. package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
  25. package/dist/command/cli/ports/port-holder.d.ts +10 -0
  26. package/dist/command/cli/ports/port-holder.js +24 -0
  27. package/dist/command/cli/setup-mcp-cli.js +6 -4
  28. package/dist/command/cli/tutorial.d.ts +2 -2
  29. package/dist/command/cli/tutorial.js +6 -9
  30. package/dist/command/cli.d.ts +6 -0
  31. package/dist/command/cli.js +28 -22
  32. package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
  33. package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
  34. package/dist/command/daemon/daemon-resolve.d.ts +14 -0
  35. package/dist/command/daemon/daemon-resolve.js +33 -2
  36. package/dist/command/setup/agent-writer.d.ts +1 -1
  37. package/dist/command/setup/agent-writer.js +3 -2
  38. package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
  39. package/dist/command/setup/bringup/connect-lease.js +1 -0
  40. package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
  41. package/dist/command/setup/bringup/dev-server-wait.js +19 -0
  42. package/dist/command/setup/init/init-port.d.ts +4 -0
  43. package/dist/command/setup/init/init-port.js +10 -0
  44. package/dist/command/setup/init/init-runtime.d.ts +10 -0
  45. package/dist/command/setup/init/init-runtime.js +18 -1
  46. package/dist/command/setup/node-effects.js +2 -1
  47. package/dist/command/setup/remaining-steps.js +2 -1
  48. package/dist/command/setup/run-setup.d.ts +5 -0
  49. package/dist/command/setup/run-setup.js +35 -27
  50. package/dist/command/setup/setup-command.d.ts +2 -1
  51. package/dist/command/setup/setup-command.js +2 -0
  52. package/dist/command/version/version-skew.js +11 -2
  53. package/dist/features/crawl/crawl-tools.js +4 -0
  54. package/dist/features/crawl/crawl.d.ts +6 -0
  55. package/dist/features/crawl/crawl.js +9 -1
  56. package/dist/features/exhaust/ledger.d.ts +7 -0
  57. package/dist/features/exhaust/ledger.js +35 -9
  58. package/dist/features/harness/app-findings.d.ts +2 -0
  59. package/dist/features/harness/app-findings.js +52 -0
  60. package/dist/features/harness/drive-report.d.ts +27 -1
  61. package/dist/features/harness/drive-report.js +162 -11
  62. package/dist/features/harness/drivers.d.ts +10 -14
  63. package/dist/features/harness/drivers.js +10 -14
  64. package/dist/features/harness/fill-values.d.ts +3 -73
  65. package/dist/features/harness/fill-values.js +3 -169
  66. package/dist/features/harness/goals.d.ts +21 -0
  67. package/dist/features/harness/goals.js +77 -0
  68. package/dist/features/harness/harness.d.ts +21 -0
  69. package/dist/features/harness/harness.js +69 -2
  70. package/dist/features/harness/platform/drive-target.d.ts +30 -0
  71. package/dist/features/harness/platform/drive-target.js +81 -0
  72. package/dist/features/harness/platform/local-apps.d.ts +94 -0
  73. package/dist/features/harness/platform/local-apps.js +204 -0
  74. package/dist/features/harness/platform/remote-drive.d.ts +180 -0
  75. package/dist/features/harness/platform/remote-drive.js +365 -0
  76. package/dist/features/harness/platform/script.d.ts +48 -0
  77. package/dist/features/harness/platform/script.js +128 -0
  78. package/dist/features/harness/platform/server-driver.d.ts +41 -0
  79. package/dist/features/harness/platform/server-driver.js +179 -0
  80. package/dist/features/harness/platform-config.d.ts +7 -0
  81. package/dist/features/harness/platform-config.js +14 -2
  82. package/dist/features/harness/script/app-controls.d.ts +5 -0
  83. package/dist/features/harness/script/app-controls.js +40 -0
  84. package/dist/features/harness/script/lane-ids.d.ts +14 -0
  85. package/dist/features/harness/script/lane-ids.js +16 -0
  86. package/dist/features/harness/script/script-run.d.ts +41 -0
  87. package/dist/features/harness/script/script-run.js +299 -0
  88. package/dist/features/license/license.d.ts +2 -2
  89. package/dist/features/visual/visual-tools.d.ts +10 -1
  90. package/dist/features/visual/visual-tools.js +40 -3
  91. package/dist/hooks/driven-by.d.ts +4 -0
  92. package/dist/hooks/driven-by.js +16 -0
  93. package/dist/hooks/hook-commands.js +15 -4
  94. package/dist/hooks/tool-log.d.ts +9 -0
  95. package/dist/hooks/tool-log.js +29 -0
  96. package/dist/index.d.ts +1 -0
  97. package/dist/index.js +76 -82
  98. package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
  99. package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
  100. package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
  101. package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
  102. package/dist/judgement/runs/artifact/run-store.js +26 -1
  103. package/dist/judgement/runs/drive-run.d.ts +22 -0
  104. package/dist/judgement/runs/drive-run.js +99 -1
  105. package/dist/judgement/runs/repair-prompt.js +1 -1
  106. package/dist/judgement/runs/reticle-runner.js +8 -1
  107. package/dist/judgement/runs/verification-sync.d.ts +3 -2
  108. package/dist/judgement/runs/verification-sync.js +4 -3
  109. package/dist/language/flows/change/gate.d.ts +10 -0
  110. package/dist/language/flows/change/gate.js +22 -0
  111. package/dist/language/flows/decision.js +34 -0
  112. package/dist/language/flows/flow-anchor.d.ts +54 -8
  113. package/dist/language/flows/flow-anchor.js +88 -3
  114. package/dist/language/flows/flow-cross-step.d.ts +15 -0
  115. package/dist/language/flows/flow-cross-step.js +21 -0
  116. package/dist/language/flows/flow-learning.js +8 -11
  117. package/dist/language/flows/flow-mutate-tools.js +5 -3
  118. package/dist/language/flows/flow-replay-run.d.ts +23 -9
  119. package/dist/language/flows/flow-replay-run.js +101 -94
  120. package/dist/language/flows/flow-replay.d.ts +23 -1
  121. package/dist/language/flows/flow-replay.js +92 -19
  122. package/dist/language/flows/flow-scope.js +5 -1
  123. package/dist/language/flows/flow-step-runners.d.ts +20 -2
  124. package/dist/language/flows/flow-step-runners.js +236 -42
  125. package/dist/language/flows/flow-store-for-session.d.ts +2 -2
  126. package/dist/language/flows/flow-store-for-session.js +2 -2
  127. package/dist/language/flows/flow-tools.js +58 -34
  128. package/dist/language/flows/flows.js +5 -2
  129. package/dist/language/flows/heal-run.js +6 -4
  130. package/dist/language/flows/recording/flake.d.ts +2 -2
  131. package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
  132. package/dist/language/flows/recording/tape/recordings.js +40 -8
  133. package/dist/language/flows/replay-from-hud.d.ts +20 -0
  134. package/dist/language/flows/replay-from-hud.js +51 -0
  135. package/dist/language/flows/replay.d.ts +9 -5
  136. package/dist/language/flows/replay.js +27 -9
  137. package/dist/language/flows/suite/suite-flakes.js +2 -0
  138. package/dist/language/flows/suite-selection.d.ts +2 -15
  139. package/dist/language/flows/suite-selection.js +4 -3
  140. package/dist/language/flows/verify-change-tools.js +8 -4
  141. package/dist/launch-chromium.d.ts +27 -5
  142. package/dist/launch-chromium.js +38 -8
  143. package/dist/memory/cloud/cloud-config.d.ts +12 -0
  144. package/dist/memory/cloud/cloud-config.js +15 -0
  145. package/dist/memory/cloud/cloud-sync.d.ts +17 -14
  146. package/dist/memory/cloud/cloud-sync.js +12 -1
  147. package/dist/memory/cloud/harness-config.d.ts +26 -1
  148. package/dist/memory/cloud/harness-config.js +99 -4
  149. package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
  150. package/dist/memory/cloud/hud-notices-source.js +128 -0
  151. package/dist/memory/cloud/memory-scope.d.ts +144 -0
  152. package/dist/memory/cloud/memory-scope.js +205 -0
  153. package/dist/memory/cloud/sync-cycle.d.ts +12 -3
  154. package/dist/memory/cloud/sync-cycle.js +16 -3
  155. package/dist/memory/cloud/sync-daemon.js +7 -5
  156. package/dist/memory/cloud/sync-disk.d.ts +1 -1
  157. package/dist/memory/cloud/sync-disk.js +5 -8
  158. package/dist/memory/impact/account-watch.d.ts +19 -0
  159. package/dist/memory/impact/account-watch.js +34 -0
  160. package/dist/memory/impact/impact-recorder.d.ts +8 -0
  161. package/dist/memory/impact/impact-recorder.js +12 -2
  162. package/dist/memory/impact/impact-store.d.ts +16 -0
  163. package/dist/memory/impact/impact-store.js +142 -26
  164. package/dist/memory/intent/intent-shard.d.ts +2 -2
  165. package/dist/memory/intent/intent-tools.js +80 -2
  166. package/dist/memory/journal/envelope.d.ts +8 -8
  167. package/dist/memory/journal/journal-recorder.js +7 -1
  168. package/dist/memory/journal/on-disk/retention.d.ts +3 -0
  169. package/dist/memory/journal/on-disk/retention.js +5 -0
  170. package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
  171. package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
  172. package/dist/memory/journal/session-end.js +6 -4
  173. package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
  174. package/dist/memory/project/artifact-root-resolver.js +94 -13
  175. package/dist/memory/project/artifact-root.d.ts +42 -5
  176. package/dist/memory/project/artifact-root.js +87 -8
  177. package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
  178. package/dist/memory/project/dir/reticle-dir.js +3 -1
  179. package/dist/memory/project/project-tools.js +22 -3
  180. package/dist/memory/project/session-root.d.ts +24 -1
  181. package/dist/memory/project/session-root.js +41 -5
  182. package/dist/memory/project/sync-status.d.ts +39 -0
  183. package/dist/memory/project/sync-status.js +90 -0
  184. package/dist/memory/recall/memory-tools.js +2 -0
  185. package/dist/memory/recall/project-memory.d.ts +19 -0
  186. package/dist/memory/recall/project-memory.js +45 -6
  187. package/dist/portal/bridge/bridge.d.ts +3 -1
  188. package/dist/portal/bridge/bridge.js +19 -2
  189. package/dist/portal/bridge/pairing-token.d.ts +9 -0
  190. package/dist/portal/bridge/pairing-token.js +17 -0
  191. package/dist/portal/input/real-input.d.ts +81 -10
  192. package/dist/portal/input/real-input.js +207 -22
  193. package/dist/portal/input/viewport-tools.js +3 -3
  194. package/dist/portal/pool/browser-pool.d.ts +22 -9
  195. package/dist/portal/pool/browser-pool.js +70 -35
  196. package/dist/portal/pool/playwright-launcher.d.ts +13 -0
  197. package/dist/portal/pool/playwright-launcher.js +17 -1
  198. package/dist/portal/pool/pool-contract.d.ts +7 -4
  199. package/dist/portal/session/command-payload.d.ts +8 -0
  200. package/dist/portal/session/command-payload.js +19 -0
  201. package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
  202. package/dist/portal/session/dev-server/dev-command.js +61 -12
  203. package/dist/portal/session/facts/departure-note.d.ts +22 -0
  204. package/dist/portal/session/facts/departure-note.js +47 -0
  205. package/dist/portal/session/facts/session-replaced.d.ts +5 -0
  206. package/dist/portal/session/facts/session-replaced.js +5 -0
  207. package/dist/portal/session/first-run-wiring.d.ts +42 -0
  208. package/dist/portal/session/first-run-wiring.js +132 -0
  209. package/dist/portal/session/human/notes-ledger.d.ts +20 -0
  210. package/dist/portal/session/human/notes-ledger.js +55 -0
  211. package/dist/portal/session/human/review-store.d.ts +12 -0
  212. package/dist/portal/session/human/review-store.js +18 -10
  213. package/dist/portal/session/live-control-tools.d.ts +0 -13
  214. package/dist/portal/session/live-control-tools.js +28 -3
  215. package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
  216. package/dist/portal/session/no-session-diagnosis.js +15 -0
  217. package/dist/portal/session/no-session-next-action.d.ts +11 -0
  218. package/dist/portal/session/no-session-next-action.js +31 -5
  219. package/dist/portal/session/no-session-watch.d.ts +14 -1
  220. package/dist/portal/session/no-session-watch.js +63 -10
  221. package/dist/portal/session/presence/session-mirror.js +3 -0
  222. package/dist/portal/session/serving-directory.d.ts +27 -0
  223. package/dist/portal/session/serving-directory.js +45 -0
  224. package/dist/portal/session/session-health.d.ts +7 -0
  225. package/dist/portal/session/session-health.js +6 -0
  226. package/dist/portal/session/session-info.d.ts +8 -1
  227. package/dist/portal/session/session-info.js +6 -0
  228. package/dist/portal/session/session-manager.d.ts +21 -0
  229. package/dist/portal/session/session-manager.js +44 -8
  230. package/dist/portal/session/session-reaper.js +4 -1
  231. package/dist/portal/session/session-successor.d.ts +5 -0
  232. package/dist/portal/session/session-tools.js +26 -3
  233. package/dist/portal/session/session.d.ts +7 -2
  234. package/dist/portal/session/session.js +21 -18
  235. package/dist/start-options.d.ts +6 -0
  236. package/dist/status-payload.d.ts +10 -0
  237. package/dist/status-payload.js +23 -0
  238. package/dist/surface/http-server.js +11 -1
  239. package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
  240. package/dist/surface/mcp/mcp-proxy.js +36 -5
  241. package/dist/surface/mcp/mcp.d.ts +11 -6
  242. package/dist/surface/mcp/mcp.js +27 -13
  243. package/dist/surface/mcp/server-instructions.d.ts +4 -2
  244. package/dist/surface/mcp/server-instructions.js +14 -4
  245. package/dist/surface/mcp/shared-params.js +2 -2
  246. package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
  247. package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
  248. package/dist/surface/tools/act/settle-in-flight.js +25 -3
  249. package/dist/surface/tools/act-sequence-tool.js +2 -2
  250. package/dist/surface/tools/act-tools.js +23 -7
  251. package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
  252. package/dist/surface/tools/assert/assert-verdict.js +17 -2
  253. package/dist/surface/tools/browser-tools.js +8 -1
  254. package/dist/surface/tools/chat-drives.d.ts +26 -0
  255. package/dist/surface/tools/chat-drives.js +165 -0
  256. package/dist/surface/tools/empty-session-list.d.ts +41 -0
  257. package/dist/surface/tools/empty-session-list.js +80 -0
  258. package/dist/surface/tools/error-recovery.d.ts +6 -0
  259. package/dist/surface/tools/error-recovery.js +37 -4
  260. package/dist/surface/tools/explore-tools.d.ts +10 -1
  261. package/dist/surface/tools/explore-tools.js +82 -13
  262. package/dist/surface/tools/feedback-tools.js +14 -0
  263. package/dist/surface/tools/harness-explore.d.ts +96 -14
  264. package/dist/surface/tools/harness-explore.js +227 -206
  265. package/dist/surface/tools/harness-plan.d.ts +12 -0
  266. package/dist/surface/tools/harness-plan.js +31 -0
  267. package/dist/surface/tools/harness-script.d.ts +8 -0
  268. package/dist/surface/tools/harness-script.js +342 -0
  269. package/dist/surface/tools/harness-toolset.d.ts +14 -1
  270. package/dist/surface/tools/harness-toolset.js +77 -6
  271. package/dist/surface/tools/invoke-tool.js +52 -1
  272. package/dist/surface/tools/lease-seed.d.ts +29 -0
  273. package/dist/surface/tools/lease-seed.js +123 -0
  274. package/dist/surface/tools/lease-tools.d.ts +3 -23
  275. package/dist/surface/tools/lease-tools.js +30 -131
  276. package/dist/surface/tools/live-call-text.js +2 -2
  277. package/dist/surface/tools/next-step.d.ts +19 -0
  278. package/dist/surface/tools/next-step.js +159 -0
  279. package/dist/surface/tools/observe-tools.js +2 -4
  280. package/dist/surface/tools/platform-moment.d.ts +12 -0
  281. package/dist/surface/tools/platform-moment.js +168 -0
  282. package/dist/surface/tools/query-hint-schema.d.ts +7 -0
  283. package/dist/surface/tools/query-hint-schema.js +10 -0
  284. package/dist/surface/tools/read-tools.js +8 -2
  285. package/dist/surface/tools/real-input-attempt.js +96 -13
  286. package/dist/surface/tools/tool-kit.d.ts +15 -1
  287. package/dist/surface/tools/tool-kit.js +4 -6
  288. package/dist/surface/tools/tools.js +20 -48
  289. package/dist/telemetry/hud-metrics.d.ts +2 -0
  290. package/dist/telemetry/hud-metrics.js +17 -0
  291. package/dist/telemetry/telemetry.d.ts +12 -0
  292. package/dist/telemetry/telemetry.js +43 -14
  293. package/dist/telemetry/verification-of.js +1 -0
  294. package/dist/wire-journal.js +4 -1
  295. package/docs/README.md +1 -1
  296. package/docs/agent-cheatsheet.md +1 -1
  297. package/docs/autodrive.md +29 -25
  298. package/docs/cli/doctor.mdx +6 -0
  299. package/docs/cli/drive.mdx +8 -2
  300. package/docs/cli/init.mdx +11 -13
  301. package/docs/cli/status.mdx +5 -1
  302. package/docs/cli/version.mdx +3 -3
  303. package/docs/cli.mdx +1 -1
  304. package/docs/desktop.mdx +5 -1
  305. package/docs/environment.mdx +3 -7
  306. package/docs/getting-started.md +13 -11
  307. package/docs/human-control.md +1 -2
  308. package/docs/index.mdx +4 -3
  309. package/docs/install-manual.mdx +2 -2
  310. package/docs/packages/tauri.mdx +2 -2
  311. package/docs/packages/vite-plugin.mdx +22 -1
  312. package/docs/packages.mdx +1 -1
  313. package/docs/quickstart.mdx +17 -7
  314. package/docs/telemetry.md +1 -1
  315. package/docs/tools/session-and-feedback.mdx +9 -9
  316. package/docs/usage.md +5 -2
  317. package/docs/what-is-recorded.md +13 -3
  318. package/docs/what-is-reticle.mdx +3 -1
  319. package/package.json +8 -12
  320. package/dist/features/harness/driver.d.ts +0 -50
  321. package/dist/features/harness/driver.js +0 -236
  322. package/dist/features/harness/jev-driver.d.ts +0 -188
  323. package/dist/features/harness/jev-driver.js +0 -678
  324. package/dist/features/harness/openai-driver.d.ts +0 -58
  325. package/dist/features/harness/openai-driver.js +0 -206
  326. package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
  327. package/dist/memory/project/dir/fill-value-store.js +0 -88
  328. package/docs/local-registry.md +0 -143
@@ -0,0 +1,42 @@
1
+ export interface WiredStep {
2
+ mark: string;
3
+ title: string;
4
+ target: string;
5
+ }
6
+ export interface WireOutcome {
7
+ ok: boolean;
8
+ /** Where init ran. */
9
+ directory: string;
10
+ /** Every step of init's plan, applied or not, as it printed them. */
11
+ steps: WiredStep[];
12
+ /** The app's url, when init got as far as the dev server. */
13
+ url?: string;
14
+ /** Why it stopped, in init's words or ours. */
15
+ error?: string;
16
+ }
17
+ export interface RunResult {
18
+ code: number;
19
+ stdout: string;
20
+ stderr: string;
21
+ }
22
+ export type RunCli = (args: readonly string[], cwd: string) => Promise<RunResult>;
23
+ export interface FirstRunWiring {
24
+ /**
25
+ * The directory init would run in, or undefined when it cannot be told without guessing: a
26
+ * daemon started in the home directory or at a filesystem root has no project of its own.
27
+ */
28
+ projectDirectory(cwd: string): string | undefined;
29
+ wire(directory: string): Promise<WireOutcome>;
30
+ }
31
+ export declare function projectDirectoryOf(cwd: string, home?: string, wired?: (dir: string) => boolean): string | undefined;
32
+ /** init's plan lines, and the one JSON object it prints last under `--json`. */
33
+ export declare function readInitOutput(stdout: string): {
34
+ steps: WiredStep[];
35
+ result: unknown;
36
+ };
37
+ export declare function firstRunWiring(options: {
38
+ /** This daemon's port, so init wires the app to it rather than choosing another. */
39
+ port: number;
40
+ cliPath: string;
41
+ run?: RunCli;
42
+ }): FirstRunWiring;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Wiring the app the first time an agent needs it, instead of at installation.
3
+ *
4
+ * Installing Reticle puts a CLI and an MCP server on the machine and touches no project. The app is
5
+ * instrumented the first time an agent actually uses Reticle against it: the daemon runs its own
6
+ * CLI's `init` in the project directory, the same codemod, dev-server start and connect a person
7
+ * would have run by hand, and reports what it changed.
8
+ *
9
+ * A child process rather than an import: the daemon never imports `@reticlehq/init`
10
+ * (library-path-boundary.test.ts), and running the CLI keeps that line where it is.
11
+ *
12
+ * Once per directory for the life of the daemon. A second call returns the first answer, so an agent
13
+ * that asks again after a failure is told the same thing rather than re-running a codemod on files
14
+ * the first run already changed.
15
+ */
16
+ import { spawn } from 'node:child_process';
17
+ import { existsSync, readFileSync } from 'node:fs';
18
+ import { homedir } from 'node:os';
19
+ import { join, resolve } from 'node:path';
20
+ import { isWebApp } from '@reticlehq/init';
21
+ import { readProjectId } from '../../command/cli/ports/resolve/cli-port.js';
22
+ /** init has a dev server to start and a page to wait for; past this it is not coming. */
23
+ const WIRE_TIMEOUT_MS = 120_000;
24
+ /** A plan line as init prints it: `[✓] Vite plugin → vite.config.ts`. */
25
+ const PLAN_LINE = /^\s*\[(.)\]\s+(.+?)\s+→\s+(.+)$/;
26
+ export function projectDirectoryOf(cwd, home = homedir(), wired = (dir) => readProjectId(dir) !== undefined) {
27
+ const dir = resolve(cwd);
28
+ if (dir === resolve(home) || dir === resolve('/'))
29
+ return undefined;
30
+ // Already wired: nothing for a first run to do, and init over a wired app proves nothing.
31
+ if (wired(dir))
32
+ return undefined;
33
+ // Only an app with a page: a daemon started in a library or API package wrote a config there.
34
+ return isWebApp('.', {
35
+ exists: (p) => existsSync(join(dir, p)),
36
+ readFile: (p) => {
37
+ try {
38
+ return readFileSync(join(dir, p), 'utf8');
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ },
44
+ })
45
+ ? dir
46
+ : undefined;
47
+ }
48
+ /** init's plan lines, and the one JSON object it prints last under `--json`. */
49
+ export function readInitOutput(stdout) {
50
+ const steps = stdout
51
+ .split('\n')
52
+ .map((line) => PLAN_LINE.exec(line))
53
+ .flatMap((m) => null === m
54
+ ? []
55
+ : [{ mark: m[1] ?? '', title: (m[2] ?? '').trim(), target: (m[3] ?? '').trim() }]);
56
+ const at = stdout.lastIndexOf('\n{\n') + 1;
57
+ let result;
58
+ try {
59
+ result = JSON.parse(stdout.slice(at));
60
+ }
61
+ catch {
62
+ result = undefined;
63
+ }
64
+ return { steps, result };
65
+ }
66
+ function nodeRun(cliPath) {
67
+ return (args, cwd) => new Promise((done) => {
68
+ const child = spawn(process.execPath, [cliPath, ...args], {
69
+ cwd,
70
+ stdio: ['ignore', 'pipe', 'pipe'],
71
+ env: process.env,
72
+ });
73
+ let stdout = '';
74
+ let stderr = '';
75
+ child.stdout.on('data', (chunk) => (stdout += chunk.toString('utf8')));
76
+ child.stderr.on('data', (chunk) => (stderr += chunk.toString('utf8')));
77
+ const timer = setTimeout(() => child.kill(), WIRE_TIMEOUT_MS);
78
+ child.on('error', (error) => {
79
+ clearTimeout(timer);
80
+ done({ code: 1, stdout, stderr: `${stderr}${error.message}` });
81
+ });
82
+ child.on('close', (code) => {
83
+ clearTimeout(timer);
84
+ done({ code: code ?? 1, stdout, stderr });
85
+ });
86
+ });
87
+ }
88
+ export function firstRunWiring(options) {
89
+ const run = options.run ?? nodeRun(options.cliPath);
90
+ const done = new Map();
91
+ return {
92
+ projectDirectory: (cwd) => projectDirectoryOf(cwd),
93
+ wire(directory) {
94
+ const held = done.get(directory);
95
+ if (held !== undefined)
96
+ return held;
97
+ const outcome = run(['init', '--no-mcp', '--json', '--port', String(options.port)], directory)
98
+ .then(({ code, stdout, stderr }) => {
99
+ // Under --json the plan lines may go to stderr, keeping stdout for the one object.
100
+ const { result } = readInitOutput(stdout);
101
+ const { steps } = readInitOutput(`${stdout}\n${stderr}`);
102
+ const record = 'object' === typeof result && null !== result
103
+ ? result
104
+ : {};
105
+ const url = 'string' === typeof record['url'] ? record['url'] : undefined;
106
+ const ok = 0 === code && true === record['ok'];
107
+ const lastError = stderr.trim().split('\n').at(-1);
108
+ return {
109
+ ok,
110
+ directory,
111
+ steps,
112
+ ...(url === undefined ? {} : { url }),
113
+ ...(ok
114
+ ? {}
115
+ : {
116
+ error: lastError !== undefined && 0 < lastError.length
117
+ ? lastError
118
+ : `init exited ${String(code)}`,
119
+ }),
120
+ };
121
+ })
122
+ .catch((error) => ({
123
+ ok: false,
124
+ directory,
125
+ steps: [],
126
+ error: error instanceof Error ? error.message : String(error),
127
+ }));
128
+ done.set(directory, outcome);
129
+ return outcome;
130
+ },
131
+ };
132
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReviewMark } from './review-store.js';
2
+ /** One note as it lands on disk: the mark without its session-local id and clock. */
3
+ type StoredNote = Omit<ReviewMark, 'id' | 'at'>;
4
+ /** What `.reticle/notes.json` holds. Keyed by content, so a note survives the session it was made in. */
5
+ export interface NotesFile {
6
+ notes: Record<string, StoredNote>;
7
+ }
8
+ /**
9
+ * Mark ids (`m1`, `m2`, …) restart with the daemon, so they cannot name a note across sessions.
10
+ * The route, anchor and words can: the same complaint on the same element is the same note.
11
+ */
12
+ export declare function noteKey(mark: Pick<ReviewMark, 'route' | 'anchor' | 'note'>): string;
13
+ /**
14
+ * Merge one session's marks into the project's notes file, so the HUD notes a human left reach the
15
+ * platform with the rest of `.reticle/` instead of dying with the session that held them.
16
+ *
17
+ * Never throws: a note that cannot be written must not break the event that carried it.
18
+ */
19
+ export declare function recordNotes(reticleRoot: string | undefined, marks: readonly ReviewMark[]): void;
20
+ export {};
@@ -0,0 +1,55 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { ReticleDir } from '@reticlehq/core';
5
+ /** Enough for a project's review history; the oldest fall off first. */
6
+ const MAX_NOTES = 500;
7
+ /**
8
+ * Mark ids (`m1`, `m2`, …) restart with the daemon, so they cannot name a note across sessions.
9
+ * The route, anchor and words can: the same complaint on the same element is the same note.
10
+ */
11
+ export function noteKey(mark) {
12
+ return createHash('sha256')
13
+ .update(`${mark.route ?? ''}\n${mark.anchor}\n${mark.note}`)
14
+ .digest('hex')
15
+ .slice(0, 16);
16
+ }
17
+ function read(path) {
18
+ try {
19
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
20
+ return { notes: 'object' === typeof parsed.notes && null !== parsed.notes ? parsed.notes : {} };
21
+ }
22
+ catch {
23
+ return { notes: {} };
24
+ }
25
+ }
26
+ /**
27
+ * Merge one session's marks into the project's notes file, so the HUD notes a human left reach the
28
+ * platform with the rest of `.reticle/` instead of dying with the session that held them.
29
+ *
30
+ * Never throws: a note that cannot be written must not break the event that carried it.
31
+ */
32
+ export function recordNotes(reticleRoot, marks) {
33
+ if (reticleRoot === undefined || 0 === marks.length)
34
+ return;
35
+ const path = join(reticleRoot, ReticleDir.NOTES_FILE);
36
+ try {
37
+ const held = read(path).notes;
38
+ for (const { id: _id, at: _at, ...note } of marks) {
39
+ const key = noteKey(note);
40
+ // Re-inserted so a note touched now counts as the newest when the oldest are dropped.
41
+ delete held[key];
42
+ held[key] = note;
43
+ }
44
+ const keys = Object.keys(held);
45
+ for (const stale of keys.slice(0, Math.max(0, keys.length - MAX_NOTES)))
46
+ delete held[stale];
47
+ mkdirSync(reticleRoot, { recursive: true });
48
+ const tmp = `${path}.${String(process.pid)}.tmp`;
49
+ writeFileSync(tmp, `${JSON.stringify({ notes: held }, null, 2)}\n`, 'utf8');
50
+ renameSync(tmp, path);
51
+ }
52
+ catch {
53
+ // Disk full or read-only checkout: the note is still in the session, just not on the platform.
54
+ }
55
+ }
@@ -30,8 +30,20 @@ interface ResolveOutcome {
30
30
  /** The retired mark's note. Absent when nothing was resolved. */
31
31
  note?: string;
32
32
  }
33
+ /**
34
+ * Per-session store of human review marks (the "annotate the bug where you see it" inbox). A mark is
35
+ * added when a HUMAN_MARK event arrives, listed by the agent via reticle_review, and retired with
36
+ * resolve when the agent claims the fix — distinct from the live-control inbox, which is drained
37
+ * (delivered-once) on read. Marks persist (read does not consume) so the agent can list, fix, and
38
+ * THEN resolve, and a fix can be verified against the same mark.
39
+ *
40
+ * No IO of its own and no clock: `onChange` is how a caller keeps it on disk (see notes-ledger.ts).
41
+ * The id is a monotonic counter (m1, m2, …) so it is deterministic and never depends on
42
+ * Math.random/Date.now; the timestamp is passed in by the caller.
43
+ */
33
44
  export declare class ReviewStore {
34
45
  #private;
46
+ constructor(onChange?: (marks: ReviewMark[]) => void);
35
47
  /** Store a new mark (status pending) stamped with the caller-supplied session-relative time. */
36
48
  add(data: HumanMarkData, at: number): ReviewMark;
37
49
  /** All marks still awaiting a fix, oldest first. Reading never consumes — resolve retires a mark. */
@@ -1,14 +1,4 @@
1
1
  import { MarkStatus } from '@reticlehq/core';
2
- /**
3
- * Per-session store of human review marks (the "annotate the bug where you see it" inbox). A mark is
4
- * added when a HUMAN_MARK event arrives, listed by the agent via reticle_review, and retired with
5
- * resolve when the agent claims the fix — distinct from the live-control inbox, which is drained
6
- * (delivered-once) on read. Marks persist (read does not consume) so the agent can list, fix, and
7
- * THEN resolve, and a fix can be verified against the same mark.
8
- *
9
- * Pure in-memory state: no IO, no clock. The id is a monotonic counter (m1, m2, …) so it is
10
- * deterministic and never depends on Math.random/Date.now; the timestamp is passed in by the caller.
11
- */
12
2
  /** Prefix on review-mark ids (m1, m2, …) — distinguishes them from command ids. */
13
3
  const MARK_ID_PREFIX = 'm';
14
4
  /**
@@ -29,8 +19,24 @@ let nextMarkId = 0;
29
19
  export function resetMarkIdsForTest() {
30
20
  nextMarkId = 0;
31
21
  }
22
+ /**
23
+ * Per-session store of human review marks (the "annotate the bug where you see it" inbox). A mark is
24
+ * added when a HUMAN_MARK event arrives, listed by the agent via reticle_review, and retired with
25
+ * resolve when the agent claims the fix — distinct from the live-control inbox, which is drained
26
+ * (delivered-once) on read. Marks persist (read does not consume) so the agent can list, fix, and
27
+ * THEN resolve, and a fix can be verified against the same mark.
28
+ *
29
+ * No IO of its own and no clock: `onChange` is how a caller keeps it on disk (see notes-ledger.ts).
30
+ * The id is a monotonic counter (m1, m2, …) so it is deterministic and never depends on
31
+ * Math.random/Date.now; the timestamp is passed in by the caller.
32
+ */
32
33
  export class ReviewStore {
33
34
  #marks = [];
35
+ /** Told the full history after every add and resolve, so a caller can keep it on disk. */
36
+ #onChange;
37
+ constructor(onChange) {
38
+ this.#onChange = onChange;
39
+ }
34
40
  /** Store a new mark (status pending) stamped with the caller-supplied session-relative time. */
35
41
  add(data, at) {
36
42
  nextMarkId += 1;
@@ -49,6 +55,7 @@ export class ReviewStore {
49
55
  if (data.route !== undefined)
50
56
  mark.route = data.route;
51
57
  this.#marks.push(mark);
58
+ this.#onChange?.(this.all());
52
59
  return mark;
53
60
  }
54
61
  /** All marks still awaiting a fix, oldest first. Reading never consumes — resolve retires a mark. */
@@ -84,6 +91,7 @@ export class ReviewStore {
84
91
  if (mark === undefined || mark.status === MarkStatus.RESOLVED)
85
92
  return { resolved: false, id };
86
93
  mark.status = MarkStatus.RESOLVED;
94
+ this.#onChange?.(this.all());
87
95
  return { resolved: true, id, note: mark.note };
88
96
  }
89
97
  }
@@ -1,15 +1,2 @@
1
1
  import type { ToolDef } from '../../surface/tools/tool-kit.js';
2
- /**
3
- * Live-control agent tools: the agent's side of the human-in-the-loop control surface.
4
- *
5
- * - reticle_end_session: terminal stop. Sets state `ended` and syncs the panel (PRESENTER) with an
6
- * optional summary. Idempotent — ending an already-ended session is a safe no-op.
7
- * - reticle_resume: clears a human pause. Sets state `active` and syncs the panel.
8
- * - reticle_messages: explicit poll — drains and returns the queued human notes.
9
- *
10
- * State changes go through `setState`, which echoes the state to the panel in a SINGLE PRESENTER
11
- * push (optionally carrying human-facing text, e.g. the end summary) — a transition never emits two
12
- * PRESENTER commands. No clock is read here — inbox stamps were assigned by the session's injected
13
- * elapsed clock at enqueue time.
14
- */
15
2
  export declare const LIVE_CONTROL_TOOLS: ToolDef[];
@@ -34,18 +34,31 @@ function endingTurnWithNothingAttached(deps, id) {
34
34
  * PRESENTER commands. No clock is read here — inbox stamps were assigned by the session's injected
35
35
  * elapsed clock at enqueue time.
36
36
  */
37
+ const DISCONNECT_NEEDS_SESSION_ID = '`disconnect: true` needs the `sessionId` of the page to detach — list them with ' +
38
+ 'reticle_session{action:"list"}; it is never picked for you, because detaching the wrong window ' +
39
+ 'cuts you off from the app.';
37
40
  export const LIVE_CONTROL_TOOLS = [
38
41
  {
39
42
  name: ReticleTool.END_SESSION,
40
43
  description: 'End this session for good — use ONLY when the whole task is complete. Sets state "ended" ' +
41
44
  '(calm, terminal) and shows the optional `summary` on the panel. If you are just finishing a ' +
42
- 'turn or waiting on the human, call reticle_session{action:"yield"} instead (revivable). Idempotent.',
43
- inputSchema: { summary: z.string().optional(), ...sessionIdShape },
45
+ 'turn or waiting on the human, call reticle_session{action:"yield"} instead (revivable). Idempotent. ' +
46
+ "With `disconnect: true` and a `sessionId` it also closes that page's connection and refuses it " +
47
+ 'until the daemon restarts — for a stray window (a hidden desktop webview, a 404 page) that keeps coming back.',
48
+ inputSchema: {
49
+ summary: z.string().optional(),
50
+ disconnect: z
51
+ .boolean()
52
+ .optional()
53
+ .describe('Also detach this page for good (needs sessionId). Reloads stay detached.'),
54
+ ...sessionIdShape,
55
+ },
44
56
  // `sessionId` is optional because there may genuinely not be one — see the no-op below. An
45
57
  // empty string would read as a real id in a log, which is worse than its absence.
46
58
  outputSchema: {
47
59
  ended: z.boolean(),
48
60
  sessionId: z.string().optional(),
61
+ disconnected: z.boolean().optional(),
49
62
  note: z.string().optional(),
50
63
  talk_to_us: z
51
64
  .string()
@@ -58,6 +71,10 @@ export const LIVE_CONTROL_TOOLS = [
58
71
  },
59
72
  handler: (deps, args) => {
60
73
  const requested = asString(args['sessionId']);
74
+ const detach = true === args['disconnect'];
75
+ // Named, never auto-picked: detaching the wrong window cuts the agent off from the app.
76
+ if (detach && requested === undefined)
77
+ throw new Error(DISCONNECT_NEEDS_SESSION_ID);
61
78
  if (endingTurnWithNothingAttached(deps, requested)) {
62
79
  return Promise.resolve({ ended: true, note: YIELD_WITHOUT_SESSION_NOTE });
63
80
  }
@@ -97,7 +114,15 @@ export const LIVE_CONTROL_TOOLS = [
97
114
  const panel = [summary, gap[0]].filter((line) => line !== undefined);
98
115
  // One PRESENTER push for the transition; the summary and the gap headline ride together.
99
116
  session.setState(SessionState.ENDED, 0 === panel.length ? undefined : panel.join('\n'));
100
- return { ended: true, sessionId: session.id, gap, talk_to_us: DiscoveryInvite.AGENT };
117
+ if (detach)
118
+ deps.sessions.park(session);
119
+ return {
120
+ ended: true,
121
+ sessionId: session.id,
122
+ ...(detach ? { disconnected: true } : {}),
123
+ gap,
124
+ talk_to_us: DiscoveryInvite.AGENT,
125
+ };
101
126
  });
102
127
  },
103
128
  },
@@ -166,6 +166,13 @@ export interface NoSessionFacts {
166
166
  * or the ring has forgotten — inventing a URL is worse than the silence this exists to end.
167
167
  */
168
168
  lastKnownUrl?: string;
169
+ /**
170
+ * Where the departed tab was seen heading, when the SDK reported a navigation just before the
171
+ * socket closed (#1256). Separate from `lastKnownUrl` — the old origin — because the 5xx probe
172
+ * and successor matching need the page the tab was ON, while the diagnosis needs where it was
173
+ * GOING. Absent unless the departure note arrived fresh.
174
+ */
175
+ departedTo?: string;
169
176
  /**
170
177
  * What a plain GET of `lastKnownUrl` answered, done out of band by the watch after the session
171
178
  * went. The one fact that separates a server error from a closed tab and from an install
@@ -12,6 +12,7 @@
12
12
  * the hot resolve() path and this stays unit-testable.
13
13
  */
14
14
  import { NoSessionReason } from '@reticlehq/core/telemetry';
15
+ import { redactUrl } from '@reticlehq/core';
15
16
  import { leaseCaveat } from './presence/lease-availability.js';
16
17
  import { DEV_SERVER_PORTS } from '../../command/cli/ports/resolve/cli-port.js';
17
18
  import { siblingListenerNote } from '../../command/cli/ports/sibling-ports.js';
@@ -404,6 +405,20 @@ export function explainNoSession(facts) {
404
405
  'the SDK cannot reconnect to it. Fix the route, then reload the tab' +
405
406
  `${orOpenCommand(facts)}.${alreadyListeningClause(listening)} ${RETRY}`);
406
407
  }
408
+ // Ranked above TAB_GONE because it is not a hedge: the SDK reported the departure while the
409
+ // old document was still alive, so this is where the tab WENT rather than one of several
410
+ // opposite situations. Ranked below ROUTE_SERVER_ERROR because that branch names a defect in
411
+ // the app itself. Only reached when the departure note arrived fresh — a stale note is no
412
+ // fact, and the tombstone already refused to carry one (#1256).
413
+ if (facts.departedTo !== undefined && '' !== facts.departedTo) {
414
+ return reason(NoSessionReason.NAVIGATED_AWAY, 'no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
415
+ // Redacted: a redirect target can carry credentials (OAuth callbacks, reset links), and
416
+ // this message is agent-facing. The raw value stays on the tombstone for matching.
417
+ `is correct. The page navigated away to ${redactUrl(facts.departedTo)} just before the ` +
418
+ 'connection closed — that is where the tab went, not a closed tab and not an install ' +
419
+ `problem. Ask the human to go back to the app${orOpenCommand(facts)}, or reload the tab. ` +
420
+ `${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, alreadyListeningClause(listening).trim());
421
+ }
407
422
  return reason(NoSessionReason.TAB_GONE, 'no browser session connected, but one WAS connected to this daemon earlier, so the wiring ' +
408
423
  `is correct. ${tabGoneWhat(facts.lastKnownUrl)} Ask the human to reopen ` +
409
424
  `the app${orOpenCommand(facts)}, or reload the tab. ${leaseAdvice(SELF_SERVE, facts)} ${RETRY}`, alreadyListeningClause(listening).trim());
@@ -29,6 +29,11 @@ interface NextActionFacts {
29
29
  initialized: boolean;
30
30
  listening: readonly number[];
31
31
  dev: DevCommand | undefined;
32
+ /**
33
+ * File-existence predicate for the project root, pure for testing.
34
+ * Used to recognize non-JS ecosystems (e.g. Flutter) when no dev script exists.
35
+ */
36
+ exists?: (file: string) => boolean;
32
37
  /** Configs found in other workspace directories: positive evidence of a scope mismatch. */
33
38
  configsElsewhere?: readonly {
34
39
  directory: string;
@@ -82,6 +87,12 @@ interface NextActionFacts {
82
87
  * its one hit is as likely to be another repo's dev server as this one's.
83
88
  */
84
89
  lastKnownUrl?: string;
90
+ /**
91
+ * Where the departed tab was seen heading, when the SDK reported it fresh (#1256). The
92
+ * next-action reason must agree with the diagnosis: when this is present the tab was not
93
+ * closed, it navigated away.
94
+ */
95
+ departedTo?: string;
85
96
  }
86
97
  export declare function nextActionFor(facts: NextActionFacts): NoSessionNextAction;
87
98
  /** The same action as one sentence, for the prose message the human reads. */
@@ -12,7 +12,8 @@
12
12
  *
13
13
  * Pure. The facts arrive from the watch; nothing here touches the disk or the clock.
14
14
  */
15
- import { NoSessionAction, RETICLE_URL_PARAM, ReticleEnv } from '@reticlehq/core';
15
+ import { NoSessionAction, RETICLE_URL_PARAM, ReticleEnv, redactUrl } from '@reticlehq/core';
16
+ import { detectNonJsEcosystem, noPackageJsonMessage } from '@reticlehq/init';
16
17
  /**
17
18
  * The departed session's url as a human would type it: the params a Reticle launcher stamps on a
18
19
  * lease are stripped, because reopening WITH them would adopt the dead lease's identity. Undefined
@@ -88,12 +89,19 @@ export function nextActionFor(facts) {
88
89
  : only === undefined
89
90
  ? {}
90
91
  : { command: `${OPEN_COMMAND} ${LOCALHOST}:${String(only)}`, port: only };
92
+ // Where the tab was seen heading, when the SDK reported it fresh — the next action must
93
+ // tell the same story as the diagnosis (#1256).
94
+ const departedTo = facts.departedTo;
91
95
  return {
92
96
  action: NoSessionAction.REOPEN_APP,
93
97
  ...target,
94
- reason: 'a session was connected to this daemon earlier, so the wiring is correct — the tab was ' +
95
- 'closed, reloaded, or the lease aged out. Reopen the app, or take one you own with ' +
96
- 'reticle_lease {action:"acquire", url}.' +
98
+ reason: 'a session was connected to this daemon earlier, so the wiring is correct — ' +
99
+ // Agrees with the diagnosis: when the SDK saw the page leave, "the tab was closed" is
100
+ // the wrong story. Redacted like the diagnosis — this reason is agent-facing too.
101
+ (departedTo === undefined || '' === departedTo
102
+ ? 'the tab was closed, reloaded, or the lease aged out. '
103
+ : `the page navigated away to ${redactUrl(departedTo)}. `) +
104
+ 'Reopen the app, or take one you own with reticle_lease {action:"acquire", url}.' +
97
105
  bound,
98
106
  };
99
107
  }
@@ -119,6 +127,20 @@ export function nextActionFor(facts) {
119
127
  if (0 === facts.listening.length) {
120
128
  const dev = facts.dev;
121
129
  if (dev === undefined) {
130
+ if (facts.exists !== undefined && !facts.exists('package.json')) {
131
+ const ecosystem = detectNonJsEcosystem(facts.exists);
132
+ if (ecosystem !== undefined) {
133
+ return {
134
+ action: NoSessionAction.START_DEV_SERVER,
135
+ reason: 'Flutter' === ecosystem
136
+ ? noPackageJsonMessage(facts.exists)
137
+ : `This looks like a ${ecosystem} project, with no package.json in this directory. ` +
138
+ 'There is no dev command to hand you. Ask the human how the app starts and which ' +
139
+ 'URL it serves. If the web app has its own package.json in another directory, ' +
140
+ 'run Reticle from that app directory instead.',
141
+ };
142
+ }
143
+ }
122
144
  return {
123
145
  action: NoSessionAction.START_DEV_SERVER,
124
146
  reason: 'nothing is listening on the ports Reticle scans, so the app is probably not running — ' +
@@ -127,9 +149,12 @@ export function nextActionFor(facts) {
127
149
  'started, and for the URL it serves on.',
128
150
  };
129
151
  }
152
+ // The app can live one directory down; the command has to be runnable from where the daemon
153
+ // stands, so it carries its own `cd` (reticle#1368).
154
+ const inApp = dev.directory === undefined ? '' : `cd ${dev.directory} && `;
130
155
  return {
131
156
  action: NoSessionAction.START_DEV_SERVER,
132
- command: dev.command,
157
+ command: `${inApp}${dev.command}`,
133
158
  ...(dev.port === undefined ? {} : { port: dev.port }),
134
159
  // "Probably", to match the branch above it and the paragraph beside it.
135
160
  //
@@ -147,6 +172,7 @@ export function nextActionFor(facts) {
147
172
  'though that scan is narrow, so if it IS up on another port, ask for its URL instead of ' +
148
173
  `starting a second one. This is the project's own ` +
149
174
  (undefined === dev.script ? 'launcher' : `\`${dev.script}\` script`) +
175
+ (dev.directory === undefined ? '' : `, in \`${dev.directory}\``) +
150
176
  ` — run it in the ` +
151
177
  'background, tell the human it is running, then call reticle_sessions again.',
152
178
  };
@@ -17,7 +17,20 @@ interface NoSessionWatchOptions {
17
17
  initialized: boolean;
18
18
  /** Where that was decided — this daemon's working directory unless a caller says otherwise. */
19
19
  directory?: string;
20
- probe?: () => Promise<number[]>;
20
+ /**
21
+ * File-existence predicate for the project directory. Injected for tests; defaults to
22
+ * checking the directory on disk.
23
+ */
24
+ exists?: (file: string) => boolean;
25
+ /**
26
+ * The background port scan. Injected for tests; production probes each port on both loopback
27
+ * families.
28
+ *
29
+ * Receives the candidate list — the well-known ports plus the ones that name THIS project
30
+ * (#1367) — so a test can pin the observation without opening a socket. A test that injects it
31
+ * owns the scan.
32
+ */
33
+ probe?: (ports: readonly number[]) => Promise<number[]>;
21
34
  /**
22
35
  * Well-known Reticle ports other than ours that currently accept a connection.
23
36
  *