@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
@@ -1,15 +1,116 @@
1
1
  import { stat } from "node:fs/promises";
2
2
  import { basename, join } from "node:path";
3
+ import { emitKeypressEvents } from "node:readline";
3
4
  import * as p from "@clack/prompts";
5
+ import { parseMonoRuntimeModelReference } from "@mono-agent/runtime-adapter";
4
6
  import { findModule } from "../modules/catalog.js";
5
- import { ALLOW_ALL_TOOLS, BUILTIN_TOOL_NAMES, isAllowAllTools } from "../modules/known-tools.js";
6
- import { isProviderSetupPiApiKeyAction, planProviderSetup, providerSetupActionCommandLine } from "../provider-setup.js";
7
- import { alwaysOnTools, composeWizardPlan, defaultAnswers, recommendedToolSelection, } from "./answers.js";
7
+ import { hasSensitivePersistedEnvironmentValue } from "../first-run-readiness.js";
8
+ import { DEFAULT_MEMORY_EMBEDDING_ENDPOINTS, discoverMemoryEmbeddingModels, memoryEmbeddingEndpointProblem, probeMemoryEmbeddingSelection, } from "../memory-embedding-service.js";
9
+ import { DEFAULT_MODEL } from "../modules/base.js";
10
+ import { ALLOW_ALL_TOOLS, APP_TOOL_NAMES, BUILTIN_TOOL_NAMES, isAllowAllTools } from "../modules/known-tools.js";
11
+ import { hasDurableProviderEnvironmentCredential, isProviderSetupPiApiKeyAction, planProviderSetup, providerSetupActionCommandLine, } from "../provider-setup.js";
12
+ import { isSupermemoryPluginInstalled } from "../supermemory-plugin.js";
13
+ import { alwaysOnTools, composeWizardPlan, defaultAnswers, effectiveFallbacks, humanizeAgentName, recommendedToolSelection, referencedSetupModelRefs, } from "./answers.js";
8
14
  import { findPreset, presetAnswers } from "./presets.js";
9
- import { channelSelectOptions, effortSelectOptions, fallbackModelSelectOptions, guard, memorySelectOptions, modelSelectOptions, presetSelectOptions, toolMultiselectOptions, WizardCancelled, } from "./prompts.js";
10
- import { defaultEffortForModelRef, discoverWizardModelCandidates, formatModelDiscoveryStatus, } from "./model-discovery.js";
11
- const LOCAL_PROVIDER_MODEL = /^pi:(?:ollama|lmstudio):/u;
15
+ import { assertConcreteWizardModelRef, channelSelectOptions, creationReviewOptions, CUSTOM_PI_MODEL_OPTION, effortSelectOptions, fallbackModelSelectOptions, formatRouteSafetyMatrix, isMixedRouteChain, memorySelectOptions, MODEL_AUTOCOMPLETE_MAX_ITEMS, modelSelectOptions, piModelSelectOptions, presetSelectOptions, ROUTE_SAFETY_OPTIONS, toolMultiselectOptions, validateWizardAgentName, validateWizardAgentPurpose, wizardCancelIntentForKey, WizardBack, WizardCancelled, WizardExitRequested, } from "./prompts.js";
16
+ import { discoverWizardModelCandidates, formatModelDiscoveryStatus, guidedPiProviderProblem, } from "./model-discovery.js";
12
17
  const SANDBOXABLE_TOOLS = new Set(["Bash", "Write", "Edit"]);
18
+ const MANAGED_MEMORY_MODULE_IDS = new Set(["memory:journal", "memory:bujo"]);
19
+ const MANUAL_EMBEDDING_MODEL = "__manual_embedding_model__";
20
+ /**
21
+ * Clack intentionally coalesces Escape and Ctrl-C into one cancel sentinel. The
22
+ * wizard owns a scoped keypress observer so the state machine can interpret
23
+ * Escape as Back and Ctrl-C as Exit without changing Clack globally.
24
+ */
25
+ async function runPrompt(invoke) {
26
+ let intent;
27
+ const input = process.stdin;
28
+ const listener = (_text, key) => {
29
+ intent = wizardCancelIntentForKey(key) ?? intent;
30
+ };
31
+ if (input.isTTY) {
32
+ emitKeypressEvents(input);
33
+ input.on("keypress", listener);
34
+ }
35
+ try {
36
+ const value = await invoke();
37
+ if (!p.isCancel(value))
38
+ return value;
39
+ if (intent === "back")
40
+ throw new WizardBack();
41
+ throw new WizardExitRequested();
42
+ }
43
+ finally {
44
+ if (input.isTTY)
45
+ input.off("keypress", listener);
46
+ }
47
+ }
48
+ async function discoverModelsInterruptibly(options) {
49
+ const controller = new AbortController();
50
+ let intent;
51
+ const input = process.stdin;
52
+ const wasRaw = input.isRaw === true;
53
+ const wasFlowing = input.readableFlowing;
54
+ const interrupt = (nextIntent) => {
55
+ intent ??= nextIntent;
56
+ controller.abort();
57
+ };
58
+ const onKeypress = (_text, key) => {
59
+ const nextIntent = wizardCancelIntentForKey(key);
60
+ if (nextIntent !== undefined)
61
+ interrupt(nextIntent);
62
+ };
63
+ const onSigint = () => interrupt("exit");
64
+ if (input.isTTY) {
65
+ emitKeypressEvents(input);
66
+ input.setRawMode?.(true);
67
+ input.resume();
68
+ input.on("keypress", onKeypress);
69
+ }
70
+ process.on("SIGINT", onSigint);
71
+ try {
72
+ const result = await discoverWizardModelCandidates({
73
+ ...options,
74
+ abortSignal: controller.signal,
75
+ });
76
+ if (!controller.signal.aborted)
77
+ return result;
78
+ }
79
+ catch (error) {
80
+ if (!controller.signal.aborted)
81
+ throw error;
82
+ }
83
+ finally {
84
+ process.off("SIGINT", onSigint);
85
+ if (input.isTTY) {
86
+ input.off("keypress", onKeypress);
87
+ input.setRawMode?.(wasRaw);
88
+ if (wasFlowing !== true)
89
+ input.pause();
90
+ }
91
+ }
92
+ if (intent === "back")
93
+ throw new WizardBack();
94
+ throw new WizardExitRequested();
95
+ }
96
+ async function select(options) {
97
+ return runPrompt(() => p.select(options));
98
+ }
99
+ async function autocomplete(options) {
100
+ return runPrompt(() => p.autocomplete(options));
101
+ }
102
+ async function confirm(options) {
103
+ return runPrompt(() => p.confirm(options));
104
+ }
105
+ async function multiselect(options) {
106
+ return runPrompt(() => p.multiselect(options));
107
+ }
108
+ async function textPrompt(options) {
109
+ return runPrompt(() => p.text(options));
110
+ }
111
+ async function passwordPrompt(options) {
112
+ return runPrompt(() => p.password(options));
113
+ }
13
114
  /**
14
115
  * The interactive `init` wizard: a colourful, step-by-step flow that COLLECTS a
15
116
  * {@link WizardAnswers} and hands it back — it never writes anything. The caller
@@ -25,6 +126,10 @@ export async function runInitWizard(ctx) {
25
126
  answers: result.answers,
26
127
  runProviderSetup: result.runProviderSetup,
27
128
  providerSetupSecrets: result.providerSetupSecrets,
129
+ providerEnvironmentSecrets: result.providerEnvironmentSecrets,
130
+ piApiKeyPersistenceByProvider: result.piApiKeyPersistenceByProvider,
131
+ credentialStates: result.credentialStates,
132
+ moduleSecrets: result.moduleSecrets,
28
133
  };
29
134
  }
30
135
  catch (error) {
@@ -35,149 +140,287 @@ export async function runInitWizard(ctx) {
35
140
  throw error;
36
141
  }
37
142
  }
143
+ /**
144
+ * Re-open an already reviewed setup at the Creation review. The caller keeps
145
+ * ownership of the original state: Escape returns `cancelled`, and no mutable
146
+ * credential or answer state is exposed unless the operator confirms a repair.
147
+ */
148
+ export async function runSetupRepairWizard(ctx) {
149
+ try {
150
+ p.log.step("Repair setup choices");
151
+ const initialStep = ctx.initialStep ?? 8;
152
+ const result = await collectInteractiveFromSeed(ctx, ctx.answers, {
153
+ initialStep,
154
+ ...(initialStep === 8 ? {} : { initialReturnToReviewStep: initialStep }),
155
+ returnToCallerOnReviewBack: true,
156
+ repairState: ctx,
157
+ });
158
+ return {
159
+ status: "answers",
160
+ answers: result.answers,
161
+ runProviderSetup: result.runProviderSetup,
162
+ providerSetupSecrets: result.providerSetupSecrets,
163
+ providerEnvironmentSecrets: result.providerEnvironmentSecrets,
164
+ piApiKeyPersistenceByProvider: result.piApiKeyPersistenceByProvider,
165
+ credentialStates: result.credentialStates,
166
+ moduleSecrets: result.moduleSecrets,
167
+ };
168
+ }
169
+ catch (error) {
170
+ if (error instanceof WizardBack || error instanceof WizardCancelled) {
171
+ p.log.info("Returning to the preflight recovery menu; previous setup choices were kept.");
172
+ return { status: "cancelled" };
173
+ }
174
+ throw error;
175
+ }
176
+ }
177
+ /**
178
+ * Re-select only primary/fallback models and effort after a failed model check.
179
+ * Capabilities, module inputs, and observability are copied from `ctx.answers`,
180
+ * and no module secret is requested again. Tool/sandbox choices are re-confirmed
181
+ * when the runtime family crosses the direct-Codex boundary or changes whether
182
+ * a provider-owned Claude/direct-OpenCode tool loop is present.
183
+ */
184
+ export async function runModelRepairWizard(ctx) {
185
+ try {
186
+ p.log.step("Repair model configuration");
187
+ for (;;) {
188
+ const draft = draftFrom(ctx.answers);
189
+ try {
190
+ const previousFamilies = selectedRuntimeModels(draft).map(runtimeFamily).join(",");
191
+ await promptModelSettings(draft, ctx);
192
+ const nextFamilies = selectedRuntimeModels(draft).map(runtimeFamily).join(",");
193
+ if (previousFamilies !== nextFamilies
194
+ || !hasExactAllowAllTools(draft)
195
+ || hasInvalidUniformManagedSrtChain(draft)) {
196
+ p.log.info("The runtime route changed, so tool and sandbox safety choices must be confirmed again.");
197
+ await promptTools(draft);
198
+ await promptSafetyPolicy(draft);
199
+ }
200
+ const answers = toWizardAnswers(draft);
201
+ const plan = await composePlanForCwd(answers, ctx.cwd);
202
+ const providerSetup = await promptProviderSetup(plan, ctx, draft.credentialStates);
203
+ if (!await confirm({ message: "Use this model configuration?", initialValue: true })) {
204
+ return { status: "cancelled" };
205
+ }
206
+ return { status: "answers", answers, ...providerSetup, credentialStates: { ...draft.credentialStates } };
207
+ }
208
+ catch (error) {
209
+ if (error instanceof WizardBack) {
210
+ p.log.info("Returning to the preflight recovery menu; previous model choices were kept.");
211
+ return { status: "cancelled" };
212
+ }
213
+ if (error instanceof WizardExitRequested) {
214
+ if (await confirmExitSetup())
215
+ throw new WizardCancelled();
216
+ continue;
217
+ }
218
+ throw error;
219
+ }
220
+ }
221
+ }
222
+ catch (error) {
223
+ if (error instanceof WizardCancelled) {
224
+ p.cancel("Model repair cancelled — previous choices were kept.");
225
+ return { status: "cancelled" };
226
+ }
227
+ throw error;
228
+ }
229
+ }
38
230
  /** Walk the flow (preset or custom), returning the fully collected answers. */
39
231
  async function collectAnswers(ctx) {
40
232
  p.intro("mono-agent init — let's build your agent");
41
- const choice = guard(await p.select({
42
- message: "Start from…",
43
- options: presetSelectOptions(),
44
- initialValue: "__custom__",
45
- }));
46
- return choice === "__custom__"
47
- ? await collectCustom(ctx)
48
- : await collectFromPreset(ctx, choice);
233
+ for (;;) {
234
+ let choice;
235
+ try {
236
+ choice = await select({
237
+ message: "Start from…",
238
+ options: presetSelectOptions(),
239
+ initialValue: "__custom__",
240
+ });
241
+ }
242
+ catch (error) {
243
+ if (error instanceof WizardBack || error instanceof WizardExitRequested) {
244
+ if (await confirmExitSetup())
245
+ throw new WizardCancelled();
246
+ continue;
247
+ }
248
+ throw error;
249
+ }
250
+ try {
251
+ return choice === "__custom__"
252
+ ? await collectCustom(ctx)
253
+ : await collectFromPreset(ctx, choice);
254
+ }
255
+ catch (error) {
256
+ // Escape from the first detail step returns to the preset chooser. Ctrl-C
257
+ // is handled inside the detail state machine and should not normally leak.
258
+ if (error instanceof WizardBack)
259
+ continue;
260
+ if (error instanceof WizardExitRequested) {
261
+ if (await confirmExitSetup())
262
+ throw new WizardCancelled();
263
+ continue;
264
+ }
265
+ throw error;
266
+ }
267
+ }
49
268
  }
50
269
  /**
51
270
  * The full custom flow: model → effort → channels → memory → per-module inputs
52
271
  * → tools → sandbox (only if code tools were chosen) → observability → summary.
53
272
  */
54
273
  async function collectCustom(ctx) {
55
- const draft = draftFrom(defaultAnswers());
56
- // 1. Model.
57
- const discovery = await discoverWizardModelCandidates();
58
- const discoveredByValue = new Map(discovery.candidates.map((candidate) => [candidate.value, candidate]));
59
- p.note(formatModelDiscoveryStatus(discovery.statuses), "Model discovery");
60
- const model = guard(await p.select({
61
- message: "Which model?",
62
- options: modelSelectOptions(discovery.candidates),
63
- initialValue: draft.model,
274
+ return await collectInteractiveFromSeed(ctx, defaultAnswers({
275
+ name: humanizeAgentName(basename(ctx.cwd)),
64
276
  }));
65
- draft.model = await resolveModelSelection(model);
66
- // Optional fallback chain.
67
- if (guard(await p.confirm({ message: "Add fallback models?", initialValue: false }))) {
68
- await promptFallbackModels(draft, discovery.candidates);
69
- }
70
- const derivedEffort = discoveredByValue.get(draft.model)?.defaultEffort ?? defaultEffortForModelRef(draft.model);
71
- const effort = guard(await p.select({
72
- message: derivedEffort === undefined
73
- ? "Reasoning effort?"
74
- : `Reasoning effort? (derived from selected model: ${derivedEffort})`,
75
- options: effortSelectOptions(derivedEffort),
76
- initialValue: derivedEffort ?? "",
77
- }));
78
- draft.effort = effort.length === 0 ? undefined : effort;
79
- // 2. Channels.
80
- const channels = guard(await p.multiselect({
81
- message: "How will you talk to this agent?",
82
- options: channelSelectOptions(),
83
- initialValues: ["channel:webhook"],
84
- required: false,
85
- }));
86
- draft.channels = [...channels];
87
- // 3. Memory (before per-module inputs so a memory module's inputs are prompted).
88
- const memory = guard(await p.select({
89
- message: "Should the agent remember across conversations?",
90
- options: memorySelectOptions(),
91
- initialValue: "",
92
- }));
93
- draft.memory = memory === "" ? undefined : memory;
94
- // 4. Per-module (non-secret) inputs for the chosen channels + memory.
95
- await promptModuleInputs(draft);
96
- // 5. Tools.
97
- await promptTools(draft);
98
- // 6. Sandbox — only meaningful once shell/file tools are in play. Allow-all
99
- // includes Bash/Write/Edit, so the sandbox question must still appear under it.
100
- if (isAllowAllTools(draft.allowedTools) || draft.allowedTools.some((tool) => SANDBOXABLE_TOOLS.has(tool))) {
101
- draft.sandbox = guard(await p.confirm({
102
- message: "Sandbox shell/file tools? (native srt, localhost-only network, fails closed if srt is missing)",
103
- initialValue: true,
104
- }));
105
- }
106
- // 7. Observability.
107
- draft.observability = guard(await p.confirm({
108
- message: "Export traces to Phoenix (best-effort OTLP, sensitive data excluded)?",
109
- initialValue: false,
110
- }));
111
- // 8. Summary + final confirm.
112
- const providerSetup = await confirmSummary(draft, ctx);
113
- return { answers: toWizardAnswers(draft), ...providerSetup };
114
277
  }
115
- async function resolveModelSelection(model) {
278
+ async function resolveModelSelection(model, opts) {
116
279
  if (model === "__pi_other__") {
117
- const provider = guard(await p.text({
118
- message: "Pi provider id",
119
- placeholder: "openai-codex",
120
- validate: (v) => {
121
- const value = (v ?? "").trim();
122
- if (value.length === 0) {
123
- return "Enter a Pi provider id (e.g. openai-codex, opencode-go, ollama, lmstudio)";
124
- }
125
- return value.includes(":") ? "Provider id cannot contain ':'." : undefined;
126
- },
127
- })).trim();
128
- const modelId = guard(await p.text({
129
- message: "Pi model id",
130
- placeholder: provider === "openai-codex" ? "gpt-5.5" : "llama3.1:8b",
131
- validate: (v) => (v ?? "").trim().length === 0
132
- ? "Enter the provider-specific model id (e.g. gpt-5.5, kimi-k2.6, llama3.1:8b)"
133
- : undefined,
134
- })).trim();
135
- return `pi:${provider}:${modelId}`;
280
+ return await promptPiModelSelection(opts);
136
281
  }
137
282
  if (model === "__other__") {
138
- return guard(await p.text({
139
- message: "Model reference",
283
+ const resolved = (await textPrompt({
284
+ message: opts.context === "primary" ? "Model reference" : "Fallback model reference",
140
285
  placeholder: "pi:ollama:llama3.1:8b",
141
- validate: (v) => (v ?? "").trim().length === 0
142
- ? "Enter a provider:model reference (e.g. pi:ollama:llama3.1:8b)"
143
- : undefined,
286
+ validate: validateFullModelReference,
144
287
  })).trim();
288
+ assertConcreteWizardModelRef(resolved);
289
+ assertGuidedModelRef(resolved);
290
+ return resolved;
145
291
  }
292
+ assertConcreteWizardModelRef(model);
293
+ assertGuidedModelRef(model);
146
294
  return model;
147
295
  }
296
+ async function promptPiModelSelection(opts) {
297
+ const options = piModelSelectOptions(opts.candidates, opts.excludedModels ?? []);
298
+ const choice = await autocomplete({
299
+ message: opts.context === "primary" ? "Other Pi model" : "Other Pi fallback model",
300
+ options,
301
+ initialValue: options[0]?.value ?? CUSTOM_PI_MODEL_OPTION,
302
+ placeholder: "Type to search Pi providers and models…",
303
+ maxItems: MODEL_AUTOCOMPLETE_MAX_ITEMS,
304
+ });
305
+ if (choice !== CUSTOM_PI_MODEL_OPTION) {
306
+ assertConcreteWizardModelRef(choice);
307
+ return choice;
308
+ }
309
+ return await promptManualPiModelRef();
310
+ }
311
+ async function promptManualPiModelRef() {
312
+ const provider = (await textPrompt({
313
+ message: "Pi provider id",
314
+ placeholder: "openai-codex",
315
+ validate: (v) => {
316
+ const value = (v ?? "").trim();
317
+ if (value.length === 0) {
318
+ return "Enter a supported Pi provider id (anthropic, github-copilot, openai-codex, opencode-go, ollama, or lmstudio)";
319
+ }
320
+ if (value.includes(":"))
321
+ return "Provider id cannot contain ':'.";
322
+ return guidedPiProviderProblem(value);
323
+ },
324
+ })).trim();
325
+ const modelId = (await textPrompt({
326
+ message: "Pi model id",
327
+ placeholder: provider === "openai-codex" ? "gpt-5.6-terra" : "llama3.1:8b",
328
+ validate: (v) => (v ?? "").trim().length === 0
329
+ ? "Enter the provider-specific model id (e.g. gpt-5.6-terra, gpt-5.6-sol, kimi-k2.6, llama3.1:8b)"
330
+ : undefined,
331
+ })).trim();
332
+ return `pi:${provider}:${modelId}`;
333
+ }
334
+ function validateFullModelReference(value) {
335
+ const trimmed = (value ?? "").trim();
336
+ if (trimmed.length === 0) {
337
+ return "Enter a provider:model reference (e.g. pi:ollama:llama3.1:8b)";
338
+ }
339
+ if (trimmed.includes(",")) {
340
+ return "Enter one model reference. Add more fallbacks one at a time.";
341
+ }
342
+ try {
343
+ assertConcreteWizardModelRef(trimmed);
344
+ }
345
+ catch (error) {
346
+ return error instanceof Error ? error.message : "Enter a concrete model reference.";
347
+ }
348
+ return guidedModelRefProblem(trimmed);
349
+ }
350
+ function assertGuidedModelRef(model) {
351
+ const problem = guidedModelRefProblem(model);
352
+ if (problem !== undefined) {
353
+ throw new Error(problem);
354
+ }
355
+ }
356
+ export function guidedModelRefProblem(model) {
357
+ let parsed;
358
+ try {
359
+ parsed = parseMonoRuntimeModelReference(model);
360
+ }
361
+ catch (error) {
362
+ return error instanceof Error ? error.message : "Enter a concrete model reference.";
363
+ }
364
+ if (parsed.sdk === "opencode") {
365
+ return "Direct OpenCode is scaffold/config-only. Choose pi:opencode-go:* for guided readiness.";
366
+ }
367
+ if (parsed.sdk === "pi" && parsed.provider !== undefined) {
368
+ return guidedPiProviderProblem(parsed.provider);
369
+ }
370
+ return parsed.sdk === "claude" || parsed.sdk === "codex"
371
+ ? undefined
372
+ : "Guided init supports Claude, Codex, supported Pi providers, and local Pi routes.";
373
+ }
148
374
  /**
149
375
  * Build an ordered fallback chain one model at a time. Each step reuses the same
150
376
  * discovered model labels as the primary picker while hiding the selected primary
151
377
  * and any fallback already chosen. `Other…` is the explicit path for custom refs.
152
378
  */
153
379
  async function promptFallbackModels(draft, candidates) {
380
+ const byValue = new Map(candidates.map((candidate) => [candidate.value, candidate]));
154
381
  for (;;) {
155
- const choice = guard(await p.select({
156
- message: `Fallback model #${draft.fallbackModels.length + 1}`,
157
- options: fallbackModelSelectOptions(candidates, draft.model, draft.fallbackModels),
158
- initialValue: "__done__",
159
- }));
382
+ const selectedModels = draft.fallbacks.map((fallback) => fallback.model);
383
+ let choice;
384
+ try {
385
+ choice = await autocomplete({
386
+ message: `Fallback model #${draft.fallbacks.length + 1}`,
387
+ options: fallbackModelSelectOptions(candidates, draft.model, selectedModels),
388
+ initialValue: "__done__",
389
+ placeholder: "Type to search all supported models…",
390
+ maxItems: MODEL_AUTOCOMPLETE_MAX_ITEMS,
391
+ });
392
+ }
393
+ catch (error) {
394
+ if (!(error instanceof WizardBack) || draft.fallbacks.length === 0)
395
+ throw error;
396
+ draft.fallbacks.pop();
397
+ continue;
398
+ }
160
399
  if (choice === "__done__") {
161
400
  return;
162
401
  }
163
- if (choice === "__other__") {
164
- const raw = guard(await p.text({
165
- message: "Custom fallback model reference(s)",
166
- placeholder: "codex:gpt-5.5, pi:ollama:llama3.1:8b",
167
- validate: (v) => splitCsv(v ?? "").length === 0
168
- ? "Enter at least one provider:model reference"
169
- : undefined,
170
- }));
171
- draft.fallbackModels.push(...splitCsv(raw));
172
- continue;
402
+ const resolved = await resolveModelSelection(choice, {
403
+ candidates,
404
+ excludedModels: [draft.model, ...selectedModels],
405
+ context: "fallback",
406
+ });
407
+ if (resolved !== draft.model && !selectedModels.includes(resolved)) {
408
+ let effort;
409
+ try {
410
+ effort = await promptEffortForModel(resolved, byValue.get(resolved));
411
+ }
412
+ catch (error) {
413
+ if (error instanceof WizardBack)
414
+ continue;
415
+ throw error;
416
+ }
417
+ draft.fallbacks.push({ model: resolved, ...(effort === undefined ? {} : { effort }) });
173
418
  }
174
- draft.fallbackModels.push(choice);
175
419
  }
176
420
  }
177
421
  /**
178
- * The preset flow: the preset fixes model/channels/memory/sandbox/observability,
179
- * so we only prompt its per-module inputs, let the operator adjust tools, and
180
- * confirm the summary.
422
+ * Presets seed every choice, but do not silently freeze them. A first run must
423
+ * still choose its model, channels, tool policy, and sandbox safety posture.
181
424
  */
182
425
  async function collectFromPreset(ctx, presetId) {
183
426
  const preset = findPreset(presetId);
@@ -186,20 +429,444 @@ async function collectFromPreset(ctx, presetId) {
186
429
  throw new WizardCancelled();
187
430
  }
188
431
  p.log.step(`Preset: ${preset.title}`);
189
- const draft = draftFrom(presetAnswers(preset));
190
- await promptModuleInputs(draft);
191
- await promptTools(draft);
192
- const providerSetup = await confirmSummary(draft, ctx);
193
- return { answers: toWizardAnswers(draft), ...providerSetup };
432
+ return await collectInteractiveFromSeed(ctx, defaultAnswers({
433
+ ...presetAnswers(preset),
434
+ name: humanizeAgentName(basename(ctx.cwd)),
435
+ }));
436
+ }
437
+ /**
438
+ * Whether a logical wizard step will actually ask the operator anything for
439
+ * the current draft. Some runtime families make Tools and Safety informative
440
+ * only; treating those as back-navigation destinations traps Escape in a
441
+ * silent forward loop.
442
+ */
443
+ function wizardStepHasInteractivePrompt(step, draft) {
444
+ switch (step) {
445
+ case 0:
446
+ case 1:
447
+ case 2:
448
+ case 3:
449
+ case 7:
450
+ case 8:
451
+ return true;
452
+ case 4:
453
+ return [...draft.channels, ...(draft.memory === undefined ? [] : [draft.memory])]
454
+ .some((id) => !MANAGED_MEMORY_MODULE_IDS.has(id)
455
+ && findModule(id)?.inputs.some((input) => input.secret !== true) === true);
456
+ case 5:
457
+ return !selectedRuntimeModels(draft).every(hasFixedAllowAllToolPolicyRef);
458
+ case 6:
459
+ return safetyPolicyHasInteractivePrompt(draft);
460
+ default:
461
+ return false;
462
+ }
463
+ }
464
+ function safetyPolicyHasInteractivePrompt(draft) {
465
+ const models = selectedRuntimeModels(draft);
466
+ const mixed = isMixedRouteChain(models);
467
+ if (mixed)
468
+ return true;
469
+ const hasSandboxableTools = isAllowAllTools(draft.allowedTools)
470
+ || draft.allowedTools.some((tool) => SANDBOXABLE_TOOLS.has(tool));
471
+ const hasPiRoute = models.some((model) => model.startsWith("pi:"));
472
+ const providerNative = models.some((model) => !model.startsWith("pi:"));
473
+ if (hasSandboxableTools && hasPiRoute)
474
+ return true;
475
+ if (draft.routeSafety === "per-route-native" && providerNative)
476
+ return true;
477
+ return hasSandboxableTools
478
+ && providerNative
479
+ && !models.every(isDirectCodexRef)
480
+ && isAllowAllTools(draft.allowedTools);
481
+ }
482
+ function previousInteractiveWizardStep(step, draft) {
483
+ for (let candidate = step - 1; candidate >= 0; candidate -= 1) {
484
+ if (wizardStepHasInteractivePrompt(candidate, draft))
485
+ return candidate;
486
+ }
487
+ return undefined;
488
+ }
489
+ /** Shared custom/preset first-run chooser; seed answers only set sensible defaults. */
490
+ async function collectInteractiveFromSeed(ctx, seed, options = {}) {
491
+ const draft = draftFrom(seed);
492
+ if (options.repairState !== undefined) {
493
+ draft.credentialStates = { ...options.repairState.credentialStates };
494
+ }
495
+ const modelDiscoveryCache = {};
496
+ let step = options.initialStep ?? 0;
497
+ let returnToReviewAfterStep = options.initialReturnToReviewStep;
498
+ const finalStep = 8;
499
+ const advanceAfter = (completedStep) => {
500
+ if (returnToReviewAfterStep === completedStep) {
501
+ returnToReviewAfterStep = undefined;
502
+ step = finalStep;
503
+ }
504
+ else {
505
+ step = completedStep + 1;
506
+ }
507
+ };
508
+ for (;;) {
509
+ const snapshot = cloneDraft(draft);
510
+ let finalizing = false;
511
+ try {
512
+ switch (step) {
513
+ case 0:
514
+ draft.name = (await textPrompt({
515
+ message: "What should this agent be called?",
516
+ // Keep the current answer visible and editable when Escape returns
517
+ // here from model selection. `defaultValue` only substitutes an
518
+ // empty submission, so Clack would otherwise render the folder-name
519
+ // placeholder and make a preserved custom name look lost.
520
+ initialValue: draft.name,
521
+ placeholder: humanizeAgentName(basename(ctx.cwd)),
522
+ validate: validateWizardAgentName,
523
+ })).trim();
524
+ draft.purpose = (await textPrompt({
525
+ message: "What Role should be saved to IDENTITY.md → ## Role?",
526
+ initialValue: draft.purpose,
527
+ placeholder: "Help with work in this folder",
528
+ validate: validateWizardAgentPurpose,
529
+ })).trim();
530
+ advanceAfter(0);
531
+ break;
532
+ case 1: {
533
+ const previousFamilies = selectedRuntimeModels(draft).map(runtimeFamily).join(",");
534
+ await promptModelSettings(draft, ctx, modelDiscoveryCache);
535
+ const nextFamilies = selectedRuntimeModels(draft).map(runtimeFamily).join(",");
536
+ if (returnToReviewAfterStep === 1
537
+ && (previousFamilies !== nextFamilies
538
+ || !hasExactAllowAllTools(draft)
539
+ || hasInvalidUniformManagedSrtChain(draft))) {
540
+ p.log.info("The runtime route changed, so tool and sandbox safety choices must be confirmed again.");
541
+ await promptTools(draft);
542
+ await promptSafetyPolicy(draft);
543
+ }
544
+ advanceAfter(1);
545
+ break;
546
+ }
547
+ case 2: {
548
+ const previousChannels = new Set(draft.channels);
549
+ draft.channels = [...await multiselect({
550
+ message: "How will you talk to this agent?",
551
+ options: channelSelectOptions({ readyOnly: true }),
552
+ initialValues: draft.channels,
553
+ required: false,
554
+ })];
555
+ if (returnToReviewAfterStep === 2) {
556
+ await promptModuleInputs(draft, draft.channels.filter((channel) => !previousChannels.has(channel)));
557
+ await promptTools(draft);
558
+ await promptSafetyPolicy(draft);
559
+ }
560
+ advanceAfter(2);
561
+ break;
562
+ }
563
+ case 3: {
564
+ const previousMemory = draft.memory;
565
+ const memory = await select({
566
+ message: "Should the agent remember across conversations?",
567
+ options: memorySelectOptions({
568
+ includeOptionalPlugins: draft.memory === "memory:supermemory"
569
+ || isSupermemoryPluginInstalled({ cwd: ctx.cwd }),
570
+ }),
571
+ initialValue: draft.memory ?? "",
572
+ });
573
+ draft.memory = memory === "" ? undefined : memory;
574
+ if (previousMemory !== draft.memory && previousMemory !== undefined) {
575
+ delete draft.moduleInputs[previousMemory];
576
+ }
577
+ if (draft.memory !== undefined && MANAGED_MEMORY_MODULE_IDS.has(draft.memory)) {
578
+ await promptManagedMemoryEmbeddingInputs(draft, ctx);
579
+ }
580
+ else if (returnToReviewAfterStep === 3
581
+ && draft.memory !== undefined
582
+ && draft.memory !== previousMemory) {
583
+ await promptModuleInputs(draft, [draft.memory]);
584
+ }
585
+ advanceAfter(3);
586
+ break;
587
+ }
588
+ case 4:
589
+ if (returnToReviewAfterStep === 4
590
+ && draft.memory !== undefined
591
+ && MANAGED_MEMORY_MODULE_IDS.has(draft.memory)) {
592
+ await promptManagedMemoryEmbeddingInputs(draft, ctx);
593
+ }
594
+ await promptModuleInputs(draft);
595
+ advanceAfter(4);
596
+ break;
597
+ case 5:
598
+ await promptTools(draft);
599
+ if (returnToReviewAfterStep === 5) {
600
+ await promptSafetyPolicy(draft);
601
+ }
602
+ advanceAfter(5);
603
+ break;
604
+ case 6:
605
+ await promptSafetyPolicy(draft);
606
+ advanceAfter(6);
607
+ break;
608
+ case 7:
609
+ draft.observability = await confirm({
610
+ message: "Export traces to Phoenix (best-effort OTLP, sensitive data excluded)?",
611
+ initialValue: draft.observability,
612
+ });
613
+ advanceAfter(7);
614
+ break;
615
+ case 8: {
616
+ const reviewed = await confirmSummary(draft, ctx, options.repairState === undefined
617
+ ? undefined
618
+ : {
619
+ existing: options.repairState,
620
+ });
621
+ if (reviewed.status === "edit") {
622
+ step = reviewed.step;
623
+ returnToReviewAfterStep = reviewed.step === finalStep ? undefined : reviewed.step;
624
+ break;
625
+ }
626
+ finalizing = true;
627
+ const answers = toWizardAnswers(draft);
628
+ const moduleSecrets = await promptRequiredModuleSecrets(answers, ctx.persistedEnv, options.repairState?.moduleSecrets);
629
+ return {
630
+ answers,
631
+ ...reviewed.providerSetup,
632
+ moduleSecrets,
633
+ credentialStates: { ...draft.credentialStates },
634
+ };
635
+ }
636
+ default:
637
+ throw new Error(`Unknown wizard step ${step}.`);
638
+ }
639
+ }
640
+ catch (error) {
641
+ restoreDraft(draft, snapshot);
642
+ if (error instanceof WizardBack) {
643
+ if (options.returnToCallerOnReviewBack === true)
644
+ throw error;
645
+ if (returnToReviewAfterStep === step) {
646
+ returnToReviewAfterStep = undefined;
647
+ step = finalStep;
648
+ continue;
649
+ }
650
+ const previous = previousInteractiveWizardStep(step, draft);
651
+ if (previous === undefined)
652
+ throw error;
653
+ // Secret/auth collection happens after the review choice. Escape there
654
+ // returns to the review rather than discarding unrelated answers.
655
+ step = step === finalStep && finalizing ? finalStep : previous;
656
+ continue;
657
+ }
658
+ if (error instanceof WizardExitRequested) {
659
+ if (await confirmExitSetup())
660
+ throw new WizardCancelled();
661
+ continue;
662
+ }
663
+ throw error;
664
+ }
665
+ }
666
+ }
667
+ async function promptSafetyPolicy(draft) {
668
+ const models = selectedRuntimeModels(draft);
669
+ const mixed = isMixedRouteChain(models);
670
+ if (mixed) {
671
+ draft.routeSafety = await select({
672
+ message: "How should safety apply across this mixed fallback chain?",
673
+ options: [...ROUTE_SAFETY_OPTIONS],
674
+ initialValue: draft.routeSafety,
675
+ });
676
+ }
677
+ else {
678
+ draft.routeSafety = "uniform";
679
+ }
680
+ const hasSandboxableTools = isAllowAllTools(draft.allowedTools)
681
+ || draft.allowedTools.some((tool) => SANDBOXABLE_TOOLS.has(tool));
682
+ const hasPiRoute = models.some((model) => model.startsWith("pi:"));
683
+ const providerNative = models.some((model) => !model.startsWith("pi:"));
684
+ const mixedPiProviderNativeChain = mixed && hasPiRoute && providerNative;
685
+ if (hasSandboxableTools && hasPiRoute) {
686
+ draft.sandbox = await confirm({
687
+ message: "Install and use managed SRT for Pi shell/file tools? (localhost-only network; setup verifies fail-closed enforcement)",
688
+ initialValue: true,
689
+ });
690
+ }
691
+ else {
692
+ draft.sandbox = false;
693
+ }
694
+ if (mixedPiProviderNativeChain && draft.routeSafety === "uniform" && draft.sandbox) {
695
+ p.note("Managed SRT applies to Pi routes only. A mixed Pi/provider-native chain therefore cannot promise one uniform managed-SRT contract.", "Safety choice required");
696
+ const resolution = await select({
697
+ message: "How should this mixed chain resolve the managed-SRT mismatch?",
698
+ options: [
699
+ {
700
+ value: "per-route-native",
701
+ label: "Use per-route native safety",
702
+ hint: "keep managed SRT for Pi and show every provider's native contract",
703
+ },
704
+ {
705
+ value: "disable-managed-srt",
706
+ label: "Disable managed SRT",
707
+ hint: "keep the uniform contract without claiming SRT on provider-native routes",
708
+ },
709
+ ],
710
+ initialValue: "per-route-native",
711
+ });
712
+ if (resolution === "per-route-native")
713
+ draft.routeSafety = "per-route-native";
714
+ else
715
+ draft.sandbox = false;
716
+ }
717
+ if (draft.routeSafety === "per-route-native" && (mixed || providerNative)) {
718
+ p.note(formatRouteSafetyMatrix({ model: draft.model, ...(draft.effort === undefined ? {} : { effort: draft.effort }) }, draft.fallbacks, draft.sandbox), "Per-route safety contract");
719
+ const accepted = await confirm({
720
+ message: "Use these per-route safety contracts? Provider-native routes cannot enforce every mono-agent capability.",
721
+ initialValue: false,
722
+ });
723
+ if (!accepted) {
724
+ draft.routeSafety = "uniform";
725
+ if (mixedPiProviderNativeChain && draft.sandbox) {
726
+ draft.sandbox = false;
727
+ p.log.info("Managed SRT disabled because this mixed chain kept the uniform compatibility contract.");
728
+ }
729
+ p.log.info("Per-route native safety was not accepted; restored the uniform compatibility contract.");
730
+ }
731
+ else {
732
+ return;
733
+ }
734
+ }
735
+ if (hasSandboxableTools && hasPiRoute && isAllowAllTools(draft.allowedTools) && !draft.sandbox) {
736
+ const accepted = await confirmHighRiskUnsandboxedAccess();
737
+ if (!accepted) {
738
+ draft.sandbox = true;
739
+ if (mixedPiProviderNativeChain && draft.routeSafety === "uniform") {
740
+ draft.routeSafety = "per-route-native";
741
+ p.note(formatRouteSafetyMatrix({ model: draft.model, ...(draft.effort === undefined ? {} : { effort: draft.effort }) }, draft.fallbacks, draft.sandbox), "Per-route safety contract");
742
+ if (!await confirm({
743
+ message: "Use these per-route safety contracts? Provider-native routes cannot enforce every mono-agent capability.",
744
+ initialValue: false,
745
+ })) {
746
+ throw new WizardBack();
747
+ }
748
+ }
749
+ else {
750
+ p.log.info("Managed SRT enabled because unsandboxed Pi allow-all access was not confirmed.");
751
+ }
752
+ }
753
+ }
754
+ else if (hasSandboxableTools
755
+ && providerNative
756
+ && !models.every(isDirectCodexRef)
757
+ && isAllowAllTools(draft.allowedTools)
758
+ && !await confirmHighRiskUnsandboxedAccess()) {
759
+ throw new WizardBack();
760
+ }
761
+ }
762
+ async function confirmHighRiskUnsandboxedAccess() {
763
+ return confirm({
764
+ message: "Proceed with high-risk unsandboxed access? The model may run shell commands, change files, access the web, and send through enabled channels.",
765
+ initialValue: false,
766
+ });
767
+ }
768
+ /** Select primary, fallbacks, and effort without touching any other answer. */
769
+ async function promptModelSettings(draft, ctx, cache = {}) {
770
+ if (cache.result === undefined)
771
+ p.log.step("Discovering supported model catalogs…");
772
+ const discovery = cache.result ??= await discoverModelsInterruptibly({
773
+ ...(ctx.piAuthPath === undefined ? {} : { piAuthPath: ctx.piAuthPath }),
774
+ ...(ctx.persistedEnv === undefined ? {} : { persistedEnv: ctx.persistedEnv }),
775
+ });
776
+ const discoveredByValue = new Map(discovery.candidates.map((candidate) => [candidate.value, candidate]));
777
+ p.note(formatModelDiscoveryStatus(discovery.statuses), "Model discovery");
778
+ const previousModel = draft.model;
779
+ const previousEffort = draft.effort;
780
+ const providerDefault = discovery.candidates.find((candidate) => candidate.providerDefault === true);
781
+ const initialModel = draft.model === DEFAULT_MODEL ? providerDefault?.value ?? draft.model : draft.model;
782
+ let substep = 0;
783
+ for (;;) {
784
+ try {
785
+ if (substep === 0) {
786
+ const model = await autocomplete({
787
+ message: "Which model?",
788
+ options: modelSelectOptions(discovery.candidates, draft.model),
789
+ initialValue: draft.model === DEFAULT_MODEL ? initialModel : draft.model,
790
+ placeholder: "Type to search Pi, Codex, Claude, and local models…",
791
+ maxItems: MODEL_AUTOCOMPLETE_MAX_ITEMS,
792
+ });
793
+ draft.model = await resolveModelSelection(model, { candidates: discovery.candidates, context: "primary" });
794
+ substep = 1;
795
+ continue;
796
+ }
797
+ if (substep === 1) {
798
+ draft.effort = await promptEffortForModel(draft.model, discoveredByValue.get(draft.model), draft.model === previousModel ? previousEffort : undefined);
799
+ substep = 2;
800
+ continue;
801
+ }
802
+ if (substep === 2) {
803
+ draft.fallbacks = [];
804
+ if (!await confirm({ message: "Add fallback models?", initialValue: false }))
805
+ break;
806
+ substep = 3;
807
+ continue;
808
+ }
809
+ await promptFallbackModels(draft, discovery.candidates);
810
+ break;
811
+ }
812
+ catch (error) {
813
+ if (!(error instanceof WizardBack) || substep === 0)
814
+ throw error;
815
+ substep -= 1;
816
+ }
817
+ }
818
+ draft.credentialStates = selectedCredentialStates(draft, discoveredByValue, ctx.persistedEnv);
819
+ }
820
+ function selectedCredentialStates(draft, candidates, persistedEnv = {}) {
821
+ const states = {};
822
+ for (const model of selectedRuntimeModels(draft)) {
823
+ const key = providerCredentialKey(model);
824
+ if (key === undefined)
825
+ continue;
826
+ const authState = candidates.get(model)?.authState;
827
+ states[key] = authState === "verified"
828
+ ? "verified"
829
+ : hasDurableProviderEnvironmentCredential(model, persistedEnv)
830
+ ? "credential_detected"
831
+ : authState === "credential_detected"
832
+ ? "credential_detected"
833
+ : "auth_required";
834
+ }
835
+ return states;
836
+ }
837
+ function providerCredentialKey(model) {
838
+ if (model.startsWith("codex:"))
839
+ return "codex";
840
+ if (model.startsWith("claude:"))
841
+ return "claude";
842
+ if (!model.startsWith("pi:"))
843
+ return undefined;
844
+ const provider = model.split(":")[1];
845
+ return provider === undefined || provider === "ollama" || provider === "lmstudio" ? undefined : `pi:${provider}`;
846
+ }
847
+ async function promptEffortForModel(model, candidate, initial) {
848
+ const supported = candidate?.supportedEfforts ?? [];
849
+ const options = effortSelectOptions(supported, candidate?.defaultEffort);
850
+ const initialValue = initial !== undefined && supported.includes(initial) ? initial : "";
851
+ const effort = await select({
852
+ message: `Reasoning effort for ${model}`,
853
+ options,
854
+ initialValue,
855
+ });
856
+ return effort === "" ? undefined : effort;
194
857
  }
195
858
  /**
196
859
  * Prompt every non-secret input of the selected channel + memory modules and store
197
- * the answers into `draft.moduleInputs`. Secret inputs are never prompted — they
198
- * only ever surface as `.env.example` placeholders.
860
+ * the answers into `draft.moduleInputs`. Required secrets are collected only after
861
+ * the write confirmation so they can never appear in the plan summary.
199
862
  */
200
- async function promptModuleInputs(draft) {
201
- const moduleIds = [...draft.channels, ...(draft.memory === undefined ? [] : [draft.memory])];
863
+ async function promptModuleInputs(draft, requestedModuleIds) {
864
+ const moduleIds = requestedModuleIds
865
+ ?? [...draft.channels, ...(draft.memory === undefined ? [] : [draft.memory])];
202
866
  for (const id of moduleIds) {
867
+ if (MANAGED_MEMORY_MODULE_IDS.has(id)) {
868
+ continue;
869
+ }
203
870
  const module = findModule(id);
204
871
  if (module === undefined) {
205
872
  continue;
@@ -208,21 +875,184 @@ async function promptModuleInputs(draft) {
208
875
  if (input.secret === true) {
209
876
  continue;
210
877
  }
211
- const answer = guard(await p.text({
878
+ const currentValue = draft.moduleInputs[module.id]?.[input.id];
879
+ const answer = await textPrompt({
212
880
  message: `${module.title}: ${input.label}`,
213
881
  placeholder: input.description,
214
882
  ...(input.default === undefined ? {} : { defaultValue: input.default }),
215
- }));
883
+ ...(currentValue === undefined ? {} : { initialValue: currentValue }),
884
+ ...(input.validate === undefined ? {} : { validate: input.validate }),
885
+ });
216
886
  const trimmed = answer.trim();
217
887
  if (trimmed.length > 0) {
218
888
  (draft.moduleInputs[module.id] ??= {})[input.id] = trimmed;
219
889
  }
890
+ else {
891
+ delete draft.moduleInputs[module.id]?.[input.id];
892
+ }
893
+ }
894
+ }
895
+ }
896
+ /**
897
+ * Collect one complete, provider-specific embedding selection for Journal or
898
+ * BuJo. The finished bag replaces the prior provider bag as a unit so switching
899
+ * providers cannot retain a stale endpoint, model, dimension, or auth-env name.
900
+ */
901
+ async function promptManagedMemoryEmbeddingInputs(draft, ctx) {
902
+ const moduleId = draft.memory;
903
+ if (moduleId === undefined || !MANAGED_MEMORY_MODULE_IDS.has(moduleId))
904
+ return;
905
+ const previous = draft.moduleInputs[moduleId] ?? {};
906
+ const previousProvider = isManagedMemoryEmbeddingProvider(previous.embeddingProvider)
907
+ ? previous.embeddingProvider
908
+ : "ollama";
909
+ const provider = await select({
910
+ message: `${findModule(moduleId)?.title ?? "Managed memory"}: embedding service`,
911
+ options: [
912
+ { value: "ollama", label: "Ollama", hint: "local /api embedding service" },
913
+ { value: "lmstudio", label: "LM Studio", hint: "local typed model catalog + /v1/embeddings" },
914
+ ],
915
+ initialValue: previousProvider,
916
+ });
917
+ const sameProvider = provider === previousProvider;
918
+ const endpoint = (await textPrompt({
919
+ message: `${providerLabel(provider)} service root`,
920
+ initialValue: sameProvider
921
+ ? previous.embeddingEndpoint ?? DEFAULT_MEMORY_EMBEDDING_ENDPOINTS[provider]
922
+ : DEFAULT_MEMORY_EMBEDDING_ENDPOINTS[provider],
923
+ placeholder: DEFAULT_MEMORY_EMBEDDING_ENDPOINTS[provider],
924
+ validate: memoryEmbeddingEndpointProblem,
925
+ })).trim().replace(/\/+$/u, "");
926
+ const apiKeyEnv = (await textPrompt({
927
+ message: `${providerLabel(provider)} API-key environment variable (optional)`,
928
+ ...(sameProvider && previous.embeddingApiKeyEnv !== undefined
929
+ ? { initialValue: previous.embeddingApiKeyEnv }
930
+ : {}),
931
+ placeholder: "Leave blank for a keyless local service",
932
+ validate: validateOptionalEmbeddingApiKeyEnv,
933
+ })).trim();
934
+ const apiKey = apiKeyEnv.length === 0 ? undefined : ctx.persistedEnv?.[apiKeyEnv];
935
+ let discoveredModels = [];
936
+ try {
937
+ discoveredModels = await discoverMemoryEmbeddingModels({
938
+ provider,
939
+ endpoint,
940
+ ...(apiKey === undefined ? {} : { apiKey }),
941
+ });
942
+ if (discoveredModels.length === 0) {
943
+ p.log.warn(`${providerLabel(provider)} did not report any models explicitly typed as embedding-capable.`);
944
+ }
945
+ }
946
+ catch (error) {
947
+ p.log.warn(`${providerLabel(provider)} embedding discovery was unavailable: ${safeErrorMessage(error)}`);
948
+ }
949
+ let model;
950
+ if (discoveredModels.length > 0) {
951
+ const initialModel = sameProvider && previous.embeddingModel !== undefined
952
+ && discoveredModels.includes(previous.embeddingModel)
953
+ ? previous.embeddingModel
954
+ : discoveredModels[0];
955
+ const selectedModel = await autocomplete({
956
+ message: `${providerLabel(provider)} embedding model`,
957
+ options: [
958
+ ...discoveredModels.map((value) => ({ value, label: value, hint: "typed embedding model" })),
959
+ { value: MANUAL_EMBEDDING_MODEL, label: "Enter another model id…", hint: "manual readiness fallback" },
960
+ ],
961
+ initialValue: initialModel,
962
+ placeholder: "Type to search embedding models…",
963
+ maxItems: MODEL_AUTOCOMPLETE_MAX_ITEMS,
964
+ });
965
+ model = selectedModel === MANUAL_EMBEDDING_MODEL
966
+ ? await promptManualEmbeddingModel(sameProvider ? previous.embeddingModel : undefined)
967
+ : selectedModel;
968
+ }
969
+ else {
970
+ model = await promptManualEmbeddingModel(sameProvider ? previous.embeddingModel : undefined);
971
+ }
972
+ let dimension;
973
+ try {
974
+ const result = await probeMemoryEmbeddingSelection({
975
+ provider,
976
+ endpoint,
977
+ model,
978
+ ...(apiKey === undefined ? {} : { apiKey }),
979
+ });
980
+ dimension = result.dimension;
981
+ p.log.info(`${providerLabel(provider)} proved ${model} with embedding dimension ${dimension}.`);
982
+ }
983
+ catch (error) {
984
+ p.log.warn(`${providerLabel(provider)} could not prove ${model} yet: ${safeErrorMessage(error)}`);
985
+ }
986
+ if (dimension === undefined) {
987
+ const enteredDimension = await textPrompt({
988
+ message: `${providerLabel(provider)} embedding dimension`,
989
+ initialValue: sameProvider ? previous.embeddingDimension ?? "768" : "768",
990
+ placeholder: "Positive integer; live readiness will verify it",
991
+ validate: validateEmbeddingDimension,
992
+ });
993
+ dimension = Number(enteredDimension.trim());
994
+ }
995
+ draft.moduleInputs[moduleId] = {
996
+ embeddingProvider: provider,
997
+ embeddingEndpoint: endpoint,
998
+ embeddingModel: model,
999
+ embeddingDimension: String(dimension),
1000
+ ...(apiKeyEnv.length === 0 ? {} : { embeddingApiKeyEnv: apiKeyEnv }),
1001
+ };
1002
+ }
1003
+ async function promptManualEmbeddingModel(initialValue) {
1004
+ return (await textPrompt({
1005
+ message: "Embedding model id",
1006
+ ...(initialValue === undefined ? {} : { initialValue }),
1007
+ placeholder: "Exact model id from the local service",
1008
+ validate: (value) => (value ?? "").trim().length > 0 ? undefined : "Enter an embedding model id.",
1009
+ })).trim();
1010
+ }
1011
+ function isManagedMemoryEmbeddingProvider(value) {
1012
+ return value === "ollama" || value === "lmstudio";
1013
+ }
1014
+ function providerLabel(provider) {
1015
+ return provider === "ollama" ? "Ollama" : "LM Studio";
1016
+ }
1017
+ function validateOptionalEmbeddingApiKeyEnv(value) {
1018
+ const trimmed = (value ?? "").trim();
1019
+ return trimmed.length === 0 || /^[A-Za-z_][A-Za-z0-9_]*$/u.test(trimmed)
1020
+ ? undefined
1021
+ : "Use an environment-variable name such as LM_STUDIO_API_KEY.";
1022
+ }
1023
+ function validateEmbeddingDimension(value) {
1024
+ const parsed = Number((value ?? "").trim());
1025
+ return Number.isSafeInteger(parsed) && parsed > 0 ? undefined : "Enter a positive integer dimension.";
1026
+ }
1027
+ function safeErrorMessage(error) {
1028
+ return error instanceof Error ? error.message : "unknown service error";
1029
+ }
1030
+ /** Collect only typed required secrets, masked and retained solely for this run. */
1031
+ async function promptRequiredModuleSecrets(answers, persistedEnv = {}, existing = {}) {
1032
+ const plan = composeWizardPlan(answers, { dirBasename: "agent", skillsRootExists: false });
1033
+ const secrets = {};
1034
+ for (const secret of plan.secrets) {
1035
+ if (!secret.required || hasNonEmptyValue(persistedEnv[secret.envVar]))
1036
+ continue;
1037
+ if (hasNonEmptyValue(existing[secret.envVar])) {
1038
+ secrets[secret.envVar] = existing[secret.envVar];
1039
+ continue;
220
1040
  }
1041
+ secrets[secret.envVar] = await passwordPrompt({
1042
+ message: `${secret.label} (${secret.envVar})`,
1043
+ validate: (value) => (value ?? "").trim().length === 0 ? "This secret is required for the selected capability." : undefined,
1044
+ clearOnError: true,
1045
+ });
221
1046
  }
1047
+ return secrets;
1048
+ }
1049
+ function hasNonEmptyValue(value) {
1050
+ return value !== undefined && value.trim().length > 0;
222
1051
  }
223
1052
  /** Short reasons annotating each always-on tool in the framing note. */
224
1053
  const ALWAYS_ON_TOOL_REASONS = {
225
1054
  MemoryRecall: "memory recall is on",
1055
+ ReadSkill: "configuration skills load on demand",
226
1056
  };
227
1057
  /** The always-on tool names annotated with their reason, e.g. `MemoryRecall (memory recall is on)`. */
228
1058
  function alwaysOnDisplay(alwaysOn) {
@@ -238,7 +1068,7 @@ function alwaysOnDisplay(alwaysOn) {
238
1068
  */
239
1069
  function channelSendTools(channels) {
240
1070
  return toolMultiselectOptions(channels)
241
- .slice(BUILTIN_TOOL_NAMES.length)
1071
+ .slice(BUILTIN_TOOL_NAMES.length + APP_TOOL_NAMES.length)
242
1072
  .map((option) => option.value)
243
1073
  .filter((value) => value !== "AskUser");
244
1074
  }
@@ -247,7 +1077,8 @@ function channelSendTools(channels) {
247
1077
  * knows what "Allow all" turns on and what is unaffected by the choice:
248
1078
  * 1. Always on — auto-provisioned, NOT gated by this choice (e.g. MemoryRecall).
249
1079
  * 2. Built-ins — file/shell/web tools.
250
- * 3. Channel tools — the send/ask tools that came with the channels you enabled,
1080
+ * 3. App tools — safe host capabilities such as bounded run inspection.
1081
+ * 4. Channel tools — the send/ask tools that came with the channels you enabled,
251
1082
  * plus AskUser (ask the human, any channel).
252
1083
  */
253
1084
  function toolSituationFraming(draft, alwaysOn) {
@@ -260,8 +1091,9 @@ function toolSituationFraming(draft, alwaysOn) {
260
1091
  ? `Always on (auto-provisioned, not affected by this choice): ${alwaysOnDisplay(alwaysOn).join(", ")}.`
261
1092
  : "Always on (auto-provisioned): none for this setup.",
262
1093
  "Built-ins: files (Read/Write/Edit/Glob/Grep), shell (Bash), web (WebFetch/WebSearch).",
1094
+ "App tools: RunHistory (read-only evidence from completed prior runs in this conversation).",
263
1095
  channelLine,
264
- '"Allow all" turns on every built-in and channel tool; you can turn specific tools off later via tools.disallowedTools.',
1096
+ '"Allow all" lets the model run shell commands, read/change files, access the web, and send through enabled channels. These actions can modify data or contact people; you can turn specific tools off later via tools.disallowedTools.',
265
1097
  ].join("\n");
266
1098
  }
267
1099
  /**
@@ -273,10 +1105,15 @@ function toolSituationFraming(draft, alwaysOn) {
273
1105
  async function promptTools(draft) {
274
1106
  const alwaysOn = alwaysOnTools(toWizardAnswers(draft));
275
1107
  p.note(toolSituationFraming(draft, alwaysOn), "Tools");
276
- const allowAll = guard(await p.confirm({
277
- message: "Allow all tools? (recommended — the model can use every built-in and your channels' tools)",
1108
+ if (selectedRuntimeModels(draft).every(hasFixedAllowAllToolPolicyRef)) {
1109
+ draft.allowedTools = [ALLOW_ALL_TOOLS];
1110
+ p.log.info("Direct Codex uses its native app-server tool set and cannot enforce mono-agent per-tool allowlists. Tool policy is fixed to allow-all; use Pi or Claude for a restrictive tool list.");
1111
+ return;
1112
+ }
1113
+ const allowAll = await confirm({
1114
+ message: "Allow all tools? (shell commands, file changes, web access, and enabled-channel sends)",
278
1115
  initialValue: true,
279
- }));
1116
+ });
280
1117
  if (allowAll) {
281
1118
  draft.allowedTools = [ALLOW_ALL_TOOLS];
282
1119
  return;
@@ -294,17 +1131,17 @@ async function pickSpecificTools(draft) {
294
1131
  const optionOrder = new Map(options.map((option, index) => [option.value, index]));
295
1132
  const recommended = recommendedToolSelection(toWizardAnswers(draft));
296
1133
  for (;;) {
297
- const tools = guard(await p.multiselect({
1134
+ const tools = await multiselect({
298
1135
  message: "Which tools may the model call?",
299
1136
  options,
300
1137
  initialValues: [...recommended],
301
1138
  required: false,
302
- }));
1139
+ });
303
1140
  if (tools.length === 0) {
304
- const proceed = guard(await p.confirm({
1141
+ const proceed = await confirm({
305
1142
  message: "⚠ Zero tools selected — the agent will be chat-only (cannot read files, run commands, or send proactively). Continue?",
306
1143
  initialValue: false,
307
- }));
1144
+ });
308
1145
  if (!proceed) {
309
1146
  continue;
310
1147
  }
@@ -313,81 +1150,320 @@ async function pickSpecificTools(draft) {
313
1150
  return;
314
1151
  }
315
1152
  }
316
- /**
317
- * Render the compact plan summary and ask for the single write-time confirmation.
318
- * Provider modules are implementation detail (auto-added for local models), so
319
- * they are excluded from the user-facing capabilities line. A "no" cancels.
320
- */
321
- async function confirmSummary(draft, ctx) {
322
- if ([draft.model, ...draft.fallbackModels].some((model) => LOCAL_PROVIDER_MODEL.test(model))) {
323
- p.note("A matching local provider block is added automatically.\nThe first turn may be slow while the model loads.", "Local model");
324
- }
1153
+ async function confirmSummary(draft, ctx, options) {
325
1154
  const answers = toWizardAnswers(draft);
326
- const plan = composeWizardPlan(answers, {
327
- dirBasename: basename(ctx.cwd),
328
- skillsRootExists: await pathExists(join(ctx.cwd, "skills")),
329
- });
1155
+ const plan = await composePlanForCwd(answers, ctx.cwd);
1156
+ const setupModelRefs = referencedSetupModelRefs(plan);
1157
+ const existingSetupModelRefs = options === undefined
1158
+ ? undefined
1159
+ : referencedSetupModelRefs(await composePlanForCwd(options.existing.answers, ctx.cwd));
1160
+ const preserveProviderSetup = options !== undefined
1161
+ && existingSetupModelRefs !== undefined
1162
+ && sameOrderedValues(setupModelRefs, existingSetupModelRefs);
1163
+ const preliminarySetupPlan = providerSetupPlan(plan, ctx, draft.credentialStates);
1164
+ // Resolve destinations before the final review; collect masked values only
1165
+ // after the operator chooses Create.
1166
+ const piApiKeyPersistenceByProvider = preserveProviderSetup
1167
+ ? { ...options.existing.piApiKeyPersistenceByProvider }
1168
+ : await selectPiApiKeyPersistence(preliminarySetupPlan, options?.existing.piApiKeyPersistenceByProvider);
1169
+ const setupPlan = providerSetupPlan(plan, ctx, draft.credentialStates, piApiKeyPersistenceByProvider);
1170
+ if (setupModelRefs.some((model) => /^pi:(?:ollama|lmstudio):/u.test(model))) {
1171
+ p.note("Local runtime or memory services are checked before readiness. The first load may take longer while models start.", "Local dependencies");
1172
+ }
330
1173
  const capabilities = plan.selectedModules
331
1174
  .filter((module) => module.kind !== "provider")
332
1175
  .map((module) => module.title);
333
- const writes = ["mono-agent.config.json", "IDENTITY.md"];
1176
+ const identityAlreadyExists = await pathExists(join(ctx.cwd, "IDENTITY.md"));
1177
+ const creates = ["mono-agent.config.json", "IDENTITY.md", ".mono-agent/artifacts/", ".mono-agent/workspace/"];
334
1178
  if (plan.envExample !== undefined) {
335
- writes.push(".env.example");
1179
+ creates.push(".env.example (placeholders only)");
336
1180
  }
337
1181
  for (const file of plan.files) {
338
- writes.push(file.path);
1182
+ creates.push(file.path);
1183
+ }
1184
+ const providerEnvironmentNames = setupPlan.actions
1185
+ .filter(isProviderSetupPiApiKeyAction)
1186
+ .filter((action) => action.persistence === "environment")
1187
+ .map((action) => action.envVar);
1188
+ const moduleSecretsToCollect = plan.secrets
1189
+ .filter((secret) => secret.required && !hasNonEmptyValue(ctx.persistedEnv?.[secret.envVar]))
1190
+ .map((secret) => secret.envVar);
1191
+ const secrets = [...new Set([
1192
+ ...moduleSecretsToCollect,
1193
+ ...providerEnvironmentNames,
1194
+ ])];
1195
+ const hardenExistingDotenv = hasSensitivePersistedEnvironmentValue(ctx.persistedEnv ?? {});
1196
+ const mutableSecretFiles = [];
1197
+ if (secrets.length > 0 || hardenExistingDotenv) {
1198
+ mutableSecretFiles.push(secrets.length > 0 ? ".env (owner-only secret merge)" : ".env (owner-only permission hardening)", ".gitignore (ensure /.env is ignored)");
339
1199
  }
340
- const secrets = plan.secrets.map((secret) => secret.envVar);
341
1200
  const toolsLine = isAllowAllTools(draft.allowedTools)
342
1201
  ? "all tools"
343
1202
  : draft.allowedTools.length > 0
344
1203
  ? draft.allowedTools.join(", ")
345
1204
  : "none (chat-only)";
346
1205
  const alwaysOn = alwaysOnTools(answers);
1206
+ const runtimeRoutes = [
1207
+ { model: draft.model, effort: draft.effort },
1208
+ ...draft.fallbacks,
1209
+ ];
1210
+ const potentiallyBilledCalls = runtimeRoutes.filter((route) => modelRefMayBill(route.model)).length;
1211
+ const providerActions = [
1212
+ ...setupPlan.detectedModelRefs.map((modelRef) => `${modelRef}: credential/sign-in detected; skip initial auth and verify with the live readiness call`),
1213
+ ...setupPlan.actions.map((action) => {
1214
+ if (isProviderSetupPiApiKeyAction(action)) {
1215
+ return action.persistence === "environment"
1216
+ ? `${action.label}: read ${action.envVar} from owner-only .env; do not write Pi auth.json`
1217
+ : `${action.label}: save credential to owner-only ${action.piAuthPath}; do not copy it to .env`;
1218
+ }
1219
+ return `${action.label}: check the current credential/service state first; if needed run ${providerSetupActionCommandLine(action)} (cwd: ${action.cwd})`;
1220
+ }),
1221
+ ...(draft.sandbox
1222
+ ? ["Managed SRT: install the pinned tool in the private user cache and run the functional fail-closed preflight"]
1223
+ : []),
1224
+ ];
347
1225
  const lines = [
348
- `Model: ${draft.model}${draft.fallbackModels.length > 0 ? ` (fallbacks: ${draft.fallbackModels.join(", ")})` : ""}`,
349
- `Effort: ${draft.effort ?? "default"}`,
1226
+ `Agent: ${draft.name}`,
1227
+ "Role target: IDENTITY.md ## Role",
1228
+ `Role text: ${draft.purpose}`,
1229
+ `Role write: ${identityAlreadyExists
1230
+ ? "Preserve the existing IDENTITY.md unchanged; the entered Role text will not be written."
1231
+ : "Create IDENTITY.md with the entered text as its ## Role body."}`,
1232
+ "Routes:",
1233
+ ...runtimeRoutes.map((route, index) => ` ${index === 0 ? "Primary" : `Fallback ${index}`}: ${route.model} [${route.effort ?? "provider default"}]`),
350
1234
  `Capabilities: ${capabilities.length > 0 ? capabilities.join(", ") : "none"}`,
351
1235
  `Tools: ${toolsLine}`,
1236
+ `Route safety: ${draft.routeSafety}`,
1237
+ ...(draft.routeSafety === "per-route-native" || !isMixedRouteChain(runtimeRoutes.map((route) => route.model))
1238
+ ? [formatRouteSafetyMatrix({ model: draft.model, ...(draft.effort === undefined ? {} : { effort: draft.effort }) }, draft.fallbacks, draft.sandbox)]
1239
+ : [
1240
+ "Uniform contract: every route must satisfy the same mono-agent tool/sandbox capabilities; an incompatible route is rejected or skipped, never silently weakened.",
1241
+ ]),
352
1242
  ...(alwaysOn.length > 0 ? [`Always on: ${alwaysOnDisplay(alwaysOn).join(", ")}`] : []),
353
- `Writes: ${writes.join(", ")}`,
354
- `Secrets: ${secrets.length > 0 ? `${secrets.join(", ")} → .env.example` : "none"}`,
1243
+ `Readiness: ${runtimeRoutes.length} real model call(s), one per selected route; ${potentiallyBilledCalls} potentially billed`,
1244
+ `Verify refs: ${setupModelRefs.join(", ")}`,
1245
+ `Provider actions: ${providerActions.length > 0 ? providerActions.join("\n ") : "none"}`,
1246
+ `Creates if missing (preserves existing scaffold paths): ${creates.join(", ")}`,
1247
+ ...(mutableSecretFiles.length > 0
1248
+ ? [`May create or update: ${mutableSecretFiles.join(", ")}`]
1249
+ : []),
1250
+ `Secret persistence: ${secrets.length > 0
1251
+ ? `${secrets.join(", ")} -> owner-only .env merge (values hidden)`
1252
+ : hardenExistingDotenv
1253
+ ? "existing sensitive .env -> owner-only permission/ignore hardening (values unchanged)"
1254
+ : "none"}`,
1255
+ ...(Object.values(piApiKeyPersistenceByProvider).some((value) => value === "secure-store")
1256
+ ? ["Pi credential persistence: selected keys -> owner-only Pi auth.json (values hidden; not copied to .env)"]
1257
+ : []),
1258
+ ...(secrets.length > 0 || hardenExistingDotenv
1259
+ ? [`Secret files: .env (${secrets.length > 0 ? "owner-only merge" : "owner-only hardening"}), .gitignore (ensure /.env is ignored)`]
1260
+ : []),
355
1261
  ];
356
- p.note(lines.join("\n"), "Review");
357
- const setupPlan = planProviderSetup({
358
- modelRefs: [answers.model, ...answers.fallbackModels],
1262
+ p.note(lines.join("\n"), "Creation review");
1263
+ const choice = await select({
1264
+ message: `Create “${draft.name}”?`,
1265
+ options: creationReviewOptions({
1266
+ setupRequired: (preserveProviderSetup
1267
+ ? options.existing.runProviderSetup
1268
+ : setupPlan.actions.length > 0) || draft.sandbox,
1269
+ }),
1270
+ initialValue: "create",
1271
+ });
1272
+ if (choice === "cancel") {
1273
+ throw new WizardCancelled();
1274
+ }
1275
+ if (choice === "edit") {
1276
+ try {
1277
+ const step = await select({
1278
+ message: "What would you like to edit?",
1279
+ options: [
1280
+ { value: "0", label: "Agent name and Role" },
1281
+ { value: "1", label: "Models and efforts" },
1282
+ { value: "2", label: "Channels" },
1283
+ { value: "3", label: "Memory" },
1284
+ { value: "4", label: "Capability details" },
1285
+ { value: "5", label: "Tools" },
1286
+ { value: "6", label: "Route safety and sandbox" },
1287
+ { value: "7", label: "Observability" },
1288
+ { value: "8", label: "Return to review" },
1289
+ ],
1290
+ initialValue: "1",
1291
+ });
1292
+ return { status: "edit", step: Number(step) };
1293
+ }
1294
+ catch (error) {
1295
+ if (error instanceof WizardBack) {
1296
+ if (options !== undefined)
1297
+ throw error;
1298
+ return { status: "edit", step: 8 };
1299
+ }
1300
+ throw error;
1301
+ }
1302
+ }
1303
+ if (preserveProviderSetup) {
1304
+ return {
1305
+ status: "create",
1306
+ providerSetup: {
1307
+ runProviderSetup: options.existing.runProviderSetup,
1308
+ providerSetupSecrets: { ...options.existing.providerSetupSecrets },
1309
+ providerEnvironmentSecrets: { ...options.existing.providerEnvironmentSecrets },
1310
+ piApiKeyPersistenceByProvider: { ...options.existing.piApiKeyPersistenceByProvider },
1311
+ },
1312
+ };
1313
+ }
1314
+ let providerSetup;
1315
+ try {
1316
+ providerSetup = await collectProviderSetup(setupPlan, setupPlan.actions.length > 0, piApiKeyPersistenceByProvider, options?.existing.providerSetupSecrets, options?.existing.providerEnvironmentSecrets);
1317
+ }
1318
+ catch (error) {
1319
+ if (error instanceof WizardBack) {
1320
+ if (options !== undefined)
1321
+ throw error;
1322
+ return { status: "edit", step: 8 };
1323
+ }
1324
+ throw error;
1325
+ }
1326
+ return {
1327
+ status: "create",
1328
+ providerSetup,
1329
+ };
1330
+ }
1331
+ function selectedRuntimeModels(draft) {
1332
+ return [draft.model, ...draft.fallbacks.map((fallback) => fallback.model)];
1333
+ }
1334
+ function sameOrderedValues(left, right) {
1335
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1336
+ }
1337
+ function hasFixedAllowAllToolPolicyRef(model) {
1338
+ return isDirectCodexRef(model) || isDirectOpenCodeRef(model);
1339
+ }
1340
+ function hasExactAllowAllTools(draft) {
1341
+ return draft.allowedTools.length === 1 && draft.allowedTools[0] === ALLOW_ALL_TOOLS;
1342
+ }
1343
+ function hasInvalidUniformManagedSrtChain(draft) {
1344
+ if (draft.routeSafety !== "uniform" || !draft.sandbox)
1345
+ return false;
1346
+ const models = selectedRuntimeModels(draft);
1347
+ return isMixedRouteChain(models)
1348
+ && models.some((model) => model.startsWith("pi:"))
1349
+ && models.some((model) => !model.startsWith("pi:"));
1350
+ }
1351
+ function isDirectCodexRef(model) {
1352
+ return model.startsWith("codex:");
1353
+ }
1354
+ function isDirectOpenCodeRef(model) {
1355
+ return model.startsWith("opencode:");
1356
+ }
1357
+ function runtimeFamily(model) {
1358
+ return model.split(":", 1)[0] ?? model;
1359
+ }
1360
+ function modelRefMayBill(model) {
1361
+ return !/^pi:(?:ollama|lmstudio):/u.test(model);
1362
+ }
1363
+ /** Build a plan using the destination-derived context for honest review/setup. */
1364
+ async function composePlanForCwd(answers, cwd) {
1365
+ return composeWizardPlan(answers, {
1366
+ dirBasename: basename(cwd),
1367
+ skillsRootExists: await pathExists(join(cwd, "skills")),
1368
+ });
1369
+ }
1370
+ /** Offer auth/preflight for every runtime and hidden memory model dependency. */
1371
+ async function promptProviderSetup(plan, ctx, credentialStates = {}) {
1372
+ const modelRefs = referencedSetupModelRefs(plan);
1373
+ p.note(modelRefs.join("\n"), "Models and services to verify");
1374
+ const preliminarySetupPlan = providerSetupPlan(plan, ctx, credentialStates);
1375
+ if (preliminarySetupPlan.actions.length === 0) {
1376
+ return { runProviderSetup: false, providerSetupSecrets: {}, providerEnvironmentSecrets: {}, piApiKeyPersistenceByProvider: {} };
1377
+ }
1378
+ const piApiKeyPersistenceByProvider = await selectPiApiKeyPersistence(preliminarySetupPlan);
1379
+ const setupPlan = providerSetupPlan(plan, ctx, credentialStates, piApiKeyPersistenceByProvider);
1380
+ p.note(setupPlan.actions
1381
+ .map(providerSetupActionReviewLine)
1382
+ .join("\n"), "Provider setup");
1383
+ const runProviderSetup = await confirm({
1384
+ message: setupPlan.actions.some((action) => action.id.startsWith("pi-login:"))
1385
+ ? "Run provider auth/preflight now? (detected credentials are reused and verified by live readiness; Pi OAuth may update the auth store)"
1386
+ : "Run provider auth/preflight now? (detected credentials are reused and verified by live readiness)",
1387
+ initialValue: false,
1388
+ });
1389
+ return collectProviderSetup(setupPlan, runProviderSetup, piApiKeyPersistenceByProvider);
1390
+ }
1391
+ function providerSetupPlan(plan, ctx, credentialStates = {}, piApiKeyPersistenceByProvider = {}) {
1392
+ const modelRefs = referencedSetupModelRefs(plan);
1393
+ const configuredPiAuthPath = typeof plan.configJson.providers?.piAuthPath === "string"
1394
+ ? plan.configJson.providers.piAuthPath
1395
+ : undefined;
1396
+ const piAuthPath = ctx.piAuthPath ?? configuredPiAuthPath;
1397
+ return planProviderSetup({
1398
+ modelRefs,
359
1399
  cwd: ctx.cwd,
360
- ...(typeof plan.configJson.providers?.piAuthPath === "string" ? { piAuthPath: plan.configJson.providers.piAuthPath } : {}),
1400
+ credentialStates,
1401
+ piApiKeyPersistenceByProvider,
1402
+ ...(piAuthPath === undefined ? {} : { piAuthPath }),
361
1403
  });
362
- let runProviderSetup = false;
1404
+ }
1405
+ async function collectProviderSetup(setupPlan, runProviderSetup, selectedPersistence, existingSetupSecrets = {}, existingEnvironmentSecrets = {}) {
363
1406
  const providerSetupSecrets = {};
364
- if (setupPlan.actions.length > 0) {
365
- p.note(setupPlan.actions
366
- .map((action) => `${action.label}: ${providerSetupActionCommandLine(action)} (cwd: ${action.cwd})`)
367
- .join("\n"), "Provider setup");
368
- runProviderSetup = guard(await p.confirm({
369
- message: setupPlan.actions.some((action) => action.id.startsWith("pi-login:"))
370
- ? "Run provider auth/preflight before writing files? (Pi OAuth setup can create/update the auth store)"
371
- : "Run provider auth/preflight before writing files?",
372
- initialValue: false,
373
- }));
374
- if (runProviderSetup) {
375
- for (const action of setupPlan.actions) {
376
- if (!isProviderSetupPiApiKeyAction(action)) {
377
- continue;
378
- }
379
- providerSetupSecrets[action.id] = guard(await p.password({
380
- message: `${action.label} (${action.envVar})`,
381
- validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
382
- clearOnError: true,
383
- }));
1407
+ const providerEnvironmentSecrets = {};
1408
+ if (!runProviderSetup) {
1409
+ return {
1410
+ runProviderSetup: false,
1411
+ providerSetupSecrets,
1412
+ providerEnvironmentSecrets,
1413
+ piApiKeyPersistenceByProvider: { ...(selectedPersistence ?? {}) },
1414
+ };
1415
+ }
1416
+ const piApiKeyPersistenceByProvider = { ...(selectedPersistence ?? {}) };
1417
+ for (const action of setupPlan.actions) {
1418
+ if (!isProviderSetupPiApiKeyAction(action))
1419
+ continue;
1420
+ if (action.persistence === "environment") {
1421
+ if (hasNonEmptyValue(existingEnvironmentSecrets[action.envVar])) {
1422
+ providerEnvironmentSecrets[action.envVar] = existingEnvironmentSecrets[action.envVar];
1423
+ continue;
384
1424
  }
1425
+ providerEnvironmentSecrets[action.envVar] = await passwordPrompt({
1426
+ message: `${action.label} (${action.envVar}, saved to owner-only .env)`,
1427
+ validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
1428
+ clearOnError: true,
1429
+ });
1430
+ continue;
385
1431
  }
1432
+ if (hasNonEmptyValue(existingSetupSecrets[action.id])) {
1433
+ providerSetupSecrets[action.id] = existingSetupSecrets[action.id];
1434
+ continue;
1435
+ }
1436
+ providerSetupSecrets[action.id] = await passwordPrompt({
1437
+ message: `${action.label} (${action.envVar})`,
1438
+ validate: (value) => (value ?? "").trim().length === 0 ? "API key is required." : undefined,
1439
+ clearOnError: true,
1440
+ });
386
1441
  }
387
- if (!guard(await p.confirm({ message: "Write these files?", initialValue: true }))) {
388
- throw new WizardCancelled();
1442
+ return { runProviderSetup, providerSetupSecrets, providerEnvironmentSecrets, piApiKeyPersistenceByProvider };
1443
+ }
1444
+ async function selectPiApiKeyPersistence(setupPlan, existing = {}) {
1445
+ const selected = {};
1446
+ for (const action of setupPlan.actions) {
1447
+ if (!isProviderSetupPiApiKeyAction(action))
1448
+ continue;
1449
+ const label = action.label.replace(/ \((?:secure store|environment)\)$/u, "");
1450
+ const existingSelection = existing[action.provider];
1451
+ selected[action.provider] = existingSelection ?? await select({
1452
+ message: `Where should ${label} store ${action.envVar}?`,
1453
+ options: [
1454
+ { value: "secure-store", label: "Store securely in Pi auth.json", hint: "owner-only Pi credential store" },
1455
+ { value: "environment", label: "Use environment variable", hint: `owner-only .env (${action.envVar})` },
1456
+ ],
1457
+ initialValue: "secure-store",
1458
+ });
389
1459
  }
390
- return { runProviderSetup, providerSetupSecrets };
1460
+ return selected;
1461
+ }
1462
+ function providerSetupActionReviewLine(action) {
1463
+ if (isProviderSetupPiApiKeyAction(action) && action.persistence === "environment") {
1464
+ return `${action.label}: read ${action.envVar} from the durable agent environment; Pi auth.json remains unchanged (cwd: ${action.cwd})`;
1465
+ }
1466
+ return `${action.label}: ${providerSetupActionCommandLine(action)} (cwd: ${action.cwd})`;
391
1467
  }
392
1468
  /** Seed a mutable draft from immutable answers (defaults or a preset). */
393
1469
  function draftFrom(answers) {
@@ -402,9 +1478,13 @@ function draftFrom(answers) {
402
1478
  moduleInputs[moduleId] = bag;
403
1479
  }
404
1480
  return {
1481
+ name: answers.name?.trim() || "Mono Agent",
1482
+ purpose: answers.purpose?.trim() || "Help the operator work effectively in this folder.",
405
1483
  model: answers.model,
406
- fallbackModels: [...answers.fallbackModels],
1484
+ fallbacks: effectiveFallbacks(answers).map((fallback) => ({ ...fallback })),
407
1485
  effort: answers.effort,
1486
+ routeSafety: answers.routeSafety,
1487
+ credentialStates: {},
408
1488
  channels: [...answers.channels],
409
1489
  memory: answers.memory,
410
1490
  sandbox: answers.sandbox,
@@ -419,10 +1499,16 @@ function draftFrom(answers) {
419
1499
  * `undefined`).
420
1500
  */
421
1501
  function toWizardAnswers(draft) {
1502
+ for (const modelRef of selectedRuntimeModels(draft)) {
1503
+ assertConcreteWizardModelRef(modelRef);
1504
+ }
422
1505
  return {
1506
+ name: draft.name,
1507
+ purpose: draft.purpose,
423
1508
  model: draft.model,
424
- fallbackModels: [...draft.fallbackModels],
1509
+ fallbacks: draft.fallbacks.map((fallback) => ({ ...fallback })),
425
1510
  ...(draft.effort === undefined ? {} : { effort: draft.effort }),
1511
+ routeSafety: draft.routeSafety,
426
1512
  channels: [...draft.channels],
427
1513
  ...(draft.memory === undefined ? {} : { memory: draft.memory }),
428
1514
  sandbox: draft.sandbox,
@@ -431,9 +1517,22 @@ function toWizardAnswers(draft) {
431
1517
  moduleInputs: draft.moduleInputs,
432
1518
  };
433
1519
  }
434
- /** Split a comma-separated string into trimmed, non-empty entries. */
435
- function splitCsv(value) {
436
- return value.split(",").map((entry) => entry.trim()).filter(Boolean);
1520
+ function cloneDraft(draft) {
1521
+ return {
1522
+ ...draft,
1523
+ fallbacks: draft.fallbacks.map((fallback) => ({ ...fallback })),
1524
+ channels: [...draft.channels],
1525
+ allowedTools: [...draft.allowedTools],
1526
+ moduleInputs: Object.fromEntries(Object.entries(draft.moduleInputs).map(([id, values]) => [id, { ...values }])),
1527
+ };
1528
+ }
1529
+ function restoreDraft(target, source) {
1530
+ Object.assign(target, cloneDraft(source));
1531
+ }
1532
+ /** A second Esc/Ctrl-C while this default-No prompt is open exits cleanly. */
1533
+ async function confirmExitSetup() {
1534
+ const answer = await p.confirm({ message: "Exit setup?", initialValue: false });
1535
+ return p.isCancel(answer) ? true : answer;
437
1536
  }
438
1537
  /** True when `path` exists (a local mirror of the CLI's private helper). */
439
1538
  async function pathExists(path) {