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