create-quiver 0.15.0 → 0.15.2

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 (96) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/CHANGELOG.md +6 -0
  3. package/README.md +33 -0
  4. package/README_FOR_AI.md +13 -6
  5. package/ROADMAP.md +5 -0
  6. package/docs/AI_CONTEXT.md.template +4 -0
  7. package/docs/AI_ONBOARDING_PROMPT.md.template +8 -0
  8. package/docs/CLI_UX_GUIDE.md +31 -2
  9. package/docs/COMMANDS.md.template +16 -5
  10. package/docs/TROUBLESHOOTING.md +70 -0
  11. package/docs/TROUBLESHOOTING.md.template +20 -0
  12. package/docs/WORKFLOW.md.template +2 -2
  13. package/docs/getting-started/installation.md +86 -0
  14. package/docs/reference/commands.md +39 -5
  15. package/package.json +1 -1
  16. package/specs/quiver-v31-ai-model-catalog-agent-selection/EVIDENCE_REPORT.md +185 -0
  17. package/specs/quiver-v31-ai-model-catalog-agent-selection/EXECUTION_PLAN.md +85 -0
  18. package/specs/quiver-v31-ai-model-catalog-agent-selection/SPEC.md +337 -0
  19. package/specs/quiver-v31-ai-model-catalog-agent-selection/STATUS.md +30 -0
  20. package/specs/quiver-v31-ai-model-catalog-agent-selection/pr.md +98 -0
  21. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +30 -0
  22. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +51 -0
  23. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-00-spec-foundation/slice.json +62 -0
  24. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/CLOSURE_BRIEF.md +32 -0
  25. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/EXECUTION_BRIEF.md +53 -0
  26. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-01-model-catalog-alias-normalization/slice.json +72 -0
  27. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/CLOSURE_BRIEF.md +36 -0
  28. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/EXECUTION_BRIEF.md +56 -0
  29. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-02-interactive-agent-set-selectors/slice.json +78 -0
  30. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/CLOSURE_BRIEF.md +36 -0
  31. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/EXECUTION_BRIEF.md +57 -0
  32. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-03-agent-doctor-repair/slice.json +77 -0
  33. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/CLOSURE_BRIEF.md +35 -0
  34. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/EXECUTION_BRIEF.md +55 -0
  35. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-04-shared-preflight-provider-errors/slice.json +84 -0
  36. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/CLOSURE_BRIEF.md +32 -0
  37. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/EXECUTION_BRIEF.md +52 -0
  38. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-05-ai-models-list/slice.json +72 -0
  39. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/CLOSURE_BRIEF.md +33 -0
  40. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/EXECUTION_BRIEF.md +58 -0
  41. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-06-docs-templates-alignment/slice.json +84 -0
  42. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/CLOSURE_BRIEF.md +32 -0
  43. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/EXECUTION_BRIEF.md +59 -0
  44. package/specs/quiver-v31-ai-model-catalog-agent-selection/slices/slice-07-tests-smokes-release-readiness/slice.json +94 -0
  45. package/specs/quiver-v32-npx-installation-guidance/EVIDENCE_REPORT.md +26 -0
  46. package/specs/quiver-v32-npx-installation-guidance/SPEC.md +55 -0
  47. package/specs/quiver-v32-npx-installation-guidance/STATUS.md +23 -0
  48. package/specs/quiver-v32-npx-installation-guidance/pr.md +72 -0
  49. package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/CLOSURE_BRIEF.md +31 -0
  50. package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/EXECUTION_BRIEF.md +56 -0
  51. package/specs/quiver-v32-npx-installation-guidance/slices/slice-00-installation-docs/slice.json +75 -0
  52. package/specs/quiver-v33-approval-ux-and-planner-progress/EVIDENCE_REPORT.md +93 -0
  53. package/specs/quiver-v33-approval-ux-and-planner-progress/EXECUTION_PLAN.md +83 -0
  54. package/specs/quiver-v33-approval-ux-and-planner-progress/SPEC.md +158 -0
  55. package/specs/quiver-v33-approval-ux-and-planner-progress/STATUS.md +31 -0
  56. package/specs/quiver-v33-approval-ux-and-planner-progress/pr.md +109 -0
  57. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/CLOSURE_BRIEF.md +30 -0
  58. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/EXECUTION_BRIEF.md +56 -0
  59. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/slice.json +65 -0
  60. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/CLOSURE_BRIEF.md +19 -0
  61. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/EXECUTION_BRIEF.md +51 -0
  62. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/slice.json +74 -0
  63. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/CLOSURE_BRIEF.md +20 -0
  64. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/EXECUTION_BRIEF.md +53 -0
  65. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/slice.json +79 -0
  66. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/CLOSURE_BRIEF.md +19 -0
  67. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/EXECUTION_BRIEF.md +50 -0
  68. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/slice.json +75 -0
  69. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/CLOSURE_BRIEF.md +19 -0
  70. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/EXECUTION_BRIEF.md +51 -0
  71. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/slice.json +73 -0
  72. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/CLOSURE_BRIEF.md +20 -0
  73. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/EXECUTION_BRIEF.md +51 -0
  74. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/slice.json +91 -0
  75. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/CLOSURE_BRIEF.md +20 -0
  76. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/EXECUTION_BRIEF.md +53 -0
  77. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/slice.json +87 -0
  78. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/CLOSURE_BRIEF.md +21 -0
  79. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/EXECUTION_BRIEF.md +55 -0
  80. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/slice.json +97 -0
  81. package/src/create-quiver/commands/ai.js +561 -46
  82. package/src/create-quiver/commands/flow.js +76 -14
  83. package/src/create-quiver/commands/spec.js +8 -1
  84. package/src/create-quiver/index.js +36 -12
  85. package/src/create-quiver/lib/agent-profiles.js +332 -4
  86. package/src/create-quiver/lib/ai/approval-candidates.js +86 -0
  87. package/src/create-quiver/lib/ai/execution-plan.js +7 -2
  88. package/src/create-quiver/lib/ai/executor.js +9 -2
  89. package/src/create-quiver/lib/ai/model-catalog.js +333 -0
  90. package/src/create-quiver/lib/ai/plan-review.js +77 -1
  91. package/src/create-quiver/lib/ai/providers.js +143 -4
  92. package/src/create-quiver/lib/ai/run-state.js +21 -4
  93. package/src/create-quiver/lib/approvals.js +103 -0
  94. package/src/create-quiver/lib/cli/selectors.js +53 -0
  95. package/src/create-quiver/lib/git.js +13 -4
  96. package/src/create-quiver/lib/paths.js +45 -3
@@ -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,
@@ -0,0 +1,86 @@
1
+ const { buildPlannerApprovalCandidates } = require('../approvals');
2
+ const { buildTechnicalPlanApprovalCandidates } = require('./plan-review');
3
+
4
+ function buildApprovalCandidateReport(projectRoot, phase) {
5
+ return phase === 'technical-plan'
6
+ ? buildTechnicalPlanApprovalCandidates(projectRoot)
7
+ : buildPlannerApprovalCandidates(projectRoot, phase);
8
+ }
9
+
10
+ function formatReviewSummary(review) {
11
+ if (!review || !review.recommendation) {
12
+ return '';
13
+ }
14
+
15
+ const parts = [`review=${review.recommendation}`];
16
+ if (review.blocking) {
17
+ parts.push('blocking');
18
+ }
19
+ if (review.required_fixes_count) {
20
+ parts.push(`required fixes=${review.required_fixes_count}`);
21
+ }
22
+ if (review.optional_hardening_count) {
23
+ parts.push(`optional=${review.optional_hardening_count}`);
24
+ }
25
+ if (review.risks_count) {
26
+ parts.push(`risks=${review.risks_count}`);
27
+ }
28
+ return parts.join(', ');
29
+ }
30
+
31
+ function formatCandidateSummary(candidate) {
32
+ if (!candidate) {
33
+ return '';
34
+ }
35
+
36
+ const parts = [
37
+ candidate.label,
38
+ candidate.current ? 'current' : 'history',
39
+ candidate.approvable ? 'approvable' : 'blocked',
40
+ candidate.reason,
41
+ formatReviewSummary(candidate.review),
42
+ ].filter(Boolean);
43
+ return parts.join(', ');
44
+ }
45
+
46
+ function approvalCandidateCommand(report, fallback = 'npx create-quiver ai approvals') {
47
+ if (report?.recommended?.next_command) {
48
+ return report.recommended.next_command;
49
+ }
50
+ if (report?.current?.next_command) {
51
+ return report.current.next_command;
52
+ }
53
+ return report?.next_command || fallback;
54
+ }
55
+
56
+ function formatApprovalDecisionLines(report) {
57
+ const lines = [];
58
+ if (!report || !Array.isArray(report.candidates)) {
59
+ return lines;
60
+ }
61
+
62
+ lines.push(`Candidates: ${report.candidates.length}`);
63
+ if (report.latest_version) {
64
+ lines.push(`Latest draft: v${report.latest_version}`);
65
+ }
66
+ if (report.current) {
67
+ lines.push(`Current candidate: ${formatCandidateSummary(report.current)}`);
68
+ }
69
+ if (report.recommended) {
70
+ lines.push(`Recommended approval: ${report.recommended.next_command}`);
71
+ } else if (report.next_command) {
72
+ lines.push(`Recommended next command: ${report.next_command}`);
73
+ }
74
+ if (report.review?.status) {
75
+ lines.push(`Review status: ${report.review.status}`);
76
+ }
77
+ return lines;
78
+ }
79
+
80
+ module.exports = {
81
+ approvalCandidateCommand,
82
+ buildApprovalCandidateReport,
83
+ formatApprovalDecisionLines,
84
+ formatCandidateSummary,
85
+ formatReviewSummary,
86
+ };
@@ -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
  });