create-quiver 0.15.0 → 0.15.1

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 (58) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +31 -0
  3. package/README_FOR_AI.md +8 -3
  4. package/ROADMAP.md +5 -0
  5. package/docs/AI_CONTEXT.md.template +4 -0
  6. package/docs/AI_ONBOARDING_PROMPT.md.template +8 -0
  7. package/docs/CLI_UX_GUIDE.md +16 -1
  8. package/docs/COMMANDS.md.template +14 -4
  9. package/docs/TROUBLESHOOTING.md +70 -0
  10. package/docs/TROUBLESHOOTING.md.template +20 -0
  11. package/docs/getting-started/installation.md +86 -0
  12. package/docs/reference/commands.md +29 -1
  13. package/package.json +1 -1
  14. package/specs/quiver-v31-ai-model-catalog-agent-selection/EVIDENCE_REPORT.md +185 -0
  15. package/specs/quiver-v31-ai-model-catalog-agent-selection/EXECUTION_PLAN.md +85 -0
  16. package/specs/quiver-v31-ai-model-catalog-agent-selection/SPEC.md +337 -0
  17. package/specs/quiver-v31-ai-model-catalog-agent-selection/STATUS.md +30 -0
  18. package/specs/quiver-v31-ai-model-catalog-agent-selection/pr.md +98 -0
  19. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +30 -0
  20. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +51 -0
  21. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/slice.json +62 -0
  22. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/CLOSURE_BRIEF.md +32 -0
  23. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/EXECUTION_BRIEF.md +53 -0
  24. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/slice.json +72 -0
  25. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/CLOSURE_BRIEF.md +36 -0
  26. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/EXECUTION_BRIEF.md +56 -0
  27. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/slice.json +78 -0
  28. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/CLOSURE_BRIEF.md +36 -0
  29. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/EXECUTION_BRIEF.md +57 -0
  30. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/slice.json +77 -0
  31. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/CLOSURE_BRIEF.md +35 -0
  32. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/EXECUTION_BRIEF.md +55 -0
  33. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/slice.json +84 -0
  34. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/CLOSURE_BRIEF.md +32 -0
  35. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/EXECUTION_BRIEF.md +52 -0
  36. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/slice.json +72 -0
  37. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/CLOSURE_BRIEF.md +33 -0
  38. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/EXECUTION_BRIEF.md +58 -0
  39. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/slice.json +84 -0
  40. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/CLOSURE_BRIEF.md +32 -0
  41. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/EXECUTION_BRIEF.md +59 -0
  42. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/slice.json +94 -0
  43. package/specs/quiver-v32-npx-installation-guidance/EVIDENCE_REPORT.md +26 -0
  44. package/specs/quiver-v32-npx-installation-guidance/SPEC.md +55 -0
  45. package/specs/quiver-v32-npx-installation-guidance/STATUS.md +23 -0
  46. package/specs/quiver-v32-npx-installation-guidance/pr.md +72 -0
  47. package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/CLOSURE_BRIEF.md +31 -0
  48. package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/EXECUTION_BRIEF.md +56 -0
  49. package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/slice.json +75 -0
  50. package/src/create-quiver/commands/ai.js +449 -34
  51. package/src/create-quiver/commands/flow.js +2 -2
  52. package/src/create-quiver/index.js +26 -10
  53. package/src/create-quiver/lib/agent-profiles.js +332 -4
  54. package/src/create-quiver/lib/ai/execution-plan.js +7 -2
  55. package/src/create-quiver/lib/ai/executor.js +9 -2
  56. package/src/create-quiver/lib/ai/model-catalog.js +333 -0
  57. package/src/create-quiver/lib/ai/providers.js +143 -4
  58. package/src/create-quiver/lib/cli/selectors.js +53 -0
@@ -22,6 +22,7 @@ const {
22
22
  runLifecycleResume: runAiLifecycleResume,
23
23
  runLifecycleRun: runAiLifecycleRun,
24
24
  runLifecycleStatus: runAiLifecycleStatus,
25
+ runModelsList: runAiModelsList,
25
26
  runOnboard,
26
27
  runPlan: runAiPlan,
27
28
  runPrepareContext: runAiPrepareContext,
@@ -172,7 +173,8 @@ const COMMAND_HELP_GROUPS = [
172
173
  ['ai resume', 'Resume guidance from the last valid lifecycle phase without chat memory.'],
173
174
  ['ai onboard', 'Run or print the planner onboarding prompt with a token-aware context pack.'],
174
175
  ['ai prepare-context', 'Preview or write docs-only AI context updates with assumptions and risks.'],
175
- ['ai agent set|list|show', 'Manage planner, executor, reviewer, and doctor provider profiles without secrets; use set --dry-run to preview.'],
176
+ ['ai agent set|list|show|doctor|repair', 'Manage, diagnose, and dry-run repair planner, executor, reviewer, and doctor provider profiles without secrets.'],
177
+ ['ai models list', 'List provider/model ids known by Quiver without claiming account availability.'],
176
178
  ['ai plan', 'Generate versioned planner drafts for acceptance criteria, technical plan, or spec phase.'],
177
179
  ['ai revise', 'Create a new planner draft from human feedback without approving it.'],
178
180
  ['ai repair-plan', 'Repair an approved technical plan into a new structured draft without mutating the approved artifact.'],
@@ -261,7 +263,8 @@ function printUsage() {
261
263
  npx create-quiver ai specs list [--json]
262
264
  npx create-quiver ai slices list [--json]
263
265
  npx create-quiver ai trace report [options]
264
- npx create-quiver ai agent <set|list|show> [role] [options]
266
+ npx create-quiver ai agent <set|list|show|doctor|repair> [role] [options]
267
+ npx create-quiver ai models list [--provider codex|claude|gemini] [--json]
265
268
  npx create-quiver ai prepare-context [options]
266
269
  npx create-quiver ai revise [options]
267
270
  npx create-quiver ai repair-plan [options]
@@ -321,7 +324,7 @@ Options:
321
324
  --allow-dirty For ai execute-slice, allow pre-existing dirty files and ignore them for scope diff
322
325
  --mode <name> Execution mode for ai execute-plan (auto, manual, delegated)
323
326
  --provider <name> Provider CLI to preflight for prepare or AI commands
324
- --model <label> Free-form model label for AI agent profiles
327
+ --model <model-id> Technical model id for AI agent profiles or provider-backed AI commands
325
328
  --version <n> Draft version to approve for AI planner phases
326
329
  --run <id> AI lifecycle run id
327
330
  --ssh-host-alias <name> SSH host alias to validate for prepare or AI commands
@@ -359,7 +362,10 @@ Examples:
359
362
  cd ./my-project && npx create-quiver ai trace report
360
363
  cd ./my-project && npx create-quiver ai agent set planner --provider codex --model gpt-5.5 --dry-run
361
364
  cd ./my-project && npx create-quiver ai agent set planner --provider codex --model gpt-5.5
365
+ cd ./my-project && npx create-quiver ai agent doctor
366
+ cd ./my-project && npx create-quiver ai agent repair --dry-run
362
367
  cd ./my-project && npx create-quiver ai agent list
368
+ cd ./my-project && npx create-quiver ai models list --provider codex
363
369
  cd ./my-project && npx create-quiver ai plan --phase acceptance --input requirements.md --dry-run
364
370
  cd ./my-project && npx create-quiver ai revise --phase acceptance --input feedback.md --dry-run
365
371
  cd ./my-project && npx create-quiver ai approve --phase acceptance --version 1
@@ -1050,10 +1056,10 @@ function parseArgs(argv) {
1050
1056
  if (result.aiCommand === 'run' && !result.aiRunCommand && positional.length > 0) {
1051
1057
  result.aiRunCommand = positional.shift();
1052
1058
  }
1053
- if ((result.aiCommand === 'specs' || result.aiCommand === 'slices' || result.aiCommand === 'trace' || result.aiCommand === 'active-slice') && !result.aiSecondaryCommand && positional.length > 0) {
1059
+ if ((result.aiCommand === 'specs' || result.aiCommand === 'slices' || result.aiCommand === 'models' || result.aiCommand === 'trace' || result.aiCommand === 'active-slice') && !result.aiSecondaryCommand && positional.length > 0) {
1054
1060
  result.aiSecondaryCommand = positional.shift();
1055
1061
  }
1056
- if ((result.aiCommand === 'specs' || result.aiCommand === 'slices') && result.aiSecondaryCommand && result.aiSecondaryCommand !== 'list') {
1062
+ if ((result.aiCommand === 'specs' || result.aiCommand === 'slices' || result.aiCommand === 'models') && result.aiSecondaryCommand && result.aiSecondaryCommand !== 'list') {
1057
1063
  throw new Error(formatError(`unsupported ai ${result.aiCommand} subcommand: ${result.aiSecondaryCommand}. Supported tasks: list`));
1058
1064
  }
1059
1065
  if (result.aiCommand === 'trace' && result.aiSecondaryCommand && result.aiSecondaryCommand !== 'report') {
@@ -2704,8 +2710,8 @@ async function resolveInteractiveInitOptions(args, targetDir, projectName, optio
2704
2710
 
2705
2711
  if (agentGuidance.value === 'show') {
2706
2712
  ux.nextSteps([
2707
- 'npx create-quiver ai agent set planner --provider codex --model "<modelo-planner>" --dry-run',
2708
- 'npx create-quiver ai agent set executor --provider claude --model "<modelo-executor>" --dry-run',
2713
+ 'npx create-quiver ai agent set planner --provider codex --model gpt-5.5 --dry-run',
2714
+ 'npx create-quiver ai agent set executor --provider claude --model claude-sonnet-4-6 --dry-run',
2709
2715
  ], { title: 'Despues de inicializar' });
2710
2716
  }
2711
2717
 
@@ -2836,7 +2842,7 @@ async function run(argv) {
2836
2842
 
2837
2843
  if (args.mode === 'ai') {
2838
2844
  if (!args.aiCommand) {
2839
- throw new Error(formatError('missing ai subcommand. Use: npx create-quiver ai onboard | prepare-context | run | active-slice | status | resume | inspect | export | specs | slices | trace | plan | revise | repair-plan | review-plan | approve | approvals | agent | prompt-slice | execute-slice | execute-plan | doctor | pr'));
2845
+ throw new Error(formatError('missing ai subcommand. Use: npx create-quiver ai onboard | prepare-context | run | active-slice | status | resume | inspect | export | specs | slices | models | trace | plan | revise | repair-plan | review-plan | approve | approvals | agent | prompt-slice | execute-slice | execute-plan | doctor | pr'));
2840
2846
  }
2841
2847
 
2842
2848
  if (args.aiCommand === 'run') {
@@ -2909,13 +2915,23 @@ async function run(argv) {
2909
2915
  return;
2910
2916
  }
2911
2917
 
2918
+ if (args.aiCommand === 'models') {
2919
+ runAiModelsList({
2920
+ json: args.json,
2921
+ provider: args.aiProviderExplicit ? args.aiProvider : undefined,
2922
+ });
2923
+ return;
2924
+ }
2925
+
2912
2926
  if (args.aiCommand === 'agent') {
2913
- runAiAgent(process.cwd(), {
2927
+ await runAiAgent(process.cwd(), {
2914
2928
  command: args.aiAgentCommand,
2915
2929
  context: args.aiContext || undefined,
2916
2930
  defaultProfile: args.aiDefaultProfile,
2917
2931
  displayName: args.aiDisplayName || undefined,
2918
2932
  id: args.aiProfileId || undefined,
2933
+ interactive: args.interactive,
2934
+ json: args.json,
2919
2935
  label: args.aiLabel || undefined,
2920
2936
  model: args.aiModel || undefined,
2921
2937
  provider: args.aiProviderExplicit ? args.aiProvider : undefined,
@@ -3120,7 +3136,7 @@ async function run(argv) {
3120
3136
  return;
3121
3137
  }
3122
3138
 
3123
- throw new Error(formatError(`unsupported ai subcommand: ${args.aiCommand}. Supported tasks: onboard, prepare-context, run, active-slice, status, resume, inspect, export, specs, slices, trace, plan, revise, repair-plan, review-plan, approve, approvals, agent, prompt-slice, execute-slice, execute-plan, doctor, pr`));
3139
+ throw new Error(formatError(`unsupported ai subcommand: ${args.aiCommand}. Supported tasks: onboard, prepare-context, run, active-slice, status, resume, inspect, export, specs, slices, models, trace, plan, revise, repair-plan, review-plan, approve, approvals, agent, prompt-slice, execute-slice, execute-plan, doctor, pr`));
3124
3140
  }
3125
3141
 
3126
3142
  if (args.mode === 'graph') {
@@ -2,6 +2,8 @@ const fs = require('node:fs');
2
2
  const path = require('node:path');
3
3
 
4
4
  const { assertSupportedProvider, formatProviderList } = require('./ai/providers');
5
+ const { normalizeModelSelection } = require('./ai/model-catalog');
6
+ const { preflightProvider } = require('./ai/preflight');
5
7
  const { quiverInternalPaths } = require('./init-layout');
6
8
 
7
9
  const AGENT_PROFILE_ROLES = Object.freeze(['planner', 'executor', 'reviewer', 'doctor']);
@@ -87,6 +89,11 @@ function normalizeStoredProfile(profile, role, fallbackId = 'default') {
87
89
  label: normalizeOptionalText(profile?.label, 'label'),
88
90
  displayName: normalizeOptionalText(profile?.displayName || profile?.display_name, 'displayName'),
89
91
  context: normalizeOptionalText(profile?.context, 'context'),
92
+ modelSource: normalizeOptionalText(profile?.modelSource || profile?.model_source, 'modelSource'),
93
+ modelAlias: normalizeOptionalText(profile?.modelAlias || profile?.model_alias, 'modelAlias'),
94
+ validation_status: normalizeOptionalText(profile?.validation_status || profile?.validationStatus, 'validation_status'),
95
+ validated_at: normalizeOptionalText(profile?.validated_at || profile?.validatedAt, 'validated_at'),
96
+ validation_error: normalizeOptionalText(profile?.validation_error || profile?.validationError, 'validation_error'),
90
97
  default: profile?.default === true,
91
98
  updated_at: profile?.updated_at || '',
92
99
  };
@@ -181,25 +188,44 @@ function setAgentProfile(projectRoot, role, options = {}) {
181
188
  function buildAgentProfileState(projectRoot, role, options = {}) {
182
189
  const normalizedRole = normalizeAgentProfileRole(role);
183
190
  const provider = assertSupportedProvider(options.provider);
184
- const model = normalizeOptionalText(options.model, 'model');
191
+ const rawModel = normalizeOptionalText(options.model, 'model');
185
192
  const label = normalizeOptionalText(options.label, 'label');
186
- const displayName = normalizeOptionalText(options.displayName || options.display_name, 'displayName');
193
+ const rawDisplayName = normalizeOptionalText(options.displayName || options.display_name, 'displayName');
187
194
  const context = normalizeOptionalText(options.context, 'context');
188
195
  const state = readAgentProfiles(projectRoot);
189
196
  const currentProfiles = getAgentProfilesForRole(projectRoot, normalizedRole);
190
197
  const currentDefault = currentProfiles.find((profile) => profile.default) || currentProfiles[0] || {};
191
- const id = normalizeProfileId(options.id || currentDefault.id || label || model || provider);
198
+ const modelSelection = rawModel
199
+ ? normalizeModelSelection(provider, rawModel, { displayName: rawDisplayName })
200
+ : null;
201
+ const id = normalizeProfileId(options.id || currentDefault.id || label || modelSelection?.model || rawModel || provider);
192
202
  const current = currentProfiles.find((profile) => profile.id === id) || {};
203
+ const model = modelSelection?.model || current.model || '';
204
+ const displayName = rawDisplayName
205
+ || modelSelection?.displayName
206
+ || current.displayName
207
+ || model
208
+ || label
209
+ || provider;
193
210
  const now = options.now instanceof Date ? options.now.toISOString() : new Date().toISOString();
194
211
  const shouldBeDefault = options.default === true || currentProfiles.length === 0 || (options.default !== false && current.default === true);
212
+ const modelSource = modelSelection?.modelSource || current.modelSource || (model ? 'custom' : '');
213
+ const modelAlias = modelSelection && modelSelection.input && modelSelection.input !== model
214
+ ? modelSelection.input
215
+ : (current.modelAlias || '');
195
216
  const profile = {
196
217
  id,
197
218
  role: normalizedRole,
198
219
  provider,
199
220
  model: model || current.model || '',
200
221
  label: label || current.label || '',
201
- displayName: displayName || current.displayName || model || label || provider,
222
+ displayName,
202
223
  context: context || current.context || '',
224
+ modelSource,
225
+ modelAlias,
226
+ validation_status: current.validation_status || (model ? 'not-tested' : ''),
227
+ validated_at: current.validated_at || '',
228
+ validation_error: current.validation_error || '',
203
229
  default: shouldBeDefault,
204
230
  updated_at: now,
205
231
  };
@@ -243,10 +269,312 @@ function resolveProfileProvider(projectRoot, role, fallbackProvider) {
243
269
  return assertSupportedProvider(fallbackProvider);
244
270
  }
245
271
 
272
+ function cloneJson(value) {
273
+ return JSON.parse(JSON.stringify(value));
274
+ }
275
+
276
+ function rawText(value) {
277
+ return String(value || '').trim();
278
+ }
279
+
280
+ function rawDisplayName(profile, role) {
281
+ return rawText(profile.displayName || profile.display_name || profile.model || profile.provider || role);
282
+ }
283
+
284
+ function collectRawAgentProfileEntries(state) {
285
+ const entries = [];
286
+ for (const role of AGENT_PROFILE_ROLES) {
287
+ const setKey = profileSetKey(role);
288
+ const setProfiles = Array.isArray(state.profile_sets?.[setKey]) ? state.profile_sets[setKey] : [];
289
+ if (setProfiles.length > 0) {
290
+ setProfiles.forEach((profile, index) => {
291
+ entries.push({
292
+ role,
293
+ profile,
294
+ id: normalizeProfileId(profile?.id || profile?.label, index === 0 ? 'default' : `${role}-${index + 1}`),
295
+ source: 'profile_sets',
296
+ setKey,
297
+ index,
298
+ });
299
+ });
300
+ continue;
301
+ }
302
+
303
+ if (state.profiles?.[role]) {
304
+ entries.push({
305
+ role,
306
+ profile: state.profiles[role],
307
+ id: normalizeProfileId(state.profiles[role]?.id || state.profiles[role]?.label, 'default'),
308
+ source: 'profiles',
309
+ index: null,
310
+ });
311
+ }
312
+ }
313
+ return entries;
314
+ }
315
+
316
+ function createAgentProfileFinding(severity, code, message, entry, extra = {}) {
317
+ return {
318
+ severity,
319
+ code,
320
+ message,
321
+ role: entry.role,
322
+ profileId: entry.id,
323
+ fix: extra.fix || '',
324
+ before: extra.before,
325
+ after: extra.after,
326
+ };
327
+ }
328
+
329
+ function classifyAgentProfileEntry(projectRoot, entry, options = {}) {
330
+ const findings = [];
331
+ const profile = entry.profile || {};
332
+ const providerInput = rawText(profile.provider).toLowerCase();
333
+ const modelInput = rawText(profile.model);
334
+ const displayName = rawDisplayName(profile, entry.role);
335
+ let provider = providerInput;
336
+ let status = 'ok';
337
+
338
+ if (!providerInput) {
339
+ findings.push(createAgentProfileFinding(
340
+ 'error',
341
+ 'missing-provider',
342
+ `Profile ${entry.role}/${entry.id} is missing provider.`,
343
+ entry,
344
+ { fix: `npx create-quiver ai agent set ${entry.role} --provider <provider> --model <model>` },
345
+ ));
346
+ } else {
347
+ try {
348
+ provider = assertSupportedProvider(providerInput);
349
+ const runPreflight = options.checkProviderCli !== false;
350
+ if (runPreflight) {
351
+ const preflight = options.preflightProvider || preflightProvider;
352
+ try {
353
+ preflight(provider, {
354
+ cwd: projectRoot,
355
+ probe: options.providerProbe,
356
+ probeArgs: options.providerProbeArgs,
357
+ });
358
+ } catch (error) {
359
+ findings.push(createAgentProfileFinding(
360
+ 'warning',
361
+ error && error.code === 'MISSING_PROVIDER_CLI' ? 'provider-cli-missing' : 'provider-cli-unverified',
362
+ `Provider CLI for '${provider}' could not be verified.`,
363
+ entry,
364
+ { fix: `Install and authenticate the ${provider} CLI, then rerun npx create-quiver ai agent doctor.` },
365
+ ));
366
+ }
367
+ }
368
+ } catch (error) {
369
+ findings.push(createAgentProfileFinding(
370
+ 'error',
371
+ 'unsupported-provider',
372
+ `Profile ${entry.role}/${entry.id} uses unsupported provider '${profile.provider}'.`,
373
+ entry,
374
+ { fix: `Use one of: ${formatProviderList()}.` },
375
+ ));
376
+ provider = providerInput;
377
+ }
378
+ }
379
+
380
+ if (!modelInput) {
381
+ findings.push(createAgentProfileFinding(
382
+ 'warning',
383
+ 'missing-model',
384
+ `Profile ${entry.role}/${entry.id} has no model selected.`,
385
+ entry,
386
+ { fix: `npx create-quiver ai agent set ${entry.role} --provider ${provider || '<provider>'} --model <model>` },
387
+ ));
388
+ } else if (providerInput && findings.every((finding) => finding.code !== 'unsupported-provider')) {
389
+ const modelSelection = normalizeModelSelection(provider, modelInput, {
390
+ displayName: rawText(profile.displayName || profile.display_name),
391
+ });
392
+ if (modelSelection.modelSource === 'catalog' && modelSelection.model !== modelInput) {
393
+ findings.push(createAgentProfileFinding(
394
+ 'warning',
395
+ 'display-model-alias',
396
+ `Profile ${entry.role}/${entry.id} stores display alias '${modelInput}' as the technical model id.`,
397
+ entry,
398
+ {
399
+ fix: 'Run npx create-quiver ai agent repair --dry-run to preview the normalized profile.',
400
+ before: { model: modelInput, displayName: rawText(profile.displayName || profile.display_name) },
401
+ after: { model: modelSelection.model, displayName: modelSelection.displayName },
402
+ },
403
+ ));
404
+ }
405
+ if (modelSelection.modelSource === 'custom') {
406
+ const validationStatus = rawText(profile.validation_status || profile.validationStatus);
407
+ if (!['validated', 'ok', 'passed'].includes(validationStatus.toLowerCase())) {
408
+ findings.push(createAgentProfileFinding(
409
+ 'warning',
410
+ 'custom-model-unvalidated',
411
+ `Profile ${entry.role}/${entry.id} uses custom model '${modelInput}' without live validation evidence.`,
412
+ entry,
413
+ { fix: `Run a dry-run or provider smoke with ${provider} before using this profile in automation.` },
414
+ ));
415
+ }
416
+ }
417
+ }
418
+
419
+ if (profile.default === true) {
420
+ findings.push(createAgentProfileFinding(
421
+ 'info',
422
+ 'default-profile',
423
+ `Profile ${entry.role}/${entry.id} is the default ${entry.role} profile.`,
424
+ entry,
425
+ ));
426
+ }
427
+
428
+ if (findings.some((finding) => finding.severity === 'error')) status = 'error';
429
+ else if (findings.some((finding) => finding.severity === 'warning')) status = 'warning';
430
+
431
+ return {
432
+ role: entry.role,
433
+ id: entry.id,
434
+ source: entry.source,
435
+ provider,
436
+ model: modelInput,
437
+ displayName,
438
+ default: profile.default === true,
439
+ status,
440
+ findings,
441
+ };
442
+ }
443
+
444
+ function addDuplicateDisplayNameFindings(checks) {
445
+ const groups = new Map();
446
+ for (const check of checks) {
447
+ const key = `${check.role}:${rawText(check.displayName).toLowerCase()}`;
448
+ if (!check.displayName) continue;
449
+ const group = groups.get(key) || [];
450
+ group.push(check);
451
+ groups.set(key, group);
452
+ }
453
+
454
+ for (const group of groups.values()) {
455
+ if (group.length < 2) continue;
456
+ const ids = group.map((item) => `${item.role}/${item.id}`).join(', ');
457
+ for (const check of group) {
458
+ check.findings.push({
459
+ severity: 'warning',
460
+ code: 'duplicate-display-name',
461
+ message: `Display name '${check.displayName}' is shared by ${ids}.`,
462
+ role: check.role,
463
+ profileId: check.id,
464
+ fix: 'Use --display-name or --id to make selector entries easier to distinguish.',
465
+ });
466
+ if (check.status !== 'error') check.status = 'warning';
467
+ }
468
+ }
469
+ }
470
+
471
+ function buildAgentProfileDoctorReport(projectRoot, options = {}) {
472
+ const state = readAgentProfiles(projectRoot);
473
+ const entries = collectRawAgentProfileEntries(state);
474
+ const checks = entries.map((entry) => classifyAgentProfileEntry(projectRoot, entry, options));
475
+ addDuplicateDisplayNameFindings(checks);
476
+ const findings = checks.flatMap((check) => check.findings);
477
+ const errors = findings.filter((finding) => finding.severity === 'error').length;
478
+ const warnings = findings.filter((finding) => finding.severity === 'warning').length;
479
+ const info = findings.filter((finding) => finding.severity === 'info').length;
480
+ const suggestedFixes = Array.from(new Set(findings
481
+ .map((finding) => finding.fix)
482
+ .filter(Boolean)));
483
+
484
+ return {
485
+ ok: errors === 0,
486
+ summary: {
487
+ profiles: checks.length,
488
+ defaults: checks.filter((check) => check.default).length,
489
+ errors,
490
+ warnings,
491
+ info,
492
+ },
493
+ checks,
494
+ findings,
495
+ suggestedFixes,
496
+ };
497
+ }
498
+
499
+ function repairProfileObject(profile, provider, modelSelection) {
500
+ const before = {
501
+ model: rawText(profile.model),
502
+ displayName: rawText(profile.displayName || profile.display_name),
503
+ modelSource: rawText(profile.modelSource || profile.model_source),
504
+ modelAlias: rawText(profile.modelAlias || profile.model_alias),
505
+ validation_status: rawText(profile.validation_status || profile.validationStatus),
506
+ };
507
+ const after = {
508
+ ...before,
509
+ model: modelSelection.model,
510
+ displayName: before.displayName || modelSelection.displayName,
511
+ modelSource: 'catalog',
512
+ modelAlias: before.model,
513
+ validation_status: before.validation_status || 'not-tested',
514
+ };
515
+
516
+ profile.model = after.model;
517
+ profile.displayName = after.displayName;
518
+ profile.modelSource = after.modelSource;
519
+ profile.modelAlias = after.modelAlias;
520
+ profile.validation_status = after.validation_status;
521
+ return { before, after, provider };
522
+ }
523
+
524
+ function buildAgentProfileRepairPlan(projectRoot, options = {}) {
525
+ const state = readAgentProfiles(projectRoot);
526
+ const nextState = cloneJson(state);
527
+ const entries = collectRawAgentProfileEntries(state);
528
+ const changes = [];
529
+
530
+ for (const entry of entries) {
531
+ const profile = entry.profile || {};
532
+ const providerInput = rawText(profile.provider).toLowerCase();
533
+ const modelInput = rawText(profile.model);
534
+ if (!providerInput || !modelInput) continue;
535
+
536
+ let provider;
537
+ try {
538
+ provider = assertSupportedProvider(providerInput);
539
+ } catch (error) {
540
+ continue;
541
+ }
542
+
543
+ const modelSelection = normalizeModelSelection(provider, modelInput, {
544
+ displayName: rawText(profile.displayName || profile.display_name),
545
+ });
546
+ if (modelSelection.modelSource !== 'catalog' || modelSelection.model === modelInput) {
547
+ continue;
548
+ }
549
+
550
+ const target = entry.source === 'profile_sets'
551
+ ? nextState.profile_sets[entry.setKey][entry.index]
552
+ : nextState.profiles[entry.role];
553
+ const repair = repairProfileObject(target, provider, modelSelection);
554
+ changes.push({
555
+ role: entry.role,
556
+ profileId: entry.id,
557
+ provider,
558
+ reason: 'display-model-alias',
559
+ before: repair.before,
560
+ after: repair.after,
561
+ });
562
+ }
563
+
564
+ return {
565
+ dryRun: true,
566
+ wouldWrite: false,
567
+ changes,
568
+ nextState: options.includeState === true ? nextState : undefined,
569
+ };
570
+ }
571
+
246
572
  module.exports = {
247
573
  AGENT_PROFILE_ROLES,
248
574
  PROFILE_STATE_VERSION,
249
575
  agentProfilesPath,
576
+ buildAgentProfileDoctorReport,
577
+ buildAgentProfileRepairPlan,
250
578
  getAgentProfileById,
251
579
  getAgentProfilesForRole,
252
580
  resolveAgentProfileDisplayName,
@@ -12,6 +12,7 @@ const { branchDelete, runGit, statusPorcelain, worktreeAdd, worktreePrune, workt
12
12
  const { withLock } = require('../locks');
13
13
  const { safeBranchName, worktreesRootForRepo } = require('../slice');
14
14
  const { buildGraph, computeLevels, detectFileConflicts, isFoundationSliceId, readAllSlices, topoSort, SliceGraphError } = require('../slice-graph');
15
+ const { resolveProviderModelSelection } = require('./providers');
15
16
  const { runExecuteSlice } = require('./executor');
16
17
 
17
18
  const EXCLUDED_STATUSES = new Set(['completed', 'skipped', 'cancelled']);
@@ -641,13 +642,17 @@ async function runExecutePlan(repoRoot, options = {}) {
641
642
  const executionMode = normalizeExecutionMode(options.mode || options.executionMode);
642
643
  const runtimeProfile = resolveExecutorRuntimeProfile(repoRoot, options);
643
644
  const provider = runtimeProfile.provider;
645
+ const explicitModel = String(options.model || '').trim();
646
+ const resolvedModel = runtimeProfile.model
647
+ ? resolveProviderModelSelection(provider, runtimeProfile.model).model
648
+ : '';
644
649
  const resolvedOptions = {
645
650
  ...options,
646
651
  mode: executionMode,
647
- model: runtimeProfile.model,
652
+ model: explicitModel ? resolvedModel : undefined,
648
653
  provider,
649
654
  profile: runtimeProfile,
650
- resolvedModel: runtimeProfile.model,
655
+ resolvedModel,
651
656
  resolvedProvider: provider,
652
657
  };
653
658
 
@@ -86,6 +86,13 @@ function resolveExecutorRuntimeProfile(repoRoot, role, options = {}) {
86
86
  };
87
87
  }
88
88
 
89
+ function runtimeModelExecutionOptions(runtimeProfile, options = {}) {
90
+ return {
91
+ model: runtimeProfile.model,
92
+ blockModelAlias: Boolean(runtimeProfile.profile && !String(options.model || '').trim()),
93
+ };
94
+ }
95
+
89
96
  function createCommandUx(options = {}) {
90
97
  if (options.ux) {
91
98
  return options.ux;
@@ -955,7 +962,7 @@ async function runExecuteSlice(repoRoot, options = {}) {
955
962
  prompt,
956
963
  cwd: canonicalRepoRoot,
957
964
  timeoutMs,
958
- model: runtimeProfile.model,
965
+ ...runtimeModelExecutionOptions(runtimeProfile, options),
959
966
  enforceModelSelection: false,
960
967
  });
961
968
  } catch (error) {
@@ -1028,7 +1035,7 @@ async function runExecuteSlice(repoRoot, options = {}) {
1028
1035
  tempRoot: options.tempRoot,
1029
1036
  tempFileName: options.tempFileName,
1030
1037
  tempFilePrefix: options.tempFilePrefix,
1031
- model: runtimeProfile.model,
1038
+ ...runtimeModelExecutionOptions(runtimeProfile, options),
1032
1039
  enforceModelSelection: Boolean(runtimeProfile.model),
1033
1040
  }),
1034
1041
  });