@mono-agent/agent-app 0.6.2 → 0.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 (313) hide show
  1. package/README.md +259 -19
  2. package/dist/account-home.d.ts +6 -0
  3. package/dist/account-home.d.ts.map +1 -0
  4. package/dist/account-home.js +9 -0
  5. package/dist/account-home.js.map +1 -0
  6. package/dist/adapter-send-proxy.d.ts +18 -0
  7. package/dist/adapter-send-proxy.d.ts.map +1 -0
  8. package/dist/adapter-send-proxy.js +229 -0
  9. package/dist/adapter-send-proxy.js.map +1 -0
  10. package/dist/adapter-send-tools-main.js +62 -4
  11. package/dist/adapter-send-tools-main.js.map +1 -1
  12. package/dist/adapter-send-tools.d.ts +33 -6
  13. package/dist/adapter-send-tools.d.ts.map +1 -1
  14. package/dist/adapter-send-tools.js +485 -87
  15. package/dist/adapter-send-tools.js.map +1 -1
  16. package/dist/app-config.d.ts +3 -1
  17. package/dist/app-config.d.ts.map +1 -1
  18. package/dist/app-config.js +16 -5
  19. package/dist/app-config.js.map +1 -1
  20. package/dist/app.d.ts +27 -0
  21. package/dist/app.d.ts.map +1 -1
  22. package/dist/app.js +761 -147
  23. package/dist/app.js.map +1 -1
  24. package/dist/background-environment.d.ts +16 -0
  25. package/dist/background-environment.d.ts.map +1 -0
  26. package/dist/background-environment.js +68 -0
  27. package/dist/background-environment.js.map +1 -0
  28. package/dist/background-runtime.d.ts +62 -0
  29. package/dist/background-runtime.d.ts.map +1 -0
  30. package/dist/background-runtime.js +1115 -0
  31. package/dist/background-runtime.js.map +1 -0
  32. package/dist/background-snapshot-key.d.ts +13 -0
  33. package/dist/background-snapshot-key.d.ts.map +1 -0
  34. package/dist/background-snapshot-key.js +194 -0
  35. package/dist/background-snapshot-key.js.map +1 -0
  36. package/dist/background-snapshot.d.ts +96 -0
  37. package/dist/background-snapshot.d.ts.map +1 -0
  38. package/dist/background-snapshot.js +450 -0
  39. package/dist/background-snapshot.js.map +1 -0
  40. package/dist/background-worker-lease.d.ts +39 -0
  41. package/dist/background-worker-lease.d.ts.map +1 -0
  42. package/dist/background-worker-lease.js +356 -0
  43. package/dist/background-worker-lease.js.map +1 -0
  44. package/dist/background.d.ts +74 -2
  45. package/dist/background.d.ts.map +1 -1
  46. package/dist/background.js +815 -73
  47. package/dist/background.js.map +1 -1
  48. package/dist/broadcast-recorder.d.ts.map +1 -1
  49. package/dist/broadcast-recorder.js +24 -2
  50. package/dist/broadcast-recorder.js.map +1 -1
  51. package/dist/channel-plugins.d.ts +2 -0
  52. package/dist/channel-plugins.d.ts.map +1 -1
  53. package/dist/channel-plugins.js +8 -0
  54. package/dist/channel-plugins.js.map +1 -1
  55. package/dist/channels.d.ts +10 -0
  56. package/dist/channels.d.ts.map +1 -1
  57. package/dist/channels.js +87 -9
  58. package/dist/channels.js.map +1 -1
  59. package/dist/cli.d.ts +86 -4
  60. package/dist/cli.d.ts.map +1 -1
  61. package/dist/cli.js +2421 -143
  62. package/dist/cli.js.map +1 -1
  63. package/dist/config-reference.d.ts.map +1 -1
  64. package/dist/config-reference.js +448 -6
  65. package/dist/config-reference.js.map +1 -1
  66. package/dist/configuration-proposal-main.d.ts +3 -0
  67. package/dist/configuration-proposal-main.d.ts.map +1 -0
  68. package/dist/configuration-proposal-main.js +13 -0
  69. package/dist/configuration-proposal-main.js.map +1 -0
  70. package/dist/configuration-proposal-tool.d.ts +33 -0
  71. package/dist/configuration-proposal-tool.d.ts.map +1 -0
  72. package/dist/configuration-proposal-tool.js +188 -0
  73. package/dist/configuration-proposal-tool.js.map +1 -0
  74. package/dist/configured-agent.d.ts +16 -0
  75. package/dist/configured-agent.d.ts.map +1 -1
  76. package/dist/configured-agent.js +165 -18
  77. package/dist/configured-agent.js.map +1 -1
  78. package/dist/consumer-contract.d.ts +3 -0
  79. package/dist/consumer-contract.d.ts.map +1 -1
  80. package/dist/consumer-contract.js +44 -1
  81. package/dist/consumer-contract.js.map +1 -1
  82. package/dist/continuation-command.d.ts +15 -0
  83. package/dist/continuation-command.d.ts.map +1 -0
  84. package/dist/continuation-command.js +170 -0
  85. package/dist/continuation-command.js.map +1 -0
  86. package/dist/continuation-config.d.ts +25 -0
  87. package/dist/continuation-config.d.ts.map +1 -0
  88. package/dist/continuation-config.js +236 -0
  89. package/dist/continuation-config.js.map +1 -0
  90. package/dist/continuation-service.d.ts +99 -0
  91. package/dist/continuation-service.d.ts.map +1 -0
  92. package/dist/continuation-service.js +1313 -0
  93. package/dist/continuation-service.js.map +1 -0
  94. package/dist/continuation-store.d.ts +104 -0
  95. package/dist/continuation-store.d.ts.map +1 -0
  96. package/dist/continuation-store.js +688 -0
  97. package/dist/continuation-store.js.map +1 -0
  98. package/dist/continuations.d.ts +167 -0
  99. package/dist/continuations.d.ts.map +1 -0
  100. package/dist/continuations.js +71 -0
  101. package/dist/continuations.js.map +1 -0
  102. package/dist/doctor.d.ts +29 -5
  103. package/dist/doctor.d.ts.map +1 -1
  104. package/dist/doctor.js +1611 -116
  105. package/dist/doctor.js.map +1 -1
  106. package/dist/first-run-managed-memory.d.ts +39 -0
  107. package/dist/first-run-managed-memory.d.ts.map +1 -0
  108. package/dist/first-run-managed-memory.js +584 -0
  109. package/dist/first-run-managed-memory.js.map +1 -0
  110. package/dist/first-run-readiness.d.ts +111 -0
  111. package/dist/first-run-readiness.d.ts.map +1 -0
  112. package/dist/first-run-readiness.js +728 -0
  113. package/dist/first-run-readiness.js.map +1 -0
  114. package/dist/index.d.ts +17 -3
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +8 -1
  117. package/dist/index.js.map +1 -1
  118. package/dist/init.d.ts +95 -3
  119. package/dist/init.d.ts.map +1 -1
  120. package/dist/init.js +1218 -24
  121. package/dist/init.js.map +1 -1
  122. package/dist/interaction-bridge.d.ts +25 -1
  123. package/dist/interaction-bridge.d.ts.map +1 -1
  124. package/dist/interaction-bridge.js +281 -19
  125. package/dist/interaction-bridge.js.map +1 -1
  126. package/dist/launchd.d.ts +16 -6
  127. package/dist/launchd.d.ts.map +1 -1
  128. package/dist/launchd.js +30 -15
  129. package/dist/launchd.js.map +1 -1
  130. package/dist/local-configuration.d.ts +99 -0
  131. package/dist/local-configuration.d.ts.map +1 -0
  132. package/dist/local-configuration.js +1299 -0
  133. package/dist/local-configuration.js.map +1 -0
  134. package/dist/managed-runtime-packages.d.ts +9 -0
  135. package/dist/managed-runtime-packages.d.ts.map +1 -0
  136. package/dist/managed-runtime-packages.js +56 -0
  137. package/dist/managed-runtime-packages.js.map +1 -0
  138. package/dist/memory-command.d.ts +4 -0
  139. package/dist/memory-command.d.ts.map +1 -1
  140. package/dist/memory-command.js +751 -27
  141. package/dist/memory-command.js.map +1 -1
  142. package/dist/memory-embedding-service.d.ts +33 -0
  143. package/dist/memory-embedding-service.d.ts.map +1 -0
  144. package/dist/memory-embedding-service.js +207 -0
  145. package/dist/memory-embedding-service.js.map +1 -0
  146. package/dist/memory-recall.d.ts +43 -14
  147. package/dist/memory-recall.d.ts.map +1 -1
  148. package/dist/memory-recall.js +103 -33
  149. package/dist/memory-recall.js.map +1 -1
  150. package/dist/memory-retrieval.d.ts +66 -0
  151. package/dist/memory-retrieval.d.ts.map +1 -0
  152. package/dist/memory-retrieval.js +332 -0
  153. package/dist/memory-retrieval.js.map +1 -0
  154. package/dist/modules/base.d.ts +6 -5
  155. package/dist/modules/base.d.ts.map +1 -1
  156. package/dist/modules/base.js +11 -7
  157. package/dist/modules/base.js.map +1 -1
  158. package/dist/modules/catalog.d.ts.map +1 -1
  159. package/dist/modules/catalog.js +141 -25
  160. package/dist/modules/catalog.js.map +1 -1
  161. package/dist/modules/index.d.ts +2 -2
  162. package/dist/modules/index.d.ts.map +1 -1
  163. package/dist/modules/index.js +1 -1
  164. package/dist/modules/index.js.map +1 -1
  165. package/dist/modules/known-tools.d.ts +3 -0
  166. package/dist/modules/known-tools.d.ts.map +1 -1
  167. package/dist/modules/known-tools.js +11 -1
  168. package/dist/modules/known-tools.js.map +1 -1
  169. package/dist/modules/types.d.ts +20 -6
  170. package/dist/modules/types.d.ts.map +1 -1
  171. package/dist/modules/types.js.map +1 -1
  172. package/dist/notify-runtime.d.ts +26 -0
  173. package/dist/notify-runtime.d.ts.map +1 -0
  174. package/dist/notify-runtime.js +27 -0
  175. package/dist/notify-runtime.js.map +1 -0
  176. package/dist/notify-tool.d.ts +51 -0
  177. package/dist/notify-tool.d.ts.map +1 -0
  178. package/dist/notify-tool.js +182 -0
  179. package/dist/notify-tool.js.map +1 -0
  180. package/dist/package-version.d.ts +3 -0
  181. package/dist/package-version.d.ts.map +1 -0
  182. package/dist/package-version.js +14 -0
  183. package/dist/package-version.js.map +1 -0
  184. package/dist/pi-auth-store-inspection.d.ts +21 -0
  185. package/dist/pi-auth-store-inspection.d.ts.map +1 -0
  186. package/dist/pi-auth-store-inspection.js +120 -0
  187. package/dist/pi-auth-store-inspection.js.map +1 -0
  188. package/dist/pi-oauth-login-main.d.ts +3 -0
  189. package/dist/pi-oauth-login-main.d.ts.map +1 -0
  190. package/dist/pi-oauth-login-main.js +14 -0
  191. package/dist/pi-oauth-login-main.js.map +1 -0
  192. package/dist/pi-oauth-login.d.ts +22 -0
  193. package/dist/pi-oauth-login.d.ts.map +1 -0
  194. package/dist/pi-oauth-login.js +79 -0
  195. package/dist/pi-oauth-login.js.map +1 -0
  196. package/dist/proactive-notify.d.ts +2 -0
  197. package/dist/proactive-notify.d.ts.map +1 -1
  198. package/dist/proactive-notify.js +1 -0
  199. package/dist/proactive-notify.js.map +1 -1
  200. package/dist/process-incarnation.d.ts +23 -0
  201. package/dist/process-incarnation.d.ts.map +1 -0
  202. package/dist/process-incarnation.js +208 -0
  203. package/dist/process-incarnation.js.map +1 -0
  204. package/dist/project-skills.d.ts +40 -0
  205. package/dist/project-skills.d.ts.map +1 -0
  206. package/dist/project-skills.js +573 -0
  207. package/dist/project-skills.js.map +1 -0
  208. package/dist/provider-setup.d.ts +117 -2
  209. package/dist/provider-setup.d.ts.map +1 -1
  210. package/dist/provider-setup.js +1505 -49
  211. package/dist/provider-setup.js.map +1 -1
  212. package/dist/readiness-probe-worker.d.ts +12 -0
  213. package/dist/readiness-probe-worker.d.ts.map +1 -0
  214. package/dist/readiness-probe-worker.js +276 -0
  215. package/dist/readiness-probe-worker.js.map +1 -0
  216. package/dist/readiness-probe.d.ts +100 -0
  217. package/dist/readiness-probe.d.ts.map +1 -0
  218. package/dist/readiness-probe.js +780 -0
  219. package/dist/readiness-probe.js.map +1 -0
  220. package/dist/recipes/base.d.ts +15 -0
  221. package/dist/recipes/base.d.ts.map +1 -0
  222. package/dist/recipes/base.js +51 -0
  223. package/dist/recipes/base.js.map +1 -0
  224. package/dist/recipes/catalog.d.ts +4 -0
  225. package/dist/recipes/catalog.d.ts.map +1 -0
  226. package/dist/recipes/catalog.js +525 -0
  227. package/dist/recipes/catalog.js.map +1 -0
  228. package/dist/recipes/index.d.ts +11 -0
  229. package/dist/recipes/index.d.ts.map +1 -0
  230. package/dist/recipes/index.js +14 -0
  231. package/dist/recipes/index.js.map +1 -0
  232. package/dist/recipes/types.d.ts +70 -0
  233. package/dist/recipes/types.d.ts.map +1 -0
  234. package/dist/recipes/types.js +15 -0
  235. package/dist/recipes/types.js.map +1 -0
  236. package/dist/request-model-override.d.ts +43 -1
  237. package/dist/request-model-override.d.ts.map +1 -1
  238. package/dist/request-model-override.js +178 -19
  239. package/dist/request-model-override.js.map +1 -1
  240. package/dist/run-history.d.ts +32 -0
  241. package/dist/run-history.d.ts.map +1 -0
  242. package/dist/run-history.js +859 -0
  243. package/dist/run-history.js.map +1 -0
  244. package/dist/runs-health.d.ts.map +1 -1
  245. package/dist/runs-health.js +12 -4
  246. package/dist/runs-health.js.map +1 -1
  247. package/dist/runtime-option-extensions.d.ts +14 -0
  248. package/dist/runtime-option-extensions.d.ts.map +1 -0
  249. package/dist/runtime-option-extensions.js +98 -0
  250. package/dist/runtime-option-extensions.js.map +1 -0
  251. package/dist/runtime-routes.d.ts +9 -0
  252. package/dist/runtime-routes.d.ts.map +1 -0
  253. package/dist/runtime-routes.js +13 -0
  254. package/dist/runtime-routes.js.map +1 -0
  255. package/dist/sandbox-manager.d.ts +86 -0
  256. package/dist/sandbox-manager.d.ts.map +1 -0
  257. package/dist/sandbox-manager.js +1402 -0
  258. package/dist/sandbox-manager.js.map +1 -0
  259. package/dist/setup.d.ts +29 -0
  260. package/dist/setup.d.ts.map +1 -0
  261. package/dist/setup.js +97 -0
  262. package/dist/setup.js.map +1 -0
  263. package/dist/supermemory-plugin.d.ts +56 -0
  264. package/dist/supermemory-plugin.d.ts.map +1 -0
  265. package/dist/supermemory-plugin.js +142 -0
  266. package/dist/supermemory-plugin.js.map +1 -0
  267. package/dist/tui-command.d.ts +56 -0
  268. package/dist/tui-command.d.ts.map +1 -1
  269. package/dist/tui-command.js +305 -31
  270. package/dist/tui-command.js.map +1 -1
  271. package/dist/web-command.d.ts +8 -0
  272. package/dist/web-command.d.ts.map +1 -1
  273. package/dist/web-command.js +174 -17
  274. package/dist/web-command.js.map +1 -1
  275. package/dist/wizard/answers.d.ts +31 -4
  276. package/dist/wizard/answers.d.ts.map +1 -1
  277. package/dist/wizard/answers.js +141 -29
  278. package/dist/wizard/answers.js.map +1 -1
  279. package/dist/wizard/from-flags.d.ts +7 -0
  280. package/dist/wizard/from-flags.d.ts.map +1 -1
  281. package/dist/wizard/from-flags.js +36 -2
  282. package/dist/wizard/from-flags.js.map +1 -1
  283. package/dist/wizard/index.d.ts +4 -4
  284. package/dist/wizard/index.d.ts.map +1 -1
  285. package/dist/wizard/index.js +2 -2
  286. package/dist/wizard/index.js.map +1 -1
  287. package/dist/wizard/model-discovery.d.ts +33 -2
  288. package/dist/wizard/model-discovery.d.ts.map +1 -1
  289. package/dist/wizard/model-discovery.js +826 -108
  290. package/dist/wizard/model-discovery.js.map +1 -1
  291. package/dist/wizard/presets.d.ts +1 -1
  292. package/dist/wizard/presets.d.ts.map +1 -1
  293. package/dist/wizard/presets.js +7 -12
  294. package/dist/wizard/presets.js.map +1 -1
  295. package/dist/wizard/prompts.d.ts +56 -9
  296. package/dist/wizard/prompts.d.ts.map +1 -1
  297. package/dist/wizard/prompts.js +172 -20
  298. package/dist/wizard/prompts.js.map +1 -1
  299. package/dist/wizard/run.d.ts +59 -3
  300. package/dist/wizard/run.d.ts.map +1 -1
  301. package/dist/wizard/run.js +1290 -191
  302. package/dist/wizard/run.js.map +1 -1
  303. package/package.json +21 -16
  304. package/resources/srt/package-lock.json +66 -0
  305. package/resources/srt/package.json +9 -0
  306. package/schema/mono-agent.config.schema.json +587 -20
  307. package/skills/mono-agent-composer/SKILL.md +6 -6
  308. package/skills/mono-agent-composer/references/config-blueprint.md +11 -10
  309. package/skills/mono-agent-composer/references/discovery-questions.md +33 -18
  310. package/skills/mono-agent-composer/references/feature-coverage.md +14 -12
  311. package/skills/mono-agent-composer/references/package-map.md +2 -2
  312. package/skills/mono-agent-composer/references/playbooks.md +8 -7
  313. package/skills/mono-agent-composer/references/validation.md +3 -3
package/dist/cli.js CHANGED
@@ -1,32 +1,42 @@
1
1
  #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
2
3
  import { stat } from "node:fs/promises";
3
4
  import { createRequire } from "node:module";
4
- import { basename, resolve } from "node:path";
5
+ import { basename, dirname, resolve } from "node:path";
5
6
  import process from "node:process";
7
+ import { emitKeypressEvents } from "node:readline";
6
8
  import { fileURLToPath } from "node:url";
7
- import { buildMonoAgentConfigView, EFFORT_LEVELS, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, } from "@mono-agent/config";
9
+ import { buildMonoAgentConfigView, EFFORT_LEVELS, findJsonSecretConfigWarnings, findRemovedConfigWarnings, MAX_AGENT_NAME_LENGTH, readMonoAgentConfigJson, redactMonoAgentConfig, } from "@mono-agent/config";
8
10
  import { listRecordedRuns } from "@mono-agent/observability";
9
- import { describeSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
11
+ import { describeSandboxEffectiveState, parseMonoRuntimeModelReference, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
10
12
  import { startMonoAgentApp } from "./app.js";
11
13
  import { describeSensitiveDataExportWarning, isAppCoreConfigError, loadAppCoreConfig, phoenixAppBaseUrl, } from "./app-config.js";
12
14
  import { runAuditRuns } from "./audit-runs.js";
13
15
  import { runBackfill } from "./backfill.js";
14
- import { defaultBackgroundDeps, resolveInstanceTarget, restartBackground, startBackground, statusBackground, stopBackground, tailLogs, } from "./background.js";
16
+ import { acquireBackgroundWorkerLease, canonicalBackgroundConfigPath, defaultBackgroundDeps, ensureBackgroundReady, forceRestartBackground, managedBackgroundEnvironment, resolveInstanceTarget, restartBackground, startBackground, statusBackground, stopBackground, tailLogs, } from "./background.js";
17
+ import { MANAGED_BACKGROUND_WORKER_ENV, sanitizeManagedBackgroundWorkerEnvironment, } from "./background-runtime.js";
18
+ import { captureBackgroundSnapshot, decodeBackgroundSnapshot, captureDurableBackgroundInputs, loadDurableBackgroundEnvironment, materializeBackgroundRuntimeInputs, } from "./background-snapshot.js";
15
19
  import { collectChannelConfigViews } from "./channel-config-view.js";
16
20
  import { formatChannelFactValue } from "./channel-fact-format.js";
17
21
  import { resolveChannelDrivers } from "./channels.js";
18
22
  import { findUnknownAppConfigWarnings } from "./config-reference.js";
19
23
  import { validateMonoAgentFolder } from "./doctor.js";
20
- import { initMonoAgentFolder } from "./init.js";
24
+ import { effectiveFirstRunEnvironment, evaluateFirstRunConfigurationReadiness, evaluateFirstRunReadiness, hasSensitivePersistedEnvironmentValue, piAuthPathBackgroundConflict, readCliConfigSnapshot, readCliDotenvFile, readCliDotenvSnapshot, resolveEffectivePiAuthPath, selectedSecretEnvironmentConflicts, selectedSecretValues, unexpectedPersistedMonoAgentOverrides, validateWizardPlanInStaging, withExactProcessEnvironment, } from "./first-run-readiness.js";
25
+ import { initMonoAgentFolder, SecretEnvConcurrentModificationError, verifySecretEnvPersistenceGuard, } from "./init.js";
21
26
  import { installComposerSkill } from "./install-skill.js";
27
+ import { deriveLaunchdLabel, launchdPathsFor } from "./launchd.js";
28
+ import { checkManagedProjectSkills, updateManagedProjectSkills } from "./project-skills.js";
22
29
  import { runMetrics } from "./metrics.js";
23
30
  import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
24
31
  import { purgeSessions } from "./sessions.js";
25
- import { composeWizardPlan } from "./wizard/answers.js";
32
+ import { composeWizardPlan, referencedSetupModelRefs } from "./wizard/answers.js";
26
33
  import { answersFromCli, isWithChannel } from "./wizard/from-flags.js";
27
34
  import { findPreset, PRESET_CATALOG, presetAnswers, presetIds, RECIPE_TO_PRESET } from "./wizard/presets.js";
28
- import { runInitWizard } from "./wizard/run.js";
29
- import { executeProviderSetupPlan, planProviderSetup, providerSetupActionCommandLine } from "./provider-setup.js";
35
+ import { runInitWizard, runSetupRepairWizard } from "./wizard/run.js";
36
+ import { detectProviderCredentialStates, executeProviderSetupPlan, isProviderSetupPiApiKeyAction, planProviderSetup, providerSetupActionCommandLine, } from "./provider-setup.js";
37
+ import { readinessProbeTimeoutMs, runAllRouteReadinessProbe } from "./readiness-probe.js";
38
+ import { checkSandboxRuntime, sandboxRuntimeStatus, setupManagedSrt, } from "./sandbox-manager.js";
39
+ import * as p from "@clack/prompts";
30
40
  import * as ui from "./ui.js";
31
41
  const DEFAULT_LOG_LINES = 200;
32
42
  // Node's maximum setInterval/setTimeout delay (2^31 - 1 ms, ~24.8 days). A
@@ -34,7 +44,7 @@ const DEFAULT_LOG_LINES = 200;
34
44
  // busy-waiting; larger values silently overflow to a 1ms delay.
35
45
  const KEEP_ALIVE_INTERVAL_MS = 2_147_483_647;
36
46
  const BACKGROUND_COMMANDS = ["start", "restart", "stop", "status", "logs"];
37
- const KNOWN_COMMANDS = ["init", "setup", "validate", "doctor", "config", "recipes", "presets", "start", "restart", "stop", "status", "logs", "tui", "web", "install-skill", "backfill", "audit-runs", "metrics", "memory"];
47
+ const KNOWN_COMMANDS = ["init", "setup", "validate", "doctor", "auth", "sandbox", "config", "recipes", "presets", "start", "restart", "stop", "status", "logs", "tui", "web", "install-skill", "backfill", "audit-runs", "metrics", "memory", "continuations"];
38
48
  export function parseCliArgs(argv) {
39
49
  const [command, ...rest] = argv;
40
50
  if (command === undefined || command === "help" || command === "--help" || command === "-h") {
@@ -58,8 +68,12 @@ export function parseCliArgs(argv) {
58
68
  : command);
59
69
  const isLogs = cmd === "logs";
60
70
  let configPath;
71
+ let name;
61
72
  let model;
62
73
  let fallbackModels;
74
+ const fallbacks = [];
75
+ let canAssignFallbackEffort = false;
76
+ let routeSafety;
63
77
  let effort;
64
78
  let memory;
65
79
  let preset;
@@ -67,8 +81,12 @@ export function parseCliArgs(argv) {
67
81
  let withChannels;
68
82
  let yes = false;
69
83
  let auth = false;
84
+ let piAuthPath;
85
+ let apiKeyStdin = false;
86
+ let codexAuthMode;
70
87
  const positionals = [];
71
88
  let envFile;
89
+ let expectedBackgroundSnapshot;
72
90
  let target;
73
91
  let force = false;
74
92
  let foreground = false;
@@ -85,16 +103,26 @@ export function parseCliArgs(argv) {
85
103
  let consumerPath;
86
104
  let agent;
87
105
  let conversation;
106
+ let local = false;
107
+ let configure = false;
108
+ let project = false;
109
+ let check = false;
110
+ let update = false;
88
111
  let staleAfterMs;
89
112
  let json = false;
113
+ let strict = false;
90
114
  let limit;
115
+ let cursor;
91
116
  let host;
92
117
  let port;
93
118
  let open;
94
119
  let allowNonLoopback;
120
+ let showAuthUrl;
95
121
  let maxRunsPerInstance;
96
122
  for (let i = 0; i < rest.length; i += 1) {
97
123
  const flag = rest[i];
124
+ if (flag !== "--fallback-effort")
125
+ canAssignFallbackEffort = false;
98
126
  switch (flag) {
99
127
  case "--config":
100
128
  configPath = requireValue(rest, ++i, flag);
@@ -140,6 +168,21 @@ export function parseCliArgs(argv) {
140
168
  case "--conversation":
141
169
  conversation = requireValue(rest, ++i, flag);
142
170
  break;
171
+ case "--local":
172
+ local = true;
173
+ break;
174
+ case "--configure":
175
+ configure = true;
176
+ break;
177
+ case "--project":
178
+ project = true;
179
+ break;
180
+ case "--check":
181
+ check = true;
182
+ break;
183
+ case "--update":
184
+ update = true;
185
+ break;
143
186
  case "--stale-after-ms": {
144
187
  const raw = requireValue(rest, ++i, flag);
145
188
  const parsed = Number(raw);
@@ -152,15 +195,24 @@ export function parseCliArgs(argv) {
152
195
  case "--json":
153
196
  json = true;
154
197
  break;
198
+ case "--strict":
199
+ strict = true;
200
+ break;
155
201
  case "--limit": {
156
202
  const raw = requireValue(rest, ++i, flag);
157
203
  const parsed = Number(raw);
158
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
159
- throw new Error("--limit must be an integer between 1 and 100.");
204
+ const maximum = cmd === "continuations" ? 500 : 100;
205
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > maximum) {
206
+ throw new Error(`--limit must be an integer between 1 and ${String(maximum)}.`);
160
207
  }
161
208
  limit = parsed;
162
209
  break;
163
210
  }
211
+ case "--cursor":
212
+ cursor = requireValue(rest, ++i, flag).trim();
213
+ if (cursor.length === 0 || cursor.length > 512)
214
+ throw new Error("--cursor must be 1-512 characters.");
215
+ break;
164
216
  case "--host":
165
217
  host = requireValue(rest, ++i, flag);
166
218
  break;
@@ -179,6 +231,9 @@ export function parseCliArgs(argv) {
179
231
  case "--allow-non-loopback":
180
232
  allowNonLoopback = true;
181
233
  break;
234
+ case "--show-auth-url":
235
+ showAuthUrl = true;
236
+ break;
182
237
  case "--max-runs": {
183
238
  const raw = requireValue(rest, ++i, flag);
184
239
  const parsed = Number(raw);
@@ -191,12 +246,55 @@ export function parseCliArgs(argv) {
191
246
  case "--model":
192
247
  model = requireValue(rest, ++i, flag);
193
248
  break;
249
+ case "--name":
250
+ name = requireValue(rest, ++i, flag).trim();
251
+ if (Array.from(name).length === 0
252
+ || Array.from(name).length > MAX_AGENT_NAME_LENGTH
253
+ || /[\u0000-\u001f\u007f]/u.test(name)) {
254
+ throw new Error(`--name must be 1-${MAX_AGENT_NAME_LENGTH} characters on one line.`);
255
+ }
256
+ break;
194
257
  case "--fallback-models":
195
258
  fallbackModels = requireValue(rest, ++i, flag)
196
259
  .split(",")
197
260
  .map((entry) => entry.trim())
198
261
  .filter((entry) => entry.length > 0);
199
262
  break;
263
+ case "--fallback": {
264
+ const fallbackModel = requireValue(rest, ++i, flag).trim();
265
+ if (fallbacks.some((entry) => entry.model === fallbackModel)) {
266
+ throw new Error(`Duplicate --fallback model \`${fallbackModel}\`.`);
267
+ }
268
+ fallbacks.push({ model: fallbackModel });
269
+ canAssignFallbackEffort = true;
270
+ break;
271
+ }
272
+ case "--fallback-effort": {
273
+ if (!canAssignFallbackEffort || fallbacks.length === 0) {
274
+ throw new Error("--fallback-effort must immediately follow the --fallback it configures.");
275
+ }
276
+ const raw = requireValue(rest, ++i, flag);
277
+ if (raw !== "provider-default" && !EFFORT_LEVELS.includes(raw)) {
278
+ throw new Error(`--fallback-effort must be provider-default or ${EFFORT_LEVELS.join(", ")}.`);
279
+ }
280
+ if (raw !== "provider-default") {
281
+ const current = fallbacks[fallbacks.length - 1];
282
+ fallbacks[fallbacks.length - 1] = {
283
+ ...current,
284
+ effort: raw,
285
+ };
286
+ }
287
+ canAssignFallbackEffort = false;
288
+ break;
289
+ }
290
+ case "--route-safety": {
291
+ const raw = requireValue(rest, ++i, flag);
292
+ if (raw !== "uniform" && raw !== "per-route-native") {
293
+ throw new Error("--route-safety must be uniform or per-route-native.");
294
+ }
295
+ routeSafety = raw;
296
+ break;
297
+ }
200
298
  case "--effort": {
201
299
  const raw = requireValue(rest, ++i, flag);
202
300
  if (!EFFORT_LEVELS.includes(raw)) {
@@ -225,6 +323,20 @@ export function parseCliArgs(argv) {
225
323
  case "--auth":
226
324
  auth = true;
227
325
  break;
326
+ case "--pi-auth-path":
327
+ piAuthPath = requireValue(rest, ++i, flag);
328
+ break;
329
+ case "--api-key-stdin":
330
+ apiKeyStdin = true;
331
+ break;
332
+ case "--codex-auth": {
333
+ const raw = requireValue(rest, ++i, flag);
334
+ if (raw !== "browser" && raw !== "device") {
335
+ throw new Error("--codex-auth must be browser or device.");
336
+ }
337
+ codexAuthMode = raw;
338
+ break;
339
+ }
228
340
  case "--with":
229
341
  withChannels = requireValue(rest, ++i, flag)
230
342
  .split(",")
@@ -234,6 +346,9 @@ export function parseCliArgs(argv) {
234
346
  case "--env-file":
235
347
  envFile = requireValue(rest, ++i, flag);
236
348
  break;
349
+ case "--expected-background-snapshot":
350
+ expectedBackgroundSnapshot = requireValue(rest, ++i, flag);
351
+ break;
237
352
  case "--target": {
238
353
  const raw = requireValue(rest, ++i, flag);
239
354
  if (raw !== "claude" && raw !== "codex" && raw !== "both") {
@@ -284,24 +399,75 @@ export function parseCliArgs(argv) {
284
399
  if (consumerPath !== undefined && cmd !== "validate" && cmd !== "audit-runs") {
285
400
  throw new Error("--consumer is only supported for `mono-agent validate` and `mono-agent audit-runs`.");
286
401
  }
287
- if ((host !== undefined || port !== undefined || open !== undefined || allowNonLoopback !== undefined || maxRunsPerInstance !== undefined) &&
402
+ if ((local || configure) && cmd !== "tui") {
403
+ throw new Error("--local and --configure are only supported for `mono-agent tui`.");
404
+ }
405
+ if (configure && local) {
406
+ throw new Error("--configure attaches to the authoritative background agent; omit --local.");
407
+ }
408
+ if ((project || check || update) && cmd !== "install-skill") {
409
+ throw new Error("--project, --check, and --update are only supported for `mono-agent install-skill`.");
410
+ }
411
+ if ((check || update) && !project) {
412
+ throw new Error("--check and --update require `mono-agent install-skill --project`.");
413
+ }
414
+ if (check && update) {
415
+ throw new Error("Choose either --check or --update for project skills.");
416
+ }
417
+ if ((host !== undefined
418
+ || port !== undefined
419
+ || open !== undefined
420
+ || allowNonLoopback !== undefined
421
+ || showAuthUrl !== undefined
422
+ || maxRunsPerInstance !== undefined) &&
288
423
  cmd !== "web") {
289
- throw new Error("--host, --port, --no-open, --allow-non-loopback, and --max-runs are only supported for `mono-agent web`.");
424
+ throw new Error("--host, --port, --no-open, --allow-non-loopback, --show-auth-url, and --max-runs are only supported for `mono-agent web`.");
290
425
  }
291
426
  if (includeMemory && cmd !== "audit-runs" && cmd !== "metrics" && cmd !== "backfill" && cmd !== "web") {
292
427
  throw new Error("--include-memory is only supported for `mono-agent audit-runs`, `mono-agent metrics`, `mono-agent backfill`, and `mono-agent web`.");
293
428
  }
294
- if (limit !== undefined && cmd !== "memory") {
295
- throw new Error("--limit is only supported for `mono-agent memory`.");
429
+ if (limit !== undefined && cmd !== "memory" && cmd !== "continuations") {
430
+ throw new Error("--limit is only supported for `mono-agent memory` and `mono-agent continuations list`.");
431
+ }
432
+ if ((limit !== undefined || cursor !== undefined) && cmd === "continuations" && (positionals[0] ?? "list") !== "list") {
433
+ throw new Error("--limit and --cursor are only supported for `mono-agent continuations list`.");
434
+ }
435
+ if (cursor !== undefined && cmd !== "continuations") {
436
+ throw new Error("--cursor is only supported for `mono-agent continuations list`.");
437
+ }
438
+ if (strict && (cmd !== "memory" || (positionals[0] ?? "stats") !== "audit")) {
439
+ throw new Error("--strict is only supported for `mono-agent memory audit`.");
296
440
  }
297
441
  if (auth && cmd !== "init") {
298
442
  throw new Error("--auth is only supported for `mono-agent init`.");
299
443
  }
444
+ if (piAuthPath !== undefined && cmd !== "auth") {
445
+ throw new Error("--pi-auth-path is only supported for `mono-agent auth`.");
446
+ }
447
+ if (apiKeyStdin && cmd !== "auth") {
448
+ throw new Error("--api-key-stdin is only supported for `mono-agent auth login <provider>`.");
449
+ }
450
+ if (codexAuthMode !== undefined && cmd !== "init" && cmd !== "auth") {
451
+ throw new Error("--codex-auth is only supported for `mono-agent init` and `mono-agent auth login codex`.");
452
+ }
453
+ if (fallbackModels !== undefined && fallbacks.length > 0) {
454
+ throw new Error("Use either legacy --fallback-models or repeated --fallback flags, not both.");
455
+ }
456
+ if (fallbackModels !== undefined && new Set(fallbackModels).size !== fallbackModels.length) {
457
+ throw new Error("--fallback-models contains a duplicate model reference.");
458
+ }
459
+ const selectedFallbackModels = fallbackModels ?? fallbacks.map((fallback) => fallback.model);
460
+ if (model !== undefined && selectedFallbackModels.includes(model)) {
461
+ throw new Error(`Primary --model \`${model}\` cannot also be a fallback.`);
462
+ }
300
463
  return {
301
464
  command: cmd,
302
465
  ...(configPath === undefined ? {} : { configPath }),
466
+ ...(name === undefined ? {} : { name }),
303
467
  ...(model === undefined ? {} : { model }),
304
468
  ...(fallbackModels === undefined ? {} : { fallbackModels }),
469
+ ...(fallbacks.length === 0 ? {} : { fallbacks }),
470
+ ...(routeSafety === undefined ? {} : { routeSafety }),
305
471
  ...(effort === undefined ? {} : { effort }),
306
472
  ...(memory === undefined ? {} : { memory }),
307
473
  ...(preset === undefined ? {} : { preset }),
@@ -309,8 +475,12 @@ export function parseCliArgs(argv) {
309
475
  ...(withChannels === undefined ? {} : { withChannels }),
310
476
  ...(yes ? { yes } : {}),
311
477
  ...(auth ? { auth } : {}),
478
+ ...(piAuthPath === undefined ? {} : { piAuthPath }),
479
+ ...(apiKeyStdin ? { apiKeyStdin } : {}),
480
+ ...(codexAuthMode === undefined ? {} : { codexAuthMode }),
312
481
  positionals,
313
482
  ...(envFile === undefined ? {} : { envFile }),
483
+ ...(expectedBackgroundSnapshot === undefined ? {} : { expectedBackgroundSnapshot }),
314
484
  ...(target === undefined ? {} : { target }),
315
485
  force,
316
486
  foreground,
@@ -327,13 +497,21 @@ export function parseCliArgs(argv) {
327
497
  ...(consumerPath === undefined ? {} : { consumerPath }),
328
498
  ...(staleAfterMs === undefined ? {} : { staleAfterMs }),
329
499
  ...(json ? { json } : {}),
500
+ ...(strict ? { strict } : {}),
330
501
  ...(limit === undefined ? {} : { limit }),
502
+ ...(cursor === undefined ? {} : { cursor }),
331
503
  ...(agent === undefined ? {} : { agent }),
332
504
  ...(conversation === undefined ? {} : { conversation }),
505
+ ...(local ? { local } : {}),
506
+ ...(configure ? { configure } : {}),
507
+ ...(project ? { project } : {}),
508
+ ...(check ? { check } : {}),
509
+ ...(update ? { update } : {}),
333
510
  ...(host === undefined ? {} : { host }),
334
511
  ...(port === undefined ? {} : { port }),
335
512
  ...(open === undefined ? {} : { open }),
336
513
  ...(allowNonLoopback === undefined ? {} : { allowNonLoopback }),
514
+ ...(showAuthUrl === undefined ? {} : { showAuthUrl }),
337
515
  ...(maxRunsPerInstance === undefined ? {} : { maxRunsPerInstance }),
338
516
  };
339
517
  }
@@ -370,14 +548,18 @@ function requireValue(args, index, flag) {
370
548
  const HELP_COMMANDS = [
371
549
  {
372
550
  signature: "mono-agent init [--preset <id>] [--with <csv>] [--yes] [--auth] [--dry-run]\n" +
373
- " [--model <ref>] [--fallback-models <csv>] [--effort <level>]\n" +
374
- " [--memory lite|journal|bujo]",
551
+ " [--name <display-name>] [--model <ref>] [--effort <level>]\n" +
552
+ " [--fallback <ref> [--fallback-effort <provider-default|level>]]...\n" +
553
+ " [--fallback-models <csv>] [--route-safety uniform|per-route-native]\n" +
554
+ " [--codex-auth browser|device] [--memory lite|journal|bujo]",
375
555
  lines: [
376
556
  "Scaffold a mono-agent in the current folder. On a TTY with no flags, launches",
377
557
  "the step-by-step wizard; with --yes or any flag, writes the default/preset",
378
558
  "scaffold non-interactively. --preset seeds a blueprint, --with adds channels,",
379
- "--effort writes runtime.effort, --auth runs supported provider auth/preflight",
380
- "commands before writing, and --dry-run previews only. Existing files are never overwritten.",
559
+ `Effort levels: ${EFFORT_LEVELS.join(", ")}; an omitted fallback effort uses that provider's default.`,
560
+ "--auth runs supported provider auth/preflight before writing; --codex-auth device supports headless hosts.",
561
+ "--dry-run previews only. Existing scaffold/config files are not overwritten;",
562
+ "guided secret setup may securely update .env and .gitignore after explicit review.",
381
563
  ],
382
564
  },
383
565
  {
@@ -392,7 +574,7 @@ const HELP_COMMANDS = [
392
574
  ],
393
575
  },
394
576
  {
395
- signature: "mono-agent validate [--preset <id>] [--consumer <path>] [--config <path>] [--env-file <path>]",
577
+ signature: "mono-agent validate [--preset <id>] [--consumer <path>] [--config <path>] [--env-file <path>] [--json]",
396
578
  lines: [
397
579
  "Load every config section and report what would run, wait, or fail.",
398
580
  "--consumer validates another agent folder read-only, including its .env.",
@@ -400,6 +582,25 @@ const HELP_COMMANDS = [
400
582
  "`mono-agent doctor` is an alias for this command.",
401
583
  ],
402
584
  },
585
+ {
586
+ signature: "mono-agent auth login <provider|codex> [--pi-auth-path <path>] [--api-key-stdin]\n" +
587
+ " [--codex-auth browser|device] [--config <path>]",
588
+ lines: [
589
+ "Run a supported bundled Pi provider login, or direct Codex browser/device login.",
590
+ "Pi credentials are promoted with owner-only no-clobber checks.",
591
+ "API-key providers prompt securely on a TTY; --api-key-stdin explicitly reads a redirected secret.",
592
+ "Path precedence: --pi-auth-path, MONO_AGENT_PI_AUTH_PATH, providers.piAuthPath, then Pi's default.",
593
+ "Supported Pi targets: anthropic, github-copilot, openai-codex, and opencode-go.",
594
+ ],
595
+ },
596
+ {
597
+ signature: "mono-agent sandbox status | setup | check",
598
+ lines: [
599
+ "Inspect, install, or functionally prove the pinned SRT sandbox runtime.",
600
+ "Managed setup is macOS-only and installs into the user's cache; it never changes PATH,",
601
+ "global npm packages, system packages, or another user's files.",
602
+ ],
603
+ },
403
604
  {
404
605
  signature: "mono-agent config [--config <path>] [--env-file <path>]",
405
606
  lines: [
@@ -437,16 +638,19 @@ const HELP_COMMANDS = [
437
638
  lines: ["Print (and optionally follow) the background instance's log files."],
438
639
  },
439
640
  {
440
- signature: "mono-agent tui [--agent <label|sourceId>] [--conversation <id>]",
641
+ signature: "mono-agent tui [--agent <label|sourceId>] [--conversation <id>]\n" +
642
+ " [--configure | --local]",
441
643
  lines: [
442
644
  "Open the operator console from any directory: live chat with full",
443
645
  "thinking/tool/telemetry insight, recorded-run replay, and config view.",
444
646
  "Discovers running agents via the trace-source registry; one running",
445
647
  "agent connects directly, several open a picker.",
648
+ "--configure opens the guided configuration chat on the authoritative",
649
+ "background agent; --local is an ordinary in-process chat only.",
446
650
  ],
447
651
  },
448
652
  {
449
- signature: "mono-agent web [--host <addr>] [--port <n>] [--no-open] [--allow-non-loopback] [--include-memory] [--max-runs <n>]",
653
+ signature: "mono-agent web [--host <addr>] [--port <n>] [--no-open] [--allow-non-loopback] [--show-auth-url] [--include-memory] [--max-runs <n>]",
450
654
  lines: [
451
655
  "Serve the read-only Session Recorder web PWA from any directory: a live",
452
656
  "flight-recorder over every agent's runs (prompt, reasoning, tools, cost).",
@@ -455,13 +659,20 @@ const HELP_COMMANDS = [
455
659
  "--include-memory also shows memory-maintenance runs. --max-runs (default",
456
660
  "200) bounds the in-memory working set; the UI still pages the full",
457
661
  "on-disk history via \"Load older\".",
662
+ "Non-loopback service use requires MONO_AGENT_WEB_AUTH_TOKEN; an",
663
+ "interactive run may generate one. --show-auth-url reveals a configured",
664
+ "token only to an interactive terminal. Authenticated URLs are not",
665
+ "auto-opened through process arguments.",
458
666
  ],
459
667
  },
460
668
  {
461
- signature: "mono-agent install-skill [--target claude|codex|both] [--force]",
669
+ signature: "mono-agent install-skill [--target claude|codex|both] [--force]\n" +
670
+ " --project (--check|--update)",
462
671
  lines: [
463
672
  "Copy the bundled mono-agent-composer skill into ~/.claude/skills and",
464
673
  "~/.agents/skills (default: both). Refuses to overwrite without --force.",
674
+ "Project mode checks or safely updates the two managed skills generated",
675
+ "by init; modified copies are never overwritten and updates retain backups.",
465
676
  ],
466
677
  },
467
678
  {
@@ -494,12 +705,23 @@ const HELP_COMMANDS = [
494
705
  ],
495
706
  },
496
707
  {
497
- signature: "mono-agent memory [stats|today|show <date>|search <query>|top]\n" +
498
- " [--limit <n>] [--json] [--config <path>] [--env-file <path>]",
708
+ signature: "mono-agent memory [stats|today|show <date>|search <query>|top|audit|inspect [id]|retry [id]|resolve <id> <reason>|rebuild|rollback|adopt-replay]\n" +
709
+ " [--limit <n>] [--strict] [--json] [--config <path>] [--env-file <path>]",
499
710
  lines: [
500
711
  "Preview the configured memory store from an agent folder. Reads the",
501
712
  "memory block from mono-agent.config.json, not the standalone memory-bujo",
502
- "env workflow. Human-first output by default; --json is for scripts.",
713
+ "env workflow. Human-first output by default; audit --strict --json is a",
714
+ "metadata-only health gate. Intake inspect/retry/resolve never print payload content.",
715
+ "adopt-replay is an explicit stopped-agent, SSH-safe BuJo trust-on-first-use",
716
+ "operation. It returns metadata only and requires rebuild before restart.",
717
+ ],
718
+ },
719
+ {
720
+ signature: "mono-agent continuations [list [--limit <n>] [--cursor <opaque>]|health|retry <id>|cancel <id>|resolve <id> delivered|not-delivered|dead-lettered [delivery-id]] [--json]",
721
+ lines: [
722
+ "Inspect and operate the authenticated durable-continuation service.",
723
+ "Ambiguous delivery cannot be retried until it is explicitly resolved;",
724
+ "no command accepts or changes a channel destination.",
503
725
  ],
504
726
  },
505
727
  ];
@@ -509,11 +731,28 @@ const HELP_NOTES = `Background mode runs the agent under launchd, keeping it ali
509
731
  commands require macOS; elsewhere use start --foreground.
510
732
 
511
733
  Init model references look like pi:<provider>:<model>, claude:claude-sonnet-4-6,
512
- codex:gpt-5.5, or opencode:<provider>:<model>. The init wizard defaults to
513
- pi:openai-codex:gpt-5.5, keeps it selectable when Pi auth setup is needed, and
514
- can save OPENCODE_API_KEY for pi:opencode-go:* refs. Direct codex:gpt-5.5 and
515
- Claude remain selectable; direct opencode:<provider>:<model> refs are for
516
- hand-authored runtime backend config.
734
+ codex:gpt-5.6-terra, codex:gpt-5.6-sol, or opencode:<provider>:<model>. The init wizard
735
+ selects the live provider-declared default when available and falls back offline to
736
+ codex:gpt-5.6-terra. Direct and Pi OpenAI-Codex Sol choices remain selectable.
737
+ Direct GPT-5.6 routes require Codex CLI 0.144.0 or newer. Guided Pi authentication
738
+ covers Anthropic, GitHub Copilot, OpenAI Codex, and OpenCode-Go. Claude remains selectable;
739
+ direct opencode:<provider>:<model> refs are for
740
+ hand-authored runtime backend config and are rejected by guided selection/readiness.
741
+
742
+ Mixed fallback chains are allowed. runtime.routeSafety=uniform (the default)
743
+ requires one compatibility-preserving contract across every route;
744
+ per-route-native makes each route's exact safety boundary explicit (Pi SRT,
745
+ Claude provider-owned permissions, Codex native sandbox, or OpenCode native).
746
+
747
+ Native mono-agent srt policy is enforced by Pi-owned tools. In uniform mode,
748
+ Claude, direct Codex, and direct OpenCode cannot silently weaken that policy.
749
+ In per-route-native mode, validate reports each provider-owned safety contract
750
+ and rejects capabilities that the selected route cannot represent.
751
+ Direct OpenCode's bridge cannot enforce an explicit runtime.effort; omit effort and
752
+ configure runtime.permissionMode deliberately for hand-authored direct routes.
753
+ It is per-run/non-resumable and rejects MCP (including auto-provisioned memory
754
+ or send tools), positive maxTurns, index skill disclosure, structured output,
755
+ live input, fast mode, and native subagents instead of silently dropping them.
517
756
 
518
757
  A .env file in the current folder is loaded automatically when present;
519
758
  already-exported shell variables take precedence.
@@ -558,14 +797,56 @@ export async function runCli(argv) {
558
797
  args = parseCliArgs(argv);
559
798
  }
560
799
  catch (error) {
800
+ if (argv[0] === "memory" && argv.includes("adopt-replay")) {
801
+ const { writeReplayAdoptionCliFailure } = await import("./memory-command.js");
802
+ writeReplayAdoptionCliFailure(argv.includes("--json"), "replay_adoption_usage");
803
+ return 2;
804
+ }
561
805
  process.stderr.write(ui.errorLine(error instanceof Error ? error.message : String(error)));
562
806
  process.stdout.write(`\n${renderHelp()}`);
563
807
  return 2;
564
808
  }
809
+ // Only the internal launchd foreground shape may honor the managed-worker
810
+ // marker. A hostile/global launchctl environment must not sanitize unrelated
811
+ // commands such as `mono-agent validate` or `mono-agent status`.
812
+ const managedBackgroundWorker = args.command === "start" && args.foreground && process.env[MANAGED_BACKGROUND_WORKER_ENV] === "1";
813
+ if (args.expectedBackgroundSnapshot !== undefined && !managedBackgroundWorker) {
814
+ process.stderr.write(ui.errorLine("--expected-background-snapshot is reserved for the managed LaunchAgent worker."));
815
+ return 2;
816
+ }
817
+ if (managedBackgroundWorker && args.expectedBackgroundSnapshot === undefined) {
818
+ process.stderr.write(ui.errorLine("Managed LaunchAgent worker is missing its approved background snapshot."));
819
+ return 0;
820
+ }
821
+ if (managedBackgroundWorker) {
822
+ sanitizeManagedBackgroundWorkerEnvironment(process.env);
823
+ }
824
+ if (args.command === "memory"
825
+ && args.positionals[0] === "adopt-replay"
826
+ && hasUnsupportedReplayAdoptionFlag(argv)) {
827
+ const { writeReplayAdoptionCliFailure } = await import("./memory-command.js");
828
+ writeReplayAdoptionCliFailure(args.json === true, "replay_adoption_usage");
829
+ return 2;
830
+ }
565
831
  const invocationCwd = process.cwd();
566
- loadCliEnvFile(args.command === "validate"
832
+ // Capture the exported shell before dotenv loading. Guided init retains only
833
+ // worker-operational values and reports shell/background credential drift;
834
+ // normal CLI commands still get the established shell-over-dotenv precedence.
835
+ const shellEnv = { ...process.env };
836
+ const envFilePath = args.command === "validate"
567
837
  ? resolveValidateContext(args, invocationCwd).envFilePath
568
- : resolve(invocationCwd, args.envFile ?? ".env"));
838
+ : resolve(invocationCwd, args.envFile ?? ".env");
839
+ let dotenvEnv = {};
840
+ if (args.command === "init") {
841
+ try {
842
+ dotenvEnv = await readCliDotenvFile(envFilePath);
843
+ }
844
+ catch (error) {
845
+ process.stderr.write(ui.errorLine(`Cannot read ${envFilePath}: ${error instanceof Error ? error.message : String(error)}`));
846
+ return 1;
847
+ }
848
+ }
849
+ loadCliEnvFile(envFilePath);
569
850
  switch (args.command) {
570
851
  case "help":
571
852
  process.stdout.write(renderHelp());
@@ -574,15 +855,19 @@ export async function runCli(argv) {
574
855
  process.stdout.write(`mono-agent ${monoAgentVersion()}\n`);
575
856
  return 0;
576
857
  case "init":
577
- return await runInit(args);
858
+ return await runInit(args, { shellEnv, dotenvEnv, dotenvPath: envFilePath });
578
859
  case "validate":
579
860
  return await runValidate(args);
861
+ case "auth":
862
+ return await runAuth(args);
863
+ case "sandbox":
864
+ return await runSandboxCommand(args);
580
865
  case "config":
581
866
  return await runConfig(args);
582
867
  case "presets":
583
868
  return runPresets(args);
584
869
  case "start":
585
- return await runStart(args);
870
+ return await runStart(args, undefined, managedBackgroundWorker);
586
871
  case "restart":
587
872
  case "stop":
588
873
  case "status":
@@ -595,8 +880,11 @@ export async function runCli(argv) {
595
880
  configPath: resolve(process.cwd(), args.configPath ?? "mono-agent.config.json"),
596
881
  cwd: process.cwd(),
597
882
  env: process.env,
883
+ ...(args.envFile === undefined ? {} : { envFile: args.envFile }),
598
884
  ...(args.agent === undefined ? {} : { agent: args.agent }),
599
885
  ...(args.conversation === undefined ? {} : { conversationId: args.conversation }),
886
+ ...(args.local === true ? { local: true } : {}),
887
+ ...(args.configure === true ? { configure: true } : {}),
600
888
  });
601
889
  }
602
890
  case "web": {
@@ -610,12 +898,25 @@ export async function runCli(argv) {
610
898
  ...(args.port === undefined ? {} : { port: args.port }),
611
899
  ...(args.open === undefined ? {} : { open: args.open }),
612
900
  ...(args.allowNonLoopback === undefined ? {} : { allowNonLoopback: args.allowNonLoopback }),
901
+ ...(args.showAuthUrl === undefined ? {} : { showAuthUrl: args.showAuthUrl }),
613
902
  includeMemory: args.includeMemory,
614
903
  ...(args.maxRunsPerInstance === undefined ? {} : { maxRunsPerInstance: args.maxRunsPerInstance }),
615
904
  });
616
905
  }
617
906
  case "install-skill":
618
907
  return await runInstallSkill(args);
908
+ case "continuations": {
909
+ const { runContinuationCommand } = await import("./continuation-command.js");
910
+ return await runContinuationCommand({
911
+ cwd: process.cwd(),
912
+ configPath: resolve(process.cwd(), args.configPath ?? "mono-agent.config.json"),
913
+ env: process.env,
914
+ positionals: args.positionals,
915
+ ...(args.json === true ? { json: true } : {}),
916
+ ...(args.limit === undefined ? {} : { limit: args.limit }),
917
+ ...(args.cursor === undefined ? {} : { cursor: args.cursor }),
918
+ });
919
+ }
619
920
  case "backfill":
620
921
  return await runBackfill({
621
922
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
@@ -654,56 +955,872 @@ export async function runCli(argv) {
654
955
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
655
956
  positionals: args.positionals,
656
957
  json: args.json === true,
958
+ strict: args.strict === true,
657
959
  ...(args.limit === undefined ? {} : { limit: args.limit }),
658
960
  });
659
961
  }
660
962
  }
661
963
  }
662
- async function runInit(args) {
964
+ /**
965
+ * `adopt-replay` is a stopped-store trust boundary, so command-global flags
966
+ * must never be silently ignored. In particular, `--dry-run` cannot be parsed
967
+ * successfully and then execute the mutation. Validate the raw invocation so
968
+ * future parser flags also fail closed unless they are explicitly admitted.
969
+ */
970
+ function hasUnsupportedReplayAdoptionFlag(argv) {
971
+ for (let index = 1; index < argv.length; index += 1) {
972
+ const token = argv[index];
973
+ if (token === "--config" || token === "--env-file") {
974
+ index += 1;
975
+ continue;
976
+ }
977
+ if (token === "--json")
978
+ continue;
979
+ if (token?.startsWith("-") === true)
980
+ return true;
981
+ }
982
+ return false;
983
+ }
984
+ const DEFAULT_SANDBOX_COMMAND_DEPENDENCIES = {
985
+ status: sandboxRuntimeStatus,
986
+ setup: setupManagedSrt,
987
+ check: checkSandboxRuntime,
988
+ };
989
+ /** App-owned sandbox lifecycle surface; safe to inject in focused CLI tests. */
990
+ export async function runSandboxCommand(args, dependencies = DEFAULT_SANDBOX_COMMAND_DEPENDENCIES) {
991
+ const [subcommand, ...extra] = args.positionals;
992
+ if ((subcommand !== "status" && subcommand !== "setup" && subcommand !== "check") || extra.length > 0) {
993
+ process.stderr.write(ui.errorLine("[sandbox_usage] Usage: mono-agent sandbox status | setup | check."));
994
+ return 2;
995
+ }
996
+ if (subcommand === "status") {
997
+ try {
998
+ printSandboxRuntimeStatus(await dependencies.status());
999
+ return 0;
1000
+ }
1001
+ catch (error) {
1002
+ process.stderr.write(ui.errorLine(`[sandbox_status_failed] ${reasonOf(error)}`));
1003
+ return 1;
1004
+ }
1005
+ }
1006
+ return await withScopedSandboxCancellation(async (signal) => {
1007
+ try {
1008
+ if (subcommand === "setup") {
1009
+ process.stdout.write(ui.heading("Sandbox setup"));
1010
+ process.stdout.write(ui.style.dim("Installing the pinned SRT copy in the user cache; no PATH, global npm, or system-package changes will be made.\n"));
1011
+ const result = await dependencies.setup({ signal, verify: true });
1012
+ printSandboxSetupResult(result);
1013
+ return 0;
1014
+ }
1015
+ process.stdout.write(ui.heading("Sandbox check"));
1016
+ const result = await dependencies.check({ signal });
1017
+ printSandboxCheckResult(result);
1018
+ return 0;
1019
+ }
1020
+ catch (error) {
1021
+ if (signal.aborted || isAbortLike(error)) {
1022
+ process.stderr.write(ui.errorLine("[sandbox_interrupted] Sandbox operation was interrupted; no partial success was claimed."));
1023
+ process.stderr.write(ui.hint(`Retry safely with \`mono-agent sandbox ${subcommand}\`.\n`));
1024
+ return 130;
1025
+ }
1026
+ const code = subcommand === "setup" ? "sandbox_setup_failed" : "sandbox_check_failed";
1027
+ process.stderr.write(ui.errorLine(`[${code}] ${reasonOf(error)}`));
1028
+ process.stderr.write(ui.hint(`Retry with \`mono-agent sandbox ${subcommand}\` after resolving the error.\n`));
1029
+ return 1;
1030
+ }
1031
+ });
1032
+ }
1033
+ function printSandboxRuntimeStatus(status) {
1034
+ process.stdout.write(ui.heading("Sandbox status"));
1035
+ process.stdout.write(` State: ${status.state}\n`);
1036
+ process.stdout.write(` Source: ${status.source}\n`);
1037
+ process.stdout.write(` Cache: ${status.installRoot}\n`);
1038
+ process.stdout.write(` Detail: ${status.message}\n`);
1039
+ }
1040
+ function printSandboxCheckResult(result) {
1041
+ printSandboxRuntimeStatus(result.status);
1042
+ process.stdout.write(ui.heading("Functional enforcement"));
1043
+ for (const check of result.checks) {
1044
+ process.stdout.write(`${check.ok ? ui.badge("ok") : ui.badge("error")}${check.id}: ${check.detail}\n`);
1045
+ }
1046
+ }
1047
+ function printSandboxSetupResult(result) {
1048
+ printSandboxRuntimeStatus(result.status);
1049
+ const action = result.repaired ? "repaired" : result.installed ? "installed" : "already installed";
1050
+ process.stdout.write(`${ui.badge("ok")}Managed SRT ${action}; integrity verification passed.\n`);
1051
+ if (result.check !== undefined)
1052
+ printSandboxCheckResult(result.check);
1053
+ }
1054
+ async function withScopedSandboxCancellation(task) {
1055
+ const controller = new AbortController();
1056
+ let interrupts = 0;
1057
+ const interrupt = () => {
1058
+ interrupts += 1;
1059
+ controller.abort();
1060
+ };
1061
+ const onKeypress = (_value, key) => {
1062
+ if (key?.name === "escape")
1063
+ interrupt();
1064
+ };
1065
+ process.on("SIGINT", interrupt);
1066
+ const restoreKeypress = attachScopedKeypress(onKeypress);
1067
+ try {
1068
+ const result = await task(controller.signal);
1069
+ return interrupts > 1 ? 130 : result;
1070
+ }
1071
+ finally {
1072
+ process.off("SIGINT", interrupt);
1073
+ restoreKeypress();
1074
+ }
1075
+ }
1076
+ function isAbortLike(error) {
1077
+ return error instanceof Error && (error.name === "AbortError" || /abort|cancel/iu.test(error.message));
1078
+ }
1079
+ function attachScopedKeypress(listener) {
1080
+ if (!process.stdin.isTTY)
1081
+ return () => undefined;
1082
+ emitKeypressEvents(process.stdin);
1083
+ const input = process.stdin;
1084
+ const wasRaw = input.isRaw === true;
1085
+ const wasFlowing = input.readableFlowing;
1086
+ input.setRawMode?.(true);
1087
+ input.resume();
1088
+ input.on("keypress", listener);
1089
+ return () => {
1090
+ input.off("keypress", listener);
1091
+ input.setRawMode?.(wasRaw);
1092
+ if (wasFlowing !== true)
1093
+ input.pause();
1094
+ };
1095
+ }
1096
+ async function runInit(args, environment) {
1097
+ const cwd = process.cwd();
663
1098
  // On an interactive TTY with no overriding flags, walk the step-by-step wizard;
664
1099
  // any flag (or a piped/non-TTY invocation) takes the silent default/preset path.
665
- const wantsWizard = process.stdin.isTTY === true && process.stdout.isTTY === true
666
- && args.yes !== true && args.preset === undefined && args.recipe === undefined
667
- && args.model === undefined && args.fallbackModels === undefined
668
- && args.effort === undefined && args.memory === undefined && args.withChannels === undefined && args.dryRun !== true;
1100
+ const wantsWizard = shouldRunInitWizard(args, process.stdin.isTTY === true, process.stdout.isTTY === true);
669
1101
  if (wantsWizard) {
670
1102
  // Existing-config pre-check — don't walk the wizard into a guaranteed no-op.
671
- if (await pathExists(resolve(process.cwd(), "mono-agent.config.json"))) {
1103
+ if (await pathExists(resolve(cwd, "mono-agent.config.json"))) {
672
1104
  process.stdout.write(ui.hint("Found an existing mono-agent.config.json — `mono-agent init` never overwrites. Run `mono-agent validate`, or start in an empty folder.\n"));
673
1105
  return 0;
674
1106
  }
675
- const outcome = await runInitWizard({ cwd: process.cwd() });
676
- if (outcome.status === "cancelled") {
1107
+ let resolvedPiAuthPath = resolveEffectivePiAuthPath({
1108
+ cwd,
1109
+ ...(nonEmptyEnv(environment.dotenvEnv.MONO_AGENT_PI_AUTH_PATH)
1110
+ ? { envPath: environment.dotenvEnv.MONO_AGENT_PI_AUTH_PATH }
1111
+ : {}),
1112
+ });
1113
+ const initial = await runInitWizard({
1114
+ cwd,
1115
+ piAuthPath: resolvedPiAuthPath,
1116
+ persistedEnv: environment.dotenvEnv,
1117
+ });
1118
+ if (initial.status === "cancelled")
677
1119
  return 1;
678
- }
679
- if (outcome.runProviderSetup) {
680
- const previewPlan = composeWizardPlan(outcome.answers, {
681
- dirBasename: basename(process.cwd()),
682
- skillsRootExists: await pathExists(resolve(process.cwd(), "skills")),
1120
+ let answers = initial.answers;
1121
+ let moduleSecrets = { ...initial.moduleSecrets };
1122
+ let providerEnvironmentSecrets = { ...initial.providerEnvironmentSecrets };
1123
+ let providerSetupSecrets = { ...initial.providerSetupSecrets };
1124
+ let piApiKeyPersistenceByProvider = { ...initial.piApiKeyPersistenceByProvider };
1125
+ let credentialStates = { ...initial.credentialStates };
1126
+ let pendingProviderSetup = initial.runProviderSetup;
1127
+ let selectedCodexAuthMode = "browser";
1128
+ let readinessProgress;
1129
+ let sandboxMutationCompleted = false;
1130
+ let deferredFailure;
1131
+ firstRun: for (;;) {
1132
+ let dotenvSnapshot = { env: {}, fingerprint: "unreadable" };
1133
+ let failure = deferredFailure;
1134
+ let configurationRecoveryStep;
1135
+ let invalidPlanStage;
1136
+ deferredFailure = undefined;
1137
+ try {
1138
+ dotenvSnapshot = await readCliDotenvSnapshot(environment.dotenvPath);
1139
+ }
1140
+ catch {
1141
+ failure ??= dotenvReadinessFailure("The persisted .env could not be read safely. Fix it before retrying setup.");
1142
+ }
1143
+ const plan = composeWizardPlan(answers, {
1144
+ dirBasename: basename(cwd),
1145
+ skillsRootExists: await pathExists(resolve(cwd, "skills")),
683
1146
  });
684
- const setup = await runProviderSetupBeforeInit({
685
- modelRefs: [outcome.answers.model, ...outcome.answers.fallbackModels],
686
- cwd: process.cwd(),
687
- auth: true,
688
- dryRun: false,
689
- ...(typeof previewPlan.configJson.providers?.piAuthPath === "string" ? { piAuthPath: previewPlan.configJson.providers.piAuthPath } : {}),
690
- apiKeys: outcome.providerSetupSecrets,
1147
+ resolvedPiAuthPath = resolveEffectivePiAuthPath({
1148
+ cwd,
1149
+ ...(nonEmptyEnv(dotenvSnapshot.env.MONO_AGENT_PI_AUTH_PATH)
1150
+ ? { envPath: dotenvSnapshot.env.MONO_AGENT_PI_AUTH_PATH }
1151
+ : {}),
1152
+ ...(nonEmptyEnv(plan.configJson.providers?.piAuthPath)
1153
+ ? { configPath: plan.configJson.providers.piAuthPath }
1154
+ : {}),
691
1155
  });
692
- if (setup === "failed") {
693
- return 1;
1156
+ const effectiveEnv = effectiveFirstRunEnvironment({
1157
+ shellEnv: environment.shellEnv,
1158
+ dotenvEnv: dotenvSnapshot.env,
1159
+ enteredSecrets: { ...moduleSecrets, ...providerEnvironmentSecrets },
1160
+ resolvedPiAuthPath,
1161
+ });
1162
+ // Re-submit every selected durable value, not only values typed during this
1163
+ // wizard session. That lets the secure merge tighten an existing .env to
1164
+ // 0600 while preserving its non-empty operator-owned values verbatim.
1165
+ const selectedSecrets = {
1166
+ ...selectedSecretValues(plan, effectiveEnv),
1167
+ ...providerEnvironmentSecrets,
1168
+ };
1169
+ const secureExistingDotenv = hasSensitivePersistedEnvironmentValue(dotenvSnapshot.env);
1170
+ const conflicts = selectedSecretEnvironmentConflicts(plan, environment.shellEnv, dotenvSnapshot.env, moduleSecrets);
1171
+ const persistedOverrides = unexpectedPersistedMonoAgentOverrides(plan, dotenvSnapshot.env);
1172
+ if (failure === undefined && persistedOverrides.length > 0) {
1173
+ failure = {
1174
+ ok: false,
1175
+ kind: "invalid_plan",
1176
+ message: `Persisted .env contains mono-agent config override${persistedOverrides.length === 1 ? "" : "s"}: ` +
1177
+ `${persistedOverrides.join(", ")}. Remove ${persistedOverrides.length === 1 ? "it" : "them"} so the ` +
1178
+ "generated config is the exact config validated and started.",
1179
+ };
1180
+ }
1181
+ if (failure === undefined && conflicts.length > 0) {
1182
+ failure = {
1183
+ ok: false,
1184
+ kind: "invalid_plan",
1185
+ message: `Selected secret${conflicts.length === 1 ? "" : "s"} ${conflicts.join(", ")} ` +
1186
+ "differ between the exported shell, persisted .env, or newly entered value. " +
1187
+ "Unset the shell value or make every source match, then retry.",
1188
+ };
1189
+ }
1190
+ if (failure === undefined && piAuthPathBackgroundConflict({
1191
+ cwd,
1192
+ shellPath: environment.shellEnv.MONO_AGENT_PI_AUTH_PATH,
1193
+ dotenvPath: dotenvSnapshot.env.MONO_AGENT_PI_AUTH_PATH,
1194
+ ...(nonEmptyEnv(plan.configJson.providers?.piAuthPath)
1195
+ ? { configPath: plan.configJson.providers.piAuthPath }
1196
+ : {}),
1197
+ })) {
1198
+ failure = {
1199
+ ok: false,
1200
+ kind: "invalid_plan",
1201
+ message: "The exported MONO_AGENT_PI_AUTH_PATH selects a different credential store than a background start. " +
1202
+ "Persist the same path in .env or providers.piAuthPath, or unset the shell override, then retry.",
1203
+ };
1204
+ }
1205
+ if (failure === undefined && pendingProviderSetup) {
1206
+ pendingProviderSetup = false;
1207
+ const modelRefs = referencedSetupModelRefs(plan);
1208
+ const credentialObservation = await withScopedPreflightCancellation(async (abortSignal) => ({
1209
+ states: await detectProviderCredentialStates({
1210
+ modelRefs,
1211
+ cwd,
1212
+ piAuthPath: resolvedPiAuthPath,
1213
+ persistedEnv: dotenvSnapshot.env,
1214
+ abortSignal,
1215
+ }),
1216
+ interrupted: abortSignal.aborted,
1217
+ }));
1218
+ if (credentialObservation.interrupted) {
1219
+ pendingProviderSetup = true;
1220
+ deferredFailure = {
1221
+ ok: false,
1222
+ kind: "cancelled",
1223
+ message: "Provider status detection was interrupted. No agent files were written.",
1224
+ interrupted: true,
1225
+ };
1226
+ continue firstRun;
1227
+ }
1228
+ credentialStates = credentialObservation.states;
1229
+ const plannedSetup = planProviderSetup({
1230
+ modelRefs,
1231
+ cwd,
1232
+ piAuthPath: resolvedPiAuthPath,
1233
+ credentialStates,
1234
+ piApiKeyPersistenceByProvider,
1235
+ });
1236
+ if (plannedSetup.actions.some((action) => action.id === "codex-login")) {
1237
+ const selected = await selectCodexAuthMode(selectedCodexAuthMode);
1238
+ if (selected === undefined)
1239
+ return 1;
1240
+ selectedCodexAuthMode = selected;
1241
+ }
1242
+ const environmentApiKeys = environmentProviderApiKeys(plannedSetup, effectiveEnv);
1243
+ const missingEnvironmentKeys = plannedSetup.actions
1244
+ .filter(isProviderSetupPiApiKeyAction)
1245
+ .filter((action) => action.persistence === "environment" && environmentApiKeys[action.id] === undefined)
1246
+ .map((action) => action.envVar);
1247
+ if (missingEnvironmentKeys.length > 0) {
1248
+ for (const envVar of missingEnvironmentKeys) {
1249
+ const answer = await p.password({
1250
+ message: `Enter ${envVar} for the agent's owner-only .env`,
1251
+ validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
1252
+ clearOnError: true,
1253
+ });
1254
+ if (p.isCancel(answer))
1255
+ return 1;
1256
+ providerEnvironmentSecrets[envVar] = answer;
1257
+ }
1258
+ pendingProviderSetup = true;
1259
+ continue firstRun;
1260
+ }
1261
+ const setup = await withScopedPreflightCancellation((abortSignal) => withExactProcessEnvironment(effectiveEnv, () => runProviderSetupBeforeInit({
1262
+ modelRefs,
1263
+ cwd,
1264
+ auth: true,
1265
+ dryRun: false,
1266
+ piAuthPath: resolvedPiAuthPath,
1267
+ apiKeys: { ...providerSetupSecrets, ...environmentApiKeys },
1268
+ codexAuthMode: selectedCodexAuthMode,
1269
+ credentialStates,
1270
+ persistedEnv: dotenvSnapshot.env,
1271
+ piApiKeyPersistenceByProvider,
1272
+ abortSignal,
1273
+ })), { keypress: false });
1274
+ if (setup === "fatal")
1275
+ return 130;
1276
+ if (setup === "interrupted") {
1277
+ pendingProviderSetup = true;
1278
+ failure = {
1279
+ ok: false,
1280
+ kind: "cancelled",
1281
+ message: "Provider setup was interrupted. No agent files were written.",
1282
+ interrupted: true,
1283
+ };
1284
+ }
1285
+ else if (setup === "failed") {
1286
+ // A plain retry must revisit provider setup rather than falling
1287
+ // through to a guaranteed-failing model turn.
1288
+ pendingProviderSetup = true;
1289
+ failure = {
1290
+ ok: false,
1291
+ kind: "provider_failed",
1292
+ message: "Provider setup did not complete. No agent files were written.",
1293
+ };
1294
+ }
1295
+ }
1296
+ if (failure === undefined) {
1297
+ if (answers.sandbox) {
1298
+ const sandboxPreflight = await runGuidedSandboxPreflight(sandboxMutationCompleted);
1299
+ sandboxMutationCompleted = sandboxMutationCompleted || sandboxPreflight.ok;
1300
+ if (!sandboxPreflight.ok)
1301
+ failure = sandboxPreflight;
1302
+ }
1303
+ }
1304
+ if (failure === undefined) {
1305
+ const configurationGate = await runConfigurationPreflightWithSpinner({
1306
+ cwd,
1307
+ answers,
1308
+ plan,
1309
+ env: effectiveEnv,
1310
+ secretValues: selectedSecrets,
1311
+ secureExistingDotenv,
1312
+ });
1313
+ if (configurationGate.interrupted === true) {
1314
+ failure = {
1315
+ ok: false,
1316
+ kind: "cancelled",
1317
+ message: "Configuration preflight was interrupted. No agent files were written.",
1318
+ interrupted: true,
1319
+ };
1320
+ }
1321
+ else if (!configurationGate.ready) {
1322
+ configurationRecoveryStep = focusedConfigurationRepairStep(configurationGate.failedSectionIds);
1323
+ invalidPlanStage = "configuration";
1324
+ failure = {
1325
+ ok: false,
1326
+ kind: "invalid_plan",
1327
+ message: `Configuration preflight did not pass: ${configurationGate.reasons.join(" ")}`,
1328
+ };
1329
+ }
1330
+ }
1331
+ if (failure === undefined) {
1332
+ const readiness = await runReadinessProbeWithSpinner({
1333
+ plan,
1334
+ effectiveEnv,
1335
+ resolvedPiAuthPath,
1336
+ ...(readinessProgress === undefined ? {} : {
1337
+ resume: {
1338
+ planFingerprint: readinessProgress.planFingerprint,
1339
+ successfulRouteKeys: readinessProgress.successfulRouteKeys,
1340
+ },
1341
+ }),
1342
+ });
1343
+ readinessProgress = mergeReadinessProgress(readinessProgress, readiness, plan);
1344
+ failure = readiness;
1345
+ }
1346
+ readyAttempt: if (failure.ok) {
1347
+ const stagedGate = await runFinalReadinessValidationWithSpinner({
1348
+ cwd,
1349
+ answers,
1350
+ plan,
1351
+ env: effectiveEnv,
1352
+ secretValues: selectedSecrets,
1353
+ secureExistingDotenv,
1354
+ verifiedCredentialModelRefs: readinessProgress?.verifiedModelRefs ?? [],
1355
+ });
1356
+ if (stagedGate.interrupted === true) {
1357
+ failure = {
1358
+ ok: false,
1359
+ kind: "cancelled",
1360
+ message: "Final readiness validation was interrupted. No agent files were written.",
1361
+ interrupted: true,
1362
+ };
1363
+ break readyAttempt;
1364
+ }
1365
+ if (stagedGate.ready) {
1366
+ const drift = await firstRunDotenvDrift(environment.dotenvPath, dotenvSnapshot);
1367
+ if (drift !== undefined) {
1368
+ failure = drift;
1369
+ break readyAttempt;
1370
+ }
1371
+ let result;
1372
+ try {
1373
+ result = await initMonoAgentFolder({
1374
+ dir: cwd,
1375
+ answers,
1376
+ env: effectiveEnv,
1377
+ secretValues: selectedSecrets,
1378
+ secureExistingDotenv,
1379
+ requireConfigCreation: true,
1380
+ });
1381
+ }
1382
+ catch (error) {
1383
+ const recovery = secretPersistenceRecoveryMessage(error);
1384
+ process.stderr.write(ui.errorLine(`The validated scaffold could not be committed safely. The agent was not started; inspect the destination before retrying.${recovery}`));
1385
+ return 1;
1386
+ }
1387
+ let committedConfigSnapshot;
1388
+ try {
1389
+ committedConfigSnapshot = await readCliConfigSnapshot(result.configPath);
1390
+ }
1391
+ catch {
1392
+ printIncompleteSetup(["The committed config could not be read back as the regular file setup created."], result.configPath);
1393
+ return 1;
1394
+ }
1395
+ if (committedConfigSnapshot.contents !== `${JSON.stringify(result.plan.configJson, null, 2)}\n`) {
1396
+ printIncompleteSetup(["The committed config does not match the exact plan setup validated."], result.configPath);
1397
+ return 1;
1398
+ }
1399
+ let committedDotenvSnapshot;
1400
+ try {
1401
+ committedDotenvSnapshot = await readCliDotenvSnapshot(environment.dotenvPath);
1402
+ }
1403
+ catch {
1404
+ printIncompleteSetup(["The committed .env could not be read back safely; no readiness claim is safe."], result.configPath);
1405
+ return 1;
1406
+ }
1407
+ // A missing default .env is a valid, durable empty environment. Keep
1408
+ // proving its absence in the approved snapshot, but do not turn that
1409
+ // default into an explicit --env-file argument: the managed worker
1410
+ // treats an explicitly named missing file as an operator error.
1411
+ const backgroundEnvFile = args.envFile === undefined
1412
+ && committedDotenvSnapshot.fingerprint === "missing"
1413
+ ? undefined
1414
+ : environment.dotenvPath;
1415
+ const postWriteConflicts = selectedSecretEnvironmentConflicts(result.plan, environment.shellEnv, committedDotenvSnapshot.env, moduleSecrets);
1416
+ const postWriteOverrides = unexpectedPersistedMonoAgentOverrides(result.plan, committedDotenvSnapshot.env);
1417
+ const postWritePiAuthConflict = piAuthPathBackgroundConflict({
1418
+ cwd,
1419
+ shellPath: environment.shellEnv.MONO_AGENT_PI_AUTH_PATH,
1420
+ dotenvPath: committedDotenvSnapshot.env.MONO_AGENT_PI_AUTH_PATH,
1421
+ ...(nonEmptyEnv(result.plan.configJson.providers?.piAuthPath)
1422
+ ? { configPath: result.plan.configJson.providers.piAuthPath }
1423
+ : {}),
1424
+ });
1425
+ if (postWriteConflicts.length > 0 || postWriteOverrides.length > 0 || postWritePiAuthConflict) {
1426
+ printIncompleteSetup(["The committed .env no longer matches the values and generated config that setup approved."], result.configPath);
1427
+ return 1;
1428
+ }
1429
+ const postWriteEnv = effectiveFirstRunEnvironment({
1430
+ shellEnv: environment.shellEnv,
1431
+ dotenvEnv: committedDotenvSnapshot.env,
1432
+ resolvedPiAuthPath,
1433
+ });
1434
+ if (!sameConcreteEnvironment(effectiveEnv, postWriteEnv)) {
1435
+ printIncompleteSetup(["The durable environment changed after the primary-model check. Retry setup before claiming readiness."], result.configPath);
1436
+ return 1;
1437
+ }
1438
+ let committedBackgroundSnapshot;
1439
+ let committedBackgroundEnvironment;
1440
+ let committedBackgroundConfigSourceFingerprint;
1441
+ try {
1442
+ const durableInputs = await captureDurableBackgroundInputs({
1443
+ cwd,
1444
+ configPath: result.configPath,
1445
+ envFile: environment.dotenvPath,
1446
+ operationalEnvironment: managedBackgroundEnvironment(postWriteEnv),
1447
+ });
1448
+ committedBackgroundSnapshot = durableInputs.snapshot;
1449
+ committedBackgroundEnvironment = durableInputs.environment;
1450
+ committedBackgroundConfigSourceFingerprint = durableInputs.configSourceFingerprint;
1451
+ }
1452
+ catch {
1453
+ printIncompleteSetup(["The complete committed config, dotenv, Identity, Soul, and MCP snapshot could not be proven safely."], result.configPath);
1454
+ return 1;
1455
+ }
1456
+ if (committedBackgroundConfigSourceFingerprint !== committedConfigSnapshot.fingerprint) {
1457
+ printIncompleteSetup(["The committed config or durable environment changed before the background snapshot was captured."], result.configPath);
1458
+ return 1;
1459
+ }
1460
+ printInitResult(result);
1461
+ let report;
1462
+ try {
1463
+ report = await validateMonoAgentFolder({
1464
+ env: postWriteEnv,
1465
+ cwd,
1466
+ configPath: result.configPath,
1467
+ liveness: true,
1468
+ verifiedCredentialModelRefs: readinessProgress?.verifiedModelRefs ?? [],
1469
+ });
1470
+ }
1471
+ catch {
1472
+ printIncompleteSetup(["Post-write validation could not complete; no readiness claim is safe."], result.configPath);
1473
+ return 1;
1474
+ }
1475
+ process.stdout.write("\n" + ui.heading("Validation"));
1476
+ for (const section of report.sections)
1477
+ process.stdout.write(formatSection(section));
1478
+ process.stdout.write(renderPlanCompleteness(result.plan.validateExpectations, "Selected capabilities", report));
1479
+ const configuredSecrets = configuredSecretNames(result, postWriteEnv);
1480
+ printSecretsChecklist(result.plan.secrets, configuredSecrets);
1481
+ const finalGate = evaluateFirstRunReadiness({
1482
+ plan: result.plan,
1483
+ report,
1484
+ secretPersistence: result.secretPersistence,
1485
+ verifiedCredentialModelRefs: readinessProgress?.verifiedModelRefs ?? [],
1486
+ });
1487
+ if (!finalGate.ready) {
1488
+ printIncompleteSetup(finalGate.reasons, result.configPath);
1489
+ return 1;
1490
+ }
1491
+ const postValidationConfigDrift = await firstRunConfigDrift(result.configPath, committedConfigSnapshot);
1492
+ if (postValidationConfigDrift !== undefined) {
1493
+ printIncompleteSetup([postValidationConfigDrift.message], result.configPath);
1494
+ return 1;
1495
+ }
1496
+ const postValidationDrift = await firstRunDotenvDrift(environment.dotenvPath, committedDotenvSnapshot);
1497
+ if (postValidationDrift !== undefined) {
1498
+ printIncompleteSetup([postValidationDrift.message], result.configPath);
1499
+ return 1;
1500
+ }
1501
+ const postValidationSecretGuard = await firstRunSecretEnvGuardFailure(environment.dotenvPath, result.secretPersistence.status === "persisted");
1502
+ if (postValidationSecretGuard !== undefined) {
1503
+ printIncompleteSetup([postValidationSecretGuard.message], result.configPath);
1504
+ return 1;
1505
+ }
1506
+ process.stdout.write(ui.badge("ok") + ui.style.green("All runtime route checks passed — every selected model produced a real no-tool response.\n") +
1507
+ ui.badge("ok") + ui.style.green("Agent configuration validated — every selected capability passed full validation.\n"));
1508
+ const preTuiConfigDrift = await firstRunConfigDrift(result.configPath, committedConfigSnapshot);
1509
+ if (preTuiConfigDrift !== undefined) {
1510
+ printIncompleteSetup([preTuiConfigDrift.message], result.configPath);
1511
+ return 1;
1512
+ }
1513
+ const preTuiDotenvDrift = await firstRunDotenvDrift(environment.dotenvPath, committedDotenvSnapshot);
1514
+ if (preTuiDotenvDrift !== undefined) {
1515
+ printIncompleteSetup([preTuiDotenvDrift.message], result.configPath);
1516
+ return 1;
1517
+ }
1518
+ const preTuiSecretGuard = await firstRunSecretEnvGuardFailure(environment.dotenvPath, result.secretPersistence.status === "persisted");
1519
+ if (preTuiSecretGuard !== undefined) {
1520
+ printIncompleteSetup([preTuiSecretGuard.message], result.configPath);
1521
+ return 1;
1522
+ }
1523
+ if (process.platform !== "darwin") {
1524
+ printUnsupportedGuidedInitHandoff(result.configPath, backgroundEnvFile);
1525
+ return 0;
1526
+ }
1527
+ process.stdout.write(ui.hint("Starting the authoritative background agent before configuration chat…"));
1528
+ let background;
1529
+ try {
1530
+ const resolvedBackgroundTarget = await resolveInstanceTarget({
1531
+ args: {
1532
+ configPath: result.configPath,
1533
+ ...(backgroundEnvFile === undefined ? {} : { envFile: backgroundEnvFile }),
1534
+ },
1535
+ env: { ...committedBackgroundEnvironment },
1536
+ cwd,
1537
+ cliPath: fileURLToPath(import.meta.url),
1538
+ requireTui: true,
1539
+ });
1540
+ const backgroundTarget = {
1541
+ ...resolvedBackgroundTarget,
1542
+ expectedSnapshot: committedBackgroundSnapshot,
1543
+ };
1544
+ background = await ensureBackgroundReady(backgroundTarget, defaultBackgroundDeps());
1545
+ }
1546
+ catch (error) {
1547
+ printUnexpectedGuidedBackgroundFailure(result.configPath, backgroundEnvFile, error);
1548
+ return 1;
1549
+ }
1550
+ if (!background.ok) {
1551
+ process.stderr.write(ui.hint("The validated agent files were preserved, but configuration chat was not opened because the background agent is not ready."));
1552
+ return 1;
1553
+ }
1554
+ process.stdout.write(ui.badge("ok") + ui.style.green("Agent ready — the background process reported startup complete.\n") +
1555
+ ui.badge("ok") + ui.style.green("Opening configuration chat on the authoritative background agent.\n"));
1556
+ const { runTui } = await import("./tui-command.js");
1557
+ return await withExactProcessEnvironment(committedBackgroundEnvironment, () => runTui({
1558
+ configPath: result.configPath,
1559
+ cwd,
1560
+ env: committedBackgroundEnvironment,
1561
+ ...(backgroundEnvFile === undefined ? {} : { envFile: backgroundEnvFile }),
1562
+ agent: background.source.sourceId,
1563
+ configure: true,
1564
+ }));
1565
+ }
1566
+ configurationRecoveryStep = focusedConfigurationRepairStep(stagedGate.failedSectionIds);
1567
+ invalidPlanStage = "final_readiness";
1568
+ failure = {
1569
+ ok: false,
1570
+ kind: "invalid_plan",
1571
+ message: `Runtime route checks passed, but the complete agent is not ready: ${stagedGate.reasons.join(" ")}`,
1572
+ };
1573
+ }
1574
+ if (failure.ok)
1575
+ throw new Error("First-run recovery reached without a failure.");
1576
+ if (failure.interrupted === true || failure.kind === "cancelled") {
1577
+ interruptedRecoveryMenu: for (;;) {
1578
+ const interruptedRecovery = await selectInterruptedFirstRunRecovery();
1579
+ if (interruptedRecovery === "cancel")
1580
+ return 1;
1581
+ if (interruptedRecovery === "restart") {
1582
+ readinessProgress = undefined;
1583
+ break interruptedRecoveryMenu;
1584
+ }
1585
+ if (interruptedRecovery === "edit") {
1586
+ const repaired = await runSetupRepairWizard({
1587
+ cwd,
1588
+ answers,
1589
+ piAuthPath: resolvedPiAuthPath,
1590
+ persistedEnv: dotenvSnapshot.env,
1591
+ moduleSecrets,
1592
+ providerSetupSecrets,
1593
+ providerEnvironmentSecrets,
1594
+ piApiKeyPersistenceByProvider,
1595
+ credentialStates,
1596
+ runProviderSetup: pendingProviderSetup,
1597
+ });
1598
+ if (repaired.status === "cancelled")
1599
+ continue interruptedRecoveryMenu;
1600
+ answers = repaired.answers;
1601
+ moduleSecrets = { ...repaired.moduleSecrets };
1602
+ providerSetupSecrets = { ...repaired.providerSetupSecrets };
1603
+ providerEnvironmentSecrets = { ...repaired.providerEnvironmentSecrets };
1604
+ piApiKeyPersistenceByProvider = { ...repaired.piApiKeyPersistenceByProvider };
1605
+ credentialStates = { ...repaired.credentialStates };
1606
+ pendingProviderSetup = repaired.runProviderSetup;
1607
+ if (pendingProviderSetup)
1608
+ readinessProgress = undefined;
1609
+ }
1610
+ break interruptedRecoveryMenu;
1611
+ }
1612
+ continue firstRun;
1613
+ }
1614
+ if (failure.message.startsWith("[sandbox_preflight_failed]")) {
1615
+ sandboxRecoveryMenu: for (;;) {
1616
+ const recovery = await selectSandboxPreflightRecovery();
1617
+ if (recovery === "cancel")
1618
+ return 1;
1619
+ if (recovery === "edit") {
1620
+ const edited = await runSetupRepairWizard({
1621
+ cwd,
1622
+ answers,
1623
+ piAuthPath: resolvedPiAuthPath,
1624
+ persistedEnv: dotenvSnapshot.env,
1625
+ moduleSecrets,
1626
+ providerSetupSecrets,
1627
+ providerEnvironmentSecrets,
1628
+ piApiKeyPersistenceByProvider,
1629
+ credentialStates,
1630
+ runProviderSetup: pendingProviderSetup,
1631
+ });
1632
+ if (edited.status === "cancelled")
1633
+ continue sandboxRecoveryMenu;
1634
+ answers = edited.answers;
1635
+ moduleSecrets = { ...edited.moduleSecrets };
1636
+ providerSetupSecrets = { ...edited.providerSetupSecrets };
1637
+ providerEnvironmentSecrets = { ...edited.providerEnvironmentSecrets };
1638
+ piApiKeyPersistenceByProvider = { ...edited.piApiKeyPersistenceByProvider };
1639
+ credentialStates = { ...edited.credentialStates };
1640
+ pendingProviderSetup = edited.runProviderSetup;
1641
+ if (pendingProviderSetup)
1642
+ readinessProgress = undefined;
1643
+ }
1644
+ break sandboxRecoveryMenu;
1645
+ }
1646
+ continue firstRun;
1647
+ }
1648
+ let recoveryFailure = failure;
1649
+ recoveryMenu: for (;;) {
1650
+ p.log.error(`[${recoveryFailure.kind}] ${recoveryFailure.message}`);
1651
+ const recovery = await selectFirstRunRecovery(recoveryFailure, configurationRecoveryStep, invalidPlanStage);
1652
+ if (recovery === "cancel")
1653
+ return 1;
1654
+ if (recovery === "save") {
1655
+ let saved;
1656
+ try {
1657
+ saved = await initMonoAgentFolder({
1658
+ dir: cwd,
1659
+ answers,
1660
+ env: effectiveEnv,
1661
+ secretValues: selectedSecrets,
1662
+ secureExistingDotenv,
1663
+ requireConfigCreation: true,
1664
+ });
1665
+ }
1666
+ catch (error) {
1667
+ const recovery = secretPersistenceRecoveryMessage(error);
1668
+ process.stderr.write(ui.errorLine(`The incomplete scaffold could not be committed safely; inspect the destination and retry.${recovery}`));
1669
+ return 1;
1670
+ }
1671
+ printInitResult(saved);
1672
+ let durableSavedEnv = {};
1673
+ if (saved.secretPersistence.status === "persisted") {
1674
+ try {
1675
+ durableSavedEnv = (await readCliDotenvSnapshot(environment.dotenvPath)).env;
1676
+ }
1677
+ catch {
1678
+ durableSavedEnv = {};
1679
+ }
1680
+ }
1681
+ printSecretsChecklist(saved.plan.secrets, configuredSecretNames(saved, durableSavedEnv));
1682
+ printIncompleteSetup([recoveryFailure.message], saved.configPath);
1683
+ return 1;
1684
+ }
1685
+ if (recovery === "edit") {
1686
+ const repaired = await runSetupRepairWizard({
1687
+ cwd,
1688
+ answers,
1689
+ piAuthPath: resolvedPiAuthPath,
1690
+ persistedEnv: dotenvSnapshot.env,
1691
+ ...(configurationRecoveryStep === undefined ? {} : { initialStep: configurationRecoveryStep }),
1692
+ moduleSecrets,
1693
+ providerSetupSecrets,
1694
+ providerEnvironmentSecrets,
1695
+ piApiKeyPersistenceByProvider,
1696
+ credentialStates,
1697
+ runProviderSetup: pendingProviderSetup,
1698
+ });
1699
+ if (repaired.status === "cancelled")
1700
+ continue recoveryMenu;
1701
+ answers = repaired.answers;
1702
+ moduleSecrets = { ...repaired.moduleSecrets };
1703
+ providerSetupSecrets = { ...repaired.providerSetupSecrets };
1704
+ providerEnvironmentSecrets = { ...repaired.providerEnvironmentSecrets };
1705
+ piApiKeyPersistenceByProvider = { ...repaired.piApiKeyPersistenceByProvider };
1706
+ credentialStates = { ...repaired.credentialStates };
1707
+ pendingProviderSetup = repaired.runProviderSetup;
1708
+ if (pendingProviderSetup)
1709
+ readinessProgress = undefined;
1710
+ continue firstRun;
1711
+ }
1712
+ if (recovery === "model") {
1713
+ const repaired = await runSetupRepairWizard({
1714
+ cwd,
1715
+ answers,
1716
+ piAuthPath: resolvedPiAuthPath,
1717
+ persistedEnv: dotenvSnapshot.env,
1718
+ initialStep: 1,
1719
+ moduleSecrets,
1720
+ providerSetupSecrets,
1721
+ providerEnvironmentSecrets,
1722
+ piApiKeyPersistenceByProvider,
1723
+ credentialStates,
1724
+ runProviderSetup: pendingProviderSetup,
1725
+ });
1726
+ if (repaired.status === "cancelled")
1727
+ continue recoveryMenu;
1728
+ answers = repaired.answers;
1729
+ moduleSecrets = { ...repaired.moduleSecrets };
1730
+ providerSetupSecrets = { ...repaired.providerSetupSecrets };
1731
+ providerEnvironmentSecrets = { ...repaired.providerEnvironmentSecrets };
1732
+ piApiKeyPersistenceByProvider = { ...repaired.piApiKeyPersistenceByProvider };
1733
+ credentialStates = { ...repaired.credentialStates };
1734
+ pendingProviderSetup = repaired.runProviderSetup;
1735
+ if (pendingProviderSetup)
1736
+ readinessProgress = undefined;
1737
+ continue firstRun;
1738
+ }
1739
+ if (recovery === "auth") {
1740
+ // Authentication can replace credential bytes without changing the
1741
+ // route/config fingerprint. Every route must be proven again.
1742
+ readinessProgress = undefined;
1743
+ if (referencedSetupModelRefs(plan).some((ref) => ref.startsWith("codex:"))) {
1744
+ const selected = await selectCodexAuthMode(selectedCodexAuthMode);
1745
+ if (selected === undefined)
1746
+ return 1;
1747
+ selectedCodexAuthMode = selected;
1748
+ }
1749
+ const setupPlan = planProviderSetup({
1750
+ modelRefs: referencedSetupModelRefs(plan),
1751
+ cwd,
1752
+ piAuthPath: resolvedPiAuthPath,
1753
+ codexAuthMode: selectedCodexAuthMode,
1754
+ forceAuthentication: true,
1755
+ });
1756
+ const prompted = await promptProviderSetupSecrets(setupPlan, providerSetupSecrets, piApiKeyPersistenceByProvider, providerEnvironmentSecrets);
1757
+ if (prompted === undefined)
1758
+ return 1;
1759
+ providerSetupSecrets = prompted.apiKeys;
1760
+ piApiKeyPersistenceByProvider = prompted.persistenceByProvider;
1761
+ providerEnvironmentSecrets = prompted.environmentSecrets;
1762
+ readinessProgress = undefined;
1763
+ const selectedSetupPlan = planProviderSetup({
1764
+ modelRefs: referencedSetupModelRefs(plan),
1765
+ cwd,
1766
+ piAuthPath: resolvedPiAuthPath,
1767
+ codexAuthMode: selectedCodexAuthMode,
1768
+ forceAuthentication: true,
1769
+ piApiKeyPersistenceByProvider,
1770
+ });
1771
+ const environmentApiKeys = environmentProviderApiKeys(selectedSetupPlan, { ...effectiveEnv, ...providerEnvironmentSecrets });
1772
+ const missingEnvironmentKeys = selectedSetupPlan.actions
1773
+ .filter(isProviderSetupPiApiKeyAction)
1774
+ .filter((action) => action.persistence === "environment" && environmentApiKeys[action.id] === undefined)
1775
+ .map((action) => action.envVar);
1776
+ if (missingEnvironmentKeys.length > 0) {
1777
+ recoveryFailure = {
1778
+ ok: false,
1779
+ kind: "provider_failed",
1780
+ message: `Add ${missingEnvironmentKeys.join(", ")} to the durable owner-only .env, then retry authentication. No agent files were written.`,
1781
+ };
1782
+ continue recoveryMenu;
1783
+ }
1784
+ const setup = await withScopedPreflightCancellation((abortSignal) => withExactProcessEnvironment(effectiveEnv, () => runProviderSetupBeforeInit({
1785
+ modelRefs: referencedSetupModelRefs(plan),
1786
+ cwd,
1787
+ auth: true,
1788
+ dryRun: false,
1789
+ piAuthPath: resolvedPiAuthPath,
1790
+ apiKeys: { ...providerSetupSecrets, ...environmentApiKeys },
1791
+ codexAuthMode: selectedCodexAuthMode,
1792
+ forceAuthentication: true,
1793
+ piApiKeyPersistenceByProvider,
1794
+ abortSignal,
1795
+ })), { keypress: false });
1796
+ if (setup === "fatal")
1797
+ return 130;
1798
+ if (setup === "interrupted") {
1799
+ pendingProviderSetup = true;
1800
+ deferredFailure = {
1801
+ ok: false,
1802
+ kind: "cancelled",
1803
+ message: "Provider setup was interrupted. No agent files were written.",
1804
+ interrupted: true,
1805
+ };
1806
+ continue firstRun;
1807
+ }
1808
+ if (setup === "failed") {
1809
+ pendingProviderSetup = true;
1810
+ recoveryFailure = {
1811
+ ok: false,
1812
+ kind: "provider_failed",
1813
+ message: "Provider setup still needs attention. No agent files were written.",
1814
+ };
1815
+ continue recoveryMenu;
1816
+ }
1817
+ pendingProviderSetup = false;
1818
+ }
1819
+ // "retry" deliberately reruns only the live checks. Provider setup is a
1820
+ // separate explicit recovery action and is never repeated automatically.
1821
+ continue firstRun;
694
1822
  }
695
1823
  }
696
- const result = await initMonoAgentFolder({ dir: process.cwd(), answers: outcome.answers });
697
- printInitResult(result);
698
- const report = await validateMonoAgentFolder({ env: process.env, cwd: process.cwd(), configPath: result.configPath });
699
- process.stdout.write("\n" + ui.heading("Validation"));
700
- for (const section of report.sections) {
701
- process.stdout.write(formatSection(section));
702
- }
703
- process.stdout.write(renderPlanCompleteness(result.plan.validateExpectations, "Selected capabilities", report));
704
- printSecretsChecklist(result.plan.secrets);
705
- printNextSteps(result.configPath);
706
- return report.ok ? 0 : 1;
707
1824
  }
708
1825
  const presetId = resolveInitPresetId(args);
709
1826
  if (presetId === "unknown") {
@@ -715,7 +1832,10 @@ async function runInit(args) {
715
1832
  }
716
1833
  const answers = answersFromCli({
717
1834
  ...(args.model === undefined ? {} : { model: args.model }),
1835
+ ...(args.name === undefined ? {} : { name: args.name }),
718
1836
  ...(args.fallbackModels === undefined ? {} : { fallbackModels: args.fallbackModels }),
1837
+ ...(args.fallbacks === undefined ? {} : { fallbacks: args.fallbacks }),
1838
+ ...(args.routeSafety === undefined ? {} : { routeSafety: args.routeSafety }),
719
1839
  ...(args.effort === undefined ? {} : { effort: args.effort }),
720
1840
  ...(args.memory === undefined ? {} : { memory: args.memory }),
721
1841
  ...(withChannels === undefined ? {} : { withChannels }),
@@ -725,24 +1845,750 @@ async function runInit(args) {
725
1845
  dirBasename: basename(process.cwd()),
726
1846
  skillsRootExists: await pathExists(resolve(process.cwd(), "skills")),
727
1847
  });
728
- const setup = await runProviderSetupBeforeInit({
729
- modelRefs: [answers.model, ...answers.fallbackModels],
730
- cwd: process.cwd(),
1848
+ const nonInteractivePiAuthPath = resolveEffectivePiAuthPath({
1849
+ cwd,
1850
+ ...(nonEmptyEnv(environment.shellEnv.MONO_AGENT_PI_AUTH_PATH)
1851
+ ? { envPath: environment.shellEnv.MONO_AGENT_PI_AUTH_PATH }
1852
+ : nonEmptyEnv(environment.dotenvEnv.MONO_AGENT_PI_AUTH_PATH)
1853
+ ? { envPath: environment.dotenvEnv.MONO_AGENT_PI_AUTH_PATH }
1854
+ : {}),
1855
+ ...(nonEmptyEnv(previewPlan.configJson.providers?.piAuthPath)
1856
+ ? { configPath: previewPlan.configJson.providers.piAuthPath }
1857
+ : {}),
1858
+ });
1859
+ const nonInteractiveEnvironment = effectiveFirstRunEnvironment({
1860
+ shellEnv: environment.shellEnv,
1861
+ dotenvEnv: environment.dotenvEnv,
1862
+ resolvedPiAuthPath: nonInteractivePiAuthPath,
1863
+ });
1864
+ const setup = await withScopedPreflightCancellation((abortSignal) => withExactProcessEnvironment(nonInteractiveEnvironment, () => runProviderSetupBeforeInit({
1865
+ modelRefs: referencedSetupModelRefs(previewPlan),
1866
+ cwd,
731
1867
  auth: args.auth === true,
732
1868
  dryRun: args.dryRun,
733
- ...(typeof previewPlan.configJson.providers?.piAuthPath === "string" ? { piAuthPath: previewPlan.configJson.providers.piAuthPath } : {}),
734
- });
1869
+ persistedEnv: environment.dotenvEnv,
1870
+ piAuthPath: nonInteractivePiAuthPath,
1871
+ ...(args.codexAuthMode === undefined ? {} : { codexAuthMode: args.codexAuthMode }),
1872
+ abortSignal,
1873
+ })), { keypress: false });
1874
+ if (setup === "interrupted" || setup === "fatal") {
1875
+ return 130;
1876
+ }
735
1877
  if (setup === "failed") {
736
1878
  return 1;
737
1879
  }
738
- const result = await initMonoAgentFolder({ dir: process.cwd(), answers, dryRun: args.dryRun });
1880
+ const result = await initMonoAgentFolder({
1881
+ dir: cwd,
1882
+ answers,
1883
+ dryRun: args.dryRun,
1884
+ // Scaffold-only init has no immediate launchd-minimal worker proof. Reject
1885
+ // both shell and persisted memory-identity overrides instead of creating a
1886
+ // generation at a different path/tier than a follow-up validate would use.
1887
+ env: { ...environment.shellEnv, ...environment.dotenvEnv },
1888
+ });
739
1889
  printInitResult(result);
740
- printSecretsChecklist(result.plan.secrets);
1890
+ printSecretsChecklist(result.plan.secrets, new Set());
741
1891
  printNextSteps(result.configPath);
742
1892
  return 0;
743
1893
  }
1894
+ const FIRST_RUN_STAGING_FAILURE_MAX_LENGTH = 500;
1895
+ const FIRST_RUN_SENSITIVE_ENV_NAME = /(api.?key|credential|password|secret|token)/iu;
1896
+ function throwIfFirstRunPreflightAborted(signal) {
1897
+ if (signal?.aborted !== true)
1898
+ return;
1899
+ const error = new Error("Preflight was interrupted.");
1900
+ error.name = "AbortError";
1901
+ throw error;
1902
+ }
1903
+ function firstRunStagingFailureDetail(error, sensitiveValues = []) {
1904
+ let message = reasonOf(error);
1905
+ for (const value of [...new Set(sensitiveValues)].filter((candidate) => candidate.length >= 4).sort((left, right) => right.length - left.length)) {
1906
+ message = message.replaceAll(value, "[secret-redacted]");
1907
+ }
1908
+ const normalized = message
1909
+ .replace(/\bBearer\s+[^\s,;]+/giu, "Bearer [secret-redacted]")
1910
+ .replace(/\b(api[ _-]?key|access[ _-]?token|auth[ _-]?token|password|secret)(\s*[=:]\s*)([^\s,;]+)/giu, (_match, label, separator) => `${label}${separator}[secret-redacted]`)
1911
+ .replace(/\s+/gu, " ")
1912
+ .trim();
1913
+ if (normalized.length === 0)
1914
+ return "Unknown staging failure.";
1915
+ return normalized.length <= FIRST_RUN_STAGING_FAILURE_MAX_LENGTH
1916
+ ? normalized
1917
+ : `${normalized.slice(0, FIRST_RUN_STAGING_FAILURE_MAX_LENGTH - 1).trimEnd()}…`;
1918
+ }
1919
+ function firstRunStagingSensitiveValues(env, explicit) {
1920
+ return [
1921
+ ...Object.entries(env)
1922
+ .filter((entry) => FIRST_RUN_SENSITIVE_ENV_NAME.test(entry[0])
1923
+ && typeof entry[1] === "string"
1924
+ && entry[1].length > 0)
1925
+ .map(([, value]) => value),
1926
+ ...Object.values(explicit),
1927
+ ];
1928
+ }
1929
+ async function runConfigurationPreflightWithSpinner(options) {
1930
+ process.stdout.write("\n" + ui.heading("Configuration preflight"));
1931
+ const spinner = p.spinner();
1932
+ try {
1933
+ return await withScopedPreflightCancellation(async (abortSignal) => {
1934
+ spinner.start("Validating generated files and selected capabilities before runtime calls");
1935
+ const gate = await assessPrewriteFirstRunConfigurationReadiness({ ...options, abortSignal });
1936
+ if (abortSignal.aborted || spinner.isCancelled) {
1937
+ spinner.cancel("Configuration preflight interrupted");
1938
+ return interruptedConfigurationAssessment();
1939
+ }
1940
+ if (gate.ready) {
1941
+ spinner.stop("Selected capabilities are ready for runtime checks");
1942
+ }
1943
+ else {
1944
+ spinner.error("Configuration preflight needs attention");
1945
+ }
1946
+ return gate;
1947
+ });
1948
+ }
1949
+ catch (error) {
1950
+ if (!isAbortLike(error))
1951
+ throw error;
1952
+ spinner.cancel("Configuration preflight interrupted");
1953
+ return interruptedConfigurationAssessment();
1954
+ }
1955
+ }
1956
+ function interruptedConfigurationAssessment() {
1957
+ return {
1958
+ ready: false,
1959
+ reasons: ["Configuration preflight was interrupted."],
1960
+ failedSectionIds: [],
1961
+ interrupted: true,
1962
+ };
1963
+ }
1964
+ async function runFinalReadinessValidationWithSpinner(options) {
1965
+ process.stdout.write("\n" + ui.heading("Final readiness validation"));
1966
+ const spinner = p.spinner();
1967
+ try {
1968
+ return await withScopedPreflightCancellation(async (abortSignal) => {
1969
+ spinner.start("Revalidating the effective files after runtime route checks");
1970
+ const gate = await assessPrewriteFirstRunReadiness({ ...options, abortSignal });
1971
+ if (abortSignal.aborted || spinner.isCancelled) {
1972
+ spinner.cancel("Final readiness validation interrupted");
1973
+ return interruptedFinalReadinessAssessment();
1974
+ }
1975
+ if (gate.ready)
1976
+ spinner.stop("Effective files and runtime routes are ready");
1977
+ else
1978
+ spinner.error("Final readiness validation needs attention");
1979
+ return gate;
1980
+ });
1981
+ }
1982
+ catch (error) {
1983
+ if (!isAbortLike(error))
1984
+ throw error;
1985
+ spinner.cancel("Final readiness validation interrupted");
1986
+ return interruptedFinalReadinessAssessment();
1987
+ }
1988
+ }
1989
+ function interruptedFinalReadinessAssessment() {
1990
+ return {
1991
+ ready: false,
1992
+ reasons: ["Final readiness validation was interrupted."],
1993
+ failedSectionIds: [],
1994
+ interrupted: true,
1995
+ };
1996
+ }
1997
+ async function assessPrewriteFirstRunConfigurationReadiness(options) {
1998
+ try {
1999
+ throwIfFirstRunPreflightAborted(options.abortSignal);
2000
+ const preview = await initMonoAgentFolder({
2001
+ dir: options.cwd,
2002
+ answers: options.answers,
2003
+ env: options.env,
2004
+ secretValues: options.secretValues,
2005
+ secureExistingDotenv: options.secureExistingDotenv,
2006
+ dryRun: true,
2007
+ });
2008
+ throwIfFirstRunPreflightAborted(options.abortSignal);
2009
+ const report = await validateWizardPlanInStaging({
2010
+ plan: options.plan,
2011
+ sourceCwd: options.cwd,
2012
+ env: options.env,
2013
+ verifiedCredentialModelRefs: [],
2014
+ ...(options.abortSignal === undefined ? {} : { abortSignal: options.abortSignal }),
2015
+ });
2016
+ const gate = evaluateFirstRunConfigurationReadiness({
2017
+ plan: options.plan,
2018
+ report,
2019
+ secretPersistence: preview.secretPersistence,
2020
+ });
2021
+ return {
2022
+ ...gate,
2023
+ failedSectionIds: configurationFailureSectionIds(options.plan, report, true),
2024
+ };
2025
+ }
2026
+ catch (error) {
2027
+ throwIfFirstRunPreflightAborted(options.abortSignal);
2028
+ if (error instanceof Error && error.name === "AbortError")
2029
+ throw error;
2030
+ return {
2031
+ ready: false,
2032
+ reasons: [
2033
+ `The complete generated configuration could not be validated safely in staging: ${firstRunStagingFailureDetail(error, firstRunStagingSensitiveValues(options.env, options.secretValues))}`,
2034
+ ],
2035
+ failedSectionIds: [],
2036
+ };
2037
+ }
2038
+ }
2039
+ async function assessPrewriteFirstRunReadiness(options) {
2040
+ try {
2041
+ throwIfFirstRunPreflightAborted(options.abortSignal);
2042
+ const preview = await initMonoAgentFolder({
2043
+ dir: options.cwd,
2044
+ answers: options.answers,
2045
+ env: options.env,
2046
+ secretValues: options.secretValues,
2047
+ secureExistingDotenv: options.secureExistingDotenv,
2048
+ dryRun: true,
2049
+ });
2050
+ throwIfFirstRunPreflightAborted(options.abortSignal);
2051
+ const report = await validateWizardPlanInStaging({
2052
+ plan: options.plan,
2053
+ sourceCwd: options.cwd,
2054
+ env: options.env,
2055
+ verifiedCredentialModelRefs: options.verifiedCredentialModelRefs,
2056
+ ...(options.abortSignal === undefined ? {} : { abortSignal: options.abortSignal }),
2057
+ });
2058
+ const gate = evaluateFirstRunReadiness({
2059
+ plan: options.plan,
2060
+ report,
2061
+ secretPersistence: preview.secretPersistence,
2062
+ verifiedCredentialModelRefs: options.verifiedCredentialModelRefs,
2063
+ });
2064
+ return {
2065
+ ...gate,
2066
+ failedSectionIds: configurationFailureSectionIds(options.plan, report, false),
2067
+ };
2068
+ }
2069
+ catch (error) {
2070
+ throwIfFirstRunPreflightAborted(options.abortSignal);
2071
+ if (error instanceof Error && error.name === "AbortError")
2072
+ throw error;
2073
+ return {
2074
+ ready: false,
2075
+ reasons: [
2076
+ `The complete generated plan could not be validated safely in staging: ${firstRunStagingFailureDetail(error, firstRunStagingSensitiveValues(options.env, options.secretValues))}`,
2077
+ ],
2078
+ failedSectionIds: [],
2079
+ };
2080
+ }
2081
+ }
2082
+ function configurationFailureSectionIds(plan, report, deferWaitingCredentials) {
2083
+ const byId = new Map(report.sections.map((section) => [section.id, section]));
2084
+ const ids = new Set();
2085
+ for (const expectation of plan.validateExpectations) {
2086
+ const actual = byId.get(expectation.sectionId)?.status;
2087
+ if (actual === expectation.mustBe
2088
+ || (deferWaitingCredentials && expectation.sectionId === "credentials" && actual === "waiting"))
2089
+ continue;
2090
+ ids.add(expectation.sectionId);
2091
+ }
2092
+ if (!report.ok) {
2093
+ for (const section of report.sections) {
2094
+ if (section.status === "error")
2095
+ ids.add(section.id);
2096
+ }
2097
+ }
2098
+ return [...ids];
2099
+ }
2100
+ function nonEmptyEnv(value) {
2101
+ return typeof value === "string" && value.trim().length > 0;
2102
+ }
2103
+ function sameConcreteEnvironment(left, right) {
2104
+ const concreteEntries = (env) => Object.entries(env)
2105
+ .filter((entry) => typeof entry[1] === "string")
2106
+ .sort(([leftName], [rightName]) => leftName.localeCompare(rightName));
2107
+ const leftEntries = concreteEntries(left);
2108
+ const rightEntries = concreteEntries(right);
2109
+ return leftEntries.length === rightEntries.length && leftEntries.every(([name, value], index) => rightEntries[index]?.[0] === name && rightEntries[index]?.[1] === value);
2110
+ }
2111
+ function dotenvReadinessFailure(message) {
2112
+ return { ok: false, kind: "invalid_plan", message };
2113
+ }
2114
+ async function firstRunDotenvDrift(path, expected) {
2115
+ let current;
2116
+ try {
2117
+ current = await readCliDotenvSnapshot(path);
2118
+ }
2119
+ catch {
2120
+ return dotenvReadinessFailure("The persisted .env became unreadable during setup. Readiness cannot be claimed.");
2121
+ }
2122
+ if (current.fingerprint === expected.fingerprint)
2123
+ return undefined;
2124
+ return dotenvReadinessFailure("The persisted .env changed while setup was validating the agent. Review the change, then retry so the exact durable values can be checked.");
2125
+ }
2126
+ async function firstRunConfigDrift(path, expected) {
2127
+ let current;
2128
+ try {
2129
+ current = await readCliConfigSnapshot(path);
2130
+ }
2131
+ catch {
2132
+ return dotenvReadinessFailure("The committed config became unreadable or unsafe during setup. Readiness cannot be claimed.");
2133
+ }
2134
+ if (current.fingerprint === expected.fingerprint)
2135
+ return undefined;
2136
+ return dotenvReadinessFailure("The committed config changed while setup was validating the agent. Review the change, then retry so the exact plan can be checked.");
2137
+ }
2138
+ async function firstRunSecretEnvGuardFailure(path, required) {
2139
+ if (!required)
2140
+ return undefined;
2141
+ try {
2142
+ if (await verifySecretEnvPersistenceGuard(path))
2143
+ return undefined;
2144
+ }
2145
+ catch {
2146
+ // Fall through to one stable, non-secret-bearing operator message.
2147
+ }
2148
+ return dotenvReadinessFailure("The committed .env is no longer owner-only, safely ignored, and untracked. Readiness cannot be claimed.");
2149
+ }
2150
+ function secretPersistenceRecoveryMessage(error) {
2151
+ if (!(error instanceof SecretEnvConcurrentModificationError)) {
2152
+ return "";
2153
+ }
2154
+ return ` ${error.message}`;
2155
+ }
2156
+ function readinessPlanIdentity(plan) {
2157
+ const displayed = readinessRoutesForDisplay(plan);
2158
+ const immutable = displayed.map((route, index) => ({
2159
+ index,
2160
+ model: route.model,
2161
+ effort: route.effort ?? null,
2162
+ }));
2163
+ return {
2164
+ fingerprint: createHash("sha256")
2165
+ .update(JSON.stringify({ version: 1, routes: immutable }))
2166
+ .digest("hex"),
2167
+ routes: displayed.map((route, index) => ({
2168
+ index,
2169
+ ...route,
2170
+ key: createHash("sha256")
2171
+ .update(JSON.stringify({ version: 1, index, model: route.model, effort: route.effort ?? null }))
2172
+ .digest("hex"),
2173
+ })),
2174
+ };
2175
+ }
2176
+ function mergeReadinessProgress(previous, result, plan) {
2177
+ const identity = readinessPlanIdentity(plan);
2178
+ const fingerprint = result.planFingerprint ?? identity.fingerprint;
2179
+ const successfulKeys = new Set(previous?.planFingerprint === fingerprint ? previous.successfulRouteKeys : []);
2180
+ const verifiedRefs = new Set(previous?.planFingerprint === fingerprint ? previous.verifiedModelRefs : []);
2181
+ const reported = result.routes ?? (result.ok
2182
+ ? identity.routes.map((route) => ({ ...route, status: "verified" }))
2183
+ : []);
2184
+ for (const route of reported) {
2185
+ if (route.status === "verified" || route.status === "skipped_verified") {
2186
+ successfulKeys.add(route.key);
2187
+ verifiedRefs.add(route.model);
2188
+ }
2189
+ }
2190
+ const currentRefs = new Set(identity.routes.map((route) => route.model));
2191
+ return {
2192
+ planFingerprint: fingerprint,
2193
+ successfulRouteKeys: [...successfulKeys],
2194
+ verifiedModelRefs: [...verifiedRefs].filter((ref) => currentRefs.has(ref)),
2195
+ };
2196
+ }
2197
+ async function runGuidedSandboxPreflight(installedEarlier) {
2198
+ process.stdout.write("\n" + ui.heading("Sandbox preflight"));
2199
+ return await withScopedPreflightCancellation(async (signal) => {
2200
+ try {
2201
+ process.stdout.write(ui.style.dim(installedEarlier
2202
+ ? "Rechecking the pinned managed SRT copy and its functional enforcement postcondition.\n"
2203
+ : "Installing the pinned managed SRT copy in the private user cache, then running the functional enforcement check.\n"));
2204
+ const setup = await setupManagedSrt({ signal, verify: true });
2205
+ if (setup.status.source !== "managed" || setup.status.state !== "ready" || setup.check === undefined) {
2206
+ throw new Error("Managed SRT setup did not return a ready managed functional-check result.");
2207
+ }
2208
+ process.stdout.write(`${ui.badge("ok")}Managed SRT ${setup.repaired ? "repaired" : setup.installed ? "installed" : "verified"}; functional postcondition passed.\n`);
2209
+ return { ok: true };
2210
+ }
2211
+ catch (error) {
2212
+ if (signal.aborted || isAbortLike(error)) {
2213
+ process.stderr.write(ui.errorLine("Preflight was interrupted."));
2214
+ return {
2215
+ ok: false,
2216
+ kind: "cancelled",
2217
+ message: "Sandbox preflight was interrupted. No agent files were written.",
2218
+ interrupted: true,
2219
+ };
2220
+ }
2221
+ return {
2222
+ ok: false,
2223
+ kind: "provider_failed",
2224
+ message: `[sandbox_preflight_failed] ${reasonOf(error)} No agent files were written; retry setup or edit the sandbox choice.`,
2225
+ };
2226
+ }
2227
+ });
2228
+ }
2229
+ async function runReadinessProbeWithSpinner(options) {
2230
+ const routes = readinessRoutesForDisplay(options.plan);
2231
+ process.stdout.write("\n" + ui.heading("Runtime readiness"));
2232
+ routes.forEach((route, index) => {
2233
+ const timeoutMs = readinessProbeTimeoutMs(parseMonoRuntimeModelReference(route.model));
2234
+ process.stdout.write(` Route ${index + 1}/${routes.length}: ${route.model} ` +
2235
+ ui.style.dim(`(effort: ${route.effort ?? "provider-default"}; up to ${Math.ceil(timeoutMs / 1_000)}s)`) +
2236
+ "\n");
2237
+ });
2238
+ process.stdout.write(ui.style.dim("Running real no-tool checks sequentially. Press Esc or Ctrl-C once to interrupt safely.\n"));
2239
+ return await withScopedPreflightCancellation(async (signal) => {
2240
+ try {
2241
+ const result = await runAllRouteReadinessProbe({
2242
+ plan: options.plan,
2243
+ hostEnv: options.effectiveEnv,
2244
+ secretValues: selectedSecretValues(options.plan, options.effectiveEnv),
2245
+ resolvedPiAuthPath: options.resolvedPiAuthPath,
2246
+ abortSignal: signal,
2247
+ ...(options.resume === undefined ? {} : { resume: options.resume }),
2248
+ onRouteStart: (route) => {
2249
+ process.stdout.write(` Checking route ${route.index + 1}/${route.total}: ${route.model} ` +
2250
+ ui.style.dim(`(effort: ${route.effort ?? "provider-default"})`) +
2251
+ "\n");
2252
+ },
2253
+ onRouteComplete: (route) => {
2254
+ const ok = route.status === "verified" || route.status === "skipped_verified";
2255
+ process.stdout.write(`${ok ? ui.badge("ok") : route.status === "interrupted" ? ui.badge("waiting") : ui.badge("error")}` +
2256
+ `Route ${route.index + 1}/${routes.length} ${route.status.replaceAll("_", " ")}\n`);
2257
+ },
2258
+ });
2259
+ process.stdout.write(ui.heading("Readiness summary"));
2260
+ printReadinessRouteSummary(result, routes);
2261
+ return result;
2262
+ }
2263
+ catch (error) {
2264
+ if (signal.aborted || isAbortLike(error)) {
2265
+ process.stderr.write(ui.errorLine("Preflight was interrupted."));
2266
+ return {
2267
+ ok: false,
2268
+ kind: "cancelled",
2269
+ message: "Preflight was interrupted before the current route completed.",
2270
+ interrupted: true,
2271
+ };
2272
+ }
2273
+ process.stderr.write(ui.errorLine("[readiness_probe_failed] Runtime readiness could not run."));
2274
+ return {
2275
+ ok: false,
2276
+ kind: "probe_failed",
2277
+ message: "Runtime readiness could not run. Review provider authentication and retry.",
2278
+ };
2279
+ }
2280
+ });
2281
+ }
2282
+ function readinessRoutesForDisplay(plan) {
2283
+ const runtime = (plan.configJson.runtime ?? {});
2284
+ const primaryEffort = typeof runtime.effort === "string" ? runtime.effort : undefined;
2285
+ const routes = [];
2286
+ if (typeof runtime.model === "string") {
2287
+ routes.push({ model: runtime.model, ...(primaryEffort === undefined ? {} : { effort: primaryEffort }) });
2288
+ }
2289
+ if (Array.isArray(runtime.fallbacks) && runtime.fallbacks.length > 0) {
2290
+ for (const raw of runtime.fallbacks) {
2291
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw))
2292
+ continue;
2293
+ const entry = raw;
2294
+ if (typeof entry.model !== "string")
2295
+ continue;
2296
+ routes.push({
2297
+ model: entry.model,
2298
+ ...(typeof entry.effort === "string" ? { effort: entry.effort } : {}),
2299
+ });
2300
+ }
2301
+ }
2302
+ else if (Array.isArray(runtime.fallbackModels)) {
2303
+ for (const model of runtime.fallbackModels) {
2304
+ if (typeof model === "string") {
2305
+ routes.push({ model, ...(primaryEffort === undefined ? {} : { effort: primaryEffort }) });
2306
+ }
2307
+ }
2308
+ }
2309
+ return routes;
2310
+ }
2311
+ function printReadinessRouteSummary(result, planned) {
2312
+ const reported = result.routes ?? planned.map((route, index) => ({
2313
+ key: `${index}:${route.model}`,
2314
+ index,
2315
+ ...route,
2316
+ status: result.ok ? "verified" : result.kind === "cancelled" ? "interrupted" : "failed",
2317
+ ...(!result.ok ? { kind: result.kind, message: result.message } : {}),
2318
+ }));
2319
+ for (const route of reported) {
2320
+ const badge = route.status === "verified" || route.status === "skipped_verified"
2321
+ ? ui.badge("ok")
2322
+ : route.status === "interrupted"
2323
+ ? ui.badge("waiting")
2324
+ : ui.badge("error");
2325
+ const state = route.status === "skipped_verified" ? "verified earlier" : route.status.replaceAll("_", " ");
2326
+ process.stdout.write(`${badge}Route ${route.index + 1}/${planned.length}: ${route.model} ` +
2327
+ ui.style.dim(`(effort: ${route.effort ?? "provider-default"})`) +
2328
+ ` — ${state}${route.message === undefined ? "" : `: ${route.message}`}\n`);
2329
+ }
2330
+ if (!result.ok && result.interrupted === true) {
2331
+ process.stderr.write(ui.errorLine("Preflight was interrupted."));
2332
+ }
2333
+ }
2334
+ async function withScopedPreflightCancellation(task, options = {}) {
2335
+ const controller = new AbortController();
2336
+ let interruptCount = 0;
2337
+ const interrupt = () => {
2338
+ interruptCount += 1;
2339
+ controller.abort();
2340
+ if (interruptCount > 1)
2341
+ process.exitCode = 130;
2342
+ };
2343
+ const onKeypress = (_value, key) => {
2344
+ if (key?.name === "escape" || (key?.ctrl === true && key.name === "c"))
2345
+ interrupt();
2346
+ };
2347
+ process.on("SIGINT", interrupt);
2348
+ const restoreKeypress = options.keypress === false
2349
+ ? () => undefined
2350
+ : attachScopedKeypress(onKeypress);
2351
+ try {
2352
+ return await task(controller.signal);
2353
+ }
2354
+ finally {
2355
+ process.off("SIGINT", interrupt);
2356
+ restoreKeypress();
2357
+ }
2358
+ }
2359
+ function focusedConfigurationRepairStep(sectionIds) {
2360
+ const mapped = new Set();
2361
+ for (const id of sectionIds) {
2362
+ if (id === "agent")
2363
+ mapped.add(0);
2364
+ else if (id === "runtime" || id === "credentials")
2365
+ mapped.add(1);
2366
+ else if (id === "memory" || id.startsWith("memory:"))
2367
+ mapped.add(3);
2368
+ else if (id === "context" || id.startsWith("channel:"))
2369
+ mapped.add(4);
2370
+ else if (id === "tools")
2371
+ mapped.add(5);
2372
+ else if (id === "sandbox")
2373
+ mapped.add(6);
2374
+ else if (id === "observability")
2375
+ mapped.add(7);
2376
+ }
2377
+ return mapped.size === 1 ? [...mapped][0] : undefined;
2378
+ }
2379
+ function configurationRecoveryEditLabel(step) {
2380
+ switch (step) {
2381
+ case 0: return "Edit agent name";
2382
+ case 1: return "Edit model routes";
2383
+ case 3: return "Edit memory";
2384
+ case 4: return "Edit capability details";
2385
+ case 5: return "Edit tools";
2386
+ case 6: return "Edit route safety and sandbox";
2387
+ case 7: return "Edit observability";
2388
+ default: return "Edit setup choices";
2389
+ }
2390
+ }
2391
+ async function selectSandboxPreflightRecovery() {
2392
+ const recovery = await p.select({
2393
+ message: "Sandbox preflight did not pass. How would you like to recover?",
2394
+ initialValue: "retry",
2395
+ options: [
2396
+ { value: "retry", label: "Retry sandbox setup and check" },
2397
+ { value: "edit", label: "Change safety or other choices" },
2398
+ { value: "cancel", label: "Cancel without writing" },
2399
+ ],
2400
+ });
2401
+ return p.isCancel(recovery) ? "cancel" : recovery;
2402
+ }
2403
+ async function selectInterruptedFirstRunRecovery() {
2404
+ const recovery = await p.select({
2405
+ message: "Preflight was interrupted. What would you like to do?",
2406
+ initialValue: "resume",
2407
+ options: [
2408
+ { value: "resume", label: "Resume preflight", hint: "keeps successful auth, SRT setup, and route checks" },
2409
+ { value: "restart", label: "Restart all checks", hint: "keeps successful auth and SRT installation" },
2410
+ { value: "edit", label: "Edit setup choices" },
2411
+ { value: "cancel", label: "Cancel without writing" },
2412
+ ],
2413
+ });
2414
+ return p.isCancel(recovery) ? "cancel" : recovery;
2415
+ }
2416
+ async function selectFirstRunRecovery(failure, configurationRepairStep, invalidPlanStage) {
2417
+ const sharedTail = [
2418
+ { value: "save", label: "Save incomplete", hint: "does not call the agent ready or start it" },
2419
+ { value: "cancel", label: "Cancel without writing" },
2420
+ ];
2421
+ const providerSetupFailed = failure.kind === "provider_failed"
2422
+ && /^Provider setup (?:did not complete|still needs attention)\./u.test(failure.message);
2423
+ const message = failure.kind === "invalid_plan"
2424
+ ? invalidPlanStage === "final_readiness"
2425
+ ? "Final readiness validation did not pass. What would you like to do?"
2426
+ : "Configuration preflight did not pass. What would you like to do?"
2427
+ : providerSetupFailed
2428
+ ? "Provider setup did not pass. What would you like to do?"
2429
+ : "Runtime readiness did not pass. What would you like to do?";
2430
+ const options = failure.kind === "invalid_plan"
2431
+ ? [
2432
+ { value: "edit", label: configurationRecoveryEditLabel(configurationRepairStep) },
2433
+ {
2434
+ value: "retry",
2435
+ label: invalidPlanStage === "final_readiness"
2436
+ ? "Retry final readiness validation"
2437
+ : "Retry configuration preflight",
2438
+ },
2439
+ ...sharedTail,
2440
+ ]
2441
+ : providerSetupFailed
2442
+ ? [
2443
+ { value: "auth", label: "Repair authentication" },
2444
+ { value: "retry", label: "Retry provider setup" },
2445
+ { value: "model", label: "Edit model routes" },
2446
+ ...sharedTail,
2447
+ ]
2448
+ : failure.kind === "provider_failed"
2449
+ ? [
2450
+ { value: "retry", label: "Retry failed route" },
2451
+ { value: "auth", label: "Repair authentication" },
2452
+ { value: "model", label: "Edit model routes" },
2453
+ ...sharedTail,
2454
+ ]
2455
+ : failure.kind === "unsupported_guided_probe"
2456
+ ? [
2457
+ { value: "model", label: "Edit model routes" },
2458
+ ...sharedTail,
2459
+ ]
2460
+ : [
2461
+ { value: "retry", label: "Retry runtime checks" },
2462
+ { value: "model", label: "Edit model routes" },
2463
+ ...sharedTail,
2464
+ ];
2465
+ const recovery = await p.select({
2466
+ message,
2467
+ initialValue: options[0]?.value ?? "cancel",
2468
+ options: [...options],
2469
+ });
2470
+ return p.isCancel(recovery) ? "cancel" : recovery;
2471
+ }
2472
+ async function promptProviderSetupSecrets(plan, existing, existingPersistence = {}, existingEnvironmentSecrets = {}) {
2473
+ const values = { ...existing };
2474
+ const persistenceByProvider = { ...existingPersistence };
2475
+ const environmentSecrets = { ...existingEnvironmentSecrets };
2476
+ for (const action of plan.actions) {
2477
+ if (!isProviderSetupPiApiKeyAction(action))
2478
+ continue;
2479
+ const reviewedPersistence = existingPersistence[action.provider];
2480
+ const persistence = reviewedPersistence ?? await p.select({
2481
+ message: `How should ${action.label} receive ${action.envVar}?`,
2482
+ initialValue: "secure-store",
2483
+ options: [
2484
+ { value: "secure-store", label: "Store securely in Pi auth.json", hint: "owner-only credential store" },
2485
+ { value: "environment", label: `Use environment variable ${action.envVar}`, hint: "save it to the agent's owner-only .env" },
2486
+ ],
2487
+ });
2488
+ if (p.isCancel(persistence))
2489
+ return undefined;
2490
+ if (persistence === "environment") {
2491
+ delete values[action.id];
2492
+ persistenceByProvider[action.provider] = "environment";
2493
+ const answer = await p.password({
2494
+ message: `Enter ${action.envVar} for the agent's owner-only .env`,
2495
+ validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
2496
+ clearOnError: true,
2497
+ });
2498
+ if (p.isCancel(answer))
2499
+ return undefined;
2500
+ environmentSecrets[action.envVar] = answer;
2501
+ continue;
2502
+ }
2503
+ persistenceByProvider[action.provider] = "secure-store";
2504
+ delete environmentSecrets[action.envVar];
2505
+ const answer = await p.password({
2506
+ message: `Enter ${action.label} (${action.envVar})`,
2507
+ validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
2508
+ clearOnError: true,
2509
+ });
2510
+ if (p.isCancel(answer))
2511
+ return undefined;
2512
+ values[action.id] = answer;
2513
+ }
2514
+ return { apiKeys: values, persistenceByProvider, environmentSecrets };
2515
+ }
2516
+ function environmentProviderApiKeys(plan, env) {
2517
+ const values = {};
2518
+ for (const action of plan.actions) {
2519
+ if (!isProviderSetupPiApiKeyAction(action) || action.persistence !== "environment")
2520
+ continue;
2521
+ const value = env[action.envVar];
2522
+ if (nonEmptyEnv(value))
2523
+ values[action.id] = value;
2524
+ }
2525
+ return values;
2526
+ }
2527
+ async function selectCodexAuthMode(initialValue) {
2528
+ const selected = await p.select({
2529
+ message: "How should Codex authenticate on this machine?",
2530
+ initialValue,
2531
+ options: [
2532
+ { value: "browser", label: "Browser login", hint: "opens a localhost callback server" },
2533
+ { value: "device", label: "Device-code login", hint: "recommended for remote or headless machines" },
2534
+ ],
2535
+ });
2536
+ return p.isCancel(selected) ? undefined : selected;
2537
+ }
2538
+ function configuredSecretNames(result, effectiveEnv) {
2539
+ const names = new Set();
2540
+ for (const secret of result.plan.secrets) {
2541
+ if (nonEmptyEnv(effectiveEnv[secret.envVar]))
2542
+ names.add(secret.envVar);
2543
+ }
2544
+ return names;
2545
+ }
2546
+ function printIncompleteSetup(reasons, configPath) {
2547
+ process.stderr.write(ui.hint("INCOMPLETE SETUP: no readiness claim was made and the agent was not started.\n"));
2548
+ for (const reason of reasons)
2549
+ process.stderr.write(ui.style.yellow(` - ${reason}\n`));
2550
+ process.stderr.write(ui.hint(`Review ${configPath}, run \`mono-agent validate\`, then retry the first turn.\n`));
2551
+ }
2552
+ export function shouldRunInitWizard(args, stdinIsTty, stdoutIsTty) {
2553
+ if (!stdinIsTty || !stdoutIsTty || args.command !== "init" || args.positionals.length > 0) {
2554
+ return false;
2555
+ }
2556
+ if (args.force || args.foreground || args.follow || args.all || args.dryRun || args.includeMemory) {
2557
+ return false;
2558
+ }
2559
+ // A bare parsed init has only these required/default keys. Treat every
2560
+ // optional key—current or future—as an overriding flag so the documented
2561
+ // "any flag is scaffold-only" contract cannot silently drift again.
2562
+ const bareKeys = new Set([
2563
+ "command",
2564
+ "positionals",
2565
+ "force",
2566
+ "foreground",
2567
+ "follow",
2568
+ "all",
2569
+ "dryRun",
2570
+ "includeMemory",
2571
+ ]);
2572
+ return Object.keys(args).every((key) => bareKeys.has(key));
2573
+ }
744
2574
  export async function runProviderSetupBeforeInit(options) {
745
- const plan = planProviderSetup(options);
2575
+ const credentialStates = options.credentialStates !== undefined
2576
+ ? options.credentialStates
2577
+ : options.forceAuthentication === true || !options.auth || options.dryRun
2578
+ ? undefined
2579
+ : await detectProviderCredentialStates({
2580
+ modelRefs: options.modelRefs,
2581
+ cwd: options.cwd,
2582
+ ...(options.piAuthPath === undefined ? {} : { piAuthPath: options.piAuthPath }),
2583
+ ...(options.persistedEnv === undefined ? {} : { persistedEnv: options.persistedEnv }),
2584
+ ...(options.abortSignal === undefined ? {} : { abortSignal: options.abortSignal }),
2585
+ });
2586
+ const plan = planProviderSetup({
2587
+ ...options,
2588
+ ...(credentialStates === undefined ? {} : { credentialStates }),
2589
+ ...(options.codexAuthMode === undefined ? {} : { codexAuthMode: options.codexAuthMode }),
2590
+ ...(options.forceAuthentication === undefined ? {} : { forceAuthentication: options.forceAuthentication }),
2591
+ });
746
2592
  if (plan.actions.length === 0) {
747
2593
  return "skipped";
748
2594
  }
@@ -757,23 +2603,191 @@ export async function runProviderSetupBeforeInit(options) {
757
2603
  }
758
2604
  process.stdout.write("\n" + ui.heading("Provider setup"));
759
2605
  printProviderSetupPlan(plan);
2606
+ if (options.abortSignal !== undefined) {
2607
+ process.stdout.write(ui.style.dim("Press Ctrl-C once to interrupt authentication safely.\n"));
2608
+ }
760
2609
  const results = await (options.execute ?? ((setupPlan) => executeProviderSetupPlan(setupPlan, {
761
2610
  ...(options.apiKeys === undefined ? {} : { apiKeys: options.apiKeys }),
2611
+ ...(options.abortSignal === undefined ? {} : { abortSignal: options.abortSignal }),
762
2612
  })))(plan);
2613
+ const interrupted = options.abortSignal?.aborted === true;
763
2614
  for (const result of results) {
764
- const badge = result.status === "ok"
765
- ? ui.badge("ok")
766
- : result.status === "skipped"
767
- ? ui.style.dim("- ")
768
- : ui.badge("error");
2615
+ const badge = interrupted && result.status === "failed"
2616
+ ? ui.badge("waiting")
2617
+ : result.status === "ok"
2618
+ ? ui.badge("ok")
2619
+ : result.status === "skipped"
2620
+ ? ui.style.dim("- ")
2621
+ : ui.badge("error");
769
2622
  process.stdout.write(`${badge}${result.action.label}: ${result.detail}\n`);
770
2623
  }
2624
+ if (results.some((result) => result.failureKind !== undefined)) {
2625
+ process.stderr.write(ui.errorLine("Provider setup ended in an unconfirmed process or credential-cleanup state. Follow the reported manual cleanup guidance before retrying; automatic recovery is disabled."));
2626
+ return "fatal";
2627
+ }
2628
+ if (interrupted) {
2629
+ process.stderr.write(ui.errorLine("Provider setup was interrupted."));
2630
+ return "interrupted";
2631
+ }
771
2632
  if (results.some((result) => result.status === "failed")) {
772
2633
  process.stderr.write(ui.errorLine("Provider setup failed; init stopped before writing files."));
773
2634
  return "failed";
774
2635
  }
775
2636
  return "ok";
776
2637
  }
2638
+ async function runAuth(args) {
2639
+ const [subcommand, provider, ...extra] = args.positionals;
2640
+ if (subcommand !== "login" || provider === undefined || extra.length > 0) {
2641
+ process.stderr.write(ui.errorLine("Usage: mono-agent auth login <provider|codex> [--pi-auth-path <path>] [--api-key-stdin] [--codex-auth browser|device] [--config <path>]."));
2642
+ return 2;
2643
+ }
2644
+ const cwd = process.cwd();
2645
+ const configPath = await canonicalBackgroundConfigPath(cwd, args.configPath);
2646
+ const directCodex = provider === "codex";
2647
+ if (directCodex && args.piAuthPath !== undefined) {
2648
+ process.stderr.write(ui.errorLine("--pi-auth-path does not apply to direct Codex login."));
2649
+ return 2;
2650
+ }
2651
+ let configuredPiAuthPath;
2652
+ try {
2653
+ configuredPiAuthPath = directCodex ? resolve(cwd, ".pi", "auth.json") : await resolvePiAuthPathForLogin({
2654
+ configPath,
2655
+ cwd,
2656
+ ...(process.env.MONO_AGENT_PI_AUTH_PATH === undefined
2657
+ ? {}
2658
+ : { envPath: process.env.MONO_AGENT_PI_AUTH_PATH }),
2659
+ ...(args.piAuthPath === undefined ? {} : { piAuthPath: args.piAuthPath }),
2660
+ });
2661
+ }
2662
+ catch (error) {
2663
+ process.stderr.write(ui.errorLine(`Cannot resolve the Pi auth path from ${configPath}: ${error instanceof Error ? error.message : String(error)}`));
2664
+ return 1;
2665
+ }
2666
+ const plan = planProviderSetup({
2667
+ modelRefs: [directCodex ? "codex:gpt-5.6-terra" : `pi:${provider}:credential-setup`],
2668
+ cwd,
2669
+ piAuthPath: configuredPiAuthPath,
2670
+ forceAuthentication: true,
2671
+ ...(args.codexAuthMode === undefined ? {} : { codexAuthMode: args.codexAuthMode }),
2672
+ });
2673
+ if (plan.actions.length === 0) {
2674
+ process.stderr.write(ui.errorLine(`Provider \`${provider}\` has no interactive auth method in the bundled ${directCodex ? "Codex" : "Pi"} provider catalog.`));
2675
+ return 2;
2676
+ }
2677
+ const apiKeyActions = plan.actions.filter(isProviderSetupPiApiKeyAction);
2678
+ if (args.apiKeyStdin === true && apiKeyActions.length !== 1) {
2679
+ process.stderr.write(ui.errorLine("--api-key-stdin is only supported when the selected provider has one bundled API-key login action."));
2680
+ return 2;
2681
+ }
2682
+ process.stdout.write("\n" + ui.heading(directCodex ? "Codex authentication" : "Pi authentication"));
2683
+ printProviderSetupPlan(plan);
2684
+ let apiKeys;
2685
+ const apiKeyAction = apiKeyActions[0];
2686
+ if (apiKeyAction !== undefined) {
2687
+ let apiKey;
2688
+ if (args.apiKeyStdin === true) {
2689
+ if (process.stdin.isTTY === true) {
2690
+ process.stderr.write(ui.errorLine("--api-key-stdin requires redirected standard input. Omit the flag to enter the key in a masked prompt."));
2691
+ return 2;
2692
+ }
2693
+ try {
2694
+ apiKey = await readApiKeyFromStdin(process.stdin);
2695
+ }
2696
+ catch {
2697
+ process.stderr.write(ui.errorLine(`Could not read a valid ${apiKeyAction.envVar} value from standard input; no credentials were written.`));
2698
+ return 1;
2699
+ }
2700
+ }
2701
+ else {
2702
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
2703
+ process.stderr.write(ui.errorLine(`Cannot securely prompt for ${apiKeyAction.envVar} without an interactive TTY. ` +
2704
+ `Run this command in a terminal, or pipe the value explicitly with --api-key-stdin; no credentials were written.`));
2705
+ return 1;
2706
+ }
2707
+ const answer = await p.password({
2708
+ message: `Enter ${apiKeyAction.label} (${apiKeyAction.envVar})`,
2709
+ validate: (value) => apiKeyInputProblem(value ?? ""),
2710
+ clearOnError: true,
2711
+ });
2712
+ if (p.isCancel(answer)) {
2713
+ process.stderr.write(ui.errorLine("Authentication was cancelled; no credentials were written."));
2714
+ return 130;
2715
+ }
2716
+ apiKey = answer.trim();
2717
+ }
2718
+ apiKeys = { [apiKeyAction.id]: apiKey };
2719
+ }
2720
+ process.stdout.write(ui.style.dim("Press Ctrl-C once to interrupt authentication safely.\n"));
2721
+ const execution = await withScopedPreflightCancellation(async (abortSignal) => ({
2722
+ results: await executeProviderSetupPlan(plan, {
2723
+ ...(apiKeys === undefined ? {} : { apiKeys }),
2724
+ abortSignal,
2725
+ }),
2726
+ interrupted: abortSignal.aborted,
2727
+ }), { keypress: false });
2728
+ const { results } = execution;
2729
+ for (const result of results) {
2730
+ const badge = execution.interrupted && result.status === "failed"
2731
+ ? ui.badge("waiting")
2732
+ : result.status === "ok"
2733
+ ? ui.badge("ok")
2734
+ : ui.badge("error");
2735
+ process.stdout.write(`${badge}${result.action.label}: ${result.detail}\n`);
2736
+ }
2737
+ if (results.some((result) => result.failureKind !== undefined)) {
2738
+ process.stderr.write(ui.errorLine("Provider setup ended in an unconfirmed process or credential-cleanup state. Follow the reported manual cleanup guidance before retrying; automatic recovery is disabled."));
2739
+ return 130;
2740
+ }
2741
+ if (execution.interrupted) {
2742
+ process.stderr.write(ui.errorLine("Authentication was interrupted; temporary credentials were cleaned up."));
2743
+ return 130;
2744
+ }
2745
+ return results.every((result) => result.status === "ok") ? 0 : 1;
2746
+ }
2747
+ const MAX_STANDALONE_API_KEY_BYTES = 65_536;
2748
+ function apiKeyInputProblem(value) {
2749
+ const normalized = value.trim();
2750
+ if (normalized.length === 0)
2751
+ return "API key is required.";
2752
+ if (normalized.includes("\0") || /[\r\n]/u.test(normalized))
2753
+ return "API key must be a single non-empty line.";
2754
+ if (Buffer.byteLength(normalized, "utf8") > MAX_STANDALONE_API_KEY_BYTES)
2755
+ return "API key is too large.";
2756
+ return undefined;
2757
+ }
2758
+ /**
2759
+ * Read one explicitly redirected API key without consulting ambient provider
2760
+ * environment variables. A single trailing line ending from `echo` is accepted;
2761
+ * embedded newlines, NUL bytes, empty input, and unbounded input fail closed.
2762
+ */
2763
+ export async function readApiKeyFromStdin(input) {
2764
+ const chunks = [];
2765
+ let size = 0;
2766
+ for await (const chunk of input) {
2767
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
2768
+ size += bytes.byteLength;
2769
+ if (size > MAX_STANDALONE_API_KEY_BYTES + 2)
2770
+ throw new Error("API key input is too large.");
2771
+ chunks.push(bytes);
2772
+ }
2773
+ const value = Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/u, "");
2774
+ const problem = apiKeyInputProblem(value);
2775
+ if (problem !== undefined)
2776
+ throw new Error(problem);
2777
+ return value.trim();
2778
+ }
2779
+ export async function resolvePiAuthPathForLogin(options) {
2780
+ // A missing config is represented by readMonoAgentConfigJson as `missing`; a
2781
+ // malformed or unreadable config throws and must remain visible to operators.
2782
+ const result = await readMonoAgentConfigJson(options.configPath);
2783
+ const configured = result.missing ? undefined : result.json.providers?.piAuthPath;
2784
+ return resolveEffectivePiAuthPath({
2785
+ cwd: options.cwd ?? dirname(resolve(options.configPath)),
2786
+ ...(nonEmptyEnv(options.piAuthPath) ? { explicitPath: options.piAuthPath } : {}),
2787
+ ...(nonEmptyEnv(options.envPath) ? { envPath: options.envPath } : {}),
2788
+ ...(nonEmptyEnv(configured) ? { configPath: configured } : {}),
2789
+ });
2790
+ }
777
2791
  function printProviderSetupPlan(plan) {
778
2792
  for (const action of plan.actions) {
779
2793
  process.stdout.write(` ${action.label}: ${providerSetupActionCommandLine(action)} ${ui.style.dim(`(cwd: ${action.cwd})`)}\n`);
@@ -820,17 +2834,56 @@ function resolveWithChannels(args) {
820
2834
  }
821
2835
  return args.withChannels.filter(isWithChannel);
822
2836
  }
2837
+ /** Safe reporting rows: paths and outcomes only, never secret contents. */
2838
+ export function initChangeDisplayRows(result) {
2839
+ const labels = {
2840
+ created: "created",
2841
+ updated: "updated",
2842
+ unchanged: "kept",
2843
+ "planned-create": "would create",
2844
+ "planned-update": "would update",
2845
+ };
2846
+ return result.changes.map((change) => ({
2847
+ label: labels[change.kind],
2848
+ path: change.path,
2849
+ unchanged: change.kind === "unchanged",
2850
+ }));
2851
+ }
2852
+ /** Human-readable, value-free proof of where the wizard Role did (or did not) land. */
2853
+ export function identityRoleDisplayLine(identityRole) {
2854
+ switch (identityRole.status) {
2855
+ case "created":
2856
+ return `Role saved to ${identityRole.path} → ${identityRole.section}. ` +
2857
+ `Edit ${identityRole.path} → ${identityRole.section} later to change it.`;
2858
+ case "preserved":
2859
+ return `${identityRole.path} already existed and was preserved; the entered Role was not written. ` +
2860
+ `Add or edit ${identityRole.path} → ${identityRole.section} to set it.`;
2861
+ case "planned-create":
2862
+ return `Dry run: Role would be saved to ${identityRole.path} → ${identityRole.section}.`;
2863
+ }
2864
+ }
2865
+ export function secretChecklistDisplayRows(secrets, configured) {
2866
+ return secrets.map((secret) => ({
2867
+ envVar: secret.envVar,
2868
+ label: secret.label,
2869
+ description: secret.description,
2870
+ status: configured.has(secret.envVar) ? "configured" : secret.required ? "missing" : "optional",
2871
+ }));
2872
+ }
823
2873
  function printInitResult(result) {
824
2874
  if (result.dryRun) {
825
2875
  process.stdout.write(ui.style.dim("Dry run — nothing was written.\n"));
826
2876
  }
827
- const verb = result.dryRun ? "would create" : "created";
828
- for (const path of result.created) {
829
- process.stdout.write(`${ui.badge("ok")}${ui.style.green(verb.padEnd(12))} ${path}\n`);
830
- }
831
- for (const path of result.skipped) {
832
- process.stdout.write(ui.style.dim(` kept ${path}`) + "\n");
2877
+ for (const row of initChangeDisplayRows(result)) {
2878
+ const prefix = row.unchanged ? " " : ui.badge("ok");
2879
+ const rendered = row.unchanged
2880
+ ? ui.style.dim(row.label.padEnd(12))
2881
+ : ui.style.green(row.label.padEnd(12));
2882
+ // Sensitive files are safe to identify by path; their contents are never
2883
+ // included in this result or printed here.
2884
+ process.stdout.write(`${prefix}${rendered} ${row.path}\n`);
833
2885
  }
2886
+ process.stdout.write(`\n${identityRoleDisplayLine(result.identityRole)}\n`);
834
2887
  if (result.knowledgeFiles.length > 0) {
835
2888
  process.stdout.write(`\nIdentity references existing knowledge: ${ui.style.cyan(result.knowledgeFiles.join(", "))}\n`);
836
2889
  }
@@ -843,29 +2896,107 @@ function printInitResult(result) {
843
2896
  process.stdout.write(` ${ui.style.cyan(module.title)} ${ui.style.dim(`(risk: ${riskColor(module.riskLevel)})`)}\n`);
844
2897
  }
845
2898
  }
846
- if (result.plan.envExample !== undefined) {
847
- process.stdout.write("\n" + ui.style.dim("Fill the secret placeholders in .env.example, then copy it to .env.\n"));
2899
+ if (result.secretPersistence.status === "persisted") {
2900
+ process.stdout.write("\n" + ui.style.dim(result.secretPersistence.changed
2901
+ ? "Required secrets were securely merged into .env (mode 0600).\n"
2902
+ : "Required secrets were already securely configured in .env.\n"));
2903
+ }
2904
+ else if (result.secretPersistence.status === "planned") {
2905
+ process.stdout.write("\n" + ui.style.dim("Dry run: required secrets would be securely merged into .env.\n"));
2906
+ }
2907
+ else if (result.secretPersistence.status === "refused") {
2908
+ process.stderr.write(ui.hint(`Automatic secret persistence was refused${result.secretPersistence.reason === undefined ? "" : ` (${result.secretPersistence.reason})`}. No secret value was written.\n` +
2909
+ (result.secretPersistence.detail === undefined ? "" : `${result.secretPersistence.detail}\n`)));
2910
+ }
2911
+ else if (result.plan.envExample !== undefined) {
2912
+ process.stdout.write("\n" + ui.style.dim("Use .env.example as a reference and add missing values to .env; do not overwrite an existing .env.\n"));
848
2913
  }
849
2914
  }
850
- function printSecretsChecklist(secrets) {
2915
+ function printSecretsChecklist(secrets, configured = new Set()) {
851
2916
  process.stdout.write("\n" + ui.heading("Secrets checklist"));
852
2917
  if (secrets.length === 0) {
853
2918
  process.stdout.write(ui.style.dim("No secrets required by the selected capabilities.\n"));
854
2919
  return;
855
2920
  }
856
- process.stdout.write(ui.style.dim("Copy .env.example to .env, then fill these variables. Secret values are never written to JSON.\n"));
857
- for (const secret of secrets) {
858
- process.stdout.write(` ${ui.style.bold(secret.envVar)} ${ui.style.dim(`- ${secret.label}: ${secret.description}`)}\n`);
2921
+ process.stdout.write(ui.style.dim("Secret values are never written to config JSON and are never printed.\n"));
2922
+ for (const secret of secretChecklistDisplayRows(secrets, configured)) {
2923
+ const status = secret.status === "configured"
2924
+ ? ui.style.green(secret.status)
2925
+ : ui.style.yellow(secret.status);
2926
+ process.stdout.write(` ${ui.style.bold(secret.envVar)} ${ui.style.dim(`- ${secret.label}: ${secret.description}`)} ${status}\n`);
859
2927
  }
860
2928
  }
861
2929
  function printNextSteps(configPath) {
2930
+ const startCommand = process.platform === "darwin" ? "mono-agent start" : "mono-agent start --foreground";
2931
+ const tuiCommand = process.platform === "darwin"
2932
+ ? `mono-agent tui --configure ${ui.style.dim("(after the agent reports ready)")}`
2933
+ : `mono-agent tui ${ui.style.dim("(ordinary chat after foreground startup; edit config files manually)")}`;
862
2934
  process.stdout.write("\n" +
863
2935
  ui.heading("Next steps") +
864
2936
  ` ${ui.style.bold("1.")} Edit ${configPath} ${ui.style.dim("(model, channels, skills, memory, sandbox)")}\n` +
865
2937
  ` ${ui.style.bold("2.")} mono-agent validate\n` +
866
- ` ${ui.style.bold("3.")} mono-agent start\n`);
2938
+ ` ${ui.style.bold("3.")} ${startCommand}\n` +
2939
+ ` ${ui.style.bold("4.")} ${tuiCommand}\n`);
2940
+ }
2941
+ function printUnsupportedGuidedInitHandoff(configPath, envFile) {
2942
+ const flags = guidedHandoffFlags(configPath, envFile);
2943
+ process.stdout.write("\n" +
2944
+ ui.heading("Manual start required") +
2945
+ ui.style.yellow("Automatic background start and configuration chat require macOS launchd.\n") +
2946
+ ui.style.dim("The validated agent files were preserved, but no agent process was started and readiness is not claimed.\n") +
2947
+ ` ${ui.style.bold("Configure manually:")} edit ${configPath} and IDENTITY.md, then run mono-agent validate${flags}\n` +
2948
+ ` ${ui.style.bold("Terminal 1:")} mono-agent start --foreground${flags}\n` +
2949
+ ` ${ui.style.bold("Terminal 2:")} mono-agent tui${flags} ${ui.style.dim("(ordinary chat after startup completes)")}\n` +
2950
+ ui.style.dim("Conversational configuration requires the managed macOS background lifecycle.\n"));
2951
+ }
2952
+ function printUnexpectedGuidedBackgroundFailure(configPath, envFile, error) {
2953
+ const flags = guidedHandoffFlags(configPath, envFile);
2954
+ const paths = launchdPathsFor(deriveLaunchdLabel(configPath));
2955
+ process.stderr.write(ui.errorLine(`The background lifecycle failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`));
2956
+ process.stderr.write(ui.style.dim("The validated agent files were preserved, but configuration chat was not opened. Retry or inspect with:\n"));
2957
+ process.stderr.write(` ${ui.style.gray("logs: ")} ${paths.stderrPath}\n` +
2958
+ ` ${paths.stdoutPath}\n` +
2959
+ ` ${ui.style.gray("retry: ")} mono-agent start${flags}\n` +
2960
+ ` ${ui.style.gray("status:")} mono-agent status${flags}\n` +
2961
+ ` ${ui.style.gray("follow:")} mono-agent logs${flags} --follow\n`);
2962
+ }
2963
+ function guidedHandoffFlags(configPath, envFile) {
2964
+ return ` --config ${shellCommandArgument(configPath)}`
2965
+ + (envFile === undefined ? "" : ` --env-file ${shellCommandArgument(envFile)}`);
2966
+ }
2967
+ function shellCommandArgument(value) {
2968
+ return /^[a-zA-Z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
867
2969
  }
868
2970
  async function runInstallSkill(args) {
2971
+ if (args.project === true) {
2972
+ try {
2973
+ if (args.update === true) {
2974
+ const result = await updateManagedProjectSkills(process.cwd());
2975
+ for (const path of result.updated) {
2976
+ process.stdout.write(`${ui.badge("ok")}${ui.style.green("updated")} ${path}\n`);
2977
+ }
2978
+ if (result.backupDir !== undefined) {
2979
+ process.stdout.write(`${ui.badge("ok")}backup ${result.backupDir}\n`);
2980
+ }
2981
+ if (result.updated.length === 0)
2982
+ process.stdout.write(`${ui.badge("ok")}project skills are current\n`);
2983
+ return 0;
2984
+ }
2985
+ const result = await checkManagedProjectSkills(process.cwd());
2986
+ for (const status of result.statuses) {
2987
+ const badge = status.status === "ready" ? ui.badge("ok") : ui.badge("error");
2988
+ process.stdout.write(`${badge}${status.name}: ${status.status} (${status.path})\n`);
2989
+ }
2990
+ if (!result.ok && args.check !== true) {
2991
+ process.stderr.write(ui.errorLine("Project skills need attention. Run `mono-agent install-skill --project --update`; modified copies require manual reconciliation."));
2992
+ }
2993
+ return result.ok ? 0 : 1;
2994
+ }
2995
+ catch (error) {
2996
+ process.stderr.write(ui.errorLine(error instanceof Error ? error.message : String(error)));
2997
+ return 1;
2998
+ }
2999
+ }
869
3000
  let result;
870
3001
  try {
871
3002
  result = await installComposerSkill({
@@ -890,22 +3021,58 @@ async function runValidate(args) {
890
3021
  configPath: context.configPath,
891
3022
  allowFilesystemWrites: context.allowFilesystemWrites,
892
3023
  });
893
- for (const section of report.sections) {
894
- process.stdout.write(formatSection(section));
895
- }
896
3024
  const preset = resolveValidatePreset(args);
897
3025
  if (preset === "unknown") {
3026
+ if (args.json === true) {
3027
+ process.stdout.write(`${JSON.stringify({ ok: false, sections: report.sections })}\n`);
3028
+ }
898
3029
  return 1;
899
3030
  }
3031
+ let presetResult;
900
3032
  if (preset !== undefined) {
901
3033
  const plan = composeWizardPlan(presetAnswers(preset), {
902
3034
  dirBasename: basename(context.cwd),
903
3035
  skillsRootExists: false,
904
3036
  });
3037
+ presetResult = {
3038
+ id: preset.id,
3039
+ expectations: plan.validateExpectations.map((expectation) => {
3040
+ const actual = report.sections.find((entry) => entry.id === expectation.sectionId)?.status ?? "missing";
3041
+ return {
3042
+ sectionId: expectation.sectionId,
3043
+ expected: expectation.mustBe,
3044
+ actual,
3045
+ met: actual === expectation.mustBe,
3046
+ };
3047
+ }),
3048
+ };
3049
+ if (args.json === true) {
3050
+ process.stdout.write(`${JSON.stringify({
3051
+ ok: report.ok,
3052
+ sections: report.sections,
3053
+ preset: presetResult,
3054
+ })}\n`);
3055
+ return report.ok ? 0 : 1;
3056
+ }
3057
+ for (const section of report.sections) {
3058
+ process.stdout.write(formatSection(section));
3059
+ }
905
3060
  process.stdout.write(renderPlanCompleteness(plan.validateExpectations, `Preset: ${preset.id}`, report));
906
3061
  }
3062
+ else if (args.json === true) {
3063
+ process.stdout.write(`${JSON.stringify({ ok: report.ok, sections: report.sections })}\n`);
3064
+ return report.ok ? 0 : 1;
3065
+ }
3066
+ else {
3067
+ for (const section of report.sections) {
3068
+ process.stdout.write(formatSection(section));
3069
+ }
3070
+ }
3071
+ const hasWaitingSections = report.sections.some((section) => section.status === "waiting");
907
3072
  process.stdout.write(report.ok
908
- ? `\n${ui.style.green("✓ Config is ready to start.")}\n${ui.style.dim("Run `mono-agent config` for the full field-by-field view.")}\n`
3073
+ ? hasWaitingSections
3074
+ ? `\n${ui.style.yellow("⚠ Config is structurally valid, but needs attention before start.")}\n${ui.style.dim("Review the waiting sections above, then re-run mono-agent validate.")}\n`
3075
+ : `\n${ui.style.green("✓ Config is ready to start.")}\n${ui.style.dim("Run `mono-agent config` for the full field-by-field view.")}\n`
909
3076
  : `\n${ui.hint("Fix the errors above, then re-run mono-agent validate.")}`);
910
3077
  process.stdout.write(ui.style.dim("Core sections activate by presence; channels need `enabled: true` — see docs/config (How sections activate).\n"));
911
3078
  return report.ok ? 0 : 1;
@@ -1151,13 +3318,19 @@ function colorDetail(status, detail) {
1151
3318
  * refuse on any `error` section. `waiting` (e.g. Ollama/Phoenix not up yet) is
1152
3319
  * runtime-soft and never blocks.
1153
3320
  */
1154
- export async function ensureStartable(args) {
1155
- const cwd = process.cwd();
1156
- const configPath = resolve(cwd, args.configPath ?? "mono-agent.config.json");
3321
+ export async function ensureStartable(args, env = process.env, options = {}) {
3322
+ const cwd = options.cwd ?? process.cwd();
3323
+ const configPath = options.configPath ?? resolve(cwd, args.configPath ?? "mono-agent.config.json");
1157
3324
  if (!(await pathExists(configPath))) {
1158
3325
  return { ok: false, code: 2, kind: "missing-config", configPath };
1159
3326
  }
1160
- const report = await validateMonoAgentFolder({ env: process.env, cwd, configPath, liveness: false });
3327
+ const report = await validateMonoAgentFolder({
3328
+ env,
3329
+ cwd,
3330
+ configPath,
3331
+ liveness: false,
3332
+ ...(options.preferAppPluginInstall === true ? { preferAppPluginInstall: true } : {}),
3333
+ });
1161
3334
  if (!report.ok) {
1162
3335
  return { ok: false, code: 1, kind: "validation", report };
1163
3336
  }
@@ -1186,66 +3359,166 @@ async function pathExists(path) {
1186
3359
  return false;
1187
3360
  }
1188
3361
  }
1189
- async function runStart(args) {
3362
+ async function runStart(args, env, managedBackgroundWorker = false) {
1190
3363
  if (args.foreground) {
1191
- return await runForeground(args);
3364
+ return await runForeground(args, env, managedBackgroundWorker);
1192
3365
  }
1193
- return await runBackgroundCommand(args, "start");
3366
+ return await runBackgroundCommand(args, "start", env);
1194
3367
  }
1195
3368
  /**
1196
3369
  * The blocking worker: builds the responder, starts every configured channel
1197
3370
  * plus traceability, and stays alive until a signal. This is what launchd
1198
3371
  * invokes (via `start --foreground`) and what users get with `--foreground`/`-f`.
1199
3372
  */
1200
- async function runForeground(args) {
1201
- const pre = await ensureStartable(args);
1202
- if (!pre.ok) {
1203
- printPreflightFailure(pre);
1204
- return pre.code;
3373
+ async function runForeground(args, env = process.env, managedBackgroundWorker = false) {
3374
+ const cwd = process.cwd();
3375
+ const configPath = await canonicalBackgroundConfigPath(cwd, args.configPath);
3376
+ const startupEnvironment = { ...env };
3377
+ let lease;
3378
+ try {
3379
+ lease = await acquireBackgroundWorkerLease(configPath);
1205
3380
  }
1206
- const app = await startMonoAgentApp({
1207
- cwd: process.cwd(),
1208
- ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
1209
- logger: consoleLogger(),
1210
- });
1211
- await printAppStatus(app);
1212
- // Block until a shutdown signal. Returning here (the old behavior) let the
1213
- // process exit immediately whenever no channel owned a live handle — e.g. a
1214
- // traceability-only config, now that the operator console is retired and the
1215
- // trace heartbeat timer is unref'd.
1216
- return await waitForShutdownSignal(app);
1217
- }
1218
- async function runBackgroundCommand(args, command) {
3381
+ catch (error) {
3382
+ process.stderr.write(ui.errorLine(`Could not acquire the worker singleton lease: ${error instanceof Error ? error.message : String(error)}`));
3383
+ return 1;
3384
+ }
3385
+ if (lease === undefined) {
3386
+ process.stderr.write(ui.errorLine(`Another foreground or managed background worker already owns ${configPath}; refusing to start a duplicate.`));
3387
+ return 1;
3388
+ }
3389
+ let runtimeInputs;
3390
+ let app;
3391
+ try {
3392
+ let backgroundSnapshot;
3393
+ if (managedBackgroundWorker) {
3394
+ try {
3395
+ backgroundSnapshot = decodeBackgroundSnapshot(args.expectedBackgroundSnapshot ?? "");
3396
+ const dotenvPath = resolve(cwd, args.envFile ?? ".env");
3397
+ if (backgroundSnapshot.configPath !== configPath || backgroundSnapshot.dotenvPath !== dotenvPath) {
3398
+ throw new Error("The approved snapshot paths do not match the managed worker arguments.");
3399
+ }
3400
+ runtimeInputs = await materializeBackgroundRuntimeInputs({
3401
+ snapshot: backgroundSnapshot,
3402
+ cwd,
3403
+ env: startupEnvironment,
3404
+ });
3405
+ }
3406
+ catch (error) {
3407
+ process.stderr.write(ui.errorLine(`Managed worker could not freeze its startup snapshot: ${error instanceof Error ? error.message : String(error)}`));
3408
+ // KeepAlive restarts only unsuccessful exits. This refusal cannot heal
3409
+ // without a new approved snapshot, so exit successfully and let the
3410
+ // controller unload/recreate the job instead of retrying forever.
3411
+ return 0;
3412
+ }
3413
+ }
3414
+ const preflightEnvironment = runtimeInputs?.environment ?? startupEnvironment;
3415
+ const pre = await ensureStartable(args, preflightEnvironment, runtimeInputs === undefined
3416
+ ? {}
3417
+ : {
3418
+ cwd,
3419
+ configPath: runtimeInputs.configPath,
3420
+ preferAppPluginInstall: true,
3421
+ });
3422
+ if (!pre.ok) {
3423
+ printPreflightFailure(pre);
3424
+ return managedBackgroundWorker ? 0 : pre.code;
3425
+ }
3426
+ try {
3427
+ await readCliConfigSnapshot(runtimeInputs?.configPath ?? configPath);
3428
+ }
3429
+ catch (error) {
3430
+ process.stderr.write(ui.errorLine(`Cannot establish the foreground config identity: ${error instanceof Error ? error.message : String(error)}`));
3431
+ return managedBackgroundWorker ? 0 : 1;
3432
+ }
3433
+ app = await startMonoAgentApp({
3434
+ cwd,
3435
+ configPath,
3436
+ ...(runtimeInputs === undefined ? {} : { configReadPath: runtimeInputs.configPath }),
3437
+ env: runtimeInputs?.environment ?? startupEnvironment,
3438
+ logger: consoleLogger(),
3439
+ ...(backgroundSnapshot === undefined ? {} : { backgroundSnapshot }),
3440
+ });
3441
+ await printAppStatus(app);
3442
+ // Block until a shutdown signal. Returning here (the old behavior) let the
3443
+ // process exit immediately whenever no channel owned a live handle — e.g. a
3444
+ // traceability-only config, now that the operator console is retired and the
3445
+ // trace heartbeat timer is unref'd.
3446
+ return await waitForShutdownSignal(app);
3447
+ }
3448
+ finally {
3449
+ await app?.stop().catch(() => undefined);
3450
+ await runtimeInputs?.dispose().catch(() => undefined);
3451
+ await lease.release().catch((error) => {
3452
+ process.stderr.write(ui.style.yellow(`⚠ Could not cleanly release worker singleton lease ${lease.path}: ${error instanceof Error ? error.message : String(error)}`) + "\n");
3453
+ });
3454
+ }
3455
+ }
3456
+ async function runBackgroundCommand(args, command, env = process.env) {
1219
3457
  const guard = requireDarwin(command);
1220
3458
  if (guard !== undefined) {
1221
3459
  return guard;
1222
3460
  }
3461
+ let controllerEnvironment;
3462
+ try {
3463
+ controllerEnvironment = await loadDurableBackgroundEnvironment({
3464
+ cwd: process.cwd(),
3465
+ ...(args.envFile === undefined ? {} : { envFile: args.envFile }),
3466
+ operationalEnvironment: managedBackgroundEnvironment(env),
3467
+ });
3468
+ }
3469
+ catch (error) {
3470
+ if (command === "start" || command === "restart") {
3471
+ process.stderr.write(ui.errorLine(`Cannot reconstruct the managed worker environment: ${error instanceof Error ? error.message : String(error)}`));
3472
+ process.stderr.write(ui.hint("No LaunchAgent changes were made. Fix the dotenv path and retry."));
3473
+ return 1;
3474
+ }
3475
+ controllerEnvironment = { ...env };
3476
+ process.stderr.write(ui.style.yellow(`⚠ Could not reconstruct the managed worker environment; ${command} will use the current shell only: ${error instanceof Error ? error.message : String(error)}`) + "\n");
3477
+ }
1223
3478
  // Refuse to launch (or relaunch) an unconfigured/broken folder BEFORE writing
1224
3479
  // the plist and bootstrapping launchctl — otherwise the worker would crash and
1225
3480
  // launchd's KeepAlive would retry it forever. stop/status/logs stay ungated so
1226
3481
  // a broken instance can still be inspected and torn down.
1227
3482
  if (command === "start" || command === "restart") {
1228
- const pre = await ensureStartable(args);
3483
+ const pre = await ensureStartable(args, controllerEnvironment);
1229
3484
  if (!pre.ok) {
1230
3485
  printPreflightFailure(pre);
1231
3486
  return pre.code;
1232
3487
  }
1233
3488
  }
1234
- const target = await resolveInstanceTarget({
3489
+ let target = await resolveInstanceTarget({
1235
3490
  args: {
1236
3491
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
1237
3492
  ...(args.envFile === undefined ? {} : { envFile: args.envFile }),
1238
3493
  },
1239
- env: process.env,
3494
+ env: controllerEnvironment,
1240
3495
  cwd: process.cwd(),
1241
3496
  cliPath: fileURLToPath(import.meta.url),
1242
3497
  });
3498
+ if (command === "start" || command === "restart") {
3499
+ try {
3500
+ const expectedSnapshot = await captureBackgroundSnapshot({
3501
+ cwd: target.cwd,
3502
+ configPath: target.configPath,
3503
+ ...(target.envFile === undefined ? {} : { envFile: target.envFile }),
3504
+ env: controllerEnvironment,
3505
+ });
3506
+ target = { ...target, expectedSnapshot };
3507
+ }
3508
+ catch (error) {
3509
+ process.stderr.write(ui.errorLine(`Cannot prove the durable background snapshot: ${error instanceof Error ? error.message : String(error)}`));
3510
+ process.stderr.write(ui.hint("No LaunchAgent changes were made. Fix the config/dotenv/Identity/Soul/MCP mismatch and retry."));
3511
+ return 1;
3512
+ }
3513
+ }
1243
3514
  const deps = defaultBackgroundDeps();
1244
3515
  switch (command) {
1245
3516
  case "start":
1246
3517
  return await startBackground(target, deps);
1247
3518
  case "restart":
1248
- return args.force ? await runForceRestart(target, deps) : await restartBackground(target, deps);
3519
+ return args.force
3520
+ ? await runForceRestart(target, deps, controllerEnvironment)
3521
+ : await restartBackground(target, deps);
1249
3522
  case "stop":
1250
3523
  return await stopBackground(target, deps);
1251
3524
  case "status":
@@ -1260,20 +3533,17 @@ async function runBackgroundCommand(args, command) {
1260
3533
  * they are deleted; the runtime recreates the store on the next session, and the
1261
3534
  * agent's durable memory lives elsewhere, so only resumable transcripts are dropped.
1262
3535
  */
1263
- async function runForceRestart(target, deps) {
1264
- const stopCode = await stopBackground(target, deps);
1265
- if (stopCode !== 0) {
1266
- return stopCode;
1267
- }
1268
- const result = await purgeSessions({ env: process.env, cwd: target.cwd, configPath: target.configPath });
1269
- if (result.removed) {
1270
- const count = result.files === 0 ? "" : ` (${result.files} session file${result.files === 1 ? "" : "s"})`;
1271
- process.stdout.write(`${ui.badge("ok")}${ui.style.bold("Cleared persisted sessions")}${count}.\n`);
1272
- }
1273
- else {
1274
- process.stdout.write(ui.style.dim("No persisted sessions to clear (in-memory or none on disk).") + "\n");
1275
- }
1276
- return await startBackground(target, deps);
3536
+ async function runForceRestart(target, deps, environment) {
3537
+ return await forceRestartBackground(target, deps, async () => {
3538
+ const result = await purgeSessions({ env: environment, cwd: target.cwd, configPath: target.configPath });
3539
+ if (result.removed) {
3540
+ const count = result.files === 0 ? "" : ` (${result.files} session file${result.files === 1 ? "" : "s"})`;
3541
+ process.stdout.write(`${ui.badge("ok")}${ui.style.bold("Cleared persisted sessions")}${count}.\n`);
3542
+ }
3543
+ else {
3544
+ process.stdout.write(ui.style.dim("No persisted sessions to clear (in-memory or none on disk).") + "\n");
3545
+ }
3546
+ });
1277
3547
  }
1278
3548
  /**
1279
3549
  * Background service mode is launchd-specific. On other platforms point the
@@ -1419,8 +3689,16 @@ export function waitForShutdownSignal(app) {
1419
3689
  clearInterval(keepAlive);
1420
3690
  void (async () => {
1421
3691
  process.stdout.write("\n" + ui.hint(`Received ${signal}; stopping mono agent app…`));
1422
- await app.stop();
1423
- resolve(0);
3692
+ try {
3693
+ await app.stop();
3694
+ resolve(0);
3695
+ }
3696
+ catch (error) {
3697
+ process.stderr.write(ui.errorLine(`Foreground shutdown failed: ${error instanceof Error ? error.message : String(error)}`));
3698
+ // Resolve so runForeground's finally block can retry idempotent app
3699
+ // cleanup and release the process-lifetime singleton lease.
3700
+ resolve(1);
3701
+ }
1424
3702
  })();
1425
3703
  };
1426
3704
  process.on("SIGINT", onSignal);