@reticlehq/server 2.8.0 → 2.10.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 (318) hide show
  1. package/SKILL.md +130 -813
  2. package/dist/bridge/bridge.d.ts +23 -0
  3. package/dist/bridge/bridge.js +80 -2
  4. package/dist/bridge/bridge.test-harness.d.ts +5 -0
  5. package/dist/bridge/bridge.test-harness.js +6 -4
  6. package/dist/capsule/capsule-store.d.ts +2 -2
  7. package/dist/capsule/causal-summary.d.ts +16 -1
  8. package/dist/capsule/causal-summary.js +2 -1
  9. package/dist/cli/cli-doctor.js +41 -13
  10. package/dist/cli/cli-flow-commands.js +8 -8
  11. package/dist/cli/cli-launch.d.ts +0 -17
  12. package/dist/cli/cli-launch.js +0 -28
  13. package/dist/cli/cli-parse.d.ts +1 -1
  14. package/dist/cli/cli-parse.js +1 -1
  15. package/dist/cli/cli-port.d.ts +24 -0
  16. package/dist/cli/cli-port.js +66 -35
  17. package/dist/cli/cli-verify.d.ts +21 -0
  18. package/dist/cli/cli-verify.js +45 -1
  19. package/dist/cli/config-discovery.d.ts +35 -0
  20. package/dist/cli/config-discovery.js +163 -0
  21. package/dist/cli/doctor-daemon-line.js +4 -1
  22. package/dist/cli/doctor-project-line.d.ts +37 -0
  23. package/dist/cli/doctor-project-line.js +35 -0
  24. package/dist/cli/doctor-rows.d.ts +51 -0
  25. package/dist/cli/doctor-rows.js +54 -0
  26. package/dist/cli/doctor-sessions-line.js +6 -3
  27. package/dist/cli/drive-attach.d.ts +99 -0
  28. package/dist/cli/drive-attach.js +203 -0
  29. package/dist/cli/drive-command.d.ts +13 -0
  30. package/dist/cli/drive-command.js +103 -0
  31. package/dist/cli/open-note.d.ts +20 -0
  32. package/dist/cli/open-note.js +37 -0
  33. package/dist/cli/status-next-action.d.ts +37 -0
  34. package/dist/cli/status-next-action.js +61 -0
  35. package/dist/cli.js +65 -70
  36. package/dist/crawl/crawl.d.ts +7 -2
  37. package/dist/crawl/crawl.js +52 -9
  38. package/dist/ee/audit-log.js +2 -2
  39. package/dist/events/body-failures.d.ts +2 -2
  40. package/dist/events/contradiction-folds.d.ts +45 -0
  41. package/dist/events/contradiction-folds.js +42 -0
  42. package/dist/events/contradictions.d.ts +51 -1
  43. package/dist/events/contradictions.js +26 -7
  44. package/dist/events/declared.d.ts +45 -0
  45. package/dist/events/declared.js +73 -0
  46. package/dist/events/echo-mismatch.d.ts +9 -3
  47. package/dist/events/echo-mismatch.js +33 -5
  48. package/dist/events/event-filters.d.ts +1 -1
  49. package/dist/events/event-filters.js +7 -4
  50. package/dist/events/predicate-eval.d.ts +17 -91
  51. package/dist/events/predicate-eval.js +85 -326
  52. package/dist/events/predicate-parse.js +31 -3
  53. package/dist/events/predicate-route.d.ts +32 -0
  54. package/dist/events/predicate-route.js +102 -0
  55. package/dist/events/predicate-schema.d.ts +99 -0
  56. package/dist/events/predicate-schema.js +309 -0
  57. package/dist/events/predicate.d.ts +8 -0
  58. package/dist/events/predicate.js +127 -5
  59. package/dist/events/stale-response.d.ts +2 -3
  60. package/dist/events/stale-response.js +49 -2
  61. package/dist/events/unit-mismatch.d.ts +2 -2
  62. package/dist/flows/annotate-tools.js +3 -0
  63. package/dist/flows/annotate.js +18 -4
  64. package/dist/flows/coverage.d.ts +38 -5
  65. package/dist/flows/coverage.js +30 -4
  66. package/dist/flows/decision.d.ts +15 -0
  67. package/dist/flows/decision.js +1 -1
  68. package/dist/flows/flow-replay-run.js +8 -1
  69. package/dist/flows/flow-tools.js +9 -1
  70. package/dist/flows/replay.js +3 -0
  71. package/dist/flows/verify-change-tools.js +1 -1
  72. package/dist/honesty/accepted-write.d.ts +3 -0
  73. package/dist/honesty/accepted-write.js +8 -3
  74. package/dist/honesty/already-true.js +15 -0
  75. package/dist/honesty/blind-spots.js +4 -0
  76. package/dist/honesty/honesty.d.ts +11 -0
  77. package/dist/honesty/honesty.js +1 -0
  78. package/dist/honesty/uncaptured-bodies.d.ts +4 -2
  79. package/dist/honesty/uncaptured-bodies.js +9 -2
  80. package/dist/honesty/unread-outcome.d.ts +8 -0
  81. package/dist/honesty/unread-outcome.js +36 -9
  82. package/dist/honesty/unsettled.d.ts +9 -0
  83. package/dist/honesty/unsettled.js +7 -3
  84. package/dist/honesty/verified.d.ts +29 -3
  85. package/dist/honesty/verified.js +67 -8
  86. package/dist/http-server.d.ts +19 -0
  87. package/dist/http-server.js +202 -4
  88. package/dist/impact/impact-recorder.d.ts +23 -0
  89. package/dist/impact/impact-recorder.js +76 -0
  90. package/dist/impact/impact-store.d.ts +47 -0
  91. package/dist/impact/impact-store.js +171 -0
  92. package/dist/index.d.ts +25 -0
  93. package/dist/index.js +60 -10
  94. package/dist/init/agent-rules.d.ts +20 -0
  95. package/dist/init/agent-rules.js +36 -0
  96. package/dist/init/astro-patch.js +27 -1
  97. package/dist/init/confirm.d.ts +21 -3
  98. package/dist/init/confirm.js +32 -5
  99. package/dist/init/connect-steps.d.ts +2 -0
  100. package/dist/init/connect-steps.js +50 -0
  101. package/dist/init/csp-check.d.ts +38 -0
  102. package/dist/init/csp-check.js +92 -0
  103. package/dist/init/csp-doctor.d.ts +25 -0
  104. package/dist/init/csp-doctor.js +49 -0
  105. package/dist/init/cursor.js +6 -9
  106. package/dist/init/detect.d.ts +8 -0
  107. package/dist/init/detect.js +33 -3
  108. package/dist/init/mcp-clients.d.ts +1 -0
  109. package/dist/init/mcp-clients.js +51 -7
  110. package/dist/init/mcp.d.ts +16 -2
  111. package/dist/init/mcp.js +23 -1
  112. package/dist/init/plan-framework.d.ts +10 -0
  113. package/dist/init/plan-framework.js +25 -1
  114. package/dist/init/plan.d.ts +7 -2
  115. package/dist/init/plan.js +66 -30
  116. package/dist/init/run.js +11 -5
  117. package/dist/init/slash-command.d.ts +1 -1
  118. package/dist/init/slash-command.js +12 -4
  119. package/dist/init/snippets.d.ts +1 -1
  120. package/dist/init/snippets.js +35 -7
  121. package/dist/init/vite-config.js +24 -2
  122. package/dist/input/network-mock-tools.js +8 -1
  123. package/dist/input/scroll-find.d.ts +6 -0
  124. package/dist/input/scroll-find.js +17 -2
  125. package/dist/input/scroll-tools.js +4 -0
  126. package/dist/journal/attach-journal.d.ts +9 -1
  127. package/dist/journal/attach-journal.js +13 -2
  128. package/dist/journal/journal-query.d.ts +13 -0
  129. package/dist/journal/journal-query.js +28 -3
  130. package/dist/journal/workspace-gitignore.d.ts +10 -0
  131. package/dist/journal/workspace-gitignore.js +59 -0
  132. package/dist/license/license.d.ts +69 -4
  133. package/dist/license/license.js +84 -15
  134. package/dist/mcp/mcp-proxy.d.ts +7 -93
  135. package/dist/mcp/mcp-proxy.js +183 -250
  136. package/dist/mcp/mcp.d.ts +27 -2
  137. package/dist/mcp/mcp.js +77 -36
  138. package/dist/mcp/proxy-daemon-probe.d.ts +8 -0
  139. package/dist/mcp/proxy-daemon-probe.js +48 -0
  140. package/dist/mcp/proxy-handshake.d.ts +82 -1
  141. package/dist/mcp/proxy-handshake.js +110 -3
  142. package/dist/mcp/proxy-lifecycle.d.ts +10 -1
  143. package/dist/mcp/proxy-lifecycle.js +11 -2
  144. package/dist/mcp/proxy-log.d.ts +63 -0
  145. package/dist/mcp/proxy-log.js +132 -0
  146. package/dist/mcp/server-instructions.d.ts +17 -0
  147. package/dist/mcp/server-instructions.js +56 -0
  148. package/dist/mcp/shared-params.d.ts +27 -0
  149. package/dist/mcp/shared-params.js +36 -0
  150. package/dist/mcp/sse-frame-parser.d.ts +19 -0
  151. package/dist/mcp/sse-frame-parser.js +41 -0
  152. package/dist/project/reticle-dir.d.ts +2 -0
  153. package/dist/project/reticle-dir.js +1 -0
  154. package/dist/session/connection-memory.d.ts +59 -0
  155. package/dist/session/connection-memory.js +108 -0
  156. package/dist/session/dev-command.d.ts +26 -0
  157. package/dist/session/dev-command.js +89 -0
  158. package/dist/session/dev-server-probe.d.ts +68 -0
  159. package/dist/session/dev-server-probe.js +102 -7
  160. package/dist/session/last-act.d.ts +26 -0
  161. package/dist/session/last-act.js +35 -1
  162. package/dist/session/live-control-tools.js +42 -5
  163. package/dist/session/no-session-diagnosis.d.ts +55 -2
  164. package/dist/session/no-session-diagnosis.js +199 -22
  165. package/dist/session/no-session-next-action.d.ts +46 -0
  166. package/dist/session/no-session-next-action.js +85 -0
  167. package/dist/session/no-session-watch.d.ts +40 -1
  168. package/dist/session/no-session-watch.js +172 -11
  169. package/dist/session/pending-commands.d.ts +12 -0
  170. package/dist/session/pending-commands.js +13 -1
  171. package/dist/session/session-manager.d.ts +4 -0
  172. package/dist/session/session-manager.js +35 -5
  173. package/dist/session/session-readiness.js +1 -1
  174. package/dist/session/session.d.ts +10 -1
  175. package/dist/session/session.js +146 -5
  176. package/dist/telemetry/app-instrumented.d.ts +8 -0
  177. package/dist/telemetry/app-instrumented.js +10 -0
  178. package/dist/telemetry/bug-found.d.ts +18 -0
  179. package/dist/telemetry/bug-found.js +14 -0
  180. package/dist/telemetry/daemon-telemetry.js +29 -0
  181. package/dist/telemetry/install-source.d.ts +20 -0
  182. package/dist/telemetry/install-source.js +34 -0
  183. package/dist/telemetry/instrumentation-stall.d.ts +57 -0
  184. package/dist/telemetry/instrumentation-stall.js +103 -0
  185. package/dist/telemetry/license-activation.d.ts +48 -0
  186. package/dist/telemetry/license-activation.js +57 -0
  187. package/dist/telemetry/session-metrics.d.ts +8 -0
  188. package/dist/telemetry/session-metrics.js +10 -0
  189. package/dist/telemetry/telemetry.d.ts +3 -1
  190. package/dist/telemetry/telemetry.js +23 -3
  191. package/dist/tools/act-tools.js +166 -29
  192. package/dist/tools/assert-verdict.js +32 -7
  193. package/dist/tools/browser-tools.js +10 -0
  194. package/dist/tools/dynamic-tools.js +17 -8
  195. package/dist/tools/error-recovery.js +10 -6
  196. package/dist/tools/feedback-tools.js +31 -6
  197. package/dist/tools/invoke-tool.d.ts +1 -1
  198. package/dist/tools/invoke-tool.js +30 -8
  199. package/dist/tools/lease-hint.d.ts +30 -8
  200. package/dist/tools/lease-hint.js +65 -14
  201. package/dist/tools/lease-tools.d.ts +11 -0
  202. package/dist/tools/lease-tools.js +141 -96
  203. package/dist/tools/observe-tools.js +9 -3
  204. package/dist/tools/react-grace.d.ts +4 -0
  205. package/dist/tools/react-grace.js +6 -3
  206. package/dist/tools/read-tools.js +47 -1
  207. package/dist/tools/resolve-target.d.ts +33 -0
  208. package/dist/tools/resolve-target.js +53 -0
  209. package/dist/tools/settle-in-flight.d.ts +15 -0
  210. package/dist/tools/settle-in-flight.js +42 -2
  211. package/dist/tools/snapshot-delta.d.ts +3 -0
  212. package/dist/tools/snapshot-delta.js +67 -6
  213. package/dist/tools/tool-kit.d.ts +38 -3
  214. package/dist/tools/tool-names.d.ts +2 -0
  215. package/dist/tools/tool-names.js +2 -0
  216. package/dist/tools/tool-surface.d.ts +60 -0
  217. package/dist/tools/tool-surface.js +84 -19
  218. package/dist/tools/tools.js +61 -5
  219. package/dist/tools/unadvertised-help.js +9 -6
  220. package/docs/README.md +1 -2
  221. package/docs/actions.mdx +41 -8
  222. package/docs/agent-cheatsheet.md +6 -4
  223. package/docs/architecture.md +6 -0
  224. package/docs/benchmarks.md +60 -12
  225. package/docs/best-practices.mdx +11 -6
  226. package/docs/capabilities.mdx +204 -57
  227. package/docs/cli/affected.mdx +6 -4
  228. package/docs/cli/capsules.mdx +9 -8
  229. package/docs/cli/cloud.mdx +30 -4
  230. package/docs/cli/doctor.mdx +41 -8
  231. package/docs/cli/drive.mdx +34 -15
  232. package/docs/cli/gate.mdx +6 -6
  233. package/docs/cli/help.mdx +8 -6
  234. package/docs/cli/hunt.mdx +20 -4
  235. package/docs/cli/identify.mdx +16 -2
  236. package/docs/cli/init.mdx +35 -8
  237. package/docs/cli/kill.mdx +5 -5
  238. package/docs/cli/license.mdx +1 -1
  239. package/docs/cli/open.mdx +6 -0
  240. package/docs/cli/restart.mdx +6 -6
  241. package/docs/cli/serve.mdx +10 -3
  242. package/docs/cli/status.mdx +29 -5
  243. package/docs/cli/stop.mdx +8 -0
  244. package/docs/cli/verify.mdx +20 -1
  245. package/docs/cli/version.mdx +1 -1
  246. package/docs/cli/watch.mdx +9 -2
  247. package/docs/cli.mdx +44 -22
  248. package/docs/debugging.md +3 -1
  249. package/docs/deploy-checks.md +31 -2
  250. package/docs/desktop.mdx +276 -80
  251. package/docs/docs.json +13 -12
  252. package/docs/enterprise.md +9 -3
  253. package/docs/faq.mdx +49 -35
  254. package/docs/fixtures.md +2 -0
  255. package/docs/flows.md +7 -5
  256. package/docs/for-agents.md +3 -3
  257. package/docs/frameworks.mdx +46 -7
  258. package/docs/gate-plan.md +4 -2
  259. package/docs/gates.md +8 -6
  260. package/docs/getting-started.md +53 -12
  261. package/docs/human-control.md +2 -0
  262. package/docs/index.mdx +43 -1
  263. package/docs/install-agentic.mdx +13 -4
  264. package/docs/install-manual.mdx +6 -4
  265. package/docs/instrumentation.mdx +7 -1
  266. package/docs/integration-patterns.md +3 -1
  267. package/docs/local-registry.md +4 -2
  268. package/docs/multi-agent-testing.md +3 -1
  269. package/docs/packages/babel-plugin.mdx +15 -6
  270. package/docs/packages/browser.mdx +2 -2
  271. package/docs/packages/core.mdx +26 -16
  272. package/docs/packages/electron.mdx +5 -7
  273. package/docs/packages/eslint-plugin.mdx +5 -3
  274. package/docs/packages/next.mdx +3 -5
  275. package/docs/packages/react.mdx +3 -3
  276. package/docs/packages/server.mdx +2 -2
  277. package/docs/packages/tauri.mdx +6 -6
  278. package/docs/packages/test.mdx +18 -7
  279. package/docs/packages/vite-plugin.mdx +4 -6
  280. package/docs/packages.mdx +14 -13
  281. package/docs/platform-integration.md +2 -0
  282. package/docs/predicates.mdx +76 -7
  283. package/docs/quickstart.mdx +51 -5
  284. package/docs/recipes.mdx +63 -23
  285. package/docs/skill-file.mdx +3 -1
  286. package/docs/state-management.mdx +2 -0
  287. package/docs/system-map.md +3 -1
  288. package/docs/telemetry-contract.md +33 -5
  289. package/docs/telemetry-events.mdx +63 -18
  290. package/docs/telemetry.md +8 -5
  291. package/docs/testing.md +35 -1
  292. package/docs/token-efficiency.md +8 -2
  293. package/docs/tools-act-and-wait.mdx +64 -14
  294. package/docs/tools-act-sequence.mdx +34 -17
  295. package/docs/tools-act.mdx +24 -11
  296. package/docs/tools-assert.mdx +69 -10
  297. package/docs/tools-console.mdx +11 -7
  298. package/docs/tools-inspect.mdx +11 -8
  299. package/docs/tools-navigate.mdx +41 -5
  300. package/docs/tools-network.mdx +42 -9
  301. package/docs/tools-observe.mdx +66 -10
  302. package/docs/tools-overview.mdx +26 -23
  303. package/docs/tools-query.mdx +52 -8
  304. package/docs/tools-session-and-feedback.mdx +35 -9
  305. package/docs/tools-sessions.mdx +39 -10
  306. package/docs/tools-snapshot.mdx +57 -3
  307. package/docs/tools-state.mdx +24 -1
  308. package/docs/tools-tools-and-run.mdx +55 -24
  309. package/docs/tools-wait-for.mdx +34 -15
  310. package/docs/troubleshooting.mdx +80 -10
  311. package/docs/usage.md +46 -24
  312. package/docs/vs-chrome-devtools-mcp.mdx +41 -0
  313. package/docs/vs-playwright-mcp.mdx +42 -2
  314. package/docs/vs-screenshots.mdx +42 -1
  315. package/docs/what-is-reticle.mdx +1 -1
  316. package/docs/why-reticle.mdx +3 -1
  317. package/package.json +5 -5
  318. package/docs/desktop-apps.md +0 -265
@@ -9,5 +9,73 @@
9
9
  * the probe refreshes in the background and the resolve path reads a cached answer. A stale answer
10
10
  * is fine here — it is a hint in an error message, not a decision.
11
11
  */
12
+ /**
13
+ * What the probe actually learned about a port. Three states, because two cannot express this.
14
+ *
15
+ * A bare TCP connect is not enough (macOS AirPlay Receiver holds 5000 on every Mac and would read as
16
+ * the user's dev server), and a document check alone is not enough either (a compiling SSR server
17
+ * reads as absent). The thing in between — accepted the connection, said nothing in time — is a
18
+ * listener we cannot classify, and reporting that honestly is the point.
19
+ */
20
+ export declare const PortState: {
21
+ /** Nothing accepted a connection on either loopback family. */
22
+ readonly CLOSED: "closed";
23
+ /** Accepted a connection, then answered nothing within the budget. Something IS there. */
24
+ readonly CONNECTED_NO_ANSWER: "connected-no-answer";
25
+ /** Answered with a document: a dev server serving an app. */
26
+ readonly SERVES_DOCUMENT: "serves-document";
27
+ };
28
+ export type PortState = (typeof PortState)[keyof typeof PortState];
29
+ /** The probe's answer for one port, however it was obtained. */
30
+ export declare function classifyPort(port: number, probe: (port: number) => Promise<PortState>): Promise<PortState>;
31
+ /**
32
+ * `localhost`, not `127.0.0.1` — so BOTH address families are tried.
33
+ *
34
+ * Measured: a plain `vite --port 4311` on this machine listens on `[::1]` only, and a probe pinned to
35
+ * the IPv4 loopback cannot see it. The old TCP probe had the same blind spot, which meant the
36
+ * diagnostic could miss the very dev server it exists to find while still reporting an unrelated
37
+ * service that happens to bind both stacks (macOS AirPlay does).
38
+ */
39
+ /**
40
+ * Both loopback addresses, BY ADDRESS — never by the name `localhost`.
41
+ *
42
+ * A dev server started with `--host` binds the wildcards `0.0.0.0` and `[::]` rather than the
43
+ * loopbacks, and a wildcard bind serves only the family it belongs to. Asking for `localhost` hands
44
+ * the choice of family to the resolver: on Windows it prefers `::1`, so a `0.0.0.0` listener is
45
+ * invisible and the scan reports "nothing is listening" about a dev server that is plainly running.
46
+ * Reported from the field on exactly that setup.
47
+ *
48
+ * `[::1]` is also how a `[::]` bind is reached, and on most stacks a `[::]` bind additionally
49
+ * accepts v4-mapped connections — so between the two, every ordinary bind is covered.
50
+ */
51
+ export declare const PROBE_HOSTS: readonly string[];
52
+ /**
53
+ * Is anything serving a document on this port, on EITHER loopback family?
54
+ *
55
+ * "Either" and not "both": one answer is enough, and requiring both would make the wildcard binds
56
+ * this exists to see fail the check again. A family that errors (no IPv6 stack at all) is an
57
+ * absence, never a rejection — a diagnostic must not throw at the tool it is diagnosing.
58
+ */
59
+ export declare function anyFamilyServes(port: number, probe?: (port: number, host: string) => Promise<boolean>): Promise<boolean>;
60
+ /**
61
+ * Every candidate port, split by what the probe could actually establish.
62
+ *
63
+ * `slow` is the set that accepted a connection and then said nothing inside the budget. Those ports
64
+ * are NOT dev servers as far as this check knows, and they are emphatically not absent — an SSR
65
+ * framework compiling its first route lands here every time. Reported separately so the diagnosis
66
+ * can stop telling people to start a server that is already running.
67
+ */
68
+ export declare function probeDevServerStates(ports?: readonly number[], probe?: (port: number) => Promise<PortState>): Promise<{
69
+ serving: number[];
70
+ slow: number[];
71
+ }>;
72
+ /**
73
+ * The strongest state either loopback family reports.
74
+ *
75
+ * Serving a document beats a bare connection beats nothing, so a server bound to only one family is
76
+ * still seen at its best — the same "either, not both" rule `anyFamilyServes` follows, extended to
77
+ * three states.
78
+ */
79
+ export declare function anyFamilyState(port: number, probe?: (port: number, host: string) => Promise<PortState>): Promise<PortState>;
12
80
  /** Every candidate port serving a document, in ascending order. Never rejects. */
13
81
  export declare function probeDevServers(ports?: readonly number[], probe?: (port: number) => Promise<boolean>): Promise<number[]>;
@@ -12,8 +12,37 @@
12
12
  import { request } from 'node:http';
13
13
  import { DEV_SERVER_PORTS } from '../cli/cli-port.js';
14
14
  import { looksLikeDevServer } from './looks-like-dev-server.js';
15
- /** Give up fast: an HTTP answer from localhost either lands quickly or nothing is there. */
15
+ /**
16
+ * Give up fast, but do not call giving up an absence.
17
+ *
18
+ * This budget suits Vite handing back a static index.html. It does NOT suit an SSR framework
19
+ * compiling a route on the first request: Nuxt and Next routinely take seconds cold, and treating
20
+ * that as "nothing is there" told a reporter to start a dev server that was already serving 57KB of
21
+ * HTML — advice that would have hit their dev lock with a second `nuxt dev`.
22
+ *
23
+ * So the budget stays short and the TIMEOUT changed meaning instead. See PortState.
24
+ */
16
25
  const HTTP_PROBE_TIMEOUT_MS = 400;
26
+ /**
27
+ * What the probe actually learned about a port. Three states, because two cannot express this.
28
+ *
29
+ * A bare TCP connect is not enough (macOS AirPlay Receiver holds 5000 on every Mac and would read as
30
+ * the user's dev server), and a document check alone is not enough either (a compiling SSR server
31
+ * reads as absent). The thing in between — accepted the connection, said nothing in time — is a
32
+ * listener we cannot classify, and reporting that honestly is the point.
33
+ */
34
+ export const PortState = {
35
+ /** Nothing accepted a connection on either loopback family. */
36
+ CLOSED: 'closed',
37
+ /** Accepted a connection, then answered nothing within the budget. Something IS there. */
38
+ CONNECTED_NO_ANSWER: 'connected-no-answer',
39
+ /** Answered with a document: a dev server serving an app. */
40
+ SERVES_DOCUMENT: 'serves-document',
41
+ };
42
+ /** The probe's answer for one port, however it was obtained. */
43
+ export function classifyPort(port, probe) {
44
+ return probe(port).catch(() => PortState.CLOSED);
45
+ }
17
46
  /**
18
47
  * `localhost`, not `127.0.0.1` — so BOTH address families are tried.
19
48
  *
@@ -22,7 +51,19 @@ const HTTP_PROBE_TIMEOUT_MS = 400;
22
51
  * diagnostic could miss the very dev server it exists to find while still reporting an unrelated
23
52
  * service that happens to bind both stacks (macOS AirPlay does).
24
53
  */
25
- const PROBE_HOST = 'localhost';
54
+ /**
55
+ * Both loopback addresses, BY ADDRESS — never by the name `localhost`.
56
+ *
57
+ * A dev server started with `--host` binds the wildcards `0.0.0.0` and `[::]` rather than the
58
+ * loopbacks, and a wildcard bind serves only the family it belongs to. Asking for `localhost` hands
59
+ * the choice of family to the resolver: on Windows it prefers `::1`, so a `0.0.0.0` listener is
60
+ * invisible and the scan reports "nothing is listening" about a dev server that is plainly running.
61
+ * Reported from the field on exactly that setup.
62
+ *
63
+ * `[::1]` is also how a `[::]` bind is reached, and on most stacks a `[::]` bind additionally
64
+ * accepts v4-mapped connections — so between the two, every ordinary bind is covered.
65
+ */
66
+ export const PROBE_HOSTS = ['127.0.0.1', '::1'];
26
67
  /**
27
68
  * Does this port serve a DOCUMENT — i.e. is it a dev server rather than merely something listening?
28
69
  *
@@ -30,23 +71,77 @@ const PROBE_HOST = 'localhost';
30
71
  * port 5000) as the user's dev server. See looks-like-dev-server for what it actually answers and
31
72
  * why "a socket accepted" is the wrong question.
32
73
  */
33
- function servesDocument(port, host = PROBE_HOST) {
74
+ function probeState(port, host) {
34
75
  return new Promise((resolve) => {
76
+ // Whether the SOCKET came up is tracked separately from whether a response did, because that is
77
+ // the whole difference between "nothing is there" and "something is there and still compiling".
78
+ let connected = false;
35
79
  const req = request({ host, port, path: '/', method: 'GET', timeout: HTTP_PROBE_TIMEOUT_MS }, (res) => {
36
80
  const answer = looksLikeDevServer(res.statusCode ?? 0, res.headers['content-type']);
37
81
  res.resume(); // drain, so the socket closes rather than lingering
38
- resolve(answer);
82
+ resolve(answer ? PortState.SERVES_DOCUMENT : PortState.CLOSED);
83
+ });
84
+ req.once('socket', (socket) => {
85
+ socket.once('connect', () => {
86
+ connected = true;
87
+ });
39
88
  });
40
89
  req.once('timeout', () => {
41
90
  req.destroy();
42
- resolve(false);
91
+ resolve(connected ? PortState.CONNECTED_NO_ANSWER : PortState.CLOSED);
43
92
  });
44
- req.once('error', () => resolve(false));
93
+ req.once('error', () => resolve(PortState.CLOSED));
45
94
  req.end();
46
95
  });
47
96
  }
97
+ /** Kept for callers that only need the yes/no. A slow listener is NOT a dev server, but it is not absent. */
98
+ function servesDocument(port, host) {
99
+ return probeState(port, host).then((state) => state === PortState.SERVES_DOCUMENT);
100
+ }
101
+ /**
102
+ * Is anything serving a document on this port, on EITHER loopback family?
103
+ *
104
+ * "Either" and not "both": one answer is enough, and requiring both would make the wildcard binds
105
+ * this exists to see fail the check again. A family that errors (no IPv6 stack at all) is an
106
+ * absence, never a rejection — a diagnostic must not throw at the tool it is diagnosing.
107
+ */
108
+ export async function anyFamilyServes(port, probe = servesDocument) {
109
+ const answers = await Promise.all(PROBE_HOSTS.map((host) => probe(port, host).catch(() => false)));
110
+ return answers.some((up) => up);
111
+ }
112
+ /**
113
+ * Every candidate port, split by what the probe could actually establish.
114
+ *
115
+ * `slow` is the set that accepted a connection and then said nothing inside the budget. Those ports
116
+ * are NOT dev servers as far as this check knows, and they are emphatically not absent — an SSR
117
+ * framework compiling its first route lands here every time. Reported separately so the diagnosis
118
+ * can stop telling people to start a server that is already running.
119
+ */
120
+ export async function probeDevServerStates(ports = [...DEV_SERVER_PORTS], probe = (p) => anyFamilyState(p)) {
121
+ const states = await Promise.all(ports.map((p) => classifyPort(p, probe).then((state) => ({ port: p, state }))));
122
+ const pick = (want) => states
123
+ .filter((s) => s.state === want)
124
+ .map((s) => s.port)
125
+ .sort((a, b) => a - b);
126
+ return { serving: pick(PortState.SERVES_DOCUMENT), slow: pick(PortState.CONNECTED_NO_ANSWER) };
127
+ }
128
+ /**
129
+ * The strongest state either loopback family reports.
130
+ *
131
+ * Serving a document beats a bare connection beats nothing, so a server bound to only one family is
132
+ * still seen at its best — the same "either, not both" rule `anyFamilyServes` follows, extended to
133
+ * three states.
134
+ */
135
+ export async function anyFamilyState(port, probe = probeState) {
136
+ const answers = await Promise.all(PROBE_HOSTS.map((host) => probe(port, host).catch(() => PortState.CLOSED)));
137
+ if (answers.includes(PortState.SERVES_DOCUMENT))
138
+ return PortState.SERVES_DOCUMENT;
139
+ if (answers.includes(PortState.CONNECTED_NO_ANSWER))
140
+ return PortState.CONNECTED_NO_ANSWER;
141
+ return PortState.CLOSED;
142
+ }
48
143
  /** Every candidate port serving a document, in ascending order. Never rejects. */
49
- export async function probeDevServers(ports = [...DEV_SERVER_PORTS], probe = (p) => servesDocument(p)) {
144
+ export async function probeDevServers(ports = [...DEV_SERVER_PORTS], probe = (p) => anyFamilyServes(p)) {
50
145
  const results = await Promise.all(ports.map((p) => probe(p).then((up) => (up ? p : null))));
51
146
  return results.filter((p) => p !== null).sort((a, b) => a - b);
52
147
  }
@@ -36,6 +36,32 @@ export declare class LastAct {
36
36
  * cannot forget to clean up state that was never written.
37
37
  */
38
38
  markActed(cursor: number, action: string | undefined, mutatedWithin: number | undefined): void;
39
+ /**
40
+ * A navigation — by URL or by reload — moves the floor without being an act.
41
+ *
42
+ * Reported from the field: an agent reloaded the page, restarted its API, then asserted four
43
+ * strings that were on the screen. Every clause passed and the verdict came back `contradicted`,
44
+ * citing hundreds of failed requests, every one against a resource that no longer existed. The
45
+ * failures were real once; they belonged to the document the reload destroyed.
46
+ *
47
+ * The floor was set only by act, act_sequence and act_and_wait, so navigating left it where it was
48
+ * — or unset, which the caller reads as "judge the whole session". `queryEvents` is journal-backed,
49
+ * so the whole session is durable history that outlives the page. A navigation has to move this or
50
+ * every rule reading the window inherits evidence from a page that is gone.
51
+ *
52
+ * Deliberately NOT `markActed`: a navigation records no act effect. The "this click did nothing"
53
+ * check reads an action with no measured mutation, and a reload written there would be accused of
54
+ * being a dead click.
55
+ */
56
+ markNavigated(cursor: number): void;
57
+ /**
58
+ * The evaluation floor: the most recent thing that changed the page, whichever kind it was.
59
+ *
60
+ * `Math.max` rather than last-writer-wins, because the two are recorded by different tools and
61
+ * their order is the agent's, not ours: an act then a reload must floor at the reload, and a reload
62
+ * then an act must floor at the act. Either one going backwards would re-admit the evidence this
63
+ * exists to exclude.
64
+ */
39
65
  cursor(): number | undefined;
40
66
  markSource(source: string | undefined): void;
41
67
  source(): string | undefined;
@@ -1,5 +1,7 @@
1
1
  export class LastAct {
2
2
  #cursor;
3
+ /** The cursor of the most recent navigation, kept apart from an act's so neither overwrites it. */
4
+ #navigated;
3
5
  #source;
4
6
  #effect;
5
7
  /**
@@ -20,8 +22,40 @@ export class LastAct {
20
22
  this.#cursor = cursor;
21
23
  this.#effect = { action, mutatedWithin };
22
24
  }
25
+ /**
26
+ * A navigation — by URL or by reload — moves the floor without being an act.
27
+ *
28
+ * Reported from the field: an agent reloaded the page, restarted its API, then asserted four
29
+ * strings that were on the screen. Every clause passed and the verdict came back `contradicted`,
30
+ * citing hundreds of failed requests, every one against a resource that no longer existed. The
31
+ * failures were real once; they belonged to the document the reload destroyed.
32
+ *
33
+ * The floor was set only by act, act_sequence and act_and_wait, so navigating left it where it was
34
+ * — or unset, which the caller reads as "judge the whole session". `queryEvents` is journal-backed,
35
+ * so the whole session is durable history that outlives the page. A navigation has to move this or
36
+ * every rule reading the window inherits evidence from a page that is gone.
37
+ *
38
+ * Deliberately NOT `markActed`: a navigation records no act effect. The "this click did nothing"
39
+ * check reads an action with no measured mutation, and a reload written there would be accused of
40
+ * being a dead click.
41
+ */
42
+ markNavigated(cursor) {
43
+ this.#navigated = cursor;
44
+ }
45
+ /**
46
+ * The evaluation floor: the most recent thing that changed the page, whichever kind it was.
47
+ *
48
+ * `Math.max` rather than last-writer-wins, because the two are recorded by different tools and
49
+ * their order is the agent's, not ours: an act then a reload must floor at the reload, and a reload
50
+ * then an act must floor at the act. Either one going backwards would re-admit the evidence this
51
+ * exists to exclude.
52
+ */
23
53
  cursor() {
24
- return this.#cursor;
54
+ if (this.#cursor === undefined)
55
+ return this.#navigated;
56
+ if (this.#navigated === undefined)
57
+ return this.#cursor;
58
+ return Math.max(this.#cursor, this.#navigated);
25
59
  }
26
60
  markSource(source) {
27
61
  this.#source = source;
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { noteEmptyRead } from '../tools/observed-nothing.js';
3
- import { AGENT_ASK_NOTICE, AGENT_WAITING_NOTICE, PresenterTone, SessionState, } from '@reticlehq/core';
3
+ import { AGENT_ASK_NOTICE, AGENT_WAITING_NOTICE, PresenterTone, SessionState, YIELD_WITHOUT_SESSION_NOTE, } from '@reticlehq/core';
4
4
  import { ReticleTool } from '../tools/tool-names.js';
5
5
  import { sessionIdShape } from '../tools/tool-kit.js';
6
6
  import { asNumber, asString } from '../tools/tools-helpers.js';
@@ -9,6 +9,20 @@ import { recoveryFor } from '../tools/error-recovery.js';
9
9
  /** Default + ceiling for the readiness wait — keep it short so a truly-missing app fails fast. */
10
10
  const WAIT_READY_DEFAULT_MS = 5000;
11
11
  const WAIT_READY_MAX_MS = 30000;
12
+ /**
13
+ * Is this a turn ending with nothing attached, rather than a call about a specific tab?
14
+ *
15
+ * Only true when the caller named NO session and none is connected. Ending a turn is a statement
16
+ * about the agent, not about a tab, so it must not fail merely because no app was ever wired — that
17
+ * made a call documented as MANDATORY fail in the most common state a daemon is in, and the agents
18
+ * that hit it stopped calling it and wrote the gap into prose instead.
19
+ *
20
+ * Naming a session that does not exist stays an error. That is a mistake about WHICH tab you are
21
+ * talking to, and swallowing it would hide a real one.
22
+ */
23
+ function endingTurnWithNothingAttached(deps, id) {
24
+ return id === undefined && 0 === deps.sessions.count();
25
+ }
12
26
  /**
13
27
  * Live-control agent tools: the agent's side of the human-in-the-loop control surface.
14
28
  *
@@ -29,9 +43,19 @@ export const LIVE_CONTROL_TOOLS = [
29
43
  '(calm, terminal) and shows the optional `summary` on the panel. If you are just finishing a ' +
30
44
  'turn or waiting on the human, call reticle_session{action:"yield"} instead (revivable). Idempotent.',
31
45
  inputSchema: { summary: z.string().optional(), ...sessionIdShape },
32
- outputSchema: { ended: z.boolean(), sessionId: z.string() },
46
+ // `sessionId` is optional because there may genuinely not be one — see the no-op below. An
47
+ // empty string would read as a real id in a log, which is worse than its absence.
48
+ outputSchema: {
49
+ ended: z.boolean(),
50
+ sessionId: z.string().optional(),
51
+ note: z.string().optional(),
52
+ },
33
53
  handler: (deps, args) => {
34
- const session = deps.sessions.resolve(asString(args['sessionId']));
54
+ const requested = asString(args['sessionId']);
55
+ if (endingTurnWithNothingAttached(deps, requested)) {
56
+ return Promise.resolve({ ended: true, note: YIELD_WITHOUT_SESSION_NOTE });
57
+ }
58
+ const session = deps.sessions.resolve(requested);
35
59
  // One PRESENTER push for the transition; the optional summary rides the same push.
36
60
  session.setState(SessionState.ENDED, asString(args['summary']));
37
61
  return Promise.resolve({ ended: true, sessionId: session.id });
@@ -55,10 +79,23 @@ export const LIVE_CONTROL_TOOLS = [
55
79
  .describe('For mode:"ask", the question to show the human on the panel.'),
56
80
  ...sessionIdShape,
57
81
  },
58
- outputSchema: { yielded: z.boolean(), mode: z.string(), sessionId: z.string() },
82
+ outputSchema: {
83
+ yielded: z.boolean(),
84
+ mode: z.string(),
85
+ sessionId: z.string().optional(),
86
+ note: z.string().optional(),
87
+ },
59
88
  handler: (deps, args) => {
60
- const session = deps.sessions.resolve(asString(args['sessionId']));
89
+ const requested = asString(args['sessionId']);
61
90
  const ask = asString(args['mode']) === PresenterTone.ASK;
91
+ if (endingTurnWithNothingAttached(deps, requested)) {
92
+ return Promise.resolve({
93
+ yielded: true,
94
+ mode: ask ? PresenterTone.ASK : PresenterTone.WAITING,
95
+ note: YIELD_WITHOUT_SESSION_NOTE,
96
+ });
97
+ }
98
+ const session = deps.sessions.resolve(requested);
62
99
  const note = asString(args['note']);
63
100
  const tone = ask ? PresenterTone.ASK : PresenterTone.WAITING;
64
101
  const text = ask && note !== undefined && note.trim().length > 0
@@ -18,13 +18,22 @@
18
18
  * Pure: everything it needs is passed in, so the probe that discovers listening ports stays out of
19
19
  * the hot resolve() path and this stays unit-testable.
20
20
  */
21
- interface NoSessionFacts {
21
+ export interface NoSessionFacts {
22
22
  /** Whether ANY session has connected to this daemon since it booted. */
23
23
  everConnected: boolean;
24
24
  /** Whether this project has been through `reticle init` (a .reticle.json / projectId is present). */
25
25
  initialized: boolean;
26
26
  /** Localhost ports with something listening that looks like a dev server. */
27
27
  listening: readonly number[];
28
+ /**
29
+ * Ports that ACCEPTED a connection and then answered nothing inside the probe budget.
30
+ *
31
+ * Not dev servers as far as the probe knows, and absolutely not absent. An SSR framework
32
+ * compiling its first route lands here every time, and calling that "nothing is listening" told a
33
+ * reporter to start a Nuxt server that was already serving — advice that would have hit their dev
34
+ * lock. Named in the message so the reader can tell the two apart.
35
+ */
36
+ slowListeners?: readonly number[];
28
37
  /** The port this daemon is on — half of the mismatch the old message asked about. */
29
38
  port: number;
30
39
  /**
@@ -53,6 +62,51 @@ interface NoSessionFacts {
53
62
  * no `.reticle.json` at all and is working perfectly.
54
63
  */
55
64
  projectPort?: number;
65
+ /**
66
+ * An app HAS connected on this port for this project before — read from durable state, not from
67
+ * this process.
68
+ *
69
+ * `everConnected` is a per-process boolean, and daemons are short-lived by design: they idle out
70
+ * and respawn. So a fresh daemon said it had "never seen one" seconds after its predecessor served
71
+ * a session, and the reader was sent hunting a `.reticle.json` that was never missing. The two
72
+ * claims mean opposite things — one is evidence about an install, the other is a fact about a
73
+ * process that booted four seconds ago — and only this one can tell them apart.
74
+ */
75
+ previouslyConnected?: boolean;
76
+ /**
77
+ * The framework `.reticle.json` declares, when there is one.
78
+ *
79
+ * Used to RANK the causes rather than to print a static differential. Nuxt is the case that made
80
+ * the difference: it does not register a new plugin on HMR, so a dev server older than the wiring
81
+ * is the single most likely cause there — and `reticle init` says so at install time while the
82
+ * hint the agent reads hours later never mentioned it.
83
+ */
84
+ framework?: string;
85
+ /**
86
+ * `.reticle.json` files found OUTSIDE this daemon's directory — a workspace app, a repo root.
87
+ *
88
+ * Editors launch MCP servers from wherever they like, and the config then sits under the app
89
+ * package while the daemon stands in the user's home. The old message read that as "this project
90
+ * has no SDK" and sent a working install back through `reticle init`.
91
+ */
92
+ configsElsewhere?: readonly {
93
+ directory: string;
94
+ projectId?: string;
95
+ }[];
96
+ /**
97
+ * Every directory the config search actually checked.
98
+ *
99
+ * "There is no `.reticle.json`" is unfalsifiable without it, and the claim was wrong often enough
100
+ * that a reader deserves to check it.
101
+ */
102
+ searchedDirectories?: readonly string[];
103
+ /**
104
+ * How long this daemon has been running with no app connected, in milliseconds.
105
+ *
106
+ * Present only when the stall clock is active and no app has arrived. Used to surface "the
107
+ * install never finished" to the user-facing diagnosis rather than only to telemetry.
108
+ */
109
+ daemonUpMs?: number;
56
110
  }
57
111
  /**
58
112
  * Every branch ends with this, and `recoveryFor` keys on it to suppress the generic no-session
@@ -61,4 +115,3 @@ interface NoSessionFacts {
61
115
  */
62
116
  export declare const SELF_RECOVERING_MARKER = "Then call reticle_sessions again \u2014 it will appear within a second of the page loading.";
63
117
  export declare function diagnoseNoSession(facts: NoSessionFacts): string;
64
- export {};