create-quiver 0.15.1 → 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 (47) hide show
  1. package/.github/workflows/ci.yml +3 -0
  2. package/README.md +2 -0
  3. package/README_FOR_AI.md +5 -3
  4. package/docs/CLI_UX_GUIDE.md +15 -1
  5. package/docs/COMMANDS.md.template +2 -1
  6. package/docs/WORKFLOW.md.template +2 -2
  7. package/docs/reference/commands.md +10 -4
  8. package/package.json +1 -1
  9. package/specs/quiver-v33-approval-ux-and-planner-progress/EVIDENCE_REPORT.md +93 -0
  10. package/specs/quiver-v33-approval-ux-and-planner-progress/EXECUTION_PLAN.md +83 -0
  11. package/specs/quiver-v33-approval-ux-and-planner-progress/SPEC.md +158 -0
  12. package/specs/quiver-v33-approval-ux-and-planner-progress/STATUS.md +31 -0
  13. package/specs/quiver-v33-approval-ux-and-planner-progress/pr.md +109 -0
  14. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/CLOSURE_BRIEF.md +30 -0
  15. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/EXECUTION_BRIEF.md +56 -0
  16. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-00-approval-ux-foundation/slice.json +65 -0
  17. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/CLOSURE_BRIEF.md +19 -0
  18. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/EXECUTION_BRIEF.md +51 -0
  19. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-01-approval-candidates-model/slice.json +74 -0
  20. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/CLOSURE_BRIEF.md +20 -0
  21. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/EXECUTION_BRIEF.md +53 -0
  22. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-02-approve-interactive-selection/slice.json +79 -0
  23. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/CLOSURE_BRIEF.md +19 -0
  24. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/EXECUTION_BRIEF.md +50 -0
  25. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-03-technical-plan-review-decision-data/slice.json +75 -0
  26. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/CLOSURE_BRIEF.md +19 -0
  27. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/EXECUTION_BRIEF.md +51 -0
  28. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-04-revise-input-guardrails/slice.json +73 -0
  29. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/CLOSURE_BRIEF.md +20 -0
  30. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/EXECUTION_BRIEF.md +51 -0
  31. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-05-provider-progress-alignment/slice.json +91 -0
  32. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/CLOSURE_BRIEF.md +20 -0
  33. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/EXECUTION_BRIEF.md +53 -0
  34. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-06-workflow-surface-integration/slice.json +87 -0
  35. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/CLOSURE_BRIEF.md +21 -0
  36. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/EXECUTION_BRIEF.md +55 -0
  37. package/specs/quiver-v33-approval-ux-and-planner-progress/slices/slice-07-docs-tests-release-readiness/slice.json +97 -0
  38. package/src/create-quiver/commands/ai.js +112 -12
  39. package/src/create-quiver/commands/flow.js +74 -12
  40. package/src/create-quiver/commands/spec.js +8 -1
  41. package/src/create-quiver/index.js +10 -2
  42. package/src/create-quiver/lib/ai/approval-candidates.js +86 -0
  43. package/src/create-quiver/lib/ai/plan-review.js +77 -1
  44. package/src/create-quiver/lib/ai/run-state.js +21 -4
  45. package/src/create-quiver/lib/approvals.js +103 -0
  46. package/src/create-quiver/lib/git.js +13 -4
  47. package/src/create-quiver/lib/paths.js +45 -3
@@ -85,6 +85,10 @@ const {
85
85
  savePlannerDraft,
86
86
  summarizePlannerApproval,
87
87
  } = require('../lib/approvals');
88
+ const {
89
+ buildApprovalCandidateReport,
90
+ formatApprovalDecisionLines,
91
+ } = require('../lib/ai/approval-candidates');
88
92
  const { assertPlannerPhaseReady, getPlannerPhaseDetails, normalizePlannerPhase, PlannerPhaseError } = require('../lib/ai/phase-gates');
89
93
  const { collectActiveSliceState, resolveProjectState } = require('../lib/project-state-resolver');
90
94
 
@@ -1224,7 +1228,11 @@ function formatApprovalStatusReport(repoRoot) {
1224
1228
  for (const phase of PLANNER_APPROVAL_PHASES) {
1225
1229
  const summary = summarizePlannerApproval(repoRoot, phase).trimEnd();
1226
1230
  const relation = classifyGlobalApprovalRelation(readPhaseApproval(repoRoot, phase), runApprovalRows);
1227
- sections.push(`${summary}\nRun relation: ${relation}`);
1231
+ const candidates = buildApprovalCandidateReport(repoRoot, phase);
1232
+ const decisionLines = formatApprovalDecisionLines(candidates)
1233
+ .map((line) => `- ${line}`)
1234
+ .join('\n');
1235
+ sections.push(`${summary}\nRun relation: ${relation}${decisionLines ? `\nApproval candidates:\n${decisionLines}` : ''}`);
1228
1236
  }
1229
1237
  sections.push(summarizePlanReview(repoRoot).trimEnd());
1230
1238
  return `${sections.join('\n\n')}\n`;
@@ -1657,7 +1665,7 @@ async function runPlan(repoRoot, options = {}) {
1657
1665
 
1658
1666
  if (options.revise === true) {
1659
1667
  if (!inputPath) {
1660
- throw new Error(formatError(`missing feedback input file for ai revise phase '${phase}'`));
1668
+ throw new Error(formatError(`missing feedback input file for ai revise phase '${phase}'. Use: npx create-quiver ai revise --phase ${phase} --input <feedback.md> --dry-run`));
1661
1669
  }
1662
1670
  const feedbackText = readTextFile(inputPath, repoRoot);
1663
1671
  const revisionInput = buildRevisionInput({
@@ -2190,20 +2198,112 @@ async function runRevise(repoRoot, options = {}) {
2190
2198
  });
2191
2199
  }
2192
2200
 
2201
+ function formatApprovalCandidateHint(candidate) {
2202
+ const parts = [];
2203
+ if (candidate.current) {
2204
+ parts.push('current');
2205
+ }
2206
+ if (candidate.created_at) {
2207
+ parts.push(candidate.created_at);
2208
+ }
2209
+ if (candidate.review?.recommendation) {
2210
+ parts.push(`review=${candidate.review.recommendation}`);
2211
+ }
2212
+ if (candidate.review?.required_fixes_count) {
2213
+ parts.push(`required fixes=${candidate.review.required_fixes_count}`);
2214
+ }
2215
+ if (candidate.review?.optional_hardening_count) {
2216
+ parts.push(`optional=${candidate.review.optional_hardening_count}`);
2217
+ }
2218
+ if (candidate.review?.risks_count) {
2219
+ parts.push(`risks=${candidate.review.risks_count}`);
2220
+ }
2221
+ parts.push(candidate.reason);
2222
+ return parts.filter(Boolean).join(', ');
2223
+ }
2224
+
2225
+ function approvalSelectionOptions(report) {
2226
+ return report.candidates.map((candidate) => ({
2227
+ label: `${candidate.label}${candidate.recommended ? ' (recommended)' : candidate.current ? ' (current)' : ' (history)'}`,
2228
+ value: String(candidate.version || ''),
2229
+ hint: formatApprovalCandidateHint(candidate),
2230
+ default: candidate.recommended === true,
2231
+ raw: candidate,
2232
+ }));
2233
+ }
2234
+
2235
+ async function resolveApprovalVersion(repoRoot, phase, options = {}) {
2236
+ if (options.version) {
2237
+ return options.version;
2238
+ }
2239
+
2240
+ const canPrompt = isInteractiveAgentPromptAvailable(options);
2241
+ const shouldPrompt = options.interactive === true || canPrompt;
2242
+ const report = buildApprovalCandidateReport(repoRoot, phase);
2243
+
2244
+ if (!shouldPrompt || !canPrompt) {
2245
+ const recommended = report.recommended?.version || report.latest_version || '<n>';
2246
+ throw new Error(formatActionableError({
2247
+ failure: `ai approve --phase ${phase} requires --version <n> when prompts are not available.`,
2248
+ impact: 'Quiver cannot safely guess which saved planner draft the human approved.',
2249
+ fix: 'Review drafts with `npx create-quiver ai approvals`, then pass the version explicitly.',
2250
+ nextCommand: `npx create-quiver ai approve --phase ${phase} --version ${recommended}`,
2251
+ }));
2252
+ }
2253
+
2254
+ if (report.candidates.length === 0) {
2255
+ throw new Error(formatActionableError({
2256
+ failure: `ai approve --phase ${phase} has no saved drafts to approve.`,
2257
+ impact: 'There is no planner artifact that can pass the approval gate.',
2258
+ fix: `Generate a ${phase} draft first.`,
2259
+ nextCommand: `npx create-quiver ai plan --phase ${phase}${phase === 'acceptance' ? ' --input <requirements.md>' : ''} --dry-run`,
2260
+ }));
2261
+ }
2262
+
2263
+ const selected = await selectOption(`¿Qué ${phase} draft querés aprobar?`, approvalSelectionOptions(report), {
2264
+ env: options.env,
2265
+ error: options.error,
2266
+ input: options.input,
2267
+ interactive: true,
2268
+ noColor: options.noColor,
2269
+ output: options.output,
2270
+ prompts: options.prompts,
2271
+ promptSelect: options.promptSelect,
2272
+ stdinIsTTY: options.stdinIsTTY,
2273
+ stdoutIsTTY: options.stdoutIsTTY,
2274
+ stderrIsTTY: options.stderrIsTTY,
2275
+ defaultValue: report.recommended?.version ? String(report.recommended.version) : undefined,
2276
+ flag: '--version',
2277
+ name: `${phase} approval version`,
2278
+ });
2279
+
2280
+ const candidate = selected.raw;
2281
+ if (!candidate?.approvable) {
2282
+ throw new Error(formatActionableError({
2283
+ failure: `${phase} draft ${selected.label} is not approvable.`,
2284
+ impact: candidate?.review?.blocking
2285
+ ? 'The current review gate blocks technical-plan approval.'
2286
+ : 'Quiver only approves the current eligible draft version.',
2287
+ fix: candidate?.reason || 'Inspect planner approvals before approving.',
2288
+ nextCommand: candidate?.next_command || `npx create-quiver ai approvals`,
2289
+ }));
2290
+ }
2291
+
2292
+ return selected.value;
2293
+ }
2294
+
2193
2295
  async function runApprove(repoRoot, options = {}) {
2194
2296
  const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
2195
2297
  if (phase === 'spec') {
2196
2298
  throw new Error(formatError(`ai approve does not support phase '${phase}'`));
2197
2299
  }
2198
2300
 
2199
- if (!options.version) {
2200
- throw new Error(formatError(`ai approve --phase ${phase} requires --version <n>. Review drafts with \`npx create-quiver ai approvals\`.`));
2201
- }
2202
-
2203
2301
  if (options.input) {
2204
2302
  throw new Error(formatError(`ai approve --phase ${phase} approves saved draft versions only. Use \`npx create-quiver ai revise --phase ${phase} --input ${options.input}\` to create a new draft first.`));
2205
2303
  }
2206
2304
 
2305
+ const version = await resolveApprovalVersion(repoRoot, phase, options);
2306
+
2207
2307
  if (phase === 'technical-plan') {
2208
2308
  const review = readPlanReview(repoRoot);
2209
2309
  if (review.status !== 'unapproved' && review.status !== 'reviewed') {
@@ -2214,24 +2314,24 @@ async function runApprove(repoRoot, options = {}) {
2214
2314
  const requiredFixes = Array.isArray(result.required_fixes) ? result.required_fixes.length : 0;
2215
2315
  throw new Error(formatError(`ai approve --phase technical-plan is blocked by plan review; approval recommendation is ${result.approval_recommendation}. Required fixes: ${requiredFixes}. Next command: ${result.next_command}`));
2216
2316
  }
2217
- assertTechnicalPlanDraftHasSpecContract(repoRoot, options.version);
2317
+ assertTechnicalPlanDraftHasSpecContract(repoRoot, version);
2218
2318
  }
2219
2319
 
2220
2320
  const inputText = '';
2221
2321
 
2222
2322
  if (options.dryRun) {
2223
- process.stdout.write(formatApprovalDryRunResult({ phase, input: options.input, version: options.version }));
2323
+ process.stdout.write(formatApprovalDryRunResult({ phase, input: options.input, version }));
2224
2324
  return {
2225
2325
  task: 'approve',
2226
2326
  phase,
2227
2327
  input: options.input,
2228
- version: options.version || null,
2328
+ version: version || null,
2229
2329
  dryRun: true,
2230
2330
  };
2231
2331
  }
2232
2332
 
2233
2333
  const result = approvePlannerPhase(repoRoot, phase, options.input || '', inputText, {
2234
- version: options.version || undefined,
2334
+ version: version || undefined,
2235
2335
  });
2236
2336
  const lifecycleRun = ensureAiRun(repoRoot, {
2237
2337
  command: `ai approve --phase ${phase}`,
@@ -2241,7 +2341,7 @@ async function runApprove(repoRoot, options = {}) {
2241
2341
  recordAiRunApproval(repoRoot, lifecycleRun.run_id, {
2242
2342
  artifact: path.relative(repoRoot, result.filePath).split(path.sep).join('/'),
2243
2343
  phase,
2244
- source_file: options.input || `draft version ${options.version}`,
2344
+ source_file: options.input || `draft version ${version}`,
2245
2345
  version: result.version || null,
2246
2346
  });
2247
2347
  updateAiRunPhase(repoRoot, lifecycleRun.run_id, phase === 'acceptance' ? 'acceptance-approved' : 'technical-plan-approved', {
@@ -2250,7 +2350,7 @@ async function runApprove(repoRoot, options = {}) {
2250
2350
  });
2251
2351
  process.stdout.write(formatApprovalResult({
2252
2352
  ...result,
2253
- sourceFile: options.input || `draft version ${options.version}`,
2353
+ sourceFile: options.input || `draft version ${version}`,
2254
2354
  }, repoRoot));
2255
2355
 
2256
2356
  return {
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
4
  const { readPhaseApproval } = require('../lib/approvals');
5
+ const { approvalCandidateCommand, buildApprovalCandidateReport, formatCandidateSummary } = require('../lib/ai/approval-candidates');
5
6
  const { readPlanReview } = require('../lib/ai/plan-review');
6
7
  const { listAgentProfiles } = require('../lib/agent-profiles');
7
8
  const { readProjectScanStatus } = require('../lib/project-scan');
@@ -89,6 +90,25 @@ function safeReadPlanReview(projectRoot) {
89
90
  }
90
91
  }
91
92
 
93
+ function safeBuildApprovalCandidateReport(projectRoot, phase) {
94
+ try {
95
+ return buildApprovalCandidateReport(projectRoot, phase);
96
+ } catch (error) {
97
+ return {
98
+ phase,
99
+ approval_status: 'invalid',
100
+ latest_version: null,
101
+ current: null,
102
+ recommended: null,
103
+ candidates: [],
104
+ history: [],
105
+ approved: null,
106
+ next_command: 'npx create-quiver ai approvals',
107
+ error: error.message,
108
+ };
109
+ }
110
+ }
111
+
92
112
  function summarizeDocs(projectRoot) {
93
113
  const docs = {
94
114
  hasProjectMap: exists(projectRoot, 'docs/PROJECT_MAP.md'),
@@ -250,11 +270,23 @@ function detectFlowState(projectRoot) {
250
270
  acceptance: safeReadApproval(projectRoot, 'acceptance'),
251
271
  technicalPlan: safeReadApproval(projectRoot, 'technical-plan'),
252
272
  };
273
+ const approvalCandidates = {
274
+ acceptance: safeBuildApprovalCandidateReport(projectRoot, 'acceptance'),
275
+ technicalPlan: safeBuildApprovalCandidateReport(projectRoot, 'technical-plan'),
276
+ };
253
277
  const planReview = safeReadPlanReview(projectRoot);
254
278
  const agents = summarizeAgentProfiles(projectRoot);
255
279
  const packageManager = detectPackageManager(projectRoot);
256
280
  const specSlugs = listSpecSlugs(projectRoot);
257
281
  const facts = buildFacts({ initialized, docs, approvals, planReview, agents, packageManager, specSlugs, state });
282
+ const acceptanceApprovalCommand = approvalCandidateCommand(
283
+ approvalCandidates.acceptance,
284
+ 'npx create-quiver ai approve --phase acceptance --version <n>',
285
+ );
286
+ const technicalPlanApprovalCommand = approvalCandidateCommand(
287
+ approvalCandidates.technicalPlan,
288
+ 'npx create-quiver ai approve --phase technical-plan --version <n>',
289
+ );
258
290
 
259
291
  if (!initialized) {
260
292
  return baseReport({
@@ -334,12 +366,12 @@ function detectFlowState(projectRoot) {
334
366
  return baseReport({
335
367
  stage: 'criteria-draft',
336
368
  label: 'acceptance criteria need approval',
337
- blockers: ['Acceptance criteria draft exists but is not approved.'],
338
- nextCommand: 'npx create-quiver ai approve --phase acceptance --version <n>',
369
+ blockers: ['Acceptance criteria draft exists but is not approved.', formatCandidateSummary(approvalCandidates.acceptance.current)].filter(Boolean),
370
+ nextCommand: acceptanceApprovalCommand,
339
371
  suggestedCommands: [
340
372
  'npx create-quiver ai approvals',
341
373
  'npx create-quiver ai revise --phase acceptance --input feedback.md --dry-run',
342
- 'npx create-quiver ai approve --phase acceptance --version <n>',
374
+ acceptanceApprovalCommand,
343
375
  ],
344
376
  facts,
345
377
  });
@@ -349,12 +381,12 @@ function detectFlowState(projectRoot) {
349
381
  return baseReport({
350
382
  stage: 'criteria-stale',
351
383
  label: 'acceptance criteria approval is stale',
352
- blockers: ['Acceptance criteria changed after approval.'],
353
- nextCommand: 'npx create-quiver ai approve --phase acceptance --version <n>',
384
+ blockers: ['Acceptance criteria changed after approval.', formatCandidateSummary(approvalCandidates.acceptance.current)].filter(Boolean),
385
+ nextCommand: acceptanceApprovalCommand,
354
386
  suggestedCommands: [
355
387
  'npx create-quiver ai approvals',
356
388
  'npx create-quiver ai revise --phase acceptance --input feedback.md --dry-run',
357
- 'npx create-quiver ai approve --phase acceptance --version <n>',
389
+ acceptanceApprovalCommand,
358
390
  ],
359
391
  facts,
360
392
  });
@@ -390,14 +422,29 @@ function detectFlowState(projectRoot) {
390
422
  });
391
423
  }
392
424
 
425
+ if (!approvalCandidates.technicalPlan.current?.approvable) {
426
+ return baseReport({
427
+ stage: 'technical-plan-review-blocked',
428
+ label: 'technical plan review requires revision',
429
+ blockers: ['Technical plan review blocks approval.', formatCandidateSummary(approvalCandidates.technicalPlan.current)].filter(Boolean),
430
+ nextCommand: approvalCandidates.technicalPlan.current?.next_command || 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
431
+ suggestedCommands: [
432
+ 'npx create-quiver ai approvals',
433
+ approvalCandidates.technicalPlan.current?.next_command || 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
434
+ 'npx create-quiver ai review-plan --dry-run',
435
+ ],
436
+ facts,
437
+ });
438
+ }
439
+
393
440
  return baseReport({
394
441
  stage: 'technical-plan-draft',
395
442
  label: 'technical plan needs approval',
396
- blockers: ['Technical plan draft was reviewed but is not approved.'],
397
- nextCommand: 'npx create-quiver ai approve --phase technical-plan --version <n>',
443
+ blockers: ['Technical plan draft was reviewed but is not approved.', formatCandidateSummary(approvalCandidates.technicalPlan.current)].filter(Boolean),
444
+ nextCommand: technicalPlanApprovalCommand,
398
445
  suggestedCommands: [
399
446
  'npx create-quiver ai approvals',
400
- 'npx create-quiver ai approve --phase technical-plan --version <n>',
447
+ technicalPlanApprovalCommand,
401
448
  ],
402
449
  facts,
403
450
  });
@@ -419,14 +466,29 @@ function detectFlowState(projectRoot) {
419
466
  });
420
467
  }
421
468
 
469
+ if (!approvalCandidates.technicalPlan.current?.approvable) {
470
+ return baseReport({
471
+ stage: 'technical-plan-review-blocked',
472
+ label: 'technical plan review requires revision',
473
+ blockers: ['Technical plan changed after approval and latest review blocks approval.', formatCandidateSummary(approvalCandidates.technicalPlan.current)].filter(Boolean),
474
+ nextCommand: approvalCandidates.technicalPlan.current?.next_command || 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
475
+ suggestedCommands: [
476
+ 'npx create-quiver ai approvals',
477
+ approvalCandidates.technicalPlan.current?.next_command || 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
478
+ 'npx create-quiver ai review-plan --dry-run',
479
+ ],
480
+ facts,
481
+ });
482
+ }
483
+
422
484
  return baseReport({
423
485
  stage: 'technical-plan-stale',
424
486
  label: 'technical plan approval is stale',
425
- blockers: ['Technical plan changed after approval and the latest draft was reviewed.'],
426
- nextCommand: 'npx create-quiver ai approve --phase technical-plan --version <n>',
487
+ blockers: ['Technical plan changed after approval and the latest draft was reviewed.', formatCandidateSummary(approvalCandidates.technicalPlan.current)].filter(Boolean),
488
+ nextCommand: technicalPlanApprovalCommand,
427
489
  suggestedCommands: [
428
490
  'npx create-quiver ai approvals',
429
- 'npx create-quiver ai approve --phase technical-plan --version <n>',
491
+ technicalPlanApprovalCommand,
430
492
  ],
431
493
  facts,
432
494
  });
@@ -7,6 +7,7 @@ const { selectOption } = require('../lib/cli/selectors');
7
7
  const { createUx } = require('../lib/cli/ux');
8
8
  const { parseJsonWithComments } = require('../lib/json');
9
9
  const { assertPathInsideRoot, validateProjectRelativePaths } = require('../lib/paths');
10
+ const { buildApprovalCandidateReport, formatReviewSummary } = require('../lib/ai/approval-candidates');
10
11
  const { resolveReviewedTechnicalPlanInput } = require('../lib/ai/plan-review');
11
12
  const {
12
13
  buildSpecGenerationManifest,
@@ -439,6 +440,11 @@ async function resolveInteractiveSpecCreateOptions(repoRoot, preview, options =
439
440
  stdinIsTTY: options.stdinIsTTY,
440
441
  stdoutIsTTY: options.stdoutIsTTY,
441
442
  };
443
+ const technicalPlanCandidates = buildApprovalCandidateReport(repoRoot, 'technical-plan');
444
+ const approvedVersion = technicalPlanCandidates.approved?.version
445
+ ? `v${technicalPlanCandidates.approved.version}`
446
+ : 'approved';
447
+ const reviewSummary = formatReviewSummary(technicalPlanCandidates.review);
442
448
  const selectedMethodology = await selectOption('¿Qué metodología aplica esta spec?', SPEC_METHODOLOGY_OPTIONS, {
443
449
  ...selectorOptions,
444
450
  defaultValue: 'wdd-sdd',
@@ -450,7 +456,7 @@ async function resolveInteractiveSpecCreateOptions(repoRoot, preview, options =
450
456
  {
451
457
  label: preview.inputPath,
452
458
  value: preview.inputPath,
453
- hint: 'Plan técnico revisado y aprobado',
459
+ hint: ['Plan técnico revisado y aprobado', approvedVersion, reviewSummary].filter(Boolean).join(', '),
454
460
  default: true,
455
461
  },
456
462
  ], {
@@ -484,6 +490,7 @@ async function resolveInteractiveSpecCreateOptions(repoRoot, preview, options =
484
490
  { label: 'Spec', value: preview.manifest.slug },
485
491
  { label: 'Metodologia', value: selectedMethodology.label },
486
492
  { label: 'Input', value: selectedInput.value },
493
+ { label: 'Plan tecnico', value: ['aprobado', approvedVersion, reviewSummary].filter(Boolean).join(', ') },
487
494
  { label: 'Target', value: preview.relativeSpecDir },
488
495
  { label: 'Archivos planificados', value: preview.preview.files.length },
489
496
  { label: 'Revision', value: selectedReview.value === 'review' ? 'abrir preview' : 'confirmacion directa' },
@@ -525,6 +525,14 @@ function parseArgs(argv) {
525
525
 
526
526
  const positional = [];
527
527
 
528
+ function missingInputValueMessage() {
529
+ const aiCommandHint = result.mode === 'ai' ? positional[0] : '';
530
+ if (aiCommandHint === 'revise') {
531
+ return `missing feedback input file for ai revise --phase ${result.aiPhase}. Use: npx create-quiver ai revise --phase ${result.aiPhase} --input <feedback.md> --dry-run`;
532
+ }
533
+ return 'missing value for --input';
534
+ }
535
+
528
536
  for (let index = 0; index < args.length; index += 1) {
529
537
  const arg = args[index];
530
538
 
@@ -875,8 +883,8 @@ function parseArgs(argv) {
875
883
 
876
884
  if (arg === '--input') {
877
885
  const value = args[++index];
878
- if (!value) {
879
- throw new Error(formatError('missing value for --input'));
886
+ if (!value || String(value).startsWith('--')) {
887
+ throw new Error(formatError(missingInputValueMessage()));
880
888
  }
881
889
  result.aiInput = value;
882
890
  continue;
@@ -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
+ };
@@ -1,7 +1,7 @@
1
1
  const fs = require('node:fs');
2
2
  const path = require('node:path');
3
3
 
4
- const { readPhaseApproval, resolveApprovedPlannerInput } = require('../approvals');
4
+ const { buildPlannerApprovalCandidates, readPhaseApproval, resolveApprovedPlannerInput } = require('../approvals');
5
5
  const { quiverInternalPaths } = require('../init-layout');
6
6
 
7
7
  const PLAN_REVIEW_PROMPT_SOURCE = 'packaged production-readiness plan review template';
@@ -186,6 +186,81 @@ function reviewBlocksApproval(review) {
186
186
  return result.blocking === true || result.approval_recommendation === 'revise';
187
187
  }
188
188
 
189
+ function normalizeReviewDecision(review) {
190
+ const result = review?.meta?.review_result || null;
191
+ const requiredFixes = normalizeList(result?.required_fixes);
192
+ const optionalHardening = normalizeList(result?.optional_hardening);
193
+ const risks = normalizeList(result?.risks);
194
+ const blocking = reviewBlocksApproval(review);
195
+ const recommendation = result?.approval_recommendation || (review.status === 'missing' || review.status === 'stale' ? 'review-required' : 'approve-with-risk');
196
+ const nextCommand = result?.next_command || (review.status === 'unapproved' || review.status === 'reviewed'
197
+ ? 'npx create-quiver ai approve --phase technical-plan --version <n>'
198
+ : 'npx create-quiver ai review-plan --dry-run');
199
+
200
+ return {
201
+ status: review.status,
202
+ recommendation,
203
+ blocking: blocking || review.status === 'missing' || review.status === 'stale',
204
+ required_fixes: requiredFixes,
205
+ optional_hardening: optionalHardening,
206
+ risks,
207
+ required_fixes_count: requiredFixes.length,
208
+ optional_hardening_count: optionalHardening.length,
209
+ risks_count: risks.length,
210
+ source_file: review.meta?.source_file || '',
211
+ source_kind: review.meta?.source_kind || null,
212
+ source_version: review.meta?.source_version || null,
213
+ reviewed_at: review.meta?.reviewed_at || '',
214
+ next_command: nextCommand,
215
+ };
216
+ }
217
+
218
+ function buildTechnicalPlanApprovalCandidates(projectRoot) {
219
+ const base = buildPlannerApprovalCandidates(projectRoot, 'technical-plan');
220
+ const review = readPlanReview(projectRoot);
221
+ const reviewDecision = normalizeReviewDecision(review);
222
+ const reviewReady = (review.status === 'unapproved' || review.status === 'reviewed') && reviewDecision.blocking !== true;
223
+ const candidates = base.candidates.map((candidate) => {
224
+ const matchesReviewVersion = !reviewDecision.source_version
225
+ || !candidate.version
226
+ || Number(reviewDecision.source_version) === Number(candidate.version);
227
+ const approvable = candidate.approvable === true && reviewReady && matchesReviewVersion;
228
+ const reason = approvable
229
+ ? `latest draft has plan-review recommendation ${reviewDecision.recommendation}`
230
+ : review.status === 'missing'
231
+ ? 'technical-plan requires production review before approval'
232
+ : review.status === 'stale'
233
+ ? 'technical-plan review is stale'
234
+ : reviewDecision.blocking
235
+ ? `plan-review blocks approval with recommendation ${reviewDecision.recommendation}`
236
+ : matchesReviewVersion
237
+ ? candidate.reason
238
+ : `plan-review targets v${reviewDecision.source_version}; candidate is v${candidate.version}`;
239
+
240
+ return {
241
+ ...candidate,
242
+ approvable,
243
+ recommended: candidate.current === true && approvable,
244
+ blocked: !approvable,
245
+ status: approvable ? 'approvable' : candidate.current ? 'blocked' : 'history',
246
+ reason,
247
+ recommended_action: approvable ? 'approve' : reviewDecision.blocking || review.status === 'missing' || review.status === 'stale' ? 'review-or-revise' : 'inspect',
248
+ next_command: approvable ? candidate.next_command : reviewDecision.next_command,
249
+ review: reviewDecision,
250
+ };
251
+ });
252
+
253
+ return {
254
+ ...base,
255
+ candidates,
256
+ current: candidates.find((candidate) => candidate.current) || null,
257
+ recommended: candidates.find((candidate) => candidate.recommended) || null,
258
+ history: candidates.filter((candidate) => !candidate.current),
259
+ review: reviewDecision,
260
+ next_command: candidates.find((candidate) => candidate.recommended)?.next_command || reviewDecision.next_command,
261
+ };
262
+ }
263
+
189
264
  function latestTechnicalPlanDraft(approval) {
190
265
  const version = Number(approval.meta?.draft?.version || 0);
191
266
  if (!version) {
@@ -435,6 +510,7 @@ module.exports = {
435
510
  PLAN_REVIEW_RECOMMENDATIONS,
436
511
  assertPlanReviewed,
437
512
  buildPlanReviewPrompt,
513
+ buildTechnicalPlanApprovalCandidates,
438
514
  derivePlanReviewResult,
439
515
  planReviewMetaPath,
440
516
  planReviewPath,
@@ -2,6 +2,7 @@ const fs = require('node:fs');
2
2
  const os = require('node:os');
3
3
  const path = require('node:path');
4
4
 
5
+ const { buildApprovalCandidateReport } = require('./approval-candidates');
5
6
  const { quiverInternalPaths } = require('../init-layout');
6
7
 
7
8
  const AI_RUN_PHASES = Object.freeze([
@@ -104,8 +105,24 @@ function phaseRank(phase) {
104
105
  return AI_RUN_PHASES.indexOf(phase);
105
106
  }
106
107
 
107
- function nextCommandForPhase(phase) {
108
+ function nextCommandForPhase(phase, projectRoot = '') {
108
109
  assertKnownPhase(phase);
110
+ if (projectRoot && phase === 'acceptance-draft') {
111
+ try {
112
+ const report = buildApprovalCandidateReport(projectRoot, 'acceptance');
113
+ return report.recommended?.next_command || report.current?.next_command || PHASE_NEXT_COMMAND[phase];
114
+ } catch {
115
+ return PHASE_NEXT_COMMAND[phase];
116
+ }
117
+ }
118
+ if (projectRoot && phase === 'technical-plan-reviewed') {
119
+ try {
120
+ const report = buildApprovalCandidateReport(projectRoot, 'technical-plan');
121
+ return report.recommended?.next_command || report.current?.next_command || PHASE_NEXT_COMMAND[phase];
122
+ } catch {
123
+ return PHASE_NEXT_COMMAND[phase];
124
+ }
125
+ }
109
126
  return PHASE_NEXT_COMMAND[phase];
110
127
  }
111
128
 
@@ -374,12 +391,12 @@ function formatAiRunStatus(projectRoot, run) {
374
391
  if (otherOpenRuns.length > 0) {
375
392
  lines.push('Other open runs:');
376
393
  for (const item of otherOpenRuns) {
377
- lines.push(`- ${item.run_id}: ${item.phase} (${item.status}) -> ${nextCommandForPhase(item.phase)}`);
394
+ lines.push(`- ${item.run_id}: ${item.phase} (${item.status}) -> ${nextCommandForPhase(item.phase, projectRoot)}`);
378
395
  }
379
396
  }
380
397
 
381
398
  lines.push(
382
- `Next safe command: ${nextCommandForPhase(run.phase)}`,
399
+ `Next safe command: ${nextCommandForPhase(run.phase, projectRoot)}`,
383
400
  '',
384
401
  );
385
402
 
@@ -400,7 +417,7 @@ function formatAiRunResume(projectRoot, run) {
400
417
  'AI run resume',
401
418
  `Run: ${run.run_id}`,
402
419
  `Current phase: ${run.phase}`,
403
- `Next safe command: ${nextCommandForPhase(run.phase)}`,
420
+ `Next safe command: ${nextCommandForPhase(run.phase, projectRoot)}`,
404
421
  `State: ${toRelativePosix(projectRoot, runStatePath(projectRoot, run.run_id))}`,
405
422
  '',
406
423
  ].join('\n');