@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
@@ -1,14 +1,21 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { homedir } from 'node:os';
4
- import { claudeAddCommand, McpClient, probeCli, terminalWidth, wrapForTerminal, } from '@reticlehq/init';
4
+ import { mcpLauncher } from '../setup/mcp-launcher.js';
5
+ import { claudePluginInstalled, probeCli, terminalWidth, wrapForTerminal } from '@reticlehq/init';
5
6
  import { tutorialShownSteps } from './tutorial.js';
6
7
  import { setupMcp, knownClientLabels } from '../setup/setup-mcp.js';
7
8
  import { reportInstallSteps } from '../setup/setup-install.js';
8
9
  import { registerOtherAgents } from '../setup/setup-command.js';
10
+ import { agentLines, agentRows, awaitedClients, claudeStateDir, connectionLines, installedAgentRows, waitForAgents, } from '../setup/agent-next-steps.js';
11
+ import { connectedAgentClients } from '../../surface/mcp/peer/agents-connected.js';
12
+ import { reticleStateHome } from '../daemon/daemon.js';
9
13
  /** How many coding agents on this machine have Reticle's tools after this run. */
10
- export function handleSetupMcp(reportStep, standalone = true) {
14
+ export function handleSetupMcp(reportStep, standalone = true,
15
+ /** Wait for the agents to connect. The caller decides: a terminal, and not CI. */
16
+ waitForConnect = false) {
11
17
  const io = {
18
+ launcher: mcpLauncher(),
12
19
  exists: (p) => existsSync(p),
13
20
  readFile: (p) => {
14
21
  try {
@@ -48,25 +55,12 @@ export function handleSetupMcp(reportStep, standalone = true) {
48
55
  : `No coding agent config found. Looked for: ${knownClientLabels().join(', ')}. Install one, then run \`reticle setup mcp\` again.`);
49
56
  return 0;
50
57
  }
51
- for (const id of result.registered)
52
- io.print(` ✓ ${id}`);
53
- for (const id of result.alreadyThere)
54
- io.print(` ✓ ${id} (already set up)`);
55
- for (const id of result.failed) {
56
- io.print(` ⚠ ${id}: Reticle registration failed; this agent may not have the tools.`);
57
- io.print(id === McpClient.CLAUDE_CODE
58
- ? ` Run this command to see the error and retry: ${claudeAddCommand().display}`
59
- : ' Check your agent config file permissions, then retry: reticle setup mcp');
58
+ const rows = agentRows(result, io.launcher);
59
+ for (const line of agentLines(rows)) {
60
+ io.print(line);
60
61
  }
61
- if (result.failed.length > 0)
62
+ if (result.failed.length > 0) {
62
63
  process.exitCode = 1;
63
- // Said, not skipped. These are the clients whose format we will not rewrite, and they used to be
64
- // counted as registered: the line below is the difference between a user who adds four lines to
65
- // one file and a user who believes they are set up and has no tools.
66
- for (const client of result.manual) {
67
- io.print(` ⚠ ${client.id}: add the reticle entry to ${client.configPath} by hand`);
68
- if (client.docs !== undefined)
69
- io.print(` see ${client.docs}`);
70
64
  }
71
65
  /*
72
66
  * The OTHER agents, which `init` already reaches and this did not.
@@ -90,10 +84,14 @@ export function handleSetupMcp(reportStep, standalone = true) {
90
84
  // closing's Next list, where it belongs with the other things to do, not between the stages.
91
85
  if (standalone) {
92
86
  io.print('');
93
- io.print('Restart your agent for it to pick up the new server.');
94
87
  io.print('Then: cd <your project> && reticle init');
88
+ if (waitForConnect) {
89
+ void reportConnections(rows, (line) => {
90
+ io.print(line);
91
+ });
92
+ }
95
93
  }
96
- return result.registered.length + result.alreadyThere.length;
94
+ return result.registered.length + result.alreadyThere.length + result.pluginActive.length;
97
95
  }
98
96
  /**
99
97
  * The one line the launchers read and strip: what they cannot know and the next steps depend on.
@@ -107,11 +105,18 @@ export const INSTALL_RESULT_PREFIX = 'RETICLE_INSTALL';
107
105
  const APP_MARKERS = ['package.json', 'index.html'];
108
106
  /** Whether the folder the installer ran in is an app: `init` runs here, or after a `cd`. */
109
107
  export const InstallAppHere = { HERE: 'here', NONE: 'none' };
108
+ /**
109
+ * `wait=yes`: this CLI has `reticle setup wait-agents`, and an agent has Reticle to wait for. A
110
+ * launcher runs the wait only when it reads this, so a launcher served from main never runs a
111
+ * subcommand that an older CLI does not have.
112
+ */
113
+ const INSTALL_WAIT = ' wait=yes';
110
114
  export function installResultLine(agents, cwd) {
111
115
  const app = APP_MARKERS.some((file) => existsSync(join(cwd, file)))
112
116
  ? InstallAppHere.HERE
113
117
  : InstallAppHere.NONE;
114
- return `${INSTALL_RESULT_PREFIX} agents=${String(agents)} app=${app}`;
118
+ const wait = 0 < agents ? INSTALL_WAIT : '';
119
+ return `${INSTALL_RESULT_PREFIX} agents=${String(agents)} app=${app}${wait}`;
115
120
  }
116
121
  /**
117
122
  * `reticle setup install` — the Node half of the one-line installer.
@@ -132,7 +137,77 @@ export function handleSetupInstall(opts, reportStep) {
132
137
  // No closing here: the installer prints the next step and the agent prompt itself, right after
133
138
  // this returns, so what this prints is progress and nothing else. Those two blocks are what the
134
139
  // ONBOARD steps below now stand for: what Reticle is for, shown before anything is asked of anyone.
135
- for (const step of tutorialShownSteps())
140
+ for (const step of tutorialShownSteps()) {
136
141
  reportStep(step);
142
+ }
137
143
  process.stdout.write(`${installResultLine(agents, process.cwd())}\n`);
138
144
  }
145
+ /** How long to wait for an agent to connect. Short: the person may not restart now. */
146
+ const CONNECT_WAIT_MS = 60_000;
147
+ const CONNECT_POLL_MS = 1000;
148
+ /** Wait only where a person watches: a terminal, and not CI. */
149
+ export function shouldWaitForAgents(isTty, isCi) {
150
+ return true === isTty && !isCi;
151
+ }
152
+ const nodeWait = {
153
+ read: () => connectedAgentClients(reticleStateHome()),
154
+ clock: {
155
+ now: () => Date.now(),
156
+ sleep: (ms) => new Promise((resolve) => {
157
+ setTimeout(resolve, ms);
158
+ }),
159
+ },
160
+ };
161
+ /** Wait (bounded) for the registered agents to connect, then say which did and what is missing. */
162
+ export async function reportConnections(rows, print, wait = nodeWait) {
163
+ const want = awaitedClients(rows);
164
+ if (0 === want.length) {
165
+ return;
166
+ }
167
+ print('');
168
+ print(`Waiting up to ${String(CONNECT_WAIT_MS / 1000)}s for your agents to connect. Press Ctrl+C to skip.`);
169
+ const seen = await waitForAgents(want, wait.read, wait.clock, CONNECT_WAIT_MS, CONNECT_POLL_MS);
170
+ for (const line of connectionLines(rows, seen)) {
171
+ print(line);
172
+ }
173
+ }
174
+ /**
175
+ * `reticle init`: the same block and wait as `reticle setup mcp`, after its restart notice. Print the
176
+ * "Your agents:" block now. Returns the wait, for the caller to run last: a no-op when
177
+ * nobody watches.
178
+ */
179
+ export function agentsBlock(rows, print, interactive, wait = nodeWait) {
180
+ for (const line of agentLines(rows)) {
181
+ print(line);
182
+ }
183
+ return interactive ? () => reportConnections(rows, print, wait) : () => Promise.resolve();
184
+ }
185
+ /** The agents on this machine and whether Reticle is in each config. Read only. */
186
+ export function readAgentRows(cwd) {
187
+ const io = {
188
+ exists: (p) => existsSync(p),
189
+ readFile: (p) => {
190
+ try {
191
+ return readFileSync(p, 'utf8');
192
+ }
193
+ catch {
194
+ return null;
195
+ }
196
+ },
197
+ homeDir: () => homedir(),
198
+ };
199
+ return installedAgentRows(io, cwd, claudePluginInstalled(io), claudeStateDir(), mcpLauncher());
200
+ }
201
+ /**
202
+ * `reticle setup wait-agents`: the installer's last step. The installer sends `setup install` output
203
+ * to a file, so that run cannot wait; this one runs with the terminal attached. Waits only where a
204
+ * person watches.
205
+ */
206
+ export async function handleSetupWaitAgents(interactive) {
207
+ if (!interactive) {
208
+ return;
209
+ }
210
+ await reportConnections(readAgentRows(process.cwd()), (line) => {
211
+ process.stdout.write(`${line}\n`);
212
+ });
213
+ }
@@ -19,6 +19,7 @@ export declare const StatusLabel: {
19
19
  readonly DAEMON: "daemon";
20
20
  readonly SESSIONS: "sessions";
21
21
  readonly AGENT_LINK: "agent link";
22
+ readonly AGENTS: "agents";
22
23
  readonly ACCOUNT: "account";
23
24
  readonly NEXT: "next";
24
25
  readonly SPLIT_BRAIN: "split brain";
@@ -19,6 +19,7 @@ export const StatusLabel = {
19
19
  DAEMON: 'daemon',
20
20
  SESSIONS: 'sessions',
21
21
  AGENT_LINK: 'agent link',
22
+ AGENTS: 'agents',
22
23
  ACCOUNT: 'account',
23
24
  NEXT: 'next',
24
25
  SPLIT_BRAIN: 'split brain',
@@ -49,11 +50,13 @@ function num(fields, key) {
49
50
  }
50
51
  function sessionViews(fields) {
51
52
  const raw = fields['sessions'];
52
- if (!Array.isArray(raw))
53
+ if (!Array.isArray(raw)) {
53
54
  return [];
55
+ }
54
56
  return raw.flatMap((s) => {
55
- if ('object' !== typeof s || null === s)
57
+ if ('object' !== typeof s || null === s) {
56
58
  return [];
59
+ }
57
60
  const r = s;
58
61
  // Each of these explains something a reader would otherwise call a bug: a throttled or hidden
59
62
  // tab answers slowly, a stale one answers about a page that has moved on, and an unresponsive
@@ -81,13 +84,15 @@ export function statusLines(fields) {
81
84
  // and "nothing is here" were the same output once, and the true sentence was said by nothing a
82
85
  // person sees — so the word belongs in the prose, not only in the event.
83
86
  const presence = str(fields, 'presence');
84
- if (presence !== undefined)
87
+ if (presence !== undefined) {
85
88
  lines.push(continuation(`port presence: ${presence}`));
89
+ }
86
90
  // The refusal a FOREIGN port produces. It is the whole answer when it is present, so it rides the
87
91
  // daemon row rather than waiting for a `next` that is not built in that branch.
88
92
  const reason = str(fields, 'reason');
89
- if (reason !== undefined)
93
+ if (reason !== undefined) {
90
94
  lines.push(continuation(reason));
95
+ }
91
96
  if (running) {
92
97
  const count = num(fields, 'sessionCount') ?? 0;
93
98
  lines.push(row(StatusLabel.SESSIONS, count > 0
@@ -99,8 +104,9 @@ export function statusLines(fields) {
99
104
  // The daemon's own explanation for an empty session list, which outranks anything this command
100
105
  // could infer — it is the one holding the connection.
101
106
  const why = str(fields, 'why');
102
- if (why !== undefined)
107
+ if (why !== undefined) {
103
108
  lines.push(continuation(why));
109
+ }
104
110
  }
105
111
  // Who is signed in, and whether this repo is linked: what `whoami` used to answer. Only when the
106
112
  // caller looked, which is what the key being present means.
@@ -112,29 +118,33 @@ export function statusLines(fields) {
112
118
  // A key the platform refused at the last sync is the answer, not "not signed in": a member
113
119
  // removed from the workspace was told to connect while the key was set, and refused.
114
120
  const refused = str(fields, 'keyRefused');
115
- lines.push(row(StatusLabel.ACCOUNT, refused !== undefined
116
- ? `${NOT_OK} refused at the last sync: ${refused}`
117
- : who === undefined
118
- ? `${NOT_OK} not signed in`
119
- : `${OK} signed in as ${who}`));
121
+ lines.push(row(StatusLabel.ACCOUNT, accountText(refused, who)));
120
122
  const apps = appLinks(fields['workspaceApps']);
121
- if (project === undefined && 0 < apps.length)
122
- for (const { app, project: linked } of apps)
123
+ if (project === undefined && 0 < apps.length) {
124
+ for (const { app, project: linked } of apps) {
123
125
  lines.push(continuation(null === linked
124
126
  ? `${app} is not linked: run \`reticle connect\` in ${app}`
125
127
  : `${app} is linked to ${linked}`));
126
- else
128
+ }
129
+ }
130
+ else {
127
131
  lines.push(continuation(project === undefined
128
132
  ? 'this repo is not linked: run `reticle connect`'
129
133
  : `this repo is linked to ${project}${workspace === undefined ? '' : ` in workspace "${workspace}"`}`));
134
+ }
130
135
  }
131
136
  const client = str(fields, 'mcpClient');
132
137
  if (client !== undefined) {
133
138
  // The state in words (`mcpClientSaid`), never the record's own name for it.
134
139
  lines.push(row(StatusLabel.AGENT_LINK, str(fields, 'mcpClientSaid') ?? client));
135
140
  const action = str(fields, 'mcpClientAction');
136
- if (action !== undefined)
141
+ if (action !== undefined) {
137
142
  lines.push(continuation(action));
143
+ }
144
+ }
145
+ // Per agent: connected, or the step still missing. The installer prints the same lines.
146
+ for (const [i, line] of agentLinesOf(fields['agents']).entries()) {
147
+ lines.push(0 === i ? row(StatusLabel.AGENTS, line) : continuation(line));
138
148
  }
139
149
  for (const [label, key] of [
140
150
  [StatusLabel.NEXT, 'nextAction'],
@@ -142,18 +152,41 @@ export function statusLines(fields) {
142
152
  [StatusLabel.UPDATE, 'updateAvailable'],
143
153
  ]) {
144
154
  const value = str(fields, key);
145
- if (value !== undefined)
155
+ if (value !== undefined) {
146
156
  lines.push(row(label, value));
157
+ }
147
158
  }
148
159
  return lines;
149
160
  }
161
+ /** The account row: refused at the last sync, not signed in, or signed in. */
162
+ function accountText(refused, who) {
163
+ if (refused !== undefined) {
164
+ return `${NOT_OK} refused at the last sync: ${refused}`;
165
+ }
166
+ return who === undefined ? `${NOT_OK} not signed in` : `${OK} signed in as ${who}`;
167
+ }
168
+ /** The `agents` field as lines, read defensively: it is the status object, not a typed value. */
169
+ function agentLinesOf(value) {
170
+ if (!Array.isArray(value)) {
171
+ return [];
172
+ }
173
+ return value.flatMap((raw) => {
174
+ if ('object' !== typeof raw || null === raw) {
175
+ return [];
176
+ }
177
+ const { said } = raw;
178
+ return 'string' === typeof said ? [said] : [];
179
+ });
180
+ }
150
181
  /** A workspace root's apps and their links, read defensively off the status fields. */
151
182
  function appLinks(value) {
152
- if (!Array.isArray(value))
183
+ if (!Array.isArray(value)) {
153
184
  return [];
185
+ }
154
186
  return value.flatMap((raw) => {
155
- if ('object' !== typeof raw || null === raw)
187
+ if ('object' !== typeof raw || null === raw) {
156
188
  return [];
189
+ }
157
190
  const { app, project } = raw;
158
191
  return 'string' === typeof app
159
192
  ? [{ app, project: 'string' === typeof project ? project : null }]
@@ -5,10 +5,14 @@ import { hasAnyAppConnectedBefore, hasProjectConnectedBefore, } from '../../memo
5
5
  import { devServersForProject } from '@reticlehq/core';
6
6
  import { readDevServers } from '../daemon/dev-servers.js';
7
7
  import { attachStatusFields } from '../../surface/mcp/attach-memory.js';
8
+ import { connectedAgentClients } from '../../surface/mcp/peer/agents-connected.js';
9
+ import { agentStatuses } from '../setup/agent-next-steps.js';
10
+ import { readAgentRows } from './setup-mcp-cli.js';
8
11
  import { probeDaemon } from '../../surface/mcp/mcp-proxy.js';
9
12
  import { availableUpdate } from '../update/update-nudge.js';
10
13
  import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
11
- import { PortPresence, probePresence, presenceIsUsable, describePresence, } from '../daemon/binding/port-presence.js';
14
+ import { PortPresence, probePresence, presenceIsUsable, } from '../daemon/binding/port-presence.js';
15
+ import { describePortState } from '../daemon/port-state.js';
12
16
  import { summarizeStatus } from './launch/cli-launch.js';
13
17
  import { readProjectId } from './ports/resolve/cli-port.js';
14
18
  import { accountStatusFields } from './cloud-cli.js';
@@ -56,6 +60,16 @@ export function reportStatus(fields, json) {
56
60
  }
57
61
  process.stdout.write(`${statusLines(fields).join('\n')}\n`);
58
62
  }
63
+ /**
64
+ * Each agent on this machine: connected, or the step still missing. The same lines the installer
65
+ * prints after its wait. Absent when no agent is on this machine.
66
+ */
67
+ function agentStatusFields() {
68
+ const rows = readAgentRows(process.cwd());
69
+ return 0 === rows.length
70
+ ? {}
71
+ : { agents: agentStatuses(rows, connectedAgentClients(reticleStateHome())) };
72
+ }
59
73
  export async function handleStatus(port, json = false) {
60
74
  // A broken session file must not cost the reader the rest of the status.
61
75
  const account = await accountStatusFields().catch(() => ({}));
@@ -81,7 +95,7 @@ export async function handleStatus(port, json = false) {
81
95
  // The OTHER half of the install, and the half this command could not see. A host that registers
82
96
  // Reticle and never lists its tools looks exactly like an abandoned install from here — same idle
83
97
  // daemon, same zero sessions — and the user sees a tool that does nothing. See attach-memory.ts.
84
- const client = attachStatusFields(reticleStateHome(), port);
98
+ const client = { ...attachStatusFields(reticleStateHome(), port), ...agentStatusFields() };
85
99
  // What the dev servers themselves said. The reason this command could previously only guess at
86
100
  // whether the app was running is that it had no way to see one; now the ones carrying Reticle
87
101
  // announce, and the advice below stops contradicting the terminal the reader is looking at.
@@ -103,7 +117,7 @@ export async function handleStatus(port, json = false) {
103
117
  port,
104
118
  running: false,
105
119
  presence,
106
- ...(presence === PortPresence.FOREIGN ? { reason: describePresence(presence, port) } : {}),
120
+ ...(presence === PortPresence.FOREIGN ? { reason: describePortState(presence, port) } : {}),
107
121
  // `init` promises this command says why the app has not connected. Without it the answer was
108
122
  // `running: false` and nothing else, which reads as "Reticle is broken" for what is usually
109
123
  // just a daemon that has not been asked to do anything yet.
@@ -1,6 +1,11 @@
1
1
  import type { CloudConfig } from '../../memory/cloud/cloud-sync.js';
2
2
  import { type FreeDrive } from '../../features/harness/platform/platform-drives.js';
3
3
  export declare const MSG_SIGN_IN_FIRST = "Link this folder first: reticle connect. It signs you in if you are not, and it is free, no card.";
4
+ /**
5
+ * The local path that needs no account and no MCP tools. An agent whose client had no reticle_*
6
+ * tools ran `try`, was told to sign in, and stopped with no verdict.
7
+ */
8
+ export declare const MSG_LOCAL_CHECK: string;
4
9
  /** The whole drive, model turns and browser included, before the CLI stops waiting for it. */
5
10
  export declare const TRY_BUDGET_MS = 120000;
6
11
  /** Model turns in the one free drive. Bounds what it costs, not what it may find. */
@@ -43,6 +48,8 @@ export declare function summarizeTry(journeys: readonly TryJourney[]): string[];
43
48
  export declare function runTry(args: TryArgs, ports: TryPorts): Promise<number>;
44
49
  /** The journeys out of an explore report, or one journey graded from the drive when unplanned. */
45
50
  export declare function journeysOf(report: Record<string, unknown>, persona?: string): TryJourney[];
51
+ /** A daemon serving `port`: the one already there, or one started now. Undefined when ready. */
52
+ export declare function ensureTryDaemon(port: number): Promise<string | undefined>;
46
53
  /** What `try` borrows from the cloud commands, passed in so neither module imports the other. */
47
54
  export interface TryCloud {
48
55
  push: () => Promise<number>;
@@ -19,7 +19,8 @@ import { describeUnsynced, unsyncedRoots } from '../../memory/cloud/unsynced-roo
19
19
  import { createNodeFileSystem } from '../../memory/project/fs/fs-port.js';
20
20
  import { defaultPairingTokenDir, readOrCreatePairingTokenSync, } from '../../portal/bridge/pairing-token.js';
21
21
  import { spawnDaemon } from '../daemon/daemon.js';
22
- import { PortPresence, describePresence, presenceIsUsable, probePresence, } from '../daemon/binding/port-presence.js';
22
+ import { PortPresence, presenceIsUsable, probePresence, } from '../daemon/binding/port-presence.js';
23
+ import { describePortState } from '../daemon/port-state.js';
23
24
  import { fetchStatus } from '../daemon/binding/daemon-status-probe.js';
24
25
  import { probeDaemon, waitForDaemonBind } from '../../surface/mcp/mcp-proxy.js';
25
26
  import { readProjectPort } from './ports/resolve/cli-port.js';
@@ -28,6 +29,12 @@ import { seeRunLine } from '../../memory/project/sync-status.js';
28
29
  import { DAEMON_INNER_COMMAND, PORT_FLAG } from './cli-parse.js';
29
30
  import { acquireLease, connectOverSse, endpointFor, exploreToEnd, refusalText, releaseLease, verdictOf, } from './adhoc-verdict.js';
30
31
  export const MSG_SIGN_IN_FIRST = 'Link this folder first: reticle connect. It signs you in if you are not, and it is free, no card.';
32
+ /**
33
+ * The local path that needs no account and no MCP tools. An agent whose client had no reticle_*
34
+ * tools ran `try`, was told to sign in, and stopped with no verdict.
35
+ */
36
+ export const MSG_LOCAL_CHECK = 'To check one thing on this machine with no account and no MCP tools: ' +
37
+ `reticle verify <url> --expect '{"kind":"text","contains":"..."}'. Exit 0 means it held.`;
31
38
  const PERSONA_FLAG = '--persona';
32
39
  /** The whole drive, model turns and browser included, before the CLI stops waiting for it. */
33
40
  export const TRY_BUDGET_MS = 120_000;
@@ -89,6 +96,7 @@ export async function runTry(args, ports) {
89
96
  const cloud = await ports.linked();
90
97
  if (null === cloud) {
91
98
  ports.fail(MSG_SIGN_IN_FIRST);
99
+ ports.fail(MSG_LOCAL_CHECK);
92
100
  return EXIT_FAIL;
93
101
  }
94
102
  const grant = await ports.requestDrive(cloud);
@@ -165,13 +173,13 @@ function tryPort(cwd) {
165
173
  return readProjectPort(cwd) ?? RETICLE_DEFAULT_PORT;
166
174
  }
167
175
  /** A daemon serving `port`: the one already there, or one started now. Undefined when ready. */
168
- async function ensureTryDaemon(port) {
176
+ export async function ensureTryDaemon(port) {
169
177
  const presence = await probePresence(port, { tcpOpen: probeDaemon, status: fetchStatus });
170
178
  if (presenceIsUsable(presence))
171
179
  return undefined;
172
180
  // A stranger on the port: a daemon spawned now could not bind, so say who holds it instead.
173
181
  if (PortPresence.FREE !== presence)
174
- return describePresence(presence, port);
182
+ return describePortState(presence, port);
175
183
  const script = process.argv[1];
176
184
  if (script === undefined)
177
185
  return 'Cannot locate the reticle daemon script.';
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { PlatformLinkVia } from '@reticlehq/core/telemetry';
2
+ import { OnboardingPhase, PlatformLinkVia } from '@reticlehq/core/telemetry';
3
3
  import { hudSignIn } from './cli/cloud-login.js';
4
- import { handleSetupMcp, handleSetupInstall } from './cli/setup-mcp-cli.js';
4
+ import { agentsBlock, handleSetupMcp, handleSetupInstall, handleSetupWaitAgents, readAgentRows, shouldWaitForAgents, } from './cli/setup-mcp-cli.js';
5
5
  import { reportStepFromCli } from './cli-onboarding.js';
6
6
  import { pathToFileURL } from 'node:url';
7
7
  import { openFailureNote, openLaunchFailureNote } from './cli/answers/open-note.js';
8
+ import { writeCiHint } from './cli/ci-hint.js';
9
+ import { currentAutomationHint, isCiEnv } from '../telemetry/automation-hint.js';
8
10
  import { realpathSync } from 'node:fs';
9
11
  import { join } from 'node:path';
10
12
  import { installCrashGuard } from './setup/setup-command.js';
@@ -18,6 +20,7 @@ import { handleHook } from './cli/hook/hook-command.js';
18
20
  import { HOOK_COMMAND } from './cli/cli-parse.js';
19
21
  import { daemonProjectAt, readDaemonRegistry, resolveDaemonForProject, } from './daemon/daemon-resolve.js';
20
22
  import { pickDaemonPortToBind } from './daemon/binding/free-port.js';
23
+ import { exitAfterClose } from './daemon/lifetime/daemon-exit.js';
21
24
  import { isSameOrAbove, portForInit, portFromEnv } from './setup/init/init-port.js';
22
25
  import { daemonStartOptions } from './cli/daemon-start-options.js';
23
26
  import { handleGate, loadNamedFlows, resolveChangedFiles } from './cli/cli-flow-commands.js';
@@ -34,7 +37,8 @@ import { cmdLink, connectInstalls, isCloudCommand, runCloudCommand } from './cli
34
37
  import { SERVER_VERSION } from './version/identity/server-version.js';
35
38
  import { log } from '../log.js';
36
39
  import { isAlive, removePid, spawnDaemon, discoverDaemonPort, writeDaemonRegistry, } from './daemon/daemon.js';
37
- import { PortPresence, probePresence, presenceIsUsable, describePresence, } from './daemon/binding/port-presence.js';
40
+ import { PortPresence, probePresence, presenceIsUsable } from './daemon/binding/port-presence.js';
41
+ import { describePortState } from './daemon/port-state.js';
38
42
  import { waitForDaemon, probeDaemon } from '../surface/mcp/mcp-proxy.js';
39
43
  import { installDaemonResilience, recordExitReason, DaemonExitReason, } from './daemon/daemon-resilience.js';
40
44
  import { IdleShutdown, resolveIdleShutdownMs, resolveIdleCheckMs } from './daemon/idle-shutdown.js';
@@ -46,11 +50,15 @@ import { NAVIGATE_NO_SESSION_ID, navigateLeftTab, openCandidates } from './cli/o
46
50
  import { fetchStatus } from './daemon/binding/daemon-status-probe.js';
47
51
  import { handleDrive } from './cli/drive/drive-command.js';
48
52
  import { handleVerify } from './cli/cli-verify.js';
53
+ import { FIRST_VERDICT_REASON, bareRunsFirstVerdict } from './cli/first-verdict.js';
54
+ import { cmdFirstVerdict } from './cli/first-verdict-node.js';
55
+ import { reportOnboardingStop } from '../telemetry/onboarding-funnel.js';
49
56
  import { runKill } from './cli/cli-kill.js';
50
57
  import { runInit, buildNodeIo, InitFailure } from '@reticlehq/init';
51
58
  import { continueAfterInit } from './setup/init/init-runtime.js';
52
59
  import { initFirstFlow } from './cli/try-command.js';
53
60
  import { handleDoctor } from './cli/cli-doctor.js';
61
+ import { writeFirstUseWarnings } from './cli/first-use-checks.js';
54
62
  import { serverInitHost } from './setup/init/init-host.js';
55
63
  import { describeLicense } from '../features/license/license.js';
56
64
  import { isLikelyDevServerPort, devServerPortWarning, readProjectPort, readProjectId, workspacePortConflict, projectDirOf, } from './cli/ports/resolve/cli-port.js';
@@ -60,7 +68,7 @@ import { renderHelp } from './cli/cli-usage.js';
60
68
  import { RENAMED_COMMANDS, renamedCommandFor, renamedNote } from './cli/cli-parse-grammar.js';
61
69
  import { handleFeedback, handleTelemetry } from '../telemetry/feedback-cli.js';
62
70
  import { installDaemonTelemetry } from '../telemetry/daemon-telemetry.js';
63
- import { reportCliRun } from '../telemetry/cli-telemetry.js';
71
+ import { reportCliRun, reportGateCompleted } from '../telemetry/cli-telemetry.js';
64
72
  // Re-exported so existing imports (and the CLI tests) keep resolving from '@/cli.js'.
65
73
  export { parseCliArgs, CLI_USAGE };
66
74
  /** Said when `connect` carries on past an init whose setup did not finish. */
@@ -94,6 +102,11 @@ async function handleInit(parsed) {
94
102
  // RETICLE_PORT counts as explicit, as it does for every other command: resolved once here, so the
95
103
  // port written into the project and the port the runtime phase binds are the same number.
96
104
  const explicit = parsed.port ?? portFromEnv(process.env);
105
+ // The bridge port matters only when init goes on to start the app.
106
+ const bridgePort = true === parsed.filesOnly || parsed.dryRun
107
+ ? undefined
108
+ : (explicit ?? readProjectPort(cwd) ?? RETICLE_DEFAULT_PORT);
109
+ await writeFirstUseWarnings(bridgePort);
97
110
  const port = await portForInit(explicit, readProjectPort(cwd), readProjectId(cwd), {
98
111
  // The registry names a daemon's owner; an older daemon that never registered is known by the
99
112
  // projects its connected pages announced.
@@ -134,7 +147,7 @@ async function handleInit(parsed) {
134
147
  deferOutcome: true,
135
148
  continuesToRuntime: true !== parsed.filesOnly && true !== parsed.dryRun,
136
149
  }, io);
137
- await continueAfterInit({ ...parsed, port }, result, io, cwd, initFirstFlow);
150
+ await continueAfterInit({ ...parsed, port }, result, io, cwd, initFirstFlow, (print, mayWait) => agentsBlock(readAgentRows(cwd), print, mayWait && shouldWaitForAgents(process.stdout.isTTY, isCiEnv(process.env))));
138
151
  return result;
139
152
  }
140
153
  // `serve`, `stop` and `restart` live in `cli/lifecycle/daemon-lifecycle.ts`: one idea, and the
@@ -194,7 +207,7 @@ function ensureDaemon(port) {
194
207
  // success for a daemon that never bound" shape. Refuse instead, with the sentence `doctor`
195
208
  // says.
196
209
  if (PortPresence.FREE !== presence) {
197
- throw new Error(describePresence(presence, port));
210
+ throw new Error(describePortState(presence, port));
198
211
  }
199
212
  const scriptPath = process.argv[1];
200
213
  if (scriptPath === undefined) {
@@ -212,6 +225,8 @@ function ensureDaemon(port) {
212
225
  */
213
226
  /** How long `reticle open` waits for the launched page to register before reporting on it. */
214
227
  const OPEN_SESSION_WAIT_MS = 8000;
228
+ /** For the heartbeat's resident memory, in MB. */
229
+ const BYTES_PER_MB = 1024 * 1024;
215
230
  /**
216
231
  * Poll until a NEW session shows up, or the wait runs out. Returns whether one did.
217
232
  *
@@ -375,24 +390,15 @@ function handleDaemonInner(parsed) {
375
390
  // to reach a socket that is still open.
376
391
  server.announceShutdown?.();
377
392
  // Awaited before the close/exit chain: `process.exit(0)` kills an in-flight POST, and this is
378
- // the one event carrying the whole session. A failed send resolves anyway (emit swallows its
379
- // own errors), so this can delay the exit by at most the send timeout, never prevent it.
380
- void daemonTelemetry
381
- // The reason rides out on the session summary, which is the only event that fires at this
382
- // exact moment. The proxy emits the matching `mcp_connection_lost` and cannot know it —
383
- // it sees a socket end and nothing more, which is how a scheduled idle exit came to make up
384
- // the large majority of "outages". See SessionSummary.exit.
385
- .shutdown(reason)
386
- .then(() => server.close())
387
- .then(() => {
393
+ // the one event carrying the whole session. The reason rides out on the session summary;
394
+ // the proxy sees only a socket end. See SessionSummary.exit. The summary and the close share
395
+ // one bound, so a close that never settles cannot keep this process on its port (#1937).
396
+ void exitAfterClose({
397
+ summary: () => daemonTelemetry.shutdown(reason),
398
+ close: () => server.close(),
399
+ }).then((code) => {
388
400
  removePid(parsed.port);
389
- process.exit(0);
390
- })
391
- .catch((err) => {
392
- const message = err instanceof Error ? err.message : String(err);
393
- log('reticle_daemon_close_error', { error: message });
394
- removePid(parsed.port);
395
- process.exit(1);
401
+ process.exit(code);
396
402
  });
397
403
  };
398
404
  // Wrapped rather than passed directly: a signal handler receives the signal name as its first
@@ -432,6 +438,7 @@ function handleDaemonInner(parsed) {
432
438
  facts: () => ({
433
439
  sessions: server.bridge.sessions.count(),
434
440
  served: everServedToolCall(),
441
+ rssMb: Math.round(process.memoryUsage().rss / BYTES_PER_MB),
435
442
  }),
436
443
  }).start();
437
444
  })
@@ -617,6 +624,17 @@ export function main() {
617
624
  void handleInit(parsed);
618
625
  break;
619
626
  case 'serve':
627
+ // Bare `reticle` in a folder Reticle has not wired: the first verdict, with no agent and no
628
+ // `init`. A wired project, and `reticle serve` itself, keep serving.
629
+ if (bareRunsFirstVerdict(argv, myProjectId !== undefined || projectPort !== undefined || portConflict !== undefined)) {
630
+ void cmdFirstVerdict(parsed.port, !parsed.headless, (row) => reportOnboardingStop({ phase: OnboardingPhase.FIRST_RUN, reason: FIRST_VERDICT_REASON, ...row }, { detach: true }))
631
+ .then((code) => process.exit(code))
632
+ .catch((cause) => {
633
+ process.stderr.write(`reticle: ${cause instanceof Error ? cause.message : String(cause)}\n`);
634
+ process.exit(1);
635
+ });
636
+ break;
637
+ }
620
638
  handleServe(parsed);
621
639
  break;
622
640
  case 'stop':
@@ -655,17 +673,24 @@ export function main() {
655
673
  process.exit(1);
656
674
  break;
657
675
  case 'doctor':
658
- void handleDoctor(parsed.port);
676
+ void handleDoctor(parsed.port, parsed.fix, reportStepFromCli);
659
677
  break;
660
678
  case 'setup-mcp':
661
- handleSetupMcp(reportStepFromCli);
679
+ void writeFirstUseWarnings().then(() => {
680
+ handleSetupMcp(reportStepFromCli, true, shouldWaitForAgents(process.stdout.isTTY, isCiEnv(process.env)));
681
+ });
682
+ break;
683
+ case 'setup-wait-agents':
684
+ void handleSetupWaitAgents(shouldWaitForAgents(process.stdout.isTTY, isCiEnv(process.env)));
662
685
  break;
663
686
  case 'setup-install':
664
- handleSetupInstall({
665
- runtimeSecs: parsed.runtimeSecs,
666
- installSecs: parsed.installSecs,
667
- mcp: parsed.mcp,
668
- }, reportStepFromCli);
687
+ void writeFirstUseWarnings().then(() => {
688
+ handleSetupInstall({
689
+ runtimeSecs: parsed.runtimeSecs,
690
+ installSecs: parsed.installSecs,
691
+ mcp: parsed.mcp,
692
+ }, reportStepFromCli);
693
+ });
669
694
  break;
670
695
  case 'open':
671
696
  handleOpen(parsed.port, parsed.url, parsed.navigate);
@@ -674,13 +699,25 @@ export function main() {
674
699
  handleDrive(parsed, readProjectId(process.cwd()));
675
700
  break;
676
701
  case 'verify':
677
- handleVerify(parsed);
702
+ void writeFirstUseWarnings().then(() => {
703
+ handleVerify(parsed, () => {
704
+ writeCiHint({ automation: currentAutomationHint(process.env), machineOutput: false });
705
+ });
706
+ });
678
707
  break;
679
708
  case 'affected':
680
- void handleAffected(parsed.files, parsed.since);
709
+ void handleAffected(parsed.files, parsed.since).then(() => {
710
+ writeCiHint({ automation: currentAutomationHint(process.env), machineOutput: false });
711
+ });
681
712
  break;
682
713
  case 'gate':
683
- void handleGate(parsed.files, parsed.since, parsed.hook, parsed.acceptCoverage);
714
+ void handleGate(parsed.files, parsed.since, parsed.hook, parsed.acceptCoverage, reportGateCompleted).then(() => {
715
+ // A stop hook's output is read by the agent, so it is a machine output mode.
716
+ writeCiHint({
717
+ automation: currentAutomationHint(process.env),
718
+ machineOutput: true === parsed.hook,
719
+ });
720
+ });
684
721
  break;
685
722
  case 'report':
686
723
  void handleReport(parsed.session, parsed.hook);