create-quiver 0.14.1 → 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 (94) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +35 -0
  3. package/README_FOR_AI.md +15 -4
  4. package/ROADMAP.md +13 -2
  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 +79 -4
  8. package/docs/COMMANDS.md.template +21 -5
  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 +33 -1
  13. package/package.json +1 -1
  14. package/specs/quiver-v30-interactive-cli-ux-agent-selection/EVIDENCE_REPORT.md +213 -0
  15. package/specs/quiver-v30-interactive-cli-ux-agent-selection/EXECUTION_PLAN.md +85 -0
  16. package/specs/quiver-v30-interactive-cli-ux-agent-selection/SPEC.md +213 -0
  17. package/specs/quiver-v30-interactive-cli-ux-agent-selection/STATUS.md +31 -0
  18. package/specs/quiver-v30-interactive-cli-ux-agent-selection/pr.md +103 -0
  19. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +33 -0
  20. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +56 -0
  21. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/slice.json +71 -0
  22. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/CLOSURE_BRIEF.md +31 -0
  23. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/EXECUTION_BRIEF.md +54 -0
  24. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/slice.json +69 -0
  25. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/CLOSURE_BRIEF.md +33 -0
  26. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/EXECUTION_BRIEF.md +56 -0
  27. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/slice.json +81 -0
  28. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/CLOSURE_BRIEF.md +32 -0
  29. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/EXECUTION_BRIEF.md +54 -0
  30. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/slice.json +75 -0
  31. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/CLOSURE_BRIEF.md +32 -0
  32. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/EXECUTION_BRIEF.md +57 -0
  33. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/slice.json +85 -0
  34. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/CLOSURE_BRIEF.md +33 -0
  35. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/EXECUTION_BRIEF.md +57 -0
  36. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/slice.json +85 -0
  37. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/CLOSURE_BRIEF.md +35 -0
  38. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/EXECUTION_BRIEF.md +55 -0
  39. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/slice.json +81 -0
  40. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/CLOSURE_BRIEF.md +34 -0
  41. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/EXECUTION_BRIEF.md +55 -0
  42. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/slice.json +85 -0
  43. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/CLOSURE_BRIEF.md +34 -0
  44. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/EXECUTION_BRIEF.md +59 -0
  45. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/slice.json +95 -0
  46. package/specs/quiver-v31-ai-model-catalog-agent-selection/EVIDENCE_REPORT.md +185 -0
  47. package/specs/quiver-v31-ai-model-catalog-agent-selection/EXECUTION_PLAN.md +85 -0
  48. package/specs/quiver-v31-ai-model-catalog-agent-selection/SPEC.md +337 -0
  49. package/specs/quiver-v31-ai-model-catalog-agent-selection/STATUS.md +30 -0
  50. package/specs/quiver-v31-ai-model-catalog-agent-selection/pr.md +98 -0
  51. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +30 -0
  52. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +51 -0
  53. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/slice.json +62 -0
  54. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/CLOSURE_BRIEF.md +32 -0
  55. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/EXECUTION_BRIEF.md +53 -0
  56. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/slice.json +72 -0
  57. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/CLOSURE_BRIEF.md +36 -0
  58. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/EXECUTION_BRIEF.md +56 -0
  59. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/slice.json +78 -0
  60. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/CLOSURE_BRIEF.md +36 -0
  61. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/EXECUTION_BRIEF.md +57 -0
  62. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/slice.json +77 -0
  63. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/CLOSURE_BRIEF.md +35 -0
  64. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/EXECUTION_BRIEF.md +55 -0
  65. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/slice.json +84 -0
  66. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/CLOSURE_BRIEF.md +32 -0
  67. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/EXECUTION_BRIEF.md +52 -0
  68. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/slice.json +72 -0
  69. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/CLOSURE_BRIEF.md +33 -0
  70. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/EXECUTION_BRIEF.md +58 -0
  71. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/slice.json +84 -0
  72. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/CLOSURE_BRIEF.md +32 -0
  73. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/EXECUTION_BRIEF.md +59 -0
  74. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/slice.json +94 -0
  75. package/specs/quiver-v32-npx-installation-guidance/EVIDENCE_REPORT.md +26 -0
  76. package/specs/quiver-v32-npx-installation-guidance/SPEC.md +55 -0
  77. package/specs/quiver-v32-npx-installation-guidance/STATUS.md +23 -0
  78. package/specs/quiver-v32-npx-installation-guidance/pr.md +72 -0
  79. package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/CLOSURE_BRIEF.md +31 -0
  80. package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/EXECUTION_BRIEF.md +56 -0
  81. package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/slice.json +75 -0
  82. package/src/create-quiver/commands/ai.js +797 -99
  83. package/src/create-quiver/commands/flow.js +2 -2
  84. package/src/create-quiver/commands/spec.js +106 -6
  85. package/src/create-quiver/index.js +557 -78
  86. package/src/create-quiver/lib/agent-profiles.js +440 -11
  87. package/src/create-quiver/lib/ai/execution-plan.js +111 -8
  88. package/src/create-quiver/lib/ai/executor.js +291 -28
  89. package/src/create-quiver/lib/ai/model-catalog.js +333 -0
  90. package/src/create-quiver/lib/ai/providers.js +211 -2
  91. package/src/create-quiver/lib/cli/selectors.js +160 -0
  92. package/src/create-quiver/lib/cli/theme.js +5 -2
  93. package/src/create-quiver/lib/cli/ux-flags.js +18 -0
  94. package/src/create-quiver/lib/cli/ux.js +100 -5
@@ -2,10 +2,12 @@ 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']);
8
- const PROFILE_STATE_VERSION = 1;
10
+ const PROFILE_STATE_VERSION = 2;
9
11
 
10
12
  function formatError(message) {
11
13
  return `create-quiver: ${message}`;
@@ -56,9 +58,51 @@ function emptyProfilesState() {
56
58
  return {
57
59
  version: PROFILE_STATE_VERSION,
58
60
  profiles: {},
61
+ profile_sets: {},
59
62
  };
60
63
  }
61
64
 
65
+ function profileSetKey(role) {
66
+ return `${normalizeAgentProfileRole(role)}s`;
67
+ }
68
+
69
+ function normalizeProfileId(value, fallback = 'default') {
70
+ const source = String(value || fallback || 'default').trim().toLowerCase();
71
+ const normalized = source
72
+ .replace(/[^a-z0-9]+/g, '-')
73
+ .replace(/^-+|-+$/g, '');
74
+ return normalized || 'default';
75
+ }
76
+
77
+ function resolveAgentProfileDisplayName(profile = {}) {
78
+ return profile.displayName || profile.model || profile.provider || profile.role || '';
79
+ }
80
+
81
+ function normalizeStoredProfile(profile, role, fallbackId = 'default') {
82
+ const normalizedRole = normalizeAgentProfileRole(role || profile?.role);
83
+ const provider = profile?.provider ? assertSupportedProvider(profile.provider) : '';
84
+ const normalized = {
85
+ id: normalizeProfileId(profile?.id || profile?.label, fallbackId),
86
+ role: normalizedRole,
87
+ provider,
88
+ model: normalizeOptionalText(profile?.model, 'model'),
89
+ label: normalizeOptionalText(profile?.label, 'label'),
90
+ displayName: normalizeOptionalText(profile?.displayName || profile?.display_name, 'displayName'),
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'),
97
+ default: profile?.default === true,
98
+ updated_at: profile?.updated_at || '',
99
+ };
100
+ if (!normalized.displayName) {
101
+ normalized.displayName = resolveAgentProfileDisplayName(normalized);
102
+ }
103
+ return normalized;
104
+ }
105
+
62
106
  function readAgentProfiles(projectRoot) {
63
107
  const filePath = agentProfilesPath(projectRoot);
64
108
  if (!fs.existsSync(filePath)) {
@@ -69,6 +113,8 @@ function readAgentProfiles(projectRoot) {
69
113
  return {
70
114
  version: state.version || PROFILE_STATE_VERSION,
71
115
  profiles: state.profiles && typeof state.profiles === 'object' ? state.profiles : {},
116
+ profile_sets: state.profile_sets && typeof state.profile_sets === 'object' ? state.profile_sets : {},
117
+ updated_at: state.updated_at || undefined,
72
118
  };
73
119
  }
74
120
 
@@ -80,18 +126,53 @@ function writeAgentProfiles(projectRoot, state) {
80
126
  }
81
127
 
82
128
  function getAgentProfile(projectRoot, role) {
129
+ const normalizedRole = normalizeAgentProfileRole(role);
130
+ const profiles = getAgentProfilesForRole(projectRoot, normalizedRole);
131
+ if (profiles.length === 0) return null;
132
+ return profiles.find((profile) => profile.default) || profiles[0];
133
+ }
134
+
135
+ function getAgentProfileById(projectRoot, role, profileId) {
136
+ const normalizedRole = normalizeAgentProfileRole(role);
137
+ const normalizedId = normalizeProfileId(profileId);
138
+ return getAgentProfilesForRole(projectRoot, normalizedRole)
139
+ .find((profile) => profile.id === normalizedId) || null;
140
+ }
141
+
142
+ function getAgentProfilesForRole(projectRoot, role) {
83
143
  const normalizedRole = normalizeAgentProfileRole(role);
84
144
  const state = readAgentProfiles(projectRoot);
85
- return state.profiles[normalizedRole] || null;
145
+ const setKey = profileSetKey(normalizedRole);
146
+ const fromSet = Array.isArray(state.profile_sets[setKey])
147
+ ? state.profile_sets[setKey].map((profile, index) => normalizeStoredProfile(profile, normalizedRole, index === 0 ? 'default' : `${normalizedRole}-${index + 1}`))
148
+ : [];
149
+ const legacy = state.profiles[normalizedRole]
150
+ ? normalizeStoredProfile({ ...state.profiles[normalizedRole], default: true }, normalizedRole, 'default')
151
+ : null;
152
+ const merged = new Map();
153
+
154
+ if (legacy) merged.set(legacy.id, legacy);
155
+ for (const profile of fromSet) {
156
+ merged.set(profile.id, profile);
157
+ }
158
+
159
+ const profiles = Array.from(merged.values()).filter((profile) => profile.provider);
160
+ if (profiles.length > 0 && !profiles.some((profile) => profile.default)) {
161
+ profiles[0].default = true;
162
+ }
163
+ return profiles;
86
164
  }
87
165
 
88
166
  function listAgentProfiles(projectRoot) {
89
- const state = readAgentProfiles(projectRoot);
90
- return AGENT_PROFILE_ROLES.map((role) => ({
91
- role,
92
- configured: Boolean(state.profiles[role]),
93
- profile: state.profiles[role] || null,
94
- }));
167
+ return AGENT_PROFILE_ROLES.map((role) => {
168
+ const profiles = getAgentProfilesForRole(projectRoot, role);
169
+ return {
170
+ role,
171
+ configured: profiles.length > 0,
172
+ profile: profiles.find((profile) => profile.default) || profiles[0] || null,
173
+ profiles,
174
+ };
175
+ });
95
176
  }
96
177
 
97
178
  function setAgentProfile(projectRoot, role, options = {}) {
@@ -107,25 +188,68 @@ function setAgentProfile(projectRoot, role, options = {}) {
107
188
  function buildAgentProfileState(projectRoot, role, options = {}) {
108
189
  const normalizedRole = normalizeAgentProfileRole(role);
109
190
  const provider = assertSupportedProvider(options.provider);
110
- const model = normalizeOptionalText(options.model, 'model');
191
+ const rawModel = normalizeOptionalText(options.model, 'model');
111
192
  const label = normalizeOptionalText(options.label, 'label');
193
+ const rawDisplayName = normalizeOptionalText(options.displayName || options.display_name, 'displayName');
112
194
  const context = normalizeOptionalText(options.context, 'context');
113
195
  const state = readAgentProfiles(projectRoot);
114
- const current = state.profiles[normalizedRole] || {};
196
+ const currentProfiles = getAgentProfilesForRole(projectRoot, normalizedRole);
197
+ const currentDefault = currentProfiles.find((profile) => profile.default) || currentProfiles[0] || {};
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);
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;
115
210
  const now = options.now instanceof Date ? options.now.toISOString() : new Date().toISOString();
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 || '');
116
216
  const profile = {
217
+ id,
117
218
  role: normalizedRole,
118
219
  provider,
119
220
  model: model || current.model || '',
120
221
  label: label || current.label || '',
222
+ displayName,
121
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 || '',
229
+ default: shouldBeDefault,
122
230
  updated_at: now,
123
231
  };
124
232
 
125
233
  state.version = PROFILE_STATE_VERSION;
234
+ const setKey = profileSetKey(normalizedRole);
235
+ const nextProfiles = currentProfiles
236
+ .filter((item) => item.id !== id)
237
+ .map((item) => ({
238
+ ...item,
239
+ default: shouldBeDefault ? false : item.default === true,
240
+ }))
241
+ .concat(profile);
242
+ if (!nextProfiles.some((item) => item.default)) {
243
+ nextProfiles[0].default = true;
244
+ }
245
+ state.profile_sets = {
246
+ ...state.profile_sets,
247
+ [setKey]: nextProfiles,
248
+ };
249
+ const defaultProfile = nextProfiles.find((item) => item.default) || profile;
126
250
  state.profiles = {
127
251
  ...state.profiles,
128
- [normalizedRole]: profile,
252
+ [normalizedRole]: defaultProfile,
129
253
  };
130
254
  state.updated_at = now;
131
255
 
@@ -145,10 +269,315 @@ function resolveProfileProvider(projectRoot, role, fallbackProvider) {
145
269
  return assertSupportedProvider(fallbackProvider);
146
270
  }
147
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
+
148
572
  module.exports = {
149
573
  AGENT_PROFILE_ROLES,
150
574
  PROFILE_STATE_VERSION,
151
575
  agentProfilesPath,
576
+ buildAgentProfileDoctorReport,
577
+ buildAgentProfileRepairPlan,
578
+ getAgentProfileById,
579
+ getAgentProfilesForRole,
580
+ resolveAgentProfileDisplayName,
152
581
  formatProviderList,
153
582
  buildAgentProfileState,
154
583
  getAgentProfile,