@reticlehq/server 3.8.1 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/LICENSE +201 -104
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/bin/launch.js +13 -0
  5. package/bin/package.json +3 -0
  6. package/bin/reticle.js +66 -8
  7. package/dist/chromium-launch-options.d.ts +25 -0
  8. package/dist/chromium-launch-options.js +21 -1
  9. package/dist/command/cli/adhoc-verdict.js +34 -7
  10. package/dist/command/cli/answers/gate-exit.d.ts +6 -2
  11. package/dist/command/cli/answers/gate-exit.js +12 -2
  12. package/dist/command/cli/ci-hint.d.ts +44 -0
  13. package/dist/command/cli/ci-hint.js +72 -0
  14. package/dist/command/cli/cli-doctor.d.ts +6 -1
  15. package/dist/command/cli/cli-doctor.js +45 -4
  16. package/dist/command/cli/cli-flow-commands.d.ts +7 -1
  17. package/dist/command/cli/cli-flow-commands.js +39 -17
  18. package/dist/command/cli/cli-kill.d.ts +21 -0
  19. package/dist/command/cli/cli-kill.js +46 -3
  20. package/dist/command/cli/cli-parse.d.ts +5 -2
  21. package/dist/command/cli/cli-parse.js +10 -5
  22. package/dist/command/cli/cli-usage.js +3 -2
  23. package/dist/command/cli/cli-verify.d.ts +6 -1
  24. package/dist/command/cli/cli-verify.js +32 -13
  25. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
  26. package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
  27. package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
  28. package/dist/command/cli/doctor/doctor-fix.js +89 -0
  29. package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
  30. package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
  31. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  32. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  33. package/dist/command/cli/doctor-repairs.d.ts +64 -0
  34. package/dist/command/cli/doctor-repairs.js +214 -0
  35. package/dist/command/cli/drive/drive-command.js +3 -7
  36. package/dist/command/cli/first-use-checks.d.ts +41 -0
  37. package/dist/command/cli/first-use-checks.js +127 -0
  38. package/dist/command/cli/first-verdict-node.d.ts +3 -0
  39. package/dist/command/cli/first-verdict-node.js +189 -0
  40. package/dist/command/cli/first-verdict.d.ts +126 -0
  41. package/dist/command/cli/first-verdict.js +267 -0
  42. package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
  43. package/dist/command/cli/launch/sdk-version-match.js +3 -2
  44. package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
  45. package/dist/command/cli/mcp-command.js +13 -4
  46. package/dist/command/cli/ports/port-holder.d.ts +2 -0
  47. package/dist/command/cli/ports/port-holder.js +11 -3
  48. package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
  49. package/dist/command/cli/setup-mcp-cli.js +98 -23
  50. package/dist/command/cli/status/status-lines.d.ts +1 -0
  51. package/dist/command/cli/status/status-lines.js +50 -17
  52. package/dist/command/cli/status-fields.js +17 -3
  53. package/dist/command/cli/try-command.d.ts +7 -0
  54. package/dist/command/cli/try-command.js +11 -3
  55. package/dist/command/cli.js +70 -33
  56. package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
  57. package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
  58. package/dist/command/daemon/daemon-argv.d.ts +9 -0
  59. package/dist/command/daemon/daemon-argv.js +11 -0
  60. package/dist/command/daemon/daemon.d.ts +10 -3
  61. package/dist/command/daemon/daemon.js +17 -6
  62. package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
  63. package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
  64. package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
  65. package/dist/command/daemon/lifetime/death-cause.js +130 -0
  66. package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
  67. package/dist/command/daemon/lifetime/heartbeat.js +4 -13
  68. package/dist/command/daemon/port-state.d.ts +14 -0
  69. package/dist/command/daemon/port-state.js +31 -0
  70. package/dist/command/setup/agent-configs.d.ts +2 -0
  71. package/dist/command/setup/agent-configs.js +42 -15
  72. package/dist/command/setup/agent-next-steps.d.ts +77 -0
  73. package/dist/command/setup/agent-next-steps.js +284 -0
  74. package/dist/command/setup/agent-writer.js +11 -6
  75. package/dist/command/setup/init/init-host.js +2 -0
  76. package/dist/command/setup/init/init-runtime.d.ts +6 -1
  77. package/dist/command/setup/init/init-runtime.js +57 -21
  78. package/dist/command/setup/mcp-launcher.d.ts +3 -0
  79. package/dist/command/setup/mcp-launcher.js +11 -0
  80. package/dist/command/setup/setup-command.js +30 -12
  81. package/dist/command/setup/setup-mcp.d.ts +8 -0
  82. package/dist/command/setup/setup-mcp.js +47 -20
  83. package/dist/command/setup/start-dev-server.d.ts +8 -0
  84. package/dist/command/setup/start-dev-server.js +43 -0
  85. package/dist/command/version/identity/server-version.d.ts +2 -0
  86. package/dist/command/version/identity/server-version.js +2 -0
  87. package/dist/command/version/version-skew.d.ts +7 -0
  88. package/dist/command/version/version-skew.js +13 -2
  89. package/dist/features/license/license.d.ts +11 -71
  90. package/dist/features/license/license.js +43 -136
  91. package/dist/features/visual/desktop-capture.d.ts +33 -0
  92. package/dist/features/visual/desktop-capture.js +94 -0
  93. package/dist/features/visual/step-shots.js +139 -35
  94. package/dist/features/visual/visual-tools.d.ts +0 -18
  95. package/dist/features/visual/visual-tools.js +2 -81
  96. package/dist/import-graph.js +5 -6
  97. package/dist/index.d.ts +3 -1
  98. package/dist/index.js +14 -5
  99. package/dist/judgement/runs/run-ledger.d.ts +7 -5
  100. package/dist/judgement/runs/run-ledger.js +98 -69
  101. package/dist/judgement/runs/verification-sync.d.ts +6 -3
  102. package/dist/judgement/runs/verification-sync.js +21 -10
  103. package/dist/language/flows/flow-store-for-session.js +7 -5
  104. package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
  105. package/dist/language/flows/recording/tape/recordings.js +41 -44
  106. package/dist/language/flows/replay.d.ts +17 -7
  107. package/dist/language/flows/replay.js +95 -44
  108. package/dist/launch-chromium.d.ts +1 -0
  109. package/dist/launch-chromium.js +49 -12
  110. package/dist/memory/cloud/sync-screenshots.js +6 -1
  111. package/dist/portal/bridge/bridge.d.ts +13 -4
  112. package/dist/portal/bridge/bridge.js +10 -9
  113. package/dist/portal/input/drive-error.d.ts +6 -0
  114. package/dist/portal/input/drive-error.js +9 -0
  115. package/dist/portal/input/network-mock.js +3 -2
  116. package/dist/portal/input/real-input-press.d.ts +34 -0
  117. package/dist/portal/input/real-input-press.js +150 -0
  118. package/dist/portal/input/real-input.d.ts +21 -23
  119. package/dist/portal/input/real-input.js +153 -194
  120. package/dist/portal/input/viewport-tools.js +6 -3
  121. package/dist/portal/pool/browser-pool.d.ts +5 -1
  122. package/dist/portal/pool/browser-pool.js +32 -38
  123. package/dist/portal/pool/context-permissions.d.ts +20 -17
  124. package/dist/portal/pool/context-permissions.js +63 -1
  125. package/dist/portal/pool/csp-widen.d.ts +62 -0
  126. package/dist/portal/pool/csp-widen.js +132 -0
  127. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  128. package/dist/portal/pool/lease-page-error.js +18 -0
  129. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  130. package/dist/portal/pool/page-error-journal.js +181 -0
  131. package/dist/portal/pool/playwright-launcher.js +21 -8
  132. package/dist/portal/pool/pool-contract.d.ts +22 -0
  133. package/dist/portal/pool/pool-lease.d.ts +35 -2
  134. package/dist/portal/pool/pool-lease.js +23 -0
  135. package/dist/portal/pool/zero-install.js +69 -1
  136. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  137. package/dist/portal/session/dev-server/dev-command.js +56 -8
  138. package/dist/portal/session/no-session-diagnosis.js +10 -3
  139. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  140. package/dist/portal/session/no-session-next-action.js +44 -4
  141. package/dist/portal/session/no-session-watch.js +3 -1
  142. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  143. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  144. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  145. package/dist/portal/session/recorded-gaps.js +5 -2
  146. package/dist/portal/session/session-manager.js +40 -21
  147. package/dist/portal/session/session.js +4 -3
  148. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  149. package/dist/portal/session/zero-install-first-call.js +34 -2
  150. package/dist/start-wiring.d.ts +29 -2
  151. package/dist/start-wiring.js +73 -9
  152. package/dist/surface/mcp/mcp-proxy.js +10 -2
  153. package/dist/surface/mcp/mcp.js +5 -3
  154. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  155. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  156. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  157. package/dist/surface/tools/act/act-preflight.js +37 -17
  158. package/dist/surface/tools/act/act-target.js +3 -1
  159. package/dist/surface/tools/act/after-match.d.ts +7 -0
  160. package/dist/surface/tools/act/after-match.js +33 -0
  161. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  162. package/dist/surface/tools/act/resolve-target.js +35 -8
  163. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  164. package/dist/surface/tools/act/sequence-grade.js +8 -1
  165. package/dist/surface/tools/act-sequence-tool.js +3 -0
  166. package/dist/surface/tools/act-tools.js +12 -12
  167. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  168. package/dist/surface/tools/browser-tools.js +75 -7
  169. package/dist/surface/tools/dynamic-tools.js +2 -1
  170. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  171. package/dist/surface/tools/empty-session-list.js +4 -2
  172. package/dist/surface/tools/error-recovery.js +43 -22
  173. package/dist/surface/tools/invoke-tool.js +27 -20
  174. package/dist/surface/tools/lease-hint.d.ts +7 -0
  175. package/dist/surface/tools/lease-hint.js +10 -3
  176. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  177. package/dist/surface/tools/lease-readiness.js +47 -0
  178. package/dist/surface/tools/lease-seed.d.ts +2 -0
  179. package/dist/surface/tools/lease-seed.js +2 -1
  180. package/dist/surface/tools/lease-tools.js +43 -15
  181. package/dist/surface/tools/real-input-attempt.js +70 -31
  182. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  183. package/dist/surface/tools/repeat-hint.js +171 -0
  184. package/dist/surface/tools/run-report.d.ts +2 -2
  185. package/dist/surface/tools/run-report.js +27 -15
  186. package/dist/surface/tools/tool-kit.d.ts +12 -0
  187. package/dist/surface/tools/tool-kit.js +7 -0
  188. package/dist/surface/tools/tools.js +26 -13
  189. package/dist/telemetry/argument-shape.js +16 -7
  190. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  191. package/dist/telemetry/cli-telemetry.js +15 -0
  192. package/dist/telemetry/init-telemetry.d.ts +2 -1
  193. package/dist/telemetry/init-telemetry.js +47 -2
  194. package/dist/telemetry/install-trace.js +23 -4
  195. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  196. package/dist/telemetry/onboarding-funnel.js +25 -3
  197. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  198. package/dist/telemetry/proxy-telemetry.js +2 -2
  199. package/dist/telemetry/session-metrics.d.ts +1 -1
  200. package/dist/telemetry/session-metrics.js +67 -27
  201. package/dist/telemetry/telemetry.d.ts +3 -1
  202. package/dist/telemetry/telemetry.js +60 -28
  203. package/dist/telemetry/tool-refused.d.ts +2 -2
  204. package/dist/telemetry/tool-refused.js +9 -2
  205. package/docs/architecture.md +3 -6
  206. package/docs/cli/doctor.mdx +41 -2
  207. package/docs/cli/help.mdx +4 -2
  208. package/docs/cli/license.mdx +4 -5
  209. package/docs/cli/setup.mdx +13 -2
  210. package/docs/cli/status.mdx +4 -0
  211. package/docs/cli.mdx +1 -1
  212. package/docs/deploy-checks.md +25 -0
  213. package/docs/desktop.mdx +1 -1
  214. package/docs/enterprise.md +15 -20
  215. package/docs/environment.mdx +1 -0
  216. package/docs/first-drive.md +1 -1
  217. package/docs/install-manual.mdx +16 -1
  218. package/docs/packages/engine.mdx +1 -1
  219. package/docs/packages/react.mdx +14 -2
  220. package/docs/packages/server.mdx +1 -1
  221. package/docs/packages/tauri.mdx +1 -1
  222. package/docs/packages/test.mdx +1 -1
  223. package/docs/packages/vite-plugin.mdx +1 -0
  224. package/docs/packages.mdx +1 -1
  225. package/docs/platform-integration.md +1 -1
  226. package/docs/skill.md +1 -1
  227. package/docs/telemetry.md +4 -2
  228. package/package.json +14 -10
  229. package/LICENSE-ENTERPRISE +0 -37
  230. package/dist/features/ee/audit-log.d.ts +0 -14
  231. package/dist/features/ee/audit-log.js +0 -11
@@ -0,0 +1,44 @@
1
+ /**
2
+ * One line at the end of `reticle gate`, `reticle verify` and `reticle affected` when they run in a
3
+ * CI runner or a container. It names the pull request check and the page that tells more about it.
4
+ *
5
+ * Rules: one line; never in an interactive terminal; never in a machine output mode; off with
6
+ * `RETICLE_NO_CI_HINT`. It goes to stderr. The stdout of `verify` carries the verdict lines that a
7
+ * pipeline reads, and stderr already carries the human notes of these commands.
8
+ *
9
+ * No network call. The latest version comes from the update manifest that the update check already
10
+ * caches on disk. When there is no cache, the line does not compare versions.
11
+ *
12
+ * The caller gives the automation hint (`currentAutomationHint`): `telemetry` already reaches this
13
+ * directory, so this directory does not reach back.
14
+ */
15
+ import { AutomationHint } from '@reticlehq/core/telemetry';
16
+ /** Set to `1` to stop the line. */
17
+ export declare const CI_HINT_SILENCE_ENV = "RETICLE_NO_CI_HINT";
18
+ /** The page that tells about the pull request check. */
19
+ export declare const CI_HINT_URL = "https://reticle.sh/ci";
20
+ export interface CiHintInput {
21
+ env: NodeJS.ProcessEnv;
22
+ automation: AutomationHint | undefined;
23
+ /** A person watches the output in a terminal. */
24
+ interactive: boolean;
25
+ /** The command writes output for a program to read (JSON, an agent hook). */
26
+ machineOutput: boolean;
27
+ currentVersion: string;
28
+ /** The latest version from the cached update check, if there is one. */
29
+ latestVersion: string | undefined;
30
+ }
31
+ /** The line, or undefined when it must not print. Pure. */
32
+ export declare function ciHintLine(input: CiHintInput): string | undefined;
33
+ export interface CiHintPorts {
34
+ env: NodeJS.ProcessEnv;
35
+ interactive: () => boolean;
36
+ latestVersion: () => string | undefined;
37
+ write: (text: string) => void;
38
+ }
39
+ /** Write the line to stderr when it applies. Call it last, after the command's own output. */
40
+ export declare function writeCiHint(args: {
41
+ automation: AutomationHint | undefined;
42
+ machineOutput: boolean;
43
+ ports?: CiHintPorts;
44
+ }): void;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * One line at the end of `reticle gate`, `reticle verify` and `reticle affected` when they run in a
3
+ * CI runner or a container. It names the pull request check and the page that tells more about it.
4
+ *
5
+ * Rules: one line; never in an interactive terminal; never in a machine output mode; off with
6
+ * `RETICLE_NO_CI_HINT`. It goes to stderr. The stdout of `verify` carries the verdict lines that a
7
+ * pipeline reads, and stderr already carries the human notes of these commands.
8
+ *
9
+ * No network call. The latest version comes from the update manifest that the update check already
10
+ * caches on disk. When there is no cache, the line does not compare versions.
11
+ *
12
+ * The caller gives the automation hint (`currentAutomationHint`): `telemetry` already reaches this
13
+ * directory, so this directory does not reach back.
14
+ */
15
+ import { AutomationHint } from '@reticlehq/core/telemetry';
16
+ import { parseMajor } from '@reticlehq/init';
17
+ import { loadManifest } from '../update/update-checker.js';
18
+ import { SERVER_VERSION } from '../version/identity/server-version.js';
19
+ /** Set to `1` to stop the line. */
20
+ export const CI_HINT_SILENCE_ENV = 'RETICLE_NO_CI_HINT';
21
+ /** The page that tells about the pull request check. */
22
+ export const CI_HINT_URL = 'https://reticle.sh/ci';
23
+ /** The env values that do not turn the line off. */
24
+ const NOT_SILENCED = new Set(['', '0', 'false']);
25
+ /** The automation hints that get the line. A hosted workspace or an agent sandbox is a person. */
26
+ const CI_HINT_AUTOMATION = new Set([
27
+ AutomationHint.CI_RUNNER,
28
+ AutomationHint.CONTAINER,
29
+ ]);
30
+ const CiHintCopy = {
31
+ CHECK: `Reticle: a pull request check shows each replay, the run history and a merge block. Read ${CI_HINT_URL}`,
32
+ BEHIND: (current, latest) => `Reticle: this engine (${current}) is a major version behind ${latest}. Update it, then read ${CI_HINT_URL}`,
33
+ };
34
+ /** The line, or undefined when it must not print. Pure. */
35
+ export function ciHintLine(input) {
36
+ if (!NOT_SILENCED.has(input.env[CI_HINT_SILENCE_ENV] ?? '')) {
37
+ return undefined;
38
+ }
39
+ if (input.interactive || input.machineOutput) {
40
+ return undefined;
41
+ }
42
+ if (input.automation === undefined || !CI_HINT_AUTOMATION.has(input.automation)) {
43
+ return undefined;
44
+ }
45
+ const current = parseMajor(input.currentVersion);
46
+ const latest = parseMajor(input.latestVersion);
47
+ if (current !== undefined && latest !== undefined && latest > current) {
48
+ return CiHintCopy.BEHIND(input.currentVersion, input.latestVersion ?? '');
49
+ }
50
+ return CiHintCopy.CHECK;
51
+ }
52
+ const realPorts = () => ({
53
+ env: process.env,
54
+ interactive: () => true === process.stdout.isTTY || true === process.stderr.isTTY,
55
+ latestVersion: () => loadManifest()?.latestVersion,
56
+ write: (text) => process.stderr.write(text),
57
+ });
58
+ /** Write the line to stderr when it applies. Call it last, after the command's own output. */
59
+ export function writeCiHint(args) {
60
+ const ports = args.ports ?? realPorts();
61
+ const line = ciHintLine({
62
+ env: ports.env,
63
+ automation: args.automation,
64
+ interactive: ports.interactive(),
65
+ machineOutput: args.machineOutput,
66
+ currentVersion: SERVER_VERSION,
67
+ latestVersion: ports.latestVersion(),
68
+ });
69
+ if (line !== undefined) {
70
+ ports.write(`${line}\n`);
71
+ }
72
+ }
@@ -1 +1,6 @@
1
- export declare function handleDoctor(port: number): Promise<void>;
1
+ import type { StepReporter } from './setup-mcp-cli.js';
2
+ /**
3
+ * `reticle doctor`, and with `fix`, `reticle doctor --fix`. Without `fix` it changes nothing and
4
+ * ends with how many rows `--fix` can repair. With `fix` the exit code is 1 while any row fails.
5
+ */
6
+ export declare function handleDoctor(port: number, fix?: boolean, reportStep?: StepReporter): Promise<void>;
@@ -11,7 +11,8 @@ import { daemonLine } from './doctor/doctor-daemon-line.js';
11
11
  import { projectWiringLine } from './doctor/doctor-project-line.js';
12
12
  import { hasProjectConnectedBefore } from '../../memory/recall/prior/connection-memory.js';
13
13
  import { sessionsLine } from './doctor/doctor-sessions-line.js';
14
- import { captureLookup, describeForeignHolder, findPortHolder } from './ports/port-holder.js';
14
+ import { captureLookup, describeForeignHolder } from './ports/port-holder.js';
15
+ import { identifyPortHolder } from '../daemon/port-state.js';
15
16
  import { chromiumHint } from './doctor/browser/chromium-hint.js';
16
17
  import { probeChromiumWithFallback } from '../../launch-chromium.js';
17
18
  import { SERVER_VERSION } from '../version/identity/server-version.js';
@@ -32,6 +33,8 @@ import { describeUnsynced } from '../../memory/cloud/unsynced-roots.js';
32
33
  import { machineUnsyncedRoots } from '../../memory/project/sync-status.js';
33
34
  import { attachState, describeAttachState } from '../../surface/mcp/attach-memory.js';
34
35
  import { agentLinkText } from './doctor/doctor-agent-link.js';
36
+ import { countNeeded, fixFooter, runDoctorFix } from './doctor/doctor-fix.js';
37
+ import { machineRepairs, mcpRegistrationRow, readLauncherLine, readMcpRegistration, } from './doctor-repairs.js';
35
38
  import { findOccupiedSiblings, siblingListenerNote } from './ports/sibling-ports.js';
36
39
  import { daemonsServingProjectElsewhere, resolveDaemonForProject, splitBrainNote, wrongDaemonNote, } from '../daemon/daemon-resolve.js';
37
40
  import { isAlive } from '../daemon/daemon.js';
@@ -73,9 +76,41 @@ function splitBrainLine(port, projectId) {
73
76
  const elsewhere = splitBrainNote(port, daemonsServingProjectElsewhere(projectId, port, home, isAlive, (other) => hasProjectConnectedBefore(home, other, projectId)));
74
77
  return elsewhere ?? wrongDaemonNote(port, resolveDaemonForProject(projectId, home, isAlive));
75
78
  }
76
- export async function handleDoctor(port) {
79
+ const writeLine = (s) => {
80
+ process.stdout.write(`${s}\n`);
81
+ };
82
+ /**
83
+ * `reticle doctor`, and with `fix`, `reticle doctor --fix`. Without `fix` it changes nothing and
84
+ * ends with how many rows `--fix` can repair. With `fix` the exit code is 1 while any row fails.
85
+ */
86
+ export async function handleDoctor(port, fix = false, reportStep = () => { }) {
87
+ const repairs = machineRepairs(port, reportStep);
88
+ if (fix) {
89
+ const green = await runDoctorFix({
90
+ doctor: (show) => doctorLines(port, show),
91
+ repairs,
92
+ print: writeLine,
93
+ });
94
+ if (!green) {
95
+ process.exitCode = 1;
96
+ }
97
+ return;
98
+ }
99
+ await doctorLines(port, true);
100
+ const footer = fixFooter(await countNeeded(repairs));
101
+ if (footer !== undefined) {
102
+ writeLine('');
103
+ writeLine(footer);
104
+ }
105
+ }
106
+ /** Every doctor row, in order. Each row is printed as it is found when `show` is true. */
107
+ async function doctorLines(port, show) {
108
+ const lines = [];
77
109
  const line = (s) => {
78
- process.stdout.write(`${s}\n`);
110
+ lines.push(s);
111
+ if (show) {
112
+ writeLine(s);
113
+ }
79
114
  };
80
115
  line('reticle doctor');
81
116
  line(doctorRow(DoctorRow.NODE, process.version));
@@ -84,6 +119,7 @@ export async function handleDoctor(port) {
84
119
  let sessions;
85
120
  let daemonStatus;
86
121
  line(doctorRow(DoctorRow.CHROMIUM, chromiumHint(await probeChromiumWithFallback())));
122
+ line(mcpRegistrationRow(readMcpRegistration()));
87
123
  // Ask the PORT, not just the pid file. "not running on :4400" has been printed about a port that
88
124
  // was demonstrably occupied, which sends the reader to start a daemon that cannot bind. The three
89
125
  // states are genuinely different problems with different fixes, so doctor names which one it is.
@@ -126,7 +162,7 @@ export async function handleDoctor(port) {
126
162
  // pipeline that also kills the agent's own MCP proxy.
127
163
  // `pid` is what our pid file recorded for this port. When it matches the process actually
128
164
  // holding it, this is our OWN daemon wedged, not a stranger — and the fix is different.
129
- line(doctorRow(DoctorRow.DAEMON, `✗ ${describeForeignHolder(port, findPortHolder(port, captureLookup), pid)}`));
165
+ line(doctorRow(DoctorRow.DAEMON, `✗ ${describeForeignHolder(port, identifyPortHolder(port, captureLookup), pid)}`));
130
166
  }
131
167
  else {
132
168
  line(doctorRow(DoctorRow.DAEMON, `✗ not running on :${port} — your agent runs \`reticle mcp\` (or \`reticle serve\`)`));
@@ -167,6 +203,10 @@ export async function handleDoctor(port) {
167
203
  // would flip agent presence on a daemon with no agents attached.
168
204
  const foreign = presence === PortPresence.FOREIGN;
169
205
  line(doctorRow(DoctorRow.AGENT_LINK, agentLinkText({ split, attachAction, foreign })));
206
+ const launcher = readLauncherLine(process.cwd());
207
+ if (launcher !== undefined) {
208
+ line(doctorRow(DoctorRow.LAUNCHER, launcher));
209
+ }
170
210
  // Remaining half of #261. The SDK dialling a port we are not on is invisible as a refused inbound,
171
211
  // but a listener on a well-known Reticle port is visible. Report the observation; do not conclude
172
212
  // it is the daemon this app wants — somebody else's daemon looks the same from here.
@@ -263,6 +303,7 @@ export async function handleDoctor(port) {
263
303
  if (keychain !== undefined) {
264
304
  line(doctorRow(DoctorRow.DESKTOP, `⚠ ${keychain}`));
265
305
  }
306
+ return lines;
266
307
  }
267
308
  /**
268
309
  * Two rows: does each agent hand its prompts to Reticle, and does the daemon send this
@@ -9,6 +9,7 @@ import { type ChangedFiles } from '../../language/flows/change/git-changed.js';
9
9
  import { type ProjectId } from '@reticlehq/core';
10
10
  import { type FileSystemPort } from '../../memory/project/fs/fs-port.js';
11
11
  import { type NamedFlow } from '../../language/flows/change/flow-sources.js';
12
+ import { type GateCompleted } from '@reticlehq/core/telemetry';
12
13
  /** Load the {name, steps} of every saved flow for the active project. */
13
14
  /** Explicit files plus, when --since is given, the git-changed files since that ref. */
14
15
  /**
@@ -30,4 +31,9 @@ export declare function handleGate(files: string[], since: string | undefined,
30
31
  /** Hook mode: prose for a human, and silence when there was simply nothing to check. */
31
32
  hook?: boolean,
32
33
  /** Accept today's coverage levels as the best before judging, for a drop that was intended. */
33
- acceptCoverage?: boolean): Promise<void>;
34
+ acceptCoverage?: boolean,
35
+ /**
36
+ * Called once with how the gate ended, on every path. `cli.ts` passes the `gate_completed`
37
+ * sender: telemetry already reaches this directory, so this directory does not reach back.
38
+ */
39
+ reportGateCompleted?: (gate: GateCompleted) => void): Promise<void>;
@@ -5,7 +5,7 @@
5
5
  * opens first. These five functions form one cohesive group: they all read the flow store and report on
6
6
  * verification state, and none of them touch daemon lifecycle, which is what the rest of cli.ts does.
7
7
  */
8
- import { GateExit, MSG_GATE_NO_FLOWS, msgGateRegression, msgGateRefused, gateExitWith, } from './answers/gate-exit.js';
8
+ import { GATE_OUTCOME, GateExit, MSG_GATE_NO_FLOWS, msgGateRegression, msgGateRefused, gateExitWith, } from './answers/gate-exit.js';
9
9
  import { knownRegression } from './known-regression.js';
10
10
  import { gateHookMessage, GATE_SKIP_ENV } from './answers/gate-hook-message.js';
11
11
  import { readProjectId } from './ports/resolve/cli-port.js';
@@ -25,6 +25,7 @@ import { detectDowngrades } from '../../judgement/outcome/assertion-integrity.js
25
25
  import { computeCoverage, flowCoverageReport } from '../../language/flows/suite/coverage.js';
26
26
  import { readFileSync, statSync } from 'node:fs';
27
27
  import { log } from '../../log.js';
28
+ import { GateOutcome } from '@reticlehq/core/telemetry';
28
29
  /** Load the {name, steps} of every saved flow for the active project. */
29
30
  /** Explicit files plus, when --since is given, the git-changed files since that ref. */
30
31
  /**
@@ -34,8 +35,9 @@ import { log } from '../../log.js';
34
35
  */
35
36
  export async function resolveChangedFiles(files, since, cwd) {
36
37
  // No ref means no question was asked of git, which is not a failure to answer one.
37
- if (since === undefined)
38
+ if (since === undefined) {
38
39
  return { files, failed: false };
40
+ }
39
41
  const changed = await changedFilesSince(since, cwd);
40
42
  return {
41
43
  files: [...new Set([...files, ...changed.files])],
@@ -56,8 +58,9 @@ export async function loadNamedFlows(fs, reticleRoot, projectId) {
56
58
  const flows = [];
57
59
  for (const name of await store.list(projectId)) {
58
60
  const loaded = await store.load(name, projectId);
59
- if (loaded.ok)
61
+ if (loaded.ok) {
60
62
  flows.push({ name: loaded.value.name, steps: loaded.value.steps });
63
+ }
61
64
  }
62
65
  return flows;
63
66
  }
@@ -81,8 +84,9 @@ function atChangedPath(cwd, file, read) {
81
84
  return undefined;
82
85
  }
83
86
  };
84
- if (isAbsolute(file))
87
+ if (isAbsolute(file)) {
85
88
  return attempt(file);
89
+ }
86
90
  const parts = file.split('/');
87
91
  const cwdParts = cwd.split(sep);
88
92
  for (let i = 0; i < parts.length; i += 1) {
@@ -91,8 +95,9 @@ function atChangedPath(cwd, file, read) {
91
95
  continue;
92
96
  }
93
97
  const found = attempt(join(cwd, ...parts.slice(i)));
94
- if (found !== undefined)
98
+ if (found !== undefined) {
95
99
  return found;
100
+ }
96
101
  }
97
102
  return undefined;
98
103
  }
@@ -122,8 +127,9 @@ async function currentPassing(fs, root, flows, affected, changed) {
122
127
  const own = 0 === sources.length ? changed : changed.filter((f) => sourceMatchesChange(f, sources));
123
128
  const stale = affected.includes(name) &&
124
129
  own.some((f) => !((changedFileModifiedAt(cwd, f) ?? Infinity) < at));
125
- if (!stale)
130
+ if (!stale) {
126
131
  passing.push(name);
132
+ }
127
133
  }
128
134
  return passing;
129
135
  }
@@ -131,7 +137,12 @@ export async function handleGate(files, since,
131
137
  /** Hook mode: prose for a human, and silence when there was simply nothing to check. */
132
138
  hook = false,
133
139
  /** Accept today's coverage levels as the best before judging, for a drop that was intended. */
134
- acceptCoverage = false) {
140
+ acceptCoverage = false,
141
+ /**
142
+ * Called once with how the gate ended, on every path. `cli.ts` passes the `gate_completed`
143
+ * sender: telemetry already reaches this directory, so this directory does not reach back.
144
+ */
145
+ reportGateCompleted = () => undefined) {
135
146
  try {
136
147
  const fs = createNodeFileSystem();
137
148
  const reticleRoot = join(process.cwd(), ReticleDir.ROOT);
@@ -207,13 +218,17 @@ acceptCoverage = false) {
207
218
  // once the exit code is: it said "pass": true over an exit of 1 or 3.
208
219
  const known = await knownRegression(reticleRoot);
209
220
  const regressed = known.broken;
210
- const local = pass
211
- ? GateExit.PASS
212
- : result.pass && flowCoverage.outcome !== undefined
213
- ? GateExit.NOTHING_TO_CHECK
214
- : GateExit.FAIL;
221
+ let local = GateExit.FAIL;
222
+ if (pass) {
223
+ local = GateExit.PASS;
224
+ }
225
+ else if (result.pass && flowCoverage.outcome !== undefined) {
226
+ local = GateExit.NOTHING_TO_CHECK;
227
+ }
215
228
  // Fails closed in CI, not in an agent's stop hook, where it would block over a key, not a change.
216
229
  const code = gateExitWith(local, regressed?.length, !hook && known.refused !== undefined);
230
+ // What the gate judged, sent before a hook's skip can turn the exit code into a pass.
231
+ reportGateCompleted({ outcome: GATE_OUTCOME[code], flowsChecked: affected.length });
217
232
  log('reticle_gate', {
218
233
  pass: GateExit.PASS === code,
219
234
  uncovered: result.uncovered,
@@ -237,14 +252,18 @@ acceptCoverage = false) {
237
252
  // A regression the platform already knows fails the build here too, with `reticle runs
238
253
  // regression`'s code, so the gate is the one command CI needs. Best-effort: unlinked or offline
239
254
  // changes nothing.
240
- if (regressed !== undefined && 0 < regressed.length)
255
+ if (regressed !== undefined && 0 < regressed.length) {
241
256
  log('reticle_gate_regression', { broken: regressed });
242
- if (known.refused !== undefined)
257
+ }
258
+ if (known.refused !== undefined) {
243
259
  process.stderr.write(`${msgGateRefused(known.refused)}\n`);
244
- if (GateExit.NOTHING_TO_CHECK === code && !hook)
260
+ }
261
+ if (GateExit.NOTHING_TO_CHECK === code && !hook) {
245
262
  process.stderr.write(`${MSG_GATE_NO_FLOWS}\n`);
246
- if (GateExit.REGRESSION === code && !hook && regressed !== undefined)
263
+ }
264
+ if (GateExit.REGRESSION === code && !hook && regressed !== undefined) {
247
265
  process.stderr.write(`${msgGateRegression(regressed)}\n`);
266
+ }
248
267
  if (GateExit.PASS !== code) {
249
268
  process.exitCode = code;
250
269
  if (hook) {
@@ -267,13 +286,16 @@ acceptCoverage = false) {
267
286
  unexecuted: result.unexecuted,
268
287
  regressed: regressed?.length ?? 0,
269
288
  });
270
- if (message !== undefined)
289
+ if (message !== undefined) {
271
290
  process.stderr.write(`${message}\n`);
291
+ }
272
292
  }
273
293
  }
274
294
  }
275
295
  catch (error) {
276
296
  log('reticle_gate_failed', { error: error instanceof Error ? error.message : String(error) });
297
+ // No flow count: the gate broke before it judged, so a count would describe nothing.
298
+ reportGateCompleted({ outcome: GateOutcome.ERROR });
277
299
  process.exitCode = GateExit.FAIL;
278
300
  }
279
301
  }
@@ -17,6 +17,7 @@
17
17
  * written, or belongs to a daemon from another checkout. `kill` asks the PORT, which is the thing the
18
18
  * user actually wants freed, and falls back to the pid file only where the lookup cannot run.
19
19
  */
20
+ import { PortPresence } from '../daemon/binding/port-presence.js';
20
21
  import { type PortHolder } from './ports/port-holder.js';
21
22
  /** What the plan says to do with the port. */
22
23
  export declare const KillAction: {
@@ -62,6 +63,26 @@ export declare function planKill(input: {
62
63
  * wrong token, or no answer: the caller then sends the signal as before.
63
64
  */
64
65
  export declare function requestShutdown(port: number, token?: string | undefined): Promise<boolean>;
66
+ /**
67
+ * How long the second look waits for `/status`. The usual probe gives up after 1 second, and on a
68
+ * busy machine a healthy daemon took longer than that. A daemon that answers slowly is busy, not
69
+ * stuck, and other agents can use it.
70
+ */
71
+ export declare const WEDGED_STATUS_TIMEOUT_MS = 10000;
72
+ /**
73
+ * Stop a Reticle daemon that holds `port` and does not answer. True when it stopped one (#1937).
74
+ *
75
+ * The MCP proxy used to move to another port when it found one. The app's SDK dials the old port,
76
+ * so the new daemon got no page. Stopping the stuck daemon keeps the port the app dials. The holder
77
+ * must have a Reticle daemon's command line, so a stranger and the proxy itself are never signalled.
78
+ */
79
+ export declare function stopWedgedDaemon(port: number, deps?: {
80
+ /** `statusTimeoutMs`: how long to wait for `/status`. Absent: the usual short probe. */
81
+ presence: (statusTimeoutMs?: number) => Promise<PortPresence>;
82
+ holder: () => PortHolder | null;
83
+ kill: () => Promise<boolean>;
84
+ wait: (ms: number) => Promise<void>;
85
+ }): Promise<boolean>;
65
86
  /** `reticle kill` — free the port. Resolves to whether the port is now free. */
66
87
  export declare function runKill(port: number, force: boolean): Promise<boolean>;
67
88
  export {};
@@ -22,7 +22,8 @@ import { isAlive, readPid, removePid } from '../daemon/daemon.js';
22
22
  import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
23
23
  import { log } from '../../log.js';
24
24
  import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
25
- import { captureLookup, findPortHolder } from './ports/port-holder.js';
25
+ import { captureLookup } from './ports/port-holder.js';
26
+ import { identifyPortHolder } from '../daemon/port-state.js';
26
27
  import * as http from 'node:http';
27
28
  import { LOOPBACK_HOST, SHUTDOWN_PATH } from '@reticlehq/core';
28
29
  import { storedPairingToken } from '../../portal/bridge/pairing-token.js';
@@ -50,7 +51,8 @@ export function planKill(input) {
50
51
  return { action: KillAction.NOTHING, forced: false, identifiedListener: false };
51
52
  return { action: KillAction.KILL, pid: recordedPid, forced: false, identifiedListener: false };
52
53
  }
53
- const ours = answersStatus || listener.pid === recordedPid;
54
+ // `reticleDaemon`: its command line says so, though it answers nothing and no pid file names it.
55
+ const ours = answersStatus || listener.pid === recordedPid || true === listener.reticleDaemon;
54
56
  if (ours || force) {
55
57
  return {
56
58
  action: KillAction.KILL,
@@ -156,12 +158,53 @@ async function terminate(pid, asked = false) {
156
158
  */
157
159
  const PROXY_SPARED_NOTE = 'only the listener was signalled. Any `reticle mcp` proxy on this port holds a client connection, ' +
158
160
  'not the port, and was left alone — it goes dormant and starts a fresh daemon on the next tool call.';
161
+ /**
162
+ * How long a daemon that holds the port gets to answer before it counts as stuck. A daemon that has
163
+ * just bound answers in far less, so a second look after this does not stop one that is starting.
164
+ */
165
+ const WEDGED_GRACE_MS = 2_000;
166
+ /**
167
+ * How long the second look waits for `/status`. The usual probe gives up after 1 second, and on a
168
+ * busy machine a healthy daemon took longer than that. A daemon that answers slowly is busy, not
169
+ * stuck, and other agents can use it.
170
+ */
171
+ export const WEDGED_STATUS_TIMEOUT_MS = 10_000;
172
+ /**
173
+ * Stop a Reticle daemon that holds `port` and does not answer. True when it stopped one (#1937).
174
+ *
175
+ * The MCP proxy used to move to another port when it found one. The app's SDK dials the old port,
176
+ * so the new daemon got no page. Stopping the stuck daemon keeps the port the app dials. The holder
177
+ * must have a Reticle daemon's command line, so a stranger and the proxy itself are never signalled.
178
+ */
179
+ export async function stopWedgedDaemon(port, deps = {
180
+ presence: (statusTimeoutMs) => probePresence(port, {
181
+ tcpOpen: probeDaemon,
182
+ status: (p) => fetchStatus(p, LOOPBACK_HOST, false, statusTimeoutMs),
183
+ }),
184
+ holder: () => identifyPortHolder(port, captureLookup),
185
+ kill: () => runKill(port, false),
186
+ wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
187
+ }) {
188
+ if (PortPresence.FOREIGN !== (await deps.presence())) {
189
+ return false;
190
+ }
191
+ const holder = deps.holder();
192
+ if (true !== holder?.reticleDaemon) {
193
+ return false;
194
+ }
195
+ await deps.wait(WEDGED_GRACE_MS);
196
+ if (PortPresence.FOREIGN !== (await deps.presence(WEDGED_STATUS_TIMEOUT_MS))) {
197
+ return false;
198
+ }
199
+ log('reticle_wedged_daemon_stopping', { port, pid: holder.pid });
200
+ return deps.kill();
201
+ }
159
202
  /** `reticle kill` — free the port. Resolves to whether the port is now free. */
160
203
  export async function runKill(port, force) {
161
204
  const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
162
205
  const recorded = readPid(port);
163
206
  const plan = planKill({
164
- listener: findPortHolder(port, captureLookup),
207
+ listener: identifyPortHolder(port, captureLookup),
165
208
  recordedPid: null !== recorded && isAlive(recorded) ? recorded : null,
166
209
  answersStatus: presence === PortPresence.DAEMON,
167
210
  force,
@@ -4,8 +4,9 @@
4
4
  * and re-exports this surface.
5
5
  */
6
6
  import { type ParsedFeedback } from './cli-parse-feedback.js';
7
+ import { DAEMON_INNER_COMMAND } from '../daemon/daemon-argv.js';
7
8
  import { HEADED_FLAG, PORT_FLAG } from './cli-parse-grammar.js';
8
- export { HEADED_FLAG, PORT_FLAG };
9
+ export { DAEMON_INNER_COMMAND, HEADED_FLAG, PORT_FLAG };
9
10
  export { DRIVE_FLAG, HEADLESS_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, } from './cli-parse-serve.js';
10
11
  export { AGENT_FLAG, BUG_FLAG, CONTACT_FLAG, FEEDBACK_KINDS, KIND_FLAG, RATING_FLAG, } from './cli-parse-feedback.js';
11
12
  import { CLI_USAGE } from './cli-usage.js';
@@ -19,7 +20,6 @@ export declare const TelemetryAction: {
19
20
  readonly DISABLE: "disable";
20
21
  };
21
22
  export type TelemetryAction = (typeof TelemetryAction)[keyof typeof TelemetryAction];
22
- export declare const DAEMON_INNER_COMMAND = "_daemon";
23
23
  /**
24
24
  * Every subcommand a person can type, as one closed vocabulary. Telemetry reports which one ran, and
25
25
  * this is what keeps that property low-cardinality and non-identifying: an argument we do not
@@ -110,8 +110,11 @@ export type CliResult = {
110
110
  } | {
111
111
  kind: 'doctor';
112
112
  port: number;
113
+ fix: boolean;
113
114
  } | {
114
115
  kind: 'setup-mcp';
116
+ } | {
117
+ kind: 'setup-wait-agents';
115
118
  } | {
116
119
  kind: 'setup-install';
117
120
  runtimeSecs: number;
@@ -6,10 +6,11 @@
6
6
  import { parseFeedbackArgs } from './cli-parse-feedback.js';
7
7
  import { HEADLESS_FLAG, parseServeFlags } from './cli-parse-serve.js';
8
8
  import { parseVerifySuffix } from './cli-parse-verify.js';
9
+ import { DAEMON_INNER_COMMAND } from '../daemon/daemon-argv.js';
9
10
  import { HEADED_FLAG, PORT_FLAG, VERIFY_COMMAND, missingOperand, missingValue, notANumber, retiredFlag, unknownArgument, CLOUD_COMMANDS, REMOVED_COMMANDS, removedNote, } from './cli-parse-grammar.js';
10
11
  // Re-exported because the daemon's own argv builder reaches for these through this module, which is
11
12
  // where the grammar has always lived as far as every caller is concerned.
12
- export { HEADED_FLAG, PORT_FLAG };
13
+ export { DAEMON_INNER_COMMAND, HEADED_FLAG, PORT_FLAG };
13
14
  export { DRIVE_FLAG, HEADLESS_FLAG, HTTP_FLAG, HTTP_PORT_FLAG, HTTP_TOKEN_FLAG, } from './cli-parse-serve.js';
14
15
  import { FORCE_FLAG } from './cli-kill.js';
15
16
  // Re-exported so every existing importer of these flags is unaffected by the file split.
@@ -44,6 +45,7 @@ const TELEMETRY_COMMAND = 'telemetry';
44
45
  const FEEDBACK_COMMAND = 'feedback';
45
46
  const DOCTOR_COMMAND = 'doctor';
46
47
  const SETUP_COMMAND = 'setup';
48
+ const SETUP_WAIT_AGENTS = 'wait-agents';
47
49
  /** `reticle hook`: the agent prompt hook. `cli.ts` runs it before this parser, and before telemetry. */
48
50
  export const HOOK_COMMAND = 'hook';
49
51
  /** The `reticle telemetry` sub-actions. Bare `reticle telemetry` means `status`. */
@@ -52,7 +54,6 @@ export const TelemetryAction = {
52
54
  ENABLE: 'enable',
53
55
  DISABLE: 'disable',
54
56
  };
55
- export const DAEMON_INNER_COMMAND = '_daemon';
56
57
  /**
57
58
  * Every subcommand a person can type, as one closed vocabulary. Telemetry reports which one ran, and
58
59
  * this is what keeps that property low-cardinality and non-identifying: an argument we do not
@@ -138,6 +139,7 @@ export function knownCommand(arg) {
138
139
  return KNOWN_COMMANDS.has(arg) ? arg : (COMMAND_FLAG_ALIASES.get(arg) ?? UNKNOWN_COMMAND);
139
140
  }
140
141
  const QUIET_FLAG = '--quiet';
142
+ const FIX_FLAG = '--fix';
141
143
  const DRY_RUN_FLAG = '--dry-run';
142
144
  const YES_FLAG = '--yes';
143
145
  const NO_MCP_FLAG = '--no-mcp';
@@ -589,7 +591,7 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
589
591
  }
590
592
  case DOCTOR_COMMAND: {
591
593
  const port = parsePortFlag(rest, defaultPort);
592
- return { kind: 'doctor', port };
594
+ return { kind: 'doctor', port, fix: rest.includes(FIX_FLAG) };
593
595
  }
594
596
  /*
595
597
  * `setup mcp` — register the MCP server with the coding agents on this machine.
@@ -621,8 +623,11 @@ export function parseCliArgs(argv, defaultPort, defaultHeadless = false) {
621
623
  mcp: !rest.includes('--no-mcp'),
622
624
  };
623
625
  }
624
- // Only `mcp` today. A bare `reticle setup` falls to help rather than guessing, because the
625
- // next subcommand here will not be a synonym for this one.
626
+ // `setup wait-agents` — the installer's last step: wait for the agents to connect.
627
+ if (SETUP_WAIT_AGENTS === what) {
628
+ return { kind: 'setup-wait-agents' };
629
+ }
630
+ // A bare `reticle setup` falls to help rather than guessing.
626
631
  if (what !== 'mcp') {
627
632
  return { kind: 'help', topic: SETUP_COMMAND };
628
633
  }
@@ -92,7 +92,7 @@ const COMMANDS = [
92
92
  {
93
93
  name: 'verify',
94
94
  synopsis: 'verify <url> [--explore] [--expect <json>]',
95
- summary: 'Check the saved flows against the running app; exit 0 means pass',
95
+ summary: 'Check saved flows, or one claim with --expect, against the running app; exit 0 = pass',
96
96
  group: HelpGroup.CHECK,
97
97
  detail: VERIFY_DETAIL,
98
98
  },
@@ -110,8 +110,9 @@ const COMMANDS = [
110
110
  },
111
111
  {
112
112
  name: 'doctor',
113
- synopsis: 'doctor [--port N]',
113
+ synopsis: 'doctor [--fix] [--port N]',
114
114
  summary: 'Find what is wrong with the setup, and what is missing from coverage',
115
+ detail: '--fix repairs what Reticle can repair safely, and names the step for the rest.',
115
116
  group: HelpGroup.MACHINE,
116
117
  },
117
118
  {
@@ -230,5 +230,10 @@ export declare function handleVerify(parsed: {
230
230
  resultsJson?: string;
231
231
  /** Acknowledge the replay guard's destructive steps ("Place order") for this run only. */
232
232
  confirmDangerous?: boolean;
233
- }): void;
233
+ },
234
+ /**
235
+ * Called once, after the last verdict line and before the exit. `cli.ts` passes the CI line:
236
+ * `telemetry` already reaches this directory, so this directory does not reach back.
237
+ */
238
+ beforeExit?: () => void): void;
234
239
  export {};