create-quiver 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +187 -518
  3. package/README_FOR_AI.md +39 -27
  4. package/ROADMAP.md +11 -0
  5. package/assets/quiver-wordmark.svg +22 -0
  6. package/docs/AI_CONTEXT.md.template +2 -0
  7. package/docs/AI_ONBOARDING_PROMPT.md.template +9 -1
  8. package/docs/CLI_UX_GUIDE.md +185 -0
  9. package/docs/COMMANDS.md.template +22 -3
  10. package/docs/GITFLOW_PR_GUIDE.md +70 -0
  11. package/docs/getting-started/linux.md +84 -0
  12. package/docs/getting-started/macos.md +85 -0
  13. package/docs/getting-started/windows-git-bash-wsl.md +78 -0
  14. package/docs/getting-started/windows-powershell.md +96 -0
  15. package/docs/reference/commands.md +98 -0
  16. package/docs/workflows/existing-project.md +131 -0
  17. package/docs/workflows/full-ai-spec-to-pr.md +311 -0
  18. package/docs/workflows/legacy-quiver-project.md +102 -0
  19. package/docs/workflows/new-project.md +76 -0
  20. package/package.json +5 -1
  21. package/specs/quiver-v29-planner-prepare-context-cli-ux/EVIDENCE_REPORT.md +163 -0
  22. package/specs/quiver-v29-planner-prepare-context-cli-ux/EXECUTION_PLAN.md +72 -0
  23. package/specs/quiver-v29-planner-prepare-context-cli-ux/SPEC.md +173 -0
  24. package/specs/quiver-v29-planner-prepare-context-cli-ux/STATUS.md +34 -0
  25. package/specs/quiver-v29-planner-prepare-context-cli-ux/pr.md +95 -0
  26. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/CLOSURE_BRIEF.md +32 -0
  27. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/EXECUTION_BRIEF.md +45 -0
  28. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/slice.json +58 -0
  29. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/CLOSURE_BRIEF.md +33 -0
  30. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/EXECUTION_BRIEF.md +49 -0
  31. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/slice.json +75 -0
  32. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/CLOSURE_BRIEF.md +32 -0
  33. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/EXECUTION_BRIEF.md +47 -0
  34. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/slice.json +71 -0
  35. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/CLOSURE_BRIEF.md +33 -0
  36. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/EXECUTION_BRIEF.md +52 -0
  37. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/slice.json +82 -0
  38. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/CLOSURE_BRIEF.md +34 -0
  39. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/EXECUTION_BRIEF.md +46 -0
  40. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/slice.json +73 -0
  41. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/CLOSURE_BRIEF.md +34 -0
  42. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/EXECUTION_BRIEF.md +46 -0
  43. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/slice.json +83 -0
  44. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/CLOSURE_BRIEF.md +31 -0
  45. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/EXECUTION_BRIEF.md +50 -0
  46. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/slice.json +95 -0
  47. package/specs/quiver-v30-interactive-cli-ux-agent-selection/EVIDENCE_REPORT.md +213 -0
  48. package/specs/quiver-v30-interactive-cli-ux-agent-selection/EXECUTION_PLAN.md +85 -0
  49. package/specs/quiver-v30-interactive-cli-ux-agent-selection/SPEC.md +213 -0
  50. package/specs/quiver-v30-interactive-cli-ux-agent-selection/STATUS.md +31 -0
  51. package/specs/quiver-v30-interactive-cli-ux-agent-selection/pr.md +103 -0
  52. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +33 -0
  53. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +56 -0
  54. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-00-spec-foundation/slice.json +71 -0
  55. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/CLOSURE_BRIEF.md +31 -0
  56. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/EXECUTION_BRIEF.md +54 -0
  57. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-01-cli-ux-runtime-progress-engine/slice.json +69 -0
  58. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/CLOSURE_BRIEF.md +33 -0
  59. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/EXECUTION_BRIEF.md +56 -0
  60. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-02-agent-profile-selection-selectors/slice.json +81 -0
  61. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/CLOSURE_BRIEF.md +32 -0
  62. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/EXECUTION_BRIEF.md +54 -0
  63. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-03-provider-model-selection-contract/slice.json +75 -0
  64. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/CLOSURE_BRIEF.md +32 -0
  65. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/EXECUTION_BRIEF.md +57 -0
  66. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-04-planner-ia-progress-flows/slice.json +85 -0
  67. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/CLOSURE_BRIEF.md +33 -0
  68. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/EXECUTION_BRIEF.md +57 -0
  69. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-05-executor-pr-progress-flows/slice.json +85 -0
  70. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/CLOSURE_BRIEF.md +35 -0
  71. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/EXECUTION_BRIEF.md +55 -0
  72. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-06-doctor-visual-json-contract/slice.json +81 -0
  73. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/CLOSURE_BRIEF.md +34 -0
  74. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/EXECUTION_BRIEF.md +55 -0
  75. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-07-interactive-init-spec-create/slice.json +85 -0
  76. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/CLOSURE_BRIEF.md +34 -0
  77. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/EXECUTION_BRIEF.md +59 -0
  78. package/specs/quiver-v30-interactive-cli-ux-agent-selection/slices/slice-08-tests-docs-cross-platform-release/slice.json +95 -0
  79. package/src/create-quiver/commands/ai.js +809 -71
  80. package/src/create-quiver/commands/spec.js +167 -5
  81. package/src/create-quiver/index.js +582 -71
  82. package/src/create-quiver/lib/agent-profiles.js +111 -10
  83. package/src/create-quiver/lib/ai/context-proposal.js +389 -0
  84. package/src/create-quiver/lib/ai/context-proposal.schema.js +31 -0
  85. package/src/create-quiver/lib/ai/execution-plan.js +106 -8
  86. package/src/create-quiver/lib/ai/executor.js +284 -28
  87. package/src/create-quiver/lib/ai/providers.js +71 -1
  88. package/src/create-quiver/lib/cli/editor.js +118 -0
  89. package/src/create-quiver/lib/cli/selectors.js +107 -0
  90. package/src/create-quiver/lib/cli/theme.js +103 -0
  91. package/src/create-quiver/lib/cli/ux-flags.js +169 -0
  92. package/src/create-quiver/lib/cli/ux.js +225 -0
@@ -1,4 +1,5 @@
1
1
  const fs = require('node:fs');
2
+ const os = require('node:os');
2
3
  const path = require('node:path');
3
4
 
4
5
  const { redactSecrets } = require('../lib/evidence');
@@ -10,6 +11,9 @@ const {
10
11
  writeRawProviderArtifact,
11
12
  } = require('../lib/ai/artifacts');
12
13
  const { buildContextPackMetadata, normalizeRole } = require('../lib/ai/context-packs');
14
+ const { parseContextProposalOutput } = require('../lib/ai/context-proposal');
15
+ const { openEditor } = require('../lib/cli/editor');
16
+ const { createUx } = require('../lib/cli/ux');
13
17
  const { runExecuteSlice, runPromptSlice } = require('../lib/ai/executor');
14
18
  const { runExecutePlan } = require('../lib/ai/execution-plan');
15
19
  const { buildPrCreatePlan, formatPreflightReport, formatPrCreateReport, preflightGitHubPr, runGhPrCreate } = require('../lib/ai/github');
@@ -53,7 +57,10 @@ const {
53
57
  agentProfilesPath,
54
58
  buildAgentProfileState,
55
59
  getAgentProfile,
60
+ getAgentProfileById,
56
61
  listAgentProfiles,
62
+ normalizeAgentProfileRole,
63
+ resolveAgentProfileDisplayName,
57
64
  resolveProfileProvider,
58
65
  setAgentProfile,
59
66
  } = require('../lib/agent-profiles');
@@ -118,11 +125,119 @@ function normalizeTimeout(timeoutMs) {
118
125
  return parsed;
119
126
  }
120
127
 
121
- function resolveProviderForProfile(repoRoot, role, provider, providerExplicit, fallbackProvider) {
122
- if (providerExplicit === true || (provider && providerExplicit !== false)) {
123
- return String(provider || fallbackProvider).trim().toLowerCase();
128
+ function profileOptionForRole(options, role) {
129
+ const normalized = normalizeAgentProfileRole(role);
130
+ if (normalized === 'planner') {
131
+ return options.plannerProfile || options.profileId || '';
124
132
  }
125
- return resolveProfileProvider(repoRoot, role, fallbackProvider);
133
+ if (normalized === 'executor') {
134
+ return options.executorProfile || options.profileId || '';
135
+ }
136
+ if (normalized === 'reviewer') {
137
+ return options.reviewerProfile || options.profileId || '';
138
+ }
139
+ if (normalized === 'doctor') {
140
+ return options.doctorProfile || options.profileId || '';
141
+ }
142
+ return options.profileId || '';
143
+ }
144
+
145
+ function resolveRuntimeAgentProfile(repoRoot, role, options = {}, fallbackProvider = DEFAULT_PLAN_PROVIDER) {
146
+ const normalizedRole = normalizeAgentProfileRole(role);
147
+ const explicitProvider = options.providerExplicit === true || (options.provider && options.providerExplicit !== false);
148
+ const explicitModel = String(options.model || '').trim();
149
+
150
+ if (explicitProvider) {
151
+ const provider = String(options.provider || fallbackProvider).trim().toLowerCase();
152
+ return {
153
+ role: normalizedRole,
154
+ profile: null,
155
+ profileId: '',
156
+ displayName: provider,
157
+ provider,
158
+ model: explicitModel,
159
+ };
160
+ }
161
+
162
+ const profileId = profileOptionForRole(options, normalizedRole);
163
+ const profile = profileId
164
+ ? getAgentProfileById(repoRoot, normalizedRole, profileId)
165
+ : getAgentProfile(repoRoot, normalizedRole);
166
+ const provider = profile?.provider || resolveProfileProvider(repoRoot, normalizedRole, fallbackProvider);
167
+
168
+ return {
169
+ role: normalizedRole,
170
+ profile,
171
+ profileId: profile?.id || profileId || '',
172
+ displayName: profile ? resolveAgentProfileDisplayName(profile) : provider,
173
+ provider,
174
+ model: explicitModel || profile?.model || '',
175
+ };
176
+ }
177
+
178
+ function createCommandUx(options = {}) {
179
+ if (options.ux) {
180
+ return options.ux;
181
+ }
182
+
183
+ return createUx({
184
+ env: options.env || process.env,
185
+ input: options.inputStream,
186
+ output: options.outputStream,
187
+ error: options.errorStream,
188
+ interactive: options.interactive,
189
+ json: options.json,
190
+ noColor: options.noColor,
191
+ prompts: options.prompts,
192
+ spinner: options.spinner,
193
+ stdinIsTTY: options.stdinIsTTY,
194
+ stdoutIsTTY: options.stdoutIsTTY,
195
+ stderrIsTTY: options.stderrIsTTY,
196
+ write: options.write,
197
+ });
198
+ }
199
+
200
+ function shouldShowHumanProgress(ux, options = {}) {
201
+ return options.progress !== false
202
+ && options.dryRun !== true
203
+ && options.printPrompt !== true
204
+ && ux?.mode?.decoration === true;
205
+ }
206
+
207
+ function plannerProgressTitle(action, runtimeProfile) {
208
+ return `${action} con ${runtimeProfile.displayName || runtimeProfile.model || runtimeProfile.provider}`;
209
+ }
210
+
211
+ function writeProgressChecks(ux, enabled, title, checks = []) {
212
+ if (!enabled) {
213
+ return;
214
+ }
215
+ ux.heading(title);
216
+ for (const check of checks) {
217
+ ux.check(check);
218
+ }
219
+ }
220
+
221
+ async function runProviderWithProgress({ ux, enabled, message = 'Ejecutando agente...', successMessage = 'Agente finalizado', failureMessage = 'Fallo ejecutando agente', run }) {
222
+ async function runAndFailOnProviderResult() {
223
+ const result = await run();
224
+ if (result && result.ok === false) {
225
+ const error = new Error(result.error?.message || 'provider run failed');
226
+ error.code = result.error?.code || 'AI_PROVIDER_RUN_FAILED';
227
+ error.providerResult = result;
228
+ throw error;
229
+ }
230
+ return result;
231
+ }
232
+
233
+ if (!enabled) {
234
+ return run();
235
+ }
236
+
237
+ return ux.withSpinner(message, runAndFailOnProviderResult, {
238
+ successMessage,
239
+ failureMessage,
240
+ });
126
241
  }
127
242
 
128
243
  function buildPlanContext({ role, context, phase, inputText, inputPath, repoRoot, revise = false }) {
@@ -206,6 +321,10 @@ function formatDryRunReport({ task, provider, role, contextPack, phase, invocati
206
321
  lines.push(`Timeout: ${invocation.timeoutMs}ms`);
207
322
  lines.push(`Prompt transport: ${invocation.promptTransport.mode}`);
208
323
  lines.push(`Prompt length: ${invocation.promptLength} bytes`);
324
+ if (invocation.modelSelection && invocation.modelSelection.model) {
325
+ lines.push(`Model: ${invocation.modelSelection.model}`);
326
+ lines.push(`Model support: ${invocation.modelSelection.supported ? 'supported' : 'unsupported'} (${invocation.modelSelection.reason})`);
327
+ }
209
328
 
210
329
  if (onboardingPlan) {
211
330
  lines.push(`Prompt source: ${onboardingPlan.promptSource}`);
@@ -232,6 +351,10 @@ function formatPromptOnlyReport({ task, provider, role, contextPack, phase, invo
232
351
  lines.push(`Timeout: ${invocation.timeoutMs}ms`);
233
352
  lines.push(`Prompt transport: ${invocation.promptTransport.mode}`);
234
353
  lines.push(`Prompt length: ${invocation.promptLength} bytes`);
354
+ if (invocation.modelSelection && invocation.modelSelection.model) {
355
+ lines.push(`Model: ${invocation.modelSelection.model}`);
356
+ lines.push(`Model support: ${invocation.modelSelection.supported ? 'supported' : 'unsupported'} (${invocation.modelSelection.reason})`);
357
+ }
235
358
 
236
359
  if (onboardingPlan) {
237
360
  lines.push(`Prompt source: ${onboardingPlan.promptSource}`);
@@ -313,6 +436,261 @@ function formatContextPreparationReport({ dryRun, plan, writePlan, writtenDocs,
313
436
  return `${lines.join('\n')}\n`;
314
437
  }
315
438
 
439
+ function truncatePromptSection(text, maxChars = 1200) {
440
+ const value = String(text || '').trimEnd();
441
+ if (value.length <= maxChars) {
442
+ return value;
443
+ }
444
+ return `${value.slice(0, maxChars).trimEnd()}\n[... truncated ${value.length - maxChars} chars ...]`;
445
+ }
446
+
447
+ function buildPrepareContextPlannerPrompt({ pack, draftPack }) {
448
+ const plan = draftPack.plan;
449
+ const allowedPaths = draftPack.docs.map((doc) => doc.path);
450
+ const sections = [
451
+ pack.prompt,
452
+ 'Task: planner-assisted Quiver context preparation.',
453
+ 'Goal: improve the docs-only onboarding context for future AI work while preserving WDD + SDD safety.',
454
+ 'Rules:',
455
+ '- Return only valid JSON. Do not include Markdown outside the JSON object.',
456
+ '- Do not modify product code, UI code, tests, migrations, dependencies, lockfiles, build files, runtime config, generated files, or paths outside the repo.',
457
+ '- Only propose writes to the allowed docs paths listed below.',
458
+ '- If information is ambiguous, document assumptions and risks instead of inventing facts.',
459
+ '- Keep human-authored content safe; Quiver will merge proposals through managed context blocks.',
460
+ '',
461
+ 'Allowed docs-only output paths:',
462
+ ...allowedPaths.map((item) => `- ${item}`),
463
+ '',
464
+ 'Project context:',
465
+ `- Project: ${plan.projectName}`,
466
+ `- Project slug: ${plan.projectSlug}`,
467
+ `- Package manager: ${plan.facts.packageManager}`,
468
+ `- Stack summary: ${plan.facts.stackSummary}`,
469
+ '',
470
+ 'Files considered by deterministic prepare-context:',
471
+ ...plan.filesConsidered.map((item) => `- ${item.path}: ${item.present ? 'present' : 'absent'}${item.reason ? ` (${item.reason})` : ''}`),
472
+ '',
473
+ 'Known assumptions:',
474
+ ...formatPathList(plan.assumptions),
475
+ '',
476
+ 'Known risks:',
477
+ ...formatPathList(plan.risks),
478
+ '',
479
+ 'Known contradictions:',
480
+ ...formatPathList(plan.contradictions),
481
+ '',
482
+ 'Deterministic candidate docs:',
483
+ ...draftPack.docs.flatMap((doc) => [
484
+ `### ${doc.path}`,
485
+ truncatePromptSection(doc.content),
486
+ ]),
487
+ '',
488
+ 'Required JSON output shape:',
489
+ JSON.stringify({
490
+ schema_version: 1,
491
+ kind: 'quiver-context-proposal',
492
+ summary: 'short summary',
493
+ assumptions: ['assumption to confirm'],
494
+ risks: ['risk to track'],
495
+ docs: [
496
+ {
497
+ path: 'docs/AI_CONTEXT.md',
498
+ action: 'update',
499
+ reason: 'why this doc should change',
500
+ content: '# AI Context\\n\\nFull proposed content or managed section content.\\n',
501
+ assumptions: [],
502
+ risks: [],
503
+ },
504
+ ],
505
+ omitted_paths: ['paths intentionally omitted'],
506
+ next_steps: ['safe next step'],
507
+ }, null, 2),
508
+ ];
509
+
510
+ return {
511
+ allowedPaths,
512
+ plan,
513
+ prompt: sections.join('\n'),
514
+ promptSource: 'quiver prepare-context planner proposal contract',
515
+ };
516
+ }
517
+
518
+ function formatPrepareContextPlannerDryRunReport({ provider, role, context, invocation, promptInfo, review, interactive }) {
519
+ const plan = promptInfo.plan;
520
+ const lines = [
521
+ 'AI prepare-context planner dry-run',
522
+ 'Mode: dry-run',
523
+ 'Planner: enabled',
524
+ `Provider: ${provider}`,
525
+ `Role: ${role}`,
526
+ `Context pack: ${context}`,
527
+ `Command: ${invocation.command} ${invocation.args.join(' ')}`.trim(),
528
+ `Prompt bytes: ${invocation.promptLength}`,
529
+ invocation.modelSelection && invocation.modelSelection.model
530
+ ? `Model: ${invocation.modelSelection.model}`
531
+ : '',
532
+ invocation.modelSelection && invocation.modelSelection.model
533
+ ? `Model support: ${invocation.modelSelection.supported ? 'supported' : 'unsupported'} (${invocation.modelSelection.reason})`
534
+ : '',
535
+ `Prompt source: ${promptInfo.promptSource}`,
536
+ `Review requested: ${review ? 'yes' : 'no'}`,
537
+ `Interactive requested: ${interactive ? 'yes' : 'no'}`,
538
+ 'Provider execution: skipped',
539
+ 'Writes: none',
540
+ 'Product code: untouched',
541
+ `Candidate docs: ${promptInfo.allowedPaths.join(', ')}`,
542
+ 'Files considered:',
543
+ ...plan.filesConsidered.map((item) => `- ${item.path}: ${item.present ? 'present' : 'absent'}`),
544
+ 'Allowed docs-only paths:',
545
+ ...promptInfo.allowedPaths.map((item) => `- ${item}`),
546
+ 'Next safe commands:',
547
+ '- npx create-quiver ai prepare-context --with-planner --print-prompt',
548
+ '- npx create-quiver ai prepare-context --with-planner --dry-run --review',
549
+ '- npx create-quiver ai prepare-context --with-planner',
550
+ ];
551
+
552
+ return `${lines.filter(Boolean).join('\n')}\n`;
553
+ }
554
+
555
+ function serializeProposalForReview(proposal) {
556
+ return {
557
+ schema_version: proposal.schemaVersion,
558
+ kind: proposal.kind,
559
+ summary: proposal.summary,
560
+ assumptions: proposal.assumptions,
561
+ risks: proposal.risks,
562
+ docs: proposal.docs.map((doc) => ({
563
+ path: doc.path,
564
+ action: doc.action,
565
+ reason: doc.reason,
566
+ content: doc.content,
567
+ assumptions: doc.assumptions,
568
+ risks: doc.risks,
569
+ })),
570
+ omitted_paths: proposal.omittedPaths,
571
+ next_steps: proposal.nextSteps,
572
+ };
573
+ }
574
+
575
+ function createProposalReviewFile(proposal, options = {}) {
576
+ const reviewDir = options.reviewDir || fs.mkdtempSync(path.join(os.tmpdir(), 'quiver-context-review-'));
577
+ const reviewPath = path.join(reviewDir, 'context-proposal.json');
578
+ fs.mkdirSync(reviewDir, { recursive: true });
579
+ fs.writeFileSync(reviewPath, `${JSON.stringify(serializeProposalForReview(proposal), null, 2)}\n`);
580
+ return reviewPath;
581
+ }
582
+
583
+ function makeReviewError(message, reviewPath, cause) {
584
+ const error = new Error(formatError(`${message}\nReview artifact: ${reviewPath}\nNext safe step: edit the artifact into valid proposal JSON or rerun with --with-planner --dry-run.`));
585
+ error.code = cause?.code || 'AI_CONTEXT_REVIEW_FAILED';
586
+ error.cause = cause;
587
+ error.reviewPath = reviewPath;
588
+ return error;
589
+ }
590
+
591
+ function createReviewTextFile(contents, options = {}) {
592
+ const reviewDir = options.reviewDir || fs.mkdtempSync(path.join(os.tmpdir(), 'quiver-review-'));
593
+ const filename = options.reviewFileName || 'review.md';
594
+ const reviewPath = path.join(reviewDir, filename);
595
+ fs.mkdirSync(reviewDir, { recursive: true });
596
+ fs.writeFileSync(reviewPath, String(contents || ''));
597
+ return reviewPath;
598
+ }
599
+
600
+ async function reviewTextWithEditor(repoRoot, contents, options = {}) {
601
+ const reviewPath = createReviewTextFile(contents, options);
602
+ const hasEditorRunner = typeof options.openEditorFn === 'function';
603
+ const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
604
+
605
+ if (!canOpenEditor) {
606
+ throw makeReviewError(`${options.reviewLabel || 'review'} requires an interactive terminal or an injected editor runner.`, reviewPath);
607
+ }
608
+
609
+ const editorResult = hasEditorRunner
610
+ ? options.openEditorFn(reviewPath, { cwd: repoRoot, env: options.env || process.env })
611
+ : openEditor(reviewPath, { cwd: repoRoot, env: options.env || process.env });
612
+
613
+ if (!editorResult || editorResult.ok !== true) {
614
+ throw makeReviewError(editorResult?.reason || `${options.reviewLabel || 'review'} was canceled.`, reviewPath);
615
+ }
616
+
617
+ return {
618
+ reviewPath,
619
+ text: fs.readFileSync(reviewPath, 'utf8'),
620
+ };
621
+ }
622
+
623
+ async function confirmInteractiveAction(message, options = {}) {
624
+ if (options.interactive !== true) {
625
+ return;
626
+ }
627
+
628
+ const ux = options.ux || createUx({
629
+ interactive: true,
630
+ promptConfirm: options.promptConfirm,
631
+ stdinIsTTY: options.stdinIsTTY,
632
+ stdoutIsTTY: options.stdoutIsTTY,
633
+ stderrIsTTY: options.stderrIsTTY,
634
+ write: options.write,
635
+ });
636
+ const confirmed = await ux.promptConfirm(message, {
637
+ initialValue: false,
638
+ });
639
+
640
+ if (!confirmed) {
641
+ const error = new Error(formatError('interactive approval declined. No files were written.'));
642
+ error.code = 'AI_INTERACTIVE_APPROVAL_DECLINED';
643
+ throw error;
644
+ }
645
+ }
646
+
647
+ async function reviewPlannerContextProposal(repoRoot, proposal, options = {}) {
648
+ const reviewPath = createProposalReviewFile(proposal, options);
649
+ const hasEditorRunner = typeof options.openEditorFn === 'function';
650
+ const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
651
+
652
+ if (!canOpenEditor) {
653
+ throw makeReviewError('ai prepare-context review requires an interactive terminal or an injected editor runner.', reviewPath);
654
+ }
655
+
656
+ const editorResult = hasEditorRunner
657
+ ? options.openEditorFn(reviewPath, { cwd: repoRoot, env: options.env || process.env })
658
+ : openEditor(reviewPath, { cwd: repoRoot, env: options.env || process.env });
659
+
660
+ if (!editorResult || editorResult.ok !== true) {
661
+ throw makeReviewError(editorResult?.reason || 'ai prepare-context review was canceled before applying docs.', reviewPath);
662
+ }
663
+
664
+ try {
665
+ return {
666
+ proposal: parseContextProposalOutput(fs.readFileSync(reviewPath, 'utf8')),
667
+ reviewPath,
668
+ };
669
+ } catch (error) {
670
+ throw makeReviewError('edited planner proposal is invalid after review.', reviewPath, error);
671
+ }
672
+ }
673
+
674
+ async function confirmPlannerContextWrites(writePlan, options = {}) {
675
+ const changed = writePlan.filter((item) => item.action !== 'skip').length;
676
+ await confirmInteractiveAction(`Apply ${changed} docs-only context update${changed === 1 ? '' : 's'}?`, options);
677
+ }
678
+
679
+ function buildPlannerContextWritePlan(repoRoot, proposal) {
680
+ const reasonByPath = new Map(proposal.docs.map((doc) => [doc.path, doc.reason]));
681
+ const draftDocs = proposal.docs
682
+ .filter((doc) => doc.action !== 'skip')
683
+ .map((doc) => ({
684
+ path: doc.path,
685
+ content: doc.content,
686
+ }));
687
+
688
+ return buildContextWritePlan(repoRoot, draftDocs).map((item) => ({
689
+ ...item,
690
+ reason: item.action === 'skip' ? item.reason : reasonByPath.get(item.path) || item.reason,
691
+ }));
692
+ }
693
+
316
694
  function writeProviderOutput(result) {
317
695
  if (result.stdout) {
318
696
  process.stdout.write(redactSecrets(result.stdout));
@@ -855,7 +1233,8 @@ function annotateGitHubError(error, scope) {
855
1233
 
856
1234
  async function runOnboard(repoRoot, options = {}) {
857
1235
  const role = normalizeRole(options.role || DEFAULT_ONBOARD_ROLE);
858
- const provider = resolveProviderForProfile(repoRoot, role, options.provider, options.providerExplicit, DEFAULT_ONBOARD_PROVIDER);
1236
+ const runtimeProfile = resolveRuntimeAgentProfile(repoRoot, role, options, DEFAULT_ONBOARD_PROVIDER);
1237
+ const provider = runtimeProfile.provider;
859
1238
  const context = options.context || DEFAULT_ONBOARD_CONTEXT;
860
1239
  const timeoutMs = normalizeTimeout(options.timeout);
861
1240
  const inputText = readTextFile(options.input, repoRoot);
@@ -868,9 +1247,15 @@ async function runOnboard(repoRoot, options = {}) {
868
1247
  prompt,
869
1248
  cwd: repoRoot,
870
1249
  timeoutMs,
1250
+ model: runtimeProfile.model,
1251
+ enforceModelSelection: false,
871
1252
  });
872
1253
  } catch (error) {
873
- throw annotateProviderError(error, 'onboard');
1254
+ if (error.providerResult) {
1255
+ result = error.providerResult;
1256
+ } else {
1257
+ throw annotateProviderError(error, 'onboard');
1258
+ }
874
1259
  }
875
1260
 
876
1261
  if (options.dryRun) {
@@ -881,6 +1266,7 @@ async function runOnboard(repoRoot, options = {}) {
881
1266
  contextPack: context,
882
1267
  invocation,
883
1268
  onboardingPlan: contextInfo.plan,
1269
+ profile: runtimeProfile,
884
1270
  };
885
1271
  process.stdout.write(formatDryRunReport(report));
886
1272
  return report;
@@ -895,23 +1281,39 @@ async function runOnboard(repoRoot, options = {}) {
895
1281
  invocation,
896
1282
  onboardingPlan: contextInfo.plan,
897
1283
  prompt,
1284
+ profile: runtimeProfile,
898
1285
  };
899
1286
  process.stdout.write(formatPromptOnlyReport(report));
900
1287
  return report;
901
1288
  }
902
1289
 
1290
+ const ux = createCommandUx(options);
1291
+ const showProgress = shouldShowHumanProgress(ux, options);
1292
+ writeProgressChecks(
1293
+ ux,
1294
+ showProgress,
1295
+ plannerProgressTitle('Ejecutando onboarding', runtimeProfile),
1296
+ ['Leyendo docs base', 'Detectando estructura', 'Preparando prompt'],
1297
+ );
1298
+
903
1299
  let result;
904
1300
  try {
905
- result = await (options.runProviderFn || runProvider)(provider, {
906
- prompt,
907
- cwd: repoRoot,
908
- timeoutMs,
909
- dryRun: false,
910
- probe: options.probe,
911
- spawn: options.spawn,
912
- tempRoot: options.tempRoot,
913
- tempFileName: options.tempFileName,
914
- tempFilePrefix: options.tempFilePrefix,
1301
+ result = await runProviderWithProgress({
1302
+ ux,
1303
+ enabled: showProgress,
1304
+ run: () => (options.runProviderFn || runProvider)(provider, {
1305
+ prompt,
1306
+ cwd: repoRoot,
1307
+ timeoutMs,
1308
+ dryRun: false,
1309
+ probe: options.probe,
1310
+ spawn: options.spawn,
1311
+ tempRoot: options.tempRoot,
1312
+ tempFileName: options.tempFileName,
1313
+ tempFilePrefix: options.tempFilePrefix,
1314
+ model: runtimeProfile.model,
1315
+ enforceModelSelection: Boolean(runtimeProfile.model),
1316
+ }),
915
1317
  });
916
1318
  } catch (error) {
917
1319
  throw annotateProviderError(error, 'onboard');
@@ -932,11 +1334,16 @@ async function runOnboard(repoRoot, options = {}) {
932
1334
  contextPack: context,
933
1335
  invocation,
934
1336
  onboardingPlan: contextInfo.plan,
1337
+ profile: runtimeProfile,
935
1338
  result,
936
1339
  };
937
1340
  }
938
1341
 
939
1342
  async function runPrepareContext(repoRoot, options = {}) {
1343
+ if (options.withPlanner === true) {
1344
+ return runPrepareContextWithPlanner(repoRoot, options);
1345
+ }
1346
+
940
1347
  const draftPack = buildContextPreparationDrafts(repoRoot);
941
1348
  const writePlan = buildContextWritePlan(repoRoot, draftPack.docs);
942
1349
  const report = {
@@ -998,10 +1405,181 @@ async function runPrepareContext(repoRoot, options = {}) {
998
1405
  };
999
1406
  }
1000
1407
 
1408
+ async function runPrepareContextWithPlanner(repoRoot, options = {}) {
1409
+ const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
1410
+ const runtimeProfile = resolveRuntimeAgentProfile(repoRoot, role, options, DEFAULT_PLAN_PROVIDER);
1411
+ const provider = runtimeProfile.provider;
1412
+ const context = options.context || DEFAULT_PLAN_CONTEXT;
1413
+ const timeoutMs = normalizeTimeout(options.timeout);
1414
+ const draftPack = buildContextPreparationDrafts(repoRoot);
1415
+ const pack = buildContextPackMetadata({
1416
+ role,
1417
+ packName: context,
1418
+ repoRoot,
1419
+ });
1420
+ const promptInfo = buildPrepareContextPlannerPrompt({ pack, draftPack });
1421
+ const prompt = promptInfo.prompt;
1422
+ assertProviderPromptWithinLimit(prompt, options.promptLimitOptions || {});
1423
+ let invocation;
1424
+
1425
+ try {
1426
+ invocation = buildProviderInvocation(provider, {
1427
+ prompt,
1428
+ cwd: repoRoot,
1429
+ timeoutMs,
1430
+ model: runtimeProfile.model,
1431
+ enforceModelSelection: false,
1432
+ });
1433
+ } catch (error) {
1434
+ if (error.providerResult) {
1435
+ result = error.providerResult;
1436
+ } else {
1437
+ throw annotateProviderError(error, 'prepare-context');
1438
+ }
1439
+ }
1440
+
1441
+ if (options.dryRun) {
1442
+ const report = {
1443
+ task: 'prepare-context',
1444
+ mode: 'planner',
1445
+ dryRun: true,
1446
+ provider,
1447
+ role,
1448
+ contextPack: context,
1449
+ invocation,
1450
+ candidateDocs: promptInfo.allowedPaths,
1451
+ plan: draftPack.plan,
1452
+ profile: runtimeProfile,
1453
+ };
1454
+ process.stdout.write(formatPrepareContextPlannerDryRunReport({
1455
+ provider,
1456
+ role,
1457
+ context,
1458
+ invocation,
1459
+ promptInfo,
1460
+ review: options.review === true,
1461
+ interactive: options.interactive === true,
1462
+ }));
1463
+ return report;
1464
+ }
1465
+
1466
+ if (options.printPrompt) {
1467
+ const report = {
1468
+ task: 'prepare-context',
1469
+ provider,
1470
+ role,
1471
+ contextPack: context,
1472
+ invocation,
1473
+ prompt,
1474
+ promptSource: promptInfo.promptSource,
1475
+ profile: runtimeProfile,
1476
+ };
1477
+ process.stdout.write(formatPromptOnlyReport(report));
1478
+ return report;
1479
+ }
1480
+
1481
+ const ux = createCommandUx(options);
1482
+ const showProgress = shouldShowHumanProgress(ux, options);
1483
+ writeProgressChecks(
1484
+ ux,
1485
+ showProgress,
1486
+ plannerProgressTitle('Ejecutando onboarding', runtimeProfile),
1487
+ ['Leyendo docs base', 'Detectando estructura', 'Preparando prompt'],
1488
+ );
1489
+
1490
+ let result;
1491
+ try {
1492
+ result = await runProviderWithProgress({
1493
+ ux,
1494
+ enabled: showProgress,
1495
+ run: () => (options.runProviderFn || runProvider)(provider, {
1496
+ prompt,
1497
+ cwd: repoRoot,
1498
+ timeoutMs,
1499
+ dryRun: false,
1500
+ probe: options.probe,
1501
+ spawn: options.spawn,
1502
+ tempRoot: options.tempRoot,
1503
+ tempFileName: options.tempFileName,
1504
+ tempFilePrefix: options.tempFilePrefix,
1505
+ model: runtimeProfile.model,
1506
+ enforceModelSelection: Boolean(runtimeProfile.model),
1507
+ }),
1508
+ });
1509
+ } catch (error) {
1510
+ throw annotateProviderError(error, 'prepare-context');
1511
+ }
1512
+
1513
+ if (!result.ok) {
1514
+ writeProviderOutput(result);
1515
+ throw annotateProviderError(result.error || new Error('provider run failed'), 'prepare-context');
1516
+ }
1517
+
1518
+ const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
1519
+ let proposal = parseContextProposalOutput(clean.cleanOutput);
1520
+ let reviewPath = '';
1521
+
1522
+ if (options.review === true) {
1523
+ const reviewed = await reviewPlannerContextProposal(repoRoot, proposal, options);
1524
+ proposal = reviewed.proposal;
1525
+ reviewPath = reviewed.reviewPath;
1526
+ }
1527
+
1528
+ const writePlan = buildPlannerContextWritePlan(repoRoot, proposal);
1529
+ await confirmPlannerContextWrites(writePlan, options);
1530
+
1531
+ const lifecycleRun = ensureAiRun(repoRoot, {
1532
+ command: 'ai prepare-context --with-planner',
1533
+ input: options.input || '',
1534
+ runId: options.runId,
1535
+ phase: 'created',
1536
+ });
1537
+ const snapshot = createContextSnapshots(repoRoot, lifecycleRun, writePlan, options.now || new Date());
1538
+ const plannedDocs = writePlan.filter((item) => item.action !== 'skip').map((item) => item.path);
1539
+
1540
+ process.stdout.write(formatContextPreparationReport({
1541
+ dryRun: false,
1542
+ plan: draftPack.plan,
1543
+ writePlan,
1544
+ writtenDocs: plannedDocs,
1545
+ snapshot,
1546
+ }));
1547
+
1548
+ const writtenDocs = writeDraftDocs(writePlan);
1549
+ updateAiRunPhase(repoRoot, lifecycleRun.run_id, 'onboarding-ready', {
1550
+ artifact: snapshot.manifestPath,
1551
+ command: 'ai prepare-context --with-planner',
1552
+ });
1553
+ process.stdout.write(formatContextPreparationReport({
1554
+ dryRun: false,
1555
+ plan: draftPack.plan,
1556
+ writePlan,
1557
+ writtenDocs,
1558
+ snapshot,
1559
+ completed: true,
1560
+ }));
1561
+
1562
+ return {
1563
+ task: 'prepare-context',
1564
+ mode: 'planner',
1565
+ dryRun: false,
1566
+ provider,
1567
+ role,
1568
+ contextPack: context,
1569
+ invocation,
1570
+ proposal,
1571
+ reviewPath,
1572
+ runId: lifecycleRun.run_id,
1573
+ snapshot,
1574
+ writtenDocs,
1575
+ };
1576
+ }
1577
+
1001
1578
  async function runPlan(repoRoot, options = {}) {
1002
1579
  const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
1003
1580
  const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
1004
- const provider = resolveProviderForProfile(repoRoot, role, options.provider, options.providerExplicit, DEFAULT_PLAN_PROVIDER);
1581
+ const runtimeProfile = resolveRuntimeAgentProfile(repoRoot, role, options, DEFAULT_PLAN_PROVIDER);
1582
+ const provider = runtimeProfile.provider;
1005
1583
  const context = options.context || DEFAULT_PLAN_CONTEXT;
1006
1584
  const timeoutMs = normalizeTimeout(options.timeout);
1007
1585
  let inputPath = options.input || '';
@@ -1103,9 +1681,15 @@ async function runPlan(repoRoot, options = {}) {
1103
1681
  prompt,
1104
1682
  cwd: repoRoot,
1105
1683
  timeoutMs,
1684
+ model: runtimeProfile.model,
1685
+ enforceModelSelection: false,
1106
1686
  });
1107
1687
  } catch (error) {
1108
- throw annotateProviderError(error, 'plan', phase);
1688
+ if (error.providerResult) {
1689
+ result = error.providerResult;
1690
+ } else {
1691
+ throw annotateProviderError(error, 'plan', phase);
1692
+ }
1109
1693
  }
1110
1694
 
1111
1695
  if (options.dryRun) {
@@ -1116,8 +1700,18 @@ async function runPlan(repoRoot, options = {}) {
1116
1700
  contextPack: contextInfo.pack.packName,
1117
1701
  phase,
1118
1702
  invocation,
1703
+ profile: runtimeProfile,
1119
1704
  };
1120
1705
  process.stdout.write(formatDryRunReport(report));
1706
+ if (options.withPlanner === true) {
1707
+ process.stdout.write('Planner mode: already active for ai plan; --with-planner is accepted for UX consistency.\n');
1708
+ }
1709
+ if (options.review === true) {
1710
+ process.stdout.write('Review requested: provider output will be opened for review before saving the draft in live mode.\n');
1711
+ }
1712
+ if (options.interactive === true) {
1713
+ process.stdout.write('Interactive requested: live mode will ask before saving the draft.\n');
1714
+ }
1121
1715
  return report;
1122
1716
  }
1123
1717
 
@@ -1130,23 +1724,39 @@ async function runPlan(repoRoot, options = {}) {
1130
1724
  phase,
1131
1725
  invocation,
1132
1726
  prompt,
1727
+ profile: runtimeProfile,
1133
1728
  };
1134
1729
  process.stdout.write(formatPromptOnlyReport(report));
1135
1730
  return report;
1136
1731
  }
1137
1732
 
1733
+ const ux = createCommandUx(options);
1734
+ const showProgress = shouldShowHumanProgress(ux, options);
1735
+ writeProgressChecks(
1736
+ ux,
1737
+ showProgress,
1738
+ plannerProgressTitle(`Ejecutando plan ${phase}`, runtimeProfile),
1739
+ ['Leyendo entrada', 'Preparando contexto', 'Preparando prompt'],
1740
+ );
1741
+
1138
1742
  let result;
1139
1743
  try {
1140
- result = await (options.runProviderFn || runProvider)(provider, {
1141
- prompt,
1142
- cwd: repoRoot,
1143
- timeoutMs,
1144
- dryRun: false,
1145
- probe: options.probe,
1146
- spawn: options.spawn,
1147
- tempRoot: options.tempRoot,
1148
- tempFileName: options.tempFileName,
1149
- tempFilePrefix: options.tempFilePrefix,
1744
+ result = await runProviderWithProgress({
1745
+ ux,
1746
+ enabled: showProgress,
1747
+ run: () => (options.runProviderFn || runProvider)(provider, {
1748
+ prompt,
1749
+ cwd: repoRoot,
1750
+ timeoutMs,
1751
+ dryRun: false,
1752
+ probe: options.probe,
1753
+ spawn: options.spawn,
1754
+ tempRoot: options.tempRoot,
1755
+ tempFileName: options.tempFileName,
1756
+ tempFilePrefix: options.tempFilePrefix,
1757
+ model: runtimeProfile.model,
1758
+ enforceModelSelection: Boolean(runtimeProfile.model),
1759
+ }),
1150
1760
  });
1151
1761
  } catch (error) {
1152
1762
  throw annotateProviderError(error, 'plan', phase);
@@ -1157,13 +1767,27 @@ async function runPlan(repoRoot, options = {}) {
1157
1767
  throw annotateProviderError(result.error || new Error('provider run failed'), 'plan', phase);
1158
1768
  }
1159
1769
 
1770
+ const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
1771
+ let cleanOutput = clean.cleanOutput;
1772
+ let reviewPath = '';
1773
+
1774
+ if (options.review === true) {
1775
+ const reviewed = await reviewTextWithEditor(repoRoot, cleanOutput, {
1776
+ ...options,
1777
+ reviewFileName: `ai-plan-${phase}-draft.md`,
1778
+ reviewLabel: `ai plan --phase ${phase} review`,
1779
+ });
1780
+ cleanOutput = reviewed.text;
1781
+ reviewPath = reviewed.reviewPath;
1782
+ }
1783
+
1784
+ await confirmInteractiveAction(`Save ${phase} planner draft?`, options);
1160
1785
  const lifecycleRun = ensureAiRun(repoRoot, {
1161
1786
  command: `ai plan --phase ${phase}`,
1162
1787
  input: inputPath,
1163
1788
  runId: options.runId,
1164
1789
  });
1165
- const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
1166
- writeCleanProviderOutput(clean);
1790
+ writeCleanProviderOutput({ cleanOutput });
1167
1791
  const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, `ai-plan-${phase}`, result, {
1168
1792
  metadata: {
1169
1793
  phase,
@@ -1174,10 +1798,11 @@ async function runPlan(repoRoot, options = {}) {
1174
1798
  input_compaction: inputCompaction,
1175
1799
  },
1176
1800
  });
1177
- const draft = savePlannerDraft(repoRoot, phase, inputPath, clean.cleanOutput, {
1801
+ const draft = savePlannerDraft(repoRoot, phase, inputPath, cleanOutput, {
1178
1802
  rawArtifactPath: rawArtifact.path,
1179
1803
  outputSource: clean.source,
1180
1804
  inputCompaction,
1805
+ reviewPath,
1181
1806
  });
1182
1807
  updateAiRunPhase(repoRoot, lifecycleRun.run_id, phase === 'acceptance' ? 'acceptance-draft' : 'technical-plan-draft', {
1183
1808
  artifact: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
@@ -1192,12 +1817,14 @@ async function runPlan(repoRoot, options = {}) {
1192
1817
  phase,
1193
1818
  invocation,
1194
1819
  result,
1820
+ reviewPath,
1195
1821
  };
1196
1822
  }
1197
1823
 
1198
1824
  async function runReviewPlan(repoRoot, options = {}) {
1199
1825
  const role = 'planner';
1200
- const provider = resolveProviderForProfile(repoRoot, 'reviewer', options.provider, options.providerExplicit, DEFAULT_PLAN_PROVIDER);
1826
+ const runtimeProfile = resolveRuntimeAgentProfile(repoRoot, 'reviewer', options, DEFAULT_PLAN_PROVIDER);
1827
+ const provider = runtimeProfile.provider;
1201
1828
  const context = options.context || DEFAULT_PLAN_CONTEXT;
1202
1829
  const timeoutMs = normalizeTimeout(options.timeout);
1203
1830
  const resolved = resolveTechnicalPlanReviewInput(repoRoot, options.input || undefined);
@@ -1221,9 +1848,15 @@ async function runReviewPlan(repoRoot, options = {}) {
1221
1848
  prompt: built.prompt,
1222
1849
  cwd: repoRoot,
1223
1850
  timeoutMs,
1851
+ model: runtimeProfile.model,
1852
+ enforceModelSelection: false,
1224
1853
  });
1225
1854
  } catch (error) {
1226
- throw annotateProviderError(error, 'review-plan');
1855
+ if (error.providerResult) {
1856
+ result = error.providerResult;
1857
+ } else {
1858
+ throw annotateProviderError(error, 'review-plan');
1859
+ }
1227
1860
  }
1228
1861
 
1229
1862
  if (options.dryRun) {
@@ -1237,6 +1870,7 @@ async function runReviewPlan(repoRoot, options = {}) {
1237
1870
  inputPath,
1238
1871
  inputKind: resolved.kind,
1239
1872
  inputVersion: resolved.version,
1873
+ profile: runtimeProfile,
1240
1874
  };
1241
1875
  process.stdout.write(formatDryRunReport({
1242
1876
  task: 'review-plan',
@@ -1268,23 +1902,39 @@ async function runReviewPlan(repoRoot, options = {}) {
1268
1902
  inputPath,
1269
1903
  inputKind: resolved.kind,
1270
1904
  inputVersion: resolved.version,
1905
+ profile: runtimeProfile,
1271
1906
  };
1272
1907
  process.stdout.write(formatPromptOnlyReport(report));
1273
1908
  return report;
1274
1909
  }
1275
1910
 
1911
+ const ux = createCommandUx(options);
1912
+ const showProgress = shouldShowHumanProgress(ux, options);
1913
+ writeProgressChecks(
1914
+ ux,
1915
+ showProgress,
1916
+ plannerProgressTitle('Ejecutando revisión del plan', runtimeProfile),
1917
+ ['Leyendo plan técnico', 'Preparando contexto', 'Preparando prompt'],
1918
+ );
1919
+
1276
1920
  let result;
1277
1921
  try {
1278
- result = await (options.runProviderFn || runProvider)(provider, {
1279
- prompt: built.prompt,
1280
- cwd: repoRoot,
1281
- timeoutMs,
1282
- dryRun: false,
1283
- probe: options.probe,
1284
- spawn: options.spawn,
1285
- tempRoot: options.tempRoot,
1286
- tempFileName: options.tempFileName,
1287
- tempFilePrefix: options.tempFilePrefix,
1922
+ result = await runProviderWithProgress({
1923
+ ux,
1924
+ enabled: showProgress,
1925
+ run: () => (options.runProviderFn || runProvider)(provider, {
1926
+ prompt: built.prompt,
1927
+ cwd: repoRoot,
1928
+ timeoutMs,
1929
+ dryRun: false,
1930
+ probe: options.probe,
1931
+ spawn: options.spawn,
1932
+ tempRoot: options.tempRoot,
1933
+ tempFileName: options.tempFileName,
1934
+ tempFilePrefix: options.tempFilePrefix,
1935
+ model: runtimeProfile.model,
1936
+ enforceModelSelection: Boolean(runtimeProfile.model),
1937
+ }),
1288
1938
  });
1289
1939
  } catch (error) {
1290
1940
  throw annotateProviderError(error, 'review-plan');
@@ -1342,7 +1992,8 @@ async function runReviewPlan(repoRoot, options = {}) {
1342
1992
 
1343
1993
  async function runRepairPlan(repoRoot, options = {}) {
1344
1994
  const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
1345
- const provider = resolveProviderForProfile(repoRoot, role, options.provider, options.providerExplicit, DEFAULT_PLAN_PROVIDER);
1995
+ const runtimeProfile = resolveRuntimeAgentProfile(repoRoot, role, options, DEFAULT_PLAN_PROVIDER);
1996
+ const provider = runtimeProfile.provider;
1346
1997
  const context = options.context || DEFAULT_PLAN_CONTEXT;
1347
1998
  const timeoutMs = normalizeTimeout(options.timeout);
1348
1999
  const source = resolveApprovedTechnicalPlanForRepair(repoRoot, options.input || '');
@@ -1362,9 +2013,15 @@ async function runRepairPlan(repoRoot, options = {}) {
1362
2013
  prompt: built.prompt,
1363
2014
  cwd: repoRoot,
1364
2015
  timeoutMs,
2016
+ model: runtimeProfile.model,
2017
+ enforceModelSelection: false,
1365
2018
  });
1366
2019
  } catch (error) {
1367
- throw annotateProviderError(error, 'repair-plan');
2020
+ if (error.providerResult) {
2021
+ providerResult = error.providerResult;
2022
+ } else {
2023
+ throw annotateProviderError(error, 'repair-plan');
2024
+ }
1368
2025
  }
1369
2026
 
1370
2027
  if (options.dryRun) {
@@ -1375,6 +2032,7 @@ async function runRepairPlan(repoRoot, options = {}) {
1375
2032
  contextPack: built.pack.packName,
1376
2033
  phase: 'technical-plan',
1377
2034
  invocation,
2035
+ profile: runtimeProfile,
1378
2036
  };
1379
2037
  process.stdout.write(formatDryRunReport(report));
1380
2038
  process.stdout.write(`Source approved artifact: ${source.path}\n`);
@@ -1394,23 +2052,39 @@ async function runRepairPlan(repoRoot, options = {}) {
1394
2052
  inputPath: source.path,
1395
2053
  inputKind: 'approved',
1396
2054
  inputVersion: source.approval.meta?.approved?.version || null,
2055
+ profile: runtimeProfile,
1397
2056
  };
1398
2057
  process.stdout.write(formatPromptOnlyReport(report));
1399
2058
  return report;
1400
2059
  }
1401
2060
 
2061
+ const ux = createCommandUx(options);
2062
+ const showProgress = shouldShowHumanProgress(ux, options);
2063
+ writeProgressChecks(
2064
+ ux,
2065
+ showProgress,
2066
+ plannerProgressTitle('Ejecutando reparación del plan', runtimeProfile),
2067
+ ['Leyendo plan aprobado', 'Preparando contexto', 'Preparando prompt'],
2068
+ );
2069
+
1402
2070
  let providerResult;
1403
2071
  try {
1404
- providerResult = await (options.runProviderFn || runProvider)(provider, {
1405
- prompt: built.prompt,
1406
- cwd: repoRoot,
1407
- timeoutMs,
1408
- dryRun: false,
1409
- probe: options.probe,
1410
- spawn: options.spawn,
1411
- tempRoot: options.tempRoot,
1412
- tempFileName: options.tempFileName,
1413
- tempFilePrefix: options.tempFilePrefix,
2072
+ providerResult = await runProviderWithProgress({
2073
+ ux,
2074
+ enabled: showProgress,
2075
+ run: () => (options.runProviderFn || runProvider)(provider, {
2076
+ prompt: built.prompt,
2077
+ cwd: repoRoot,
2078
+ timeoutMs,
2079
+ dryRun: false,
2080
+ probe: options.probe,
2081
+ spawn: options.spawn,
2082
+ tempRoot: options.tempRoot,
2083
+ tempFileName: options.tempFileName,
2084
+ tempFilePrefix: options.tempFilePrefix,
2085
+ model: runtimeProfile.model,
2086
+ enforceModelSelection: Boolean(runtimeProfile.model),
2087
+ }),
1414
2088
  });
1415
2089
  } catch (error) {
1416
2090
  throw annotateProviderError(error, 'repair-plan');
@@ -1746,10 +2420,13 @@ function runLifecycleRun(repoRoot, options = {}) {
1746
2420
 
1747
2421
  function formatAgentProfile(profile) {
1748
2422
  const lines = [
2423
+ `ID: ${profile.id || 'default'}`,
1749
2424
  `Role: ${profile.role}`,
1750
2425
  `Provider: ${profile.provider}`,
1751
2426
  `Model: ${profile.model || '(not set)'}`,
1752
2427
  `Label: ${profile.label || '(not set)'}`,
2428
+ `Display name: ${resolveAgentProfileDisplayName(profile) || '(not set)'}`,
2429
+ `Default: ${profile.default === true ? 'yes' : 'no'}`,
1753
2430
  `Context: ${profile.context || '(not set)'}`,
1754
2431
  `Updated: ${profile.updated_at}`,
1755
2432
  ];
@@ -1765,7 +2442,9 @@ function formatAgentProfileList(profiles) {
1765
2442
  }
1766
2443
  const model = item.profile.model ? ` model=${item.profile.model}` : '';
1767
2444
  const label = item.profile.label ? ` label=${item.profile.label}` : '';
1768
- lines.push(`- ${item.role}: provider=${item.profile.provider}${model}${label}`);
2445
+ const displayName = resolveAgentProfileDisplayName(item.profile);
2446
+ const alternatives = item.profiles.length > 1 ? ` options=${item.profiles.length}` : '';
2447
+ lines.push(`- ${item.role}: provider=${item.profile.provider}${model}${label} displayName=${displayName}${alternatives}`);
1769
2448
  }
1770
2449
  return `${lines.join('\n')}\n`;
1771
2450
  }
@@ -1798,6 +2477,9 @@ function runAgent(repoRoot, options = {}) {
1798
2477
  if (options.dryRun) {
1799
2478
  const preview = buildAgentProfileState(repoRoot, options.role, {
1800
2479
  context: options.context,
2480
+ default: options.defaultProfile,
2481
+ displayName: options.displayName,
2482
+ id: options.id,
1801
2483
  label: options.label,
1802
2484
  model: options.model,
1803
2485
  provider: options.provider,
@@ -1813,6 +2495,9 @@ function runAgent(repoRoot, options = {}) {
1813
2495
  }
1814
2496
  const result = setAgentProfile(repoRoot, options.role, {
1815
2497
  context: options.context,
2498
+ default: options.defaultProfile,
2499
+ displayName: options.displayName,
2500
+ id: options.id,
1816
2501
  label: options.label,
1817
2502
  model: options.model,
1818
2503
  provider: options.provider,
@@ -1832,10 +2517,14 @@ function runAgent(repoRoot, options = {}) {
1832
2517
  if (!options.role) {
1833
2518
  throw new Error(formatError('missing agent role. Use: npx create-quiver ai agent show <planner|executor|reviewer|doctor>'));
1834
2519
  }
1835
- const profile = getAgentProfile(repoRoot, options.role);
2520
+ const profile = options.id
2521
+ ? getAgentProfileById(repoRoot, options.role, options.id)
2522
+ : getAgentProfile(repoRoot, options.role);
1836
2523
  if (!profile) {
1837
2524
  throw new Error(formatActionableError({
1838
- failure: `agent profile '${options.role}' is not configured.`,
2525
+ failure: options.id
2526
+ ? `agent profile '${options.role}/${options.id}' is not configured.`
2527
+ : `agent profile '${options.role}' is not configured.`,
1839
2528
  impact: 'Quiver will fall back to default provider behavior and may use the wrong model/cost profile.',
1840
2529
  fix: `Configure the ${options.role} profile with a supported provider and optional model label.`,
1841
2530
  nextCommand: `npx create-quiver ai agent set ${options.role} --provider <provider> --model <label>`,
@@ -1896,20 +2585,32 @@ async function runGitHubTask(repoRoot, options = {}, mode = 'pr') {
1896
2585
  async function runPr(repoRoot, options = {}) {
1897
2586
  const dryRun = options.dryRun === true;
1898
2587
  const create = options.create === true;
2588
+ const ux = createCommandUx(options);
2589
+ const showProgress = create && !dryRun && shouldShowHumanProgress(ux, options);
2590
+ if (showProgress) {
2591
+ ux.heading('Creando PR con gh');
2592
+ }
1899
2593
  let preflight;
1900
2594
 
1901
2595
  try {
1902
- preflight = await (options.preflightFn || preflightGitHubPr)(repoRoot, {
1903
- remote: options.remote,
1904
- sshHostAlias: options.sshHostAlias,
1905
- identityFile: options.identityFile,
1906
- gitFlowGuidePath: options.gitFlowGuidePath,
1907
- ghCommand: options.ghCommand,
1908
- ghProbe: options.ghProbe,
1909
- ghAuthProbe: options.ghAuthProbe,
1910
- ghProbeArgs: options.ghProbeArgs,
1911
- ghAuthArgs: options.ghAuthArgs,
1912
- blockedBranches: options.blockedBranches,
2596
+ preflight = await runProviderWithProgress({
2597
+ ux,
2598
+ enabled: showProgress,
2599
+ message: 'Ejecutando preflight de GitHub...',
2600
+ successMessage: 'Preflight de GitHub listo',
2601
+ failureMessage: 'Falló preflight de GitHub',
2602
+ run: () => (options.preflightFn || preflightGitHubPr)(repoRoot, {
2603
+ remote: options.remote,
2604
+ sshHostAlias: options.sshHostAlias,
2605
+ identityFile: options.identityFile,
2606
+ gitFlowGuidePath: options.gitFlowGuidePath,
2607
+ ghCommand: options.ghCommand,
2608
+ ghProbe: options.ghProbe,
2609
+ ghAuthProbe: options.ghAuthProbe,
2610
+ ghProbeArgs: options.ghProbeArgs,
2611
+ ghAuthArgs: options.ghAuthArgs,
2612
+ blockedBranches: options.blockedBranches,
2613
+ }),
1913
2614
  });
1914
2615
  } catch (error) {
1915
2616
  throw annotateGitHubError(error, 'pr');
@@ -1924,10 +2625,38 @@ async function runPr(repoRoot, options = {}) {
1924
2625
  prBodyPath: options.prBodyPath,
1925
2626
  title: options.title,
1926
2627
  });
2628
+ if (showProgress) {
2629
+ ux.check('Cuerpo del PR preparado');
2630
+ }
1927
2631
  } catch (error) {
1928
2632
  throw annotateGitHubError(error, 'pr');
1929
2633
  }
1930
2634
 
2635
+ if (options.review === true) {
2636
+ const hasEditorRunner = typeof options.openEditorFn === 'function';
2637
+ const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
2638
+ if (!canOpenEditor) {
2639
+ throw annotateGitHubError(makeReviewError('ai pr --review requires an interactive terminal or an injected editor runner.', plan.prBodyPath), 'pr');
2640
+ }
2641
+ const editorResult = hasEditorRunner
2642
+ ? options.openEditorFn(plan.prBodyPath, { cwd: repoRoot, env: options.env || process.env })
2643
+ : openEditor(plan.prBodyPath, { cwd: repoRoot, env: options.env || process.env });
2644
+ if (!editorResult || editorResult.ok !== true) {
2645
+ throw annotateGitHubError(makeReviewError(editorResult?.reason || 'ai pr review was canceled.', plan.prBodyPath), 'pr');
2646
+ }
2647
+ try {
2648
+ plan = buildPrCreatePlan(repoRoot, preflight, {
2649
+ baseBranch: options.baseBranch,
2650
+ ghCommand: options.ghCommand,
2651
+ input: options.input,
2652
+ prBodyPath: options.prBodyPath,
2653
+ title: options.title,
2654
+ });
2655
+ } catch (error) {
2656
+ throw annotateGitHubError(error, 'pr');
2657
+ }
2658
+ }
2659
+
1931
2660
  if (dryRun || !create) {
1932
2661
  process.stdout.write(formatPrCreateReport({ preflight, plan }, { dryRun, create }));
1933
2662
  return {
@@ -1939,10 +2668,19 @@ async function runPr(repoRoot, options = {}) {
1939
2668
  };
1940
2669
  }
1941
2670
 
2671
+ await confirmInteractiveAction(`Create GitHub PR '${plan.title}'?`, options);
2672
+
1942
2673
  let result;
1943
2674
  try {
1944
- result = runGhPrCreate(plan, {
1945
- ghCreateRunner: options.ghCreateRunner,
2675
+ result = await runProviderWithProgress({
2676
+ ux,
2677
+ enabled: showProgress,
2678
+ message: 'Creando PR en GitHub...',
2679
+ successMessage: 'PR creado',
2680
+ failureMessage: 'Falló creación del PR',
2681
+ run: () => runGhPrCreate(plan, {
2682
+ ghCreateRunner: options.ghCreateRunner,
2683
+ }),
1946
2684
  });
1947
2685
  } catch (error) {
1948
2686
  throw annotateGitHubError(error, 'pr');