@ouro.bot/cli 0.1.0-alpha.326 → 0.1.0-alpha.328

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.
package/changelog.json CHANGED
@@ -1,6 +1,19 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.328",
6
+ "changes": [
7
+ "fix(auth): keep provider switches model-aware so changing providers updates incompatible model pairings to safe provider defaults instead of carrying stale model names across OpenAI Codex, Anthropic, MiniMax, Azure, and GitHub Copilot.",
8
+ "fix(auth): rewrite provider auth/failover guidance into multiline actionable sections with provider error details, model-mismatch repair commands, and ready-provider switch prompts."
9
+ ]
10
+ },
11
+ {
12
+ "version": "0.1.0-alpha.327",
13
+ "changes": [
14
+ "fix(daemon): make startup stability polling respect stdout TTY capability so captured `ouro up` output is plain append-only text without raw ANSI cursor-control or color escapes, while interactive terminals keep in-place progress rendering."
15
+ ]
16
+ },
4
17
  {
5
18
  "version": "0.1.0-alpha.326",
6
19
  "changes": [
@@ -48,33 +48,29 @@ const path = __importStar(require("path"));
48
48
  const runtime_1 = require("../../nerves/runtime");
49
49
  const identity_1 = require("../identity");
50
50
  const migrate_config_1 = require("../migrate-config");
51
+ const provider_models_1 = require("../provider-models");
51
52
  const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
52
53
  const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
53
54
  const DEFAULT_SECRETS_TEMPLATE = {
54
55
  providers: {
55
56
  azure: {
56
- modelName: "gpt-4o-mini",
57
57
  apiKey: "",
58
58
  endpoint: "",
59
59
  deployment: "",
60
60
  apiVersion: "2025-04-01-preview",
61
61
  },
62
62
  minimax: {
63
- model: "MiniMax-M2.7",
64
63
  apiKey: "",
65
64
  },
66
65
  anthropic: {
67
- model: "claude-opus-4-6",
68
66
  setupToken: "",
69
67
  refreshToken: "",
70
68
  expiresAt: 0,
71
69
  },
72
70
  "openai-codex": {
73
- model: "gpt-5.4",
74
71
  oauthAccessToken: "",
75
72
  },
76
73
  "github-copilot": {
77
- model: "claude-sonnet-4.6",
78
74
  githubToken: "",
79
75
  baseUrl: "",
80
76
  },
@@ -176,16 +172,27 @@ function readAgentConfigForAgent(agentName, bundlesRoot = (0, identity_1.getAgen
176
172
  function writeAgentProviderSelection(agentName, facing, provider, bundlesRoot = (0, identity_1.getAgentBundlesRoot)()) {
177
173
  const { configPath, config } = readAgentConfigForAgent(agentName, bundlesRoot);
178
174
  const facingKey = facing === "human" ? "humanFacing" : "agentFacing";
175
+ const previousFacing = config[facingKey];
176
+ const resolved = (0, provider_models_1.resolveModelForProviderSelection)(provider, previousFacing.model);
179
177
  const nextConfig = {
180
178
  ...config,
181
- [facingKey]: { ...config[facingKey], provider },
179
+ [facingKey]: { ...previousFacing, provider, model: resolved.model },
182
180
  };
183
181
  fs.writeFileSync(configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
184
182
  (0, runtime_1.emitNervesEvent)({
185
183
  component: "daemon",
186
184
  event: "daemon.auth_provider_selected",
187
185
  message: "updated agent provider selection after auth flow",
188
- meta: { agentName, facing, provider, configPath },
186
+ meta: {
187
+ agentName,
188
+ facing,
189
+ provider,
190
+ previousProvider: previousFacing.provider,
191
+ previousModel: previousFacing.model,
192
+ model: resolved.model,
193
+ preservedModel: resolved.preserved,
194
+ configPath,
195
+ },
189
196
  });
190
197
  return configPath;
191
198
  }
@@ -32,6 +32,7 @@ const obligations_1 = require("../arc/obligations");
32
32
  const tool_loop_1 = require("./tool-loop");
33
33
  const packets_1 = require("../arc/packets");
34
34
  const tool_friction_1 = require("./tool-friction");
35
+ const provider_models_1 = require("./provider-models");
35
36
  const _providerRuntimes = {
36
37
  human: null,
37
38
  agent: null,
@@ -405,6 +406,33 @@ const RETRY_LABELS = {
405
406
  "network-error": "network error",
406
407
  "unknown": "error",
407
408
  };
409
+ function buildAuthFailureGuidance(provider, model, agentName, detail) {
410
+ const mismatch = (0, provider_models_1.getProviderModelMismatchMessage)(provider, model);
411
+ const modelLabel = model
412
+ ? mismatch
413
+ ? `${provider} [configured model: ${model}]`
414
+ : `${provider} (${model})`
415
+ : provider;
416
+ const lines = [`${modelLabel} authentication failed.`];
417
+ const cleanDetail = detail.replace(/\s+/g, " ").trim();
418
+ if (cleanDetail)
419
+ lines.push(`provider detail: ${cleanDetail.length > 300 ? `${cleanDetail.slice(0, 297)}...` : cleanDetail}`);
420
+ lines.push("");
421
+ lines.push("To keep using this provider:");
422
+ lines.push(` 1. Run \`ouro auth --agent ${agentName} --provider ${provider}\``);
423
+ if (mismatch) {
424
+ const defaultModel = (0, provider_models_1.getDefaultModelForProvider)(provider);
425
+ lines.push("");
426
+ lines.push("Config warning:");
427
+ lines.push(` - ${mismatch}`);
428
+ lines.push(" - Repair the configured model with:");
429
+ lines.push(` \`ouro config model --agent ${agentName} --facing human ${defaultModel}\``);
430
+ lines.push(` \`ouro config model --agent ${agentName} --facing agent ${defaultModel}\``);
431
+ }
432
+ lines.push("");
433
+ lines.push(`To use another configured provider instead, run \`ouro auth switch --agent ${agentName} --provider <provider>\`.`);
434
+ return lines.join("\n");
435
+ }
408
436
  async function runAgent(messages, callbacks, channel, signal, options) {
409
437
  const facing = (0, channel_1.channelToFacing)(channel);
410
438
  const providerRuntime = getProviderRuntime(facing);
@@ -1025,9 +1053,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1025
1053
  if (terminalErrorClassification === "auth-failure") {
1026
1054
  const agentName = (0, identity_2.getAgentName)();
1027
1055
  const currentProvider = providerRuntime.id;
1028
- callbacks.onError(new Error(`${currentProvider} (${providerRuntime.model}) encountered an error. ` +
1029
- `Run \`ouro auth --agent ${agentName} --provider ${currentProvider}\` to refresh credentials, ` +
1030
- `or \`ouro auth switch --agent ${agentName} --provider <other>\` to switch providers.`), "terminal");
1056
+ callbacks.onError(new Error(buildAuthFailureGuidance(currentProvider, providerRuntime.model, agentName, terminalError.message)), "terminal");
1031
1057
  }
1032
1058
  else {
1033
1059
  callbacks.onError(terminalError, "terminal");
@@ -70,6 +70,7 @@ const launchd_1 = require("./launchd");
70
70
  const socket_client_1 = require("./socket-client");
71
71
  const session_activity_1 = require("../session-activity");
72
72
  const auth_flow_1 = require("../auth/auth-flow");
73
+ const provider_models_1 = require("../provider-models");
73
74
  const cli_parse_1 = require("./cli-parse");
74
75
  const provider_discovery_1 = require("./provider-discovery");
75
76
  // ── Default implementations ──
@@ -305,13 +306,7 @@ async function defaultRunSerpentGuide() {
305
306
  const existingBundleCount = (0, specialist_orchestrator_1.listExistingBundles)((0, identity_1.getAgentBundlesRoot)()).length;
306
307
  const hatchVerb = existingBundleCount > 0 ? "let's hatch a new agent." : "let's hatch your first agent.";
307
308
  // Default models per provider (used when entering new credentials)
308
- const defaultModels = {
309
- anthropic: "claude-opus-4-6",
310
- minimax: "MiniMax-M2.7",
311
- "openai-codex": "gpt-5.4",
312
- "github-copilot": "claude-sonnet-4.6",
313
- azure: "",
314
- };
309
+ const defaultModels = provider_models_1.DEFAULT_PROVIDER_MODELS;
315
310
  // Scan environment variables for API keys using the shared helper
316
311
  const envCreds = (0, provider_discovery_1.scanEnvVarCredentials)(process.env);
317
312
  const envDiscovered = [];
@@ -167,6 +167,8 @@ async function ensureDaemonRunning(deps) {
167
167
  daemonPid: runtimeResult.startedPid ?? null,
168
168
  /* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
169
169
  writeRaw: (text) => process.stdout.write(text),
170
+ /* v8 ignore next -- thin wrapper: real stdout TTY detection injected for captured-output safety @preserve */
171
+ isTTY: process.stdout.isTTY === true,
170
172
  /* v8 ignore next -- thin wrapper: real Date.now() injected for testability @preserve */
171
173
  now: () => Date.now(),
172
174
  /* v8 ignore next -- thin wrapper: real setTimeout injected for testability @preserve */
@@ -205,6 +207,8 @@ async function ensureDaemonRunning(deps) {
205
207
  daemonPid: lastPid,
206
208
  /* v8 ignore next -- thin wrapper: raw process.stdout.write for ANSI cursor control @preserve */
207
209
  writeRaw: (text) => process.stdout.write(text),
210
+ /* v8 ignore next -- thin wrapper: real stdout TTY detection injected for captured-output safety @preserve */
211
+ isTTY: process.stdout.isTTY === true,
208
212
  /* v8 ignore next -- thin wrapper: real Date.now() injected for testability @preserve */
209
213
  now: () => Date.now(),
210
214
  /* v8 ignore next -- thin wrapper: real setTimeout injected for testability @preserve */
@@ -1772,7 +1776,16 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
1772
1776
  (0, auth_flow_1.writeAgentProviderSelection)(command.agent, "human", command.provider, deps.bundlesRoot);
1773
1777
  (0, auth_flow_1.writeAgentProviderSelection)(command.agent, "agent", command.provider, deps.bundlesRoot);
1774
1778
  }
1775
- const message = `switched ${command.agent} to ${command.provider} (verified working)`;
1779
+ const { config: updatedConfig } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent, deps.bundlesRoot);
1780
+ const facingSummary = command.facing
1781
+ ? (() => {
1782
+ const facingConfig = command.facing === "human" ? updatedConfig.humanFacing : updatedConfig.agentFacing;
1783
+ return `${command.facing} model: ${facingConfig.model}`;
1784
+ })()
1785
+ : updatedConfig.humanFacing.model === updatedConfig.agentFacing.model
1786
+ ? `model: ${updatedConfig.humanFacing.model}`
1787
+ : `human model: ${updatedConfig.humanFacing.model}; agent model: ${updatedConfig.agentFacing.model}`;
1788
+ const message = `switched ${command.agent} to ${command.provider} (${facingSummary}; verified working)`;
1776
1789
  deps.writeStdout(message);
1777
1790
  return message;
1778
1791
  }
@@ -68,66 +68,57 @@ function assessStability(payload, now) {
68
68
  * Build an ANSI string for in-place terminal display during polling.
69
69
  * Uses cursor-up and line-clear escapes to overwrite previous output.
70
70
  */
71
- function renderStartupProgress(payload, elapsed, prevLineCount = 0) {
71
+ function renderStartupProgress(payload, elapsed, prevLineCount = 0, options = {}) {
72
+ const isTTY = options.isTTY ?? true;
72
73
  const frameIndex = Math.floor(elapsed / 100) % SPINNER_FRAMES.length;
73
74
  const spinner = SPINNER_FRAMES[frameIndex];
74
75
  const lines = [];
75
76
  const elapsedSec = (elapsed / 1000).toFixed(1);
76
- lines.push(`${spinner} ${BOLD}waiting for agents${RESET} ${DIM}(${elapsedSec}s)${RESET}`);
77
+ lines.push(isTTY
78
+ ? `${spinner} ${BOLD}waiting for agents${RESET} ${DIM}(${elapsedSec}s)${RESET}`
79
+ : `${spinner} waiting for agents (${elapsedSec}s)`);
77
80
  for (const worker of payload.workers) {
78
- const statusColor = worker.status === "running" ? GREEN
79
- : worker.status === "crashed" ? RED
80
- : YELLOW;
81
- const statusText = `${statusColor}${worker.status}${RESET}`;
81
+ const statusText = isTTY ? colorStatus(worker.status) : worker.status;
82
82
  lines.push(` ${worker.agent}/${worker.worker}: ${statusText}`);
83
83
  }
84
- let output = "";
85
- if (prevLineCount > 0) {
86
- output += `\x1b[${prevLineCount}A`;
87
- }
88
- for (const line of lines) {
89
- output += `\x1b[2K${line}\n`;
90
- }
91
- return output;
84
+ return renderStartupLines(lines, prevLineCount, isTTY);
92
85
  }
93
86
  /**
94
87
  * Render a pre-socket status line showing what the daemon is doing.
95
88
  */
96
- function renderWaitingForDaemon(elapsed, latestEvent, prevLineCount = 0) {
89
+ function renderWaitingForDaemon(elapsed, latestEvent, prevLineCount = 0, options = {}) {
90
+ const isTTY = options.isTTY ?? true;
97
91
  const elapsedSec = (elapsed / 1000).toFixed(1);
98
92
  const frameIndex = Math.floor(elapsed / 100) % SPINNER_FRAMES.length;
99
93
  const spinner = SPINNER_FRAMES[frameIndex];
100
94
  const lines = [];
101
- lines.push(`${spinner} ${BOLD}waiting for daemon${RESET} ${DIM}(${elapsedSec}s)${RESET}`);
95
+ lines.push(isTTY
96
+ ? `${spinner} ${BOLD}waiting for daemon${RESET} ${DIM}(${elapsedSec}s)${RESET}`
97
+ : `${spinner} waiting for daemon (${elapsedSec}s)`);
102
98
  if (latestEvent) {
103
- lines.push(` ${DIM}${latestEvent}${RESET}`);
104
- }
105
- let output = "";
106
- if (prevLineCount > 0) {
107
- output += `\x1b[${prevLineCount}A`;
108
- }
109
- for (const line of lines) {
110
- output += `\x1b[2K${line}\n`;
99
+ lines.push(isTTY ? ` ${DIM}${latestEvent}${RESET}` : ` ${latestEvent}`);
111
100
  }
112
- return output;
101
+ return renderStartupLines(lines, prevLineCount, isTTY);
113
102
  }
114
103
  /**
115
104
  * Render the final summary after all agents have resolved.
116
105
  */
117
- function renderFinalSummary(result) {
106
+ function renderFinalSummary(result, isTTY) {
118
107
  const lines = [];
119
108
  for (const agent of result.stable) {
120
- lines.push(` ${GREEN}\u2713${RESET} ${agent}: ${GREEN}stable${RESET}`);
109
+ lines.push(isTTY ? ` ${GREEN}\u2713${RESET} ${agent}: ${GREEN}stable${RESET}` : ` \u2713 ${agent}: stable`);
121
110
  }
122
111
  for (const d of result.degraded) {
123
- lines.push(` ${RED}\u2717${RESET} ${d.agent}: ${RED}degraded${RESET}`);
112
+ lines.push(isTTY ? ` ${RED}\u2717${RESET} ${d.agent}: ${RED}degraded${RESET}` : ` \u2717 ${d.agent}: degraded`);
124
113
  if (d.errorReason !== "unknown error") {
125
- lines.push(` ${DIM}error: ${d.errorReason}${RESET}`);
114
+ lines.push(isTTY ? ` ${DIM}error: ${d.errorReason}${RESET}` : ` error: ${d.errorReason}`);
126
115
  }
127
116
  if (d.fixHint !== "check daemon logs") {
128
- lines.push(` ${DIM}fix: ${d.fixHint}${RESET}`);
117
+ lines.push(isTTY ? ` ${DIM}fix: ${d.fixHint}${RESET}` : ` fix: ${d.fixHint}`);
129
118
  }
130
119
  }
120
+ if (!isTTY)
121
+ return lines.join("\n") + "\n";
131
122
  return lines.map((line) => `\x1b[2K${line}`).join("\n") + "\n";
132
123
  }
133
124
  // ── Polling loop ──
@@ -141,6 +132,7 @@ function renderFinalSummary(result) {
141
132
  async function pollDaemonStartup(deps) {
142
133
  const startTime = deps.now();
143
134
  let prevLineCount = 0;
135
+ const isTTY = deps.isTTY ?? true;
144
136
  const isAlive = deps.isProcessAlive ?? defaultIsProcessAlive;
145
137
  (0, runtime_1.emitNervesEvent)({
146
138
  component: "daemon",
@@ -169,7 +161,7 @@ async function pollDaemonStartup(deps) {
169
161
  meta: { pid: deps.daemonPid, lastEvent: latestEvent },
170
162
  });
171
163
  // Clear the waiting line
172
- if (prevLineCount > 0) {
164
+ if (isTTY && prevLineCount > 0) {
173
165
  let clear = `\x1b[${prevLineCount}A`;
174
166
  for (let i = 0; i < prevLineCount; i++)
175
167
  clear += `\x1b[2K\n`;
@@ -182,12 +174,12 @@ async function pollDaemonStartup(deps) {
182
174
  }
183
175
  // Show what the daemon is doing from its log
184
176
  const latestEvent = deps.readLatestDaemonEvent?.() ?? null;
185
- const output = renderWaitingForDaemon(elapsed, latestEvent, prevLineCount);
177
+ const output = renderWaitingForDaemon(elapsed, latestEvent, prevLineCount, { isTTY });
186
178
  deps.writeRaw(output);
187
179
  prevLineCount = latestEvent ? 2 : 1;
188
180
  }
189
181
  if (payload) {
190
- const output = renderStartupProgress(payload, elapsed, prevLineCount);
182
+ const output = renderStartupProgress(payload, elapsed, prevLineCount, { isTTY });
191
183
  deps.writeRaw(output);
192
184
  prevLineCount = payload.workers.length + 1;
193
185
  const assessment = assessStability(payload, now);
@@ -196,7 +188,7 @@ async function pollDaemonStartup(deps) {
196
188
  stable: assessment.stable,
197
189
  degraded: assessment.degraded,
198
190
  };
199
- const summary = renderFinalSummary(result);
191
+ const summary = renderFinalSummary(result, isTTY);
200
192
  deps.writeRaw(summary);
201
193
  (0, runtime_1.emitNervesEvent)({
202
194
  component: "daemon",
@@ -214,6 +206,24 @@ async function pollDaemonStartup(deps) {
214
206
  await deps.sleep(POLL_INTERVAL_MS);
215
207
  }
216
208
  }
209
+ function colorStatus(status) {
210
+ const statusColor = status === "running" ? GREEN
211
+ : status === "crashed" ? RED
212
+ : YELLOW;
213
+ return `${statusColor}${status}${RESET}`;
214
+ }
215
+ function renderStartupLines(lines, prevLineCount, isTTY) {
216
+ if (!isTTY)
217
+ return lines.join("\n") + "\n";
218
+ let output = "";
219
+ if (prevLineCount > 0) {
220
+ output += `\x1b[${prevLineCount}A`;
221
+ }
222
+ for (const line of lines) {
223
+ output += `\x1b[2K${line}\n`;
224
+ }
225
+ return output;
226
+ }
217
227
  /* v8 ignore start -- process liveness check: uses real process.kill(0), tested via deployment @preserve */
218
228
  function defaultIsProcessAlive(pid) {
219
229
  try {
@@ -42,6 +42,7 @@ const identity_1 = require("../identity");
42
42
  const config_1 = require("../config");
43
43
  const runtime_1 = require("../../nerves/runtime");
44
44
  const auth_flow_1 = require("../auth/auth-flow");
45
+ const provider_models_1 = require("../provider-models");
45
46
  const habit_parser_1 = require("../habits/habit-parser");
46
47
  const hatch_specialist_1 = require("./hatch-specialist");
47
48
  function requiredCredentialKeys(provider) {
@@ -124,9 +125,10 @@ function writeDiaryScaffold(bundleRoot) {
124
125
  }
125
126
  function writeHatchlingAgentConfig(bundleRoot, input) {
126
127
  const template = (0, identity_1.buildDefaultAgentTemplate)(input.agentName);
128
+ const model = (0, provider_models_1.getDefaultModelForProvider)(input.provider);
127
129
  template.provider = input.provider;
128
- template.humanFacing = { provider: input.provider, model: template.humanFacing.model };
129
- template.agentFacing = { provider: input.provider, model: template.agentFacing.model };
130
+ template.humanFacing = { provider: input.provider, model };
131
+ template.agentFacing = { provider: input.provider, model };
130
132
  template.enabled = true;
131
133
  fs.writeFileSync(path.join(bundleRoot, "agent.json"), `${JSON.stringify(template, null, 2)}\n`, "utf-8");
132
134
  }
@@ -2,15 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildFailoverContext = buildFailoverContext;
4
4
  exports.handleFailoverReply = handleFailoverReply;
5
+ const provider_models_1 = require("./provider-models");
5
6
  const runtime_1 = require("../nerves/runtime");
6
- const FAILING_PROVIDER_LABELS = {
7
- "auth-failure": "its credentials need to be refreshed",
8
- "usage-limit": "has also hit its usage limit",
9
- "rate-limit": "is also being rate limited",
10
- "server-error": "is also experiencing an outage",
11
- "network-error": "could not be reached",
12
- "unknown": "could not be reached",
13
- };
14
7
  const CLASSIFICATION_LABELS = {
15
8
  "auth-failure": "authentication failed",
16
9
  "usage-limit": "hit its usage limit",
@@ -19,10 +12,43 @@ const CLASSIFICATION_LABELS = {
19
12
  "network-error": "is unreachable (network error)",
20
13
  "unknown": "encountered an error",
21
14
  };
22
- function buildFailoverContext(_errorMessage, classification, currentProvider, currentModel, agentName, inventory, providerModels) {
15
+ function formatProviderWithModel(provider, model) {
16
+ if (!model)
17
+ return provider;
18
+ if ((0, provider_models_1.getProviderModelMismatchMessage)(provider, model)) {
19
+ return `${provider} [configured model: ${model}]`;
20
+ }
21
+ return `${provider} (${model})`;
22
+ }
23
+ function formatErrorDetail(errorMessage, errorSummary) {
24
+ const detail = errorMessage.replace(/\s+/g, " ").trim();
25
+ if (!detail || detail === errorSummary)
26
+ return "";
27
+ return detail.length > 300 ? `${detail.slice(0, 297)}...` : detail;
28
+ }
29
+ function formatFailingProviderLine(provider, classification, agentName) {
30
+ const authCommand = `ouro auth --agent ${agentName} --provider ${provider}`;
31
+ switch (classification) {
32
+ case "auth-failure":
33
+ return ` - ${provider}: credentials need to be refreshed. Run \`${authCommand}\`.`;
34
+ case "network-error":
35
+ return ` - ${provider}: could not be reached. Check network/provider availability; if credentials may be stale, run \`${authCommand}\`.`;
36
+ case "server-error":
37
+ return ` - ${provider}: provider outage or server error. Retry later; if it keeps failing, run \`${authCommand}\`.`;
38
+ case "rate-limit":
39
+ return ` - ${provider}: rate limited. Wait and retry, or switch to a ready provider below.`;
40
+ case "usage-limit":
41
+ return ` - ${provider}: usage limit hit. Wait for quota reset, raise quota, or switch to a ready provider below.`;
42
+ case "unknown":
43
+ return ` - ${provider}: could not be reached. Run \`${authCommand}\` if credentials may be stale.`;
44
+ }
45
+ }
46
+ function buildFailoverContext(errorMessage, classification, currentProvider, currentModel, agentName, inventory, providerModels) {
23
47
  const label = CLASSIFICATION_LABELS[classification];
24
- const providerWithModel = currentModel ? `${currentProvider} (${currentModel})` : currentProvider;
48
+ const providerWithModel = formatProviderWithModel(currentProvider, currentModel);
25
49
  const errorSummary = `${providerWithModel} ${label}`;
50
+ const errorDetail = formatErrorDetail(errorMessage, errorSummary);
51
+ const modelMismatch = (0, provider_models_1.getProviderModelMismatchMessage)(currentProvider, currentModel);
26
52
  const workingProviders = [];
27
53
  const unconfiguredProviders = [];
28
54
  const failingProviders = [];
@@ -39,27 +65,48 @@ function buildFailoverContext(_errorMessage, classification, currentProvider, cu
39
65
  }
40
66
  }
41
67
  const lines = [`${errorSummary}.`];
68
+ if (errorDetail) {
69
+ lines.push(`provider detail: ${errorDetail}`);
70
+ }
71
+ if (classification === "auth-failure") {
72
+ lines.push("");
73
+ lines.push("To keep using the current provider:");
74
+ lines.push(` 1. Run \`ouro auth --agent ${agentName} --provider ${currentProvider}\``);
75
+ }
76
+ if (modelMismatch) {
77
+ const defaultModel = (0, provider_models_1.getDefaultModelForProvider)(currentProvider);
78
+ lines.push("");
79
+ lines.push("Config warning:");
80
+ lines.push(` - ${modelMismatch}`);
81
+ lines.push(" - Repair the configured model with:");
82
+ lines.push(` \`ouro config model --agent ${agentName} --facing human ${defaultModel}\``);
83
+ lines.push(` \`ouro config model --agent ${agentName} --facing agent ${defaultModel}\``);
84
+ }
42
85
  if (workingProviders.length > 0) {
43
- const switchDescriptions = workingProviders.map((p) => {
44
- const model = providerModels[p];
45
- return model ? `${p} (${model})` : /* v8 ignore next -- defensive: model always present in secrets @preserve */ p;
46
- });
47
- const switchOptions = workingProviders.map((p) => `"switch to ${p}"`).join(" or ");
48
- lines.push(`these providers are ready to go: ${switchDescriptions.join(", ")}.`);
49
- lines.push(`reply ${switchOptions} to continue.`);
86
+ lines.push("");
87
+ lines.push("Ready providers:");
88
+ for (const provider of workingProviders) {
89
+ const model = (0, provider_models_1.resolveModelForProviderDisplay)(provider, providerModels[provider]);
90
+ lines.push(` - ${provider} (${model}): reply "switch to ${provider}"`);
91
+ }
50
92
  }
51
93
  if (failingProviders.length > 0) {
94
+ lines.push("");
95
+ lines.push("Configured but unavailable:");
52
96
  for (const { provider, classification } of failingProviders) {
53
- /* v8 ignore next -- defensive: all classifications have labels @preserve */
54
- const detail = FAILING_PROVIDER_LABELS[classification] ?? "could not be reached";
55
- lines.push(`${provider} is configured but ${detail}. run \`ouro auth --agent ${agentName} --provider ${provider}\` to refresh.`);
97
+ lines.push(formatFailingProviderLine(provider, classification, agentName));
56
98
  }
57
99
  }
58
100
  if (unconfiguredProviders.length > 0) {
59
- lines.push(`to set up ${unconfiguredProviders.join(", ")}, run \`ouro auth --agent ${agentName}\` in terminal.`);
101
+ lines.push("");
102
+ lines.push("Not configured:");
103
+ for (const provider of unconfiguredProviders) {
104
+ lines.push(` - ${provider}: run \`ouro auth --agent ${agentName} --provider ${provider}\``);
105
+ }
60
106
  }
61
107
  if (workingProviders.length === 0 && unconfiguredProviders.length === 0 && failingProviders.length === 0) {
62
- lines.push(`no other providers are available. run \`ouro auth --agent ${agentName}\` in terminal to configure one.`);
108
+ lines.push("");
109
+ lines.push(`No other providers are available. Run \`ouro auth --agent ${agentName}\` in terminal to configure one.`);
63
110
  }
64
111
  (0, runtime_1.emitNervesEvent)({
65
112
  component: "engine",
@@ -74,7 +121,7 @@ function buildFailoverContext(_errorMessage, classification, currentProvider, cu
74
121
  agentName,
75
122
  workingProviders,
76
123
  unconfiguredProviders,
77
- userMessage: lines.join(" "),
124
+ userMessage: lines.join("\n"),
78
125
  };
79
126
  }
80
127
  function handleFailoverReply(reply, context) {
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_PROVIDER_MODELS = void 0;
4
+ exports.getProviderDisplayName = getProviderDisplayName;
5
+ exports.getDefaultModelForProvider = getDefaultModelForProvider;
6
+ exports.isModelClearlyIncompatibleWithProvider = isModelClearlyIncompatibleWithProvider;
7
+ exports.resolveModelForProviderSelection = resolveModelForProviderSelection;
8
+ exports.resolveModelForProviderDisplay = resolveModelForProviderDisplay;
9
+ exports.getProviderModelMismatchMessage = getProviderModelMismatchMessage;
10
+ const runtime_1 = require("../nerves/runtime");
11
+ exports.DEFAULT_PROVIDER_MODELS = {
12
+ anthropic: "claude-opus-4-6",
13
+ azure: "gpt-4o-mini",
14
+ minimax: "MiniMax-M2.7",
15
+ "openai-codex": "gpt-5.4",
16
+ "github-copilot": "claude-sonnet-4.6",
17
+ };
18
+ const PROVIDER_NAMES = {
19
+ anthropic: "Anthropic",
20
+ azure: "Azure OpenAI",
21
+ minimax: "MiniMax",
22
+ "openai-codex": "OpenAI Codex",
23
+ "github-copilot": "GitHub Copilot",
24
+ };
25
+ function normalized(model) {
26
+ return model.trim().toLowerCase();
27
+ }
28
+ function getProviderDisplayName(provider) {
29
+ return PROVIDER_NAMES[provider];
30
+ }
31
+ function getDefaultModelForProvider(provider) {
32
+ return exports.DEFAULT_PROVIDER_MODELS[provider];
33
+ }
34
+ function isModelClearlyIncompatibleWithProvider(provider, model) {
35
+ const value = normalized(model);
36
+ if (!value)
37
+ return true;
38
+ switch (provider) {
39
+ case "anthropic":
40
+ return !value.startsWith("claude-");
41
+ case "minimax":
42
+ return !value.startsWith("minimax");
43
+ case "openai-codex":
44
+ return value.startsWith("claude-") || value.startsWith("minimax");
45
+ case "azure":
46
+ return value.startsWith("claude-") || value.startsWith("minimax");
47
+ case "github-copilot":
48
+ return false;
49
+ }
50
+ }
51
+ function resolveModelForProviderSelection(provider, currentModel) {
52
+ const trimmed = currentModel.trim();
53
+ if (trimmed && !isModelClearlyIncompatibleWithProvider(provider, trimmed)) {
54
+ return { model: trimmed, preserved: true };
55
+ }
56
+ const model = getDefaultModelForProvider(provider);
57
+ (0, runtime_1.emitNervesEvent)({
58
+ component: "config/identity",
59
+ event: "config_identity.provider_model_defaulted",
60
+ message: "defaulted provider model during provider selection",
61
+ meta: { provider, previousModel: currentModel, model },
62
+ });
63
+ return { model, preserved: false };
64
+ }
65
+ function resolveModelForProviderDisplay(provider, modelHint) {
66
+ const hint = modelHint?.trim() ?? "";
67
+ if (hint && !isModelClearlyIncompatibleWithProvider(provider, hint))
68
+ return hint;
69
+ return getDefaultModelForProvider(provider);
70
+ }
71
+ function getProviderModelMismatchMessage(provider, model) {
72
+ const trimmed = model.trim();
73
+ if (!isModelClearlyIncompatibleWithProvider(provider, trimmed))
74
+ return null;
75
+ const providerName = getProviderDisplayName(provider);
76
+ const defaultModel = getDefaultModelForProvider(provider);
77
+ if (!trimmed) {
78
+ return `${providerName} has no model set. Suggested model: ${defaultModel}.`;
79
+ }
80
+ return `${providerName} is currently paired with ${trimmed}, which does not look like a model for ${providerName}. Suggested model: ${defaultModel}.`;
81
+ }
@@ -54,6 +54,7 @@ const obligations_1 = require("../arc/obligations");
54
54
  const provider_failover_1 = require("../heart/provider-failover");
55
55
  const provider_ping_1 = require("../heart/provider-ping");
56
56
  const auth_flow_1 = require("../heart/auth/auth-flow");
57
+ const provider_models_1 = require("../heart/provider-models");
57
58
  const tempo_1 = require("../heart/tempo");
58
59
  const temporal_view_1 = require("../heart/temporal-view");
59
60
  const start_of_turn_packet_1 = require("../heart/start-of-turn-packet");
@@ -162,14 +163,15 @@ async function handleInboundTurn(input) {
162
163
  try {
163
164
  const { secrets } = (0, auth_flow_1.loadAgentSecrets)(failoverAgentName);
164
165
  const cfg = secrets.providers[failoverAction.provider];
165
- return cfg?.model ?? cfg?.modelName ?? "";
166
+ const hint = cfg?.model ?? cfg?.modelName;
167
+ return (0, provider_models_1.resolveModelForProviderDisplay)(failoverAction.provider, typeof hint === "string" ? hint : "");
166
168
  /* v8 ignore next 2 -- defensive: secrets read failure @preserve */
167
169
  }
168
170
  catch {
169
- return "";
171
+ return (0, provider_models_1.resolveModelForProviderDisplay)(failoverAction.provider);
170
172
  }
171
173
  })();
172
- const newProviderLabel = newProviderSecrets ? `${failoverAction.provider} (${newProviderSecrets})` : failoverAction.provider;
174
+ const newProviderLabel = `${failoverAction.provider} (${newProviderSecrets})`;
173
175
  input.messages = [{
174
176
  role: "user",
175
177
  content: `[provider switch: ${pendingContext.errorSummary}. switched to ${newProviderLabel}. your conversation history is intact — respond to the user's last message.]`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.326",
3
+ "version": "0.1.0-alpha.328",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",
@@ -31,6 +31,7 @@
31
31
  "test:coverage": "node scripts/run-coverage-gate.cjs",
32
32
  "build": "tsc && (cd packages/outlook-ui && npm install --ignore-scripts 2>/dev/null && npm run build && cp -r dist ../../dist/outlook-ui) || echo 'outlook-ui build skipped'",
33
33
  "lint": "eslint src/",
34
+ "release:smoke": "node scripts/release-smoke.cjs",
34
35
  "audit:nerves": "npm run build && node dist/nerves/coverage/cli-main.js"
35
36
  },
36
37
  "dependencies": {