create-quiver 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +183 -511
  3. package/README_FOR_AI.md +36 -26
  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 +125 -0
  9. package/docs/COMMANDS.md.template +19 -4
  10. package/docs/GITFLOW_PR_GUIDE.md +70 -0
  11. package/docs/TROUBLESHOOTING.md.template +29 -0
  12. package/docs/WORKFLOW.md.template +13 -12
  13. package/docs/getting-started/linux.md +84 -0
  14. package/docs/getting-started/macos.md +85 -0
  15. package/docs/getting-started/windows-git-bash-wsl.md +78 -0
  16. package/docs/getting-started/windows-powershell.md +96 -0
  17. package/docs/reference/commands.md +94 -0
  18. package/docs/workflows/existing-project.md +131 -0
  19. package/docs/workflows/full-ai-spec-to-pr.md +311 -0
  20. package/docs/workflows/legacy-quiver-project.md +102 -0
  21. package/docs/workflows/new-project.md +76 -0
  22. package/package.json +5 -1
  23. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
  24. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
  25. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
  26. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
  27. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
  28. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
  29. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
  30. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
  31. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
  32. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
  33. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
  34. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
  35. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
  36. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
  37. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
  38. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
  39. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
  40. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
  41. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
  42. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
  43. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
  44. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
  45. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
  46. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
  47. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
  48. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  49. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
  50. package/specs/quiver-v29-planner-prepare-context-cli-ux/EVIDENCE_REPORT.md +163 -0
  51. package/specs/quiver-v29-planner-prepare-context-cli-ux/EXECUTION_PLAN.md +72 -0
  52. package/specs/quiver-v29-planner-prepare-context-cli-ux/SPEC.md +173 -0
  53. package/specs/quiver-v29-planner-prepare-context-cli-ux/STATUS.md +34 -0
  54. package/specs/quiver-v29-planner-prepare-context-cli-ux/pr.md +95 -0
  55. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/CLOSURE_BRIEF.md +32 -0
  56. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/EXECUTION_BRIEF.md +45 -0
  57. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/slice.json +58 -0
  58. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/CLOSURE_BRIEF.md +33 -0
  59. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/EXECUTION_BRIEF.md +49 -0
  60. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/slice.json +75 -0
  61. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/CLOSURE_BRIEF.md +32 -0
  62. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/EXECUTION_BRIEF.md +47 -0
  63. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/slice.json +71 -0
  64. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/CLOSURE_BRIEF.md +33 -0
  65. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/EXECUTION_BRIEF.md +52 -0
  66. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/slice.json +82 -0
  67. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/CLOSURE_BRIEF.md +34 -0
  68. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/EXECUTION_BRIEF.md +46 -0
  69. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/slice.json +73 -0
  70. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/CLOSURE_BRIEF.md +34 -0
  71. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/EXECUTION_BRIEF.md +46 -0
  72. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/slice.json +83 -0
  73. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/CLOSURE_BRIEF.md +31 -0
  74. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/EXECUTION_BRIEF.md +50 -0
  75. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/slice.json +95 -0
  76. package/src/create-quiver/commands/ai.js +938 -16
  77. package/src/create-quiver/commands/spec.js +74 -2
  78. package/src/create-quiver/index.js +91 -5
  79. package/src/create-quiver/lib/ai/context-packs.js +2 -2
  80. package/src/create-quiver/lib/ai/context-proposal.js +389 -0
  81. package/src/create-quiver/lib/ai/context-proposal.schema.js +31 -0
  82. package/src/create-quiver/lib/ai/export-state.js +52 -5
  83. package/src/create-quiver/lib/ai/github.js +14 -2
  84. package/src/create-quiver/lib/ai/plan-review.js +159 -0
  85. package/src/create-quiver/lib/ai/run-state.js +17 -2
  86. package/src/create-quiver/lib/ai/spec-generator.js +15 -0
  87. package/src/create-quiver/lib/cli/editor.js +118 -0
  88. package/src/create-quiver/lib/cli/theme.js +100 -0
  89. package/src/create-quiver/lib/cli/ux-flags.js +151 -0
  90. package/src/create-quiver/lib/cli/ux.js +130 -0
  91. package/src/create-quiver/lib/project-state-resolver.js +195 -1
  92. package/src/create-quiver/lib/spec-worktrees.js +50 -2
@@ -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');
@@ -26,18 +30,25 @@ const {
26
30
  PLAN_REVIEW_PROMPT_SOURCE,
27
31
  buildPlanReviewPrompt,
28
32
  readPlanReview,
33
+ reviewBlocksApproval,
29
34
  resolveReviewedTechnicalPlanInput,
30
35
  resolveTechnicalPlanReviewInput,
31
36
  savePlanReview,
32
37
  summarizePlanReview,
33
38
  } = require('../lib/ai/plan-review');
34
- const { buildSpecGenerationManifest, describeSpecGeneration, generateSpecArtifacts } = require('../lib/ai/spec-generator');
39
+ const {
40
+ buildSpecGenerationManifest,
41
+ describeSpecGeneration,
42
+ generateSpecArtifacts,
43
+ validateTechnicalPlanSpecContract,
44
+ } = require('../lib/ai/spec-generator');
35
45
  const { buildProviderInvocation, runProvider } = require('../lib/ai/providers');
36
46
  const {
37
47
  createAiRun,
38
48
  ensureAiRun,
39
49
  formatAiRunResume,
40
50
  formatAiRunStatus,
51
+ listAiRuns,
41
52
  recordAiRunApproval,
42
53
  resolveAiRun,
43
54
  updateAiRunPhase,
@@ -53,12 +64,15 @@ const {
53
64
  const {
54
65
  PLANNER_APPROVAL_PHASES,
55
66
  approvePlannerPhase,
67
+ findDraftVersion,
68
+ latestDraftVersion,
56
69
  readPhaseApproval,
57
70
  resolveApprovedPlannerInput,
58
71
  savePlannerDraft,
59
72
  summarizePlannerApproval,
60
73
  } = require('../lib/approvals');
61
74
  const { assertPlannerPhaseReady, getPlannerPhaseDetails, normalizePlannerPhase, PlannerPhaseError } = require('../lib/ai/phase-gates');
75
+ const { collectActiveSliceState, resolveProjectState } = require('../lib/project-state-resolver');
62
76
 
63
77
  const DEFAULT_ONBOARD_PROVIDER = 'codex';
64
78
  const DEFAULT_ONBOARD_ROLE = 'planner';
@@ -133,6 +147,13 @@ function buildPlanContext({ role, context, phase, inputText, inputPath, repoRoot
133
147
  : 'Task: produce a technical plan only. Do not create files or modify product code.',
134
148
  ];
135
149
 
150
+ if (phaseDetails.phase === 'technical-plan') {
151
+ sections.push(
152
+ 'Required output contract: include a fenced json block with `{ "spec": { "slices": [...] } }` so Quiver can create specs after review and approval.',
153
+ 'Each `spec.slices[]` item must include at least `slice_id`, `title`, `objective`, and `files`.',
154
+ );
155
+ }
156
+
136
157
  if (relativeInputPath) {
137
158
  sections.push(`Input file: ${relativeInputPath}`);
138
159
  }
@@ -296,6 +317,255 @@ function formatContextPreparationReport({ dryRun, plan, writePlan, writtenDocs,
296
317
  return `${lines.join('\n')}\n`;
297
318
  }
298
319
 
320
+ function truncatePromptSection(text, maxChars = 1200) {
321
+ const value = String(text || '').trimEnd();
322
+ if (value.length <= maxChars) {
323
+ return value;
324
+ }
325
+ return `${value.slice(0, maxChars).trimEnd()}\n[... truncated ${value.length - maxChars} chars ...]`;
326
+ }
327
+
328
+ function buildPrepareContextPlannerPrompt({ pack, draftPack }) {
329
+ const plan = draftPack.plan;
330
+ const allowedPaths = draftPack.docs.map((doc) => doc.path);
331
+ const sections = [
332
+ pack.prompt,
333
+ 'Task: planner-assisted Quiver context preparation.',
334
+ 'Goal: improve the docs-only onboarding context for future AI work while preserving WDD + SDD safety.',
335
+ 'Rules:',
336
+ '- Return only valid JSON. Do not include Markdown outside the JSON object.',
337
+ '- Do not modify product code, UI code, tests, migrations, dependencies, lockfiles, build files, runtime config, generated files, or paths outside the repo.',
338
+ '- Only propose writes to the allowed docs paths listed below.',
339
+ '- If information is ambiguous, document assumptions and risks instead of inventing facts.',
340
+ '- Keep human-authored content safe; Quiver will merge proposals through managed context blocks.',
341
+ '',
342
+ 'Allowed docs-only output paths:',
343
+ ...allowedPaths.map((item) => `- ${item}`),
344
+ '',
345
+ 'Project context:',
346
+ `- Project: ${plan.projectName}`,
347
+ `- Project slug: ${plan.projectSlug}`,
348
+ `- Package manager: ${plan.facts.packageManager}`,
349
+ `- Stack summary: ${plan.facts.stackSummary}`,
350
+ '',
351
+ 'Files considered by deterministic prepare-context:',
352
+ ...plan.filesConsidered.map((item) => `- ${item.path}: ${item.present ? 'present' : 'absent'}${item.reason ? ` (${item.reason})` : ''}`),
353
+ '',
354
+ 'Known assumptions:',
355
+ ...formatPathList(plan.assumptions),
356
+ '',
357
+ 'Known risks:',
358
+ ...formatPathList(plan.risks),
359
+ '',
360
+ 'Known contradictions:',
361
+ ...formatPathList(plan.contradictions),
362
+ '',
363
+ 'Deterministic candidate docs:',
364
+ ...draftPack.docs.flatMap((doc) => [
365
+ `### ${doc.path}`,
366
+ truncatePromptSection(doc.content),
367
+ ]),
368
+ '',
369
+ 'Required JSON output shape:',
370
+ JSON.stringify({
371
+ schema_version: 1,
372
+ kind: 'quiver-context-proposal',
373
+ summary: 'short summary',
374
+ assumptions: ['assumption to confirm'],
375
+ risks: ['risk to track'],
376
+ docs: [
377
+ {
378
+ path: 'docs/AI_CONTEXT.md',
379
+ action: 'update',
380
+ reason: 'why this doc should change',
381
+ content: '# AI Context\\n\\nFull proposed content or managed section content.\\n',
382
+ assumptions: [],
383
+ risks: [],
384
+ },
385
+ ],
386
+ omitted_paths: ['paths intentionally omitted'],
387
+ next_steps: ['safe next step'],
388
+ }, null, 2),
389
+ ];
390
+
391
+ return {
392
+ allowedPaths,
393
+ plan,
394
+ prompt: sections.join('\n'),
395
+ promptSource: 'quiver prepare-context planner proposal contract',
396
+ };
397
+ }
398
+
399
+ function formatPrepareContextPlannerDryRunReport({ provider, role, context, invocation, promptInfo, review, interactive }) {
400
+ const plan = promptInfo.plan;
401
+ const lines = [
402
+ 'AI prepare-context planner dry-run',
403
+ 'Mode: dry-run',
404
+ 'Planner: enabled',
405
+ `Provider: ${provider}`,
406
+ `Role: ${role}`,
407
+ `Context pack: ${context}`,
408
+ `Command: ${invocation.command} ${invocation.args.join(' ')}`.trim(),
409
+ `Prompt bytes: ${invocation.promptLength}`,
410
+ `Prompt source: ${promptInfo.promptSource}`,
411
+ `Review requested: ${review ? 'yes' : 'no'}`,
412
+ `Interactive requested: ${interactive ? 'yes' : 'no'}`,
413
+ 'Provider execution: skipped',
414
+ 'Writes: none',
415
+ 'Product code: untouched',
416
+ `Candidate docs: ${promptInfo.allowedPaths.join(', ')}`,
417
+ 'Files considered:',
418
+ ...plan.filesConsidered.map((item) => `- ${item.path}: ${item.present ? 'present' : 'absent'}`),
419
+ 'Allowed docs-only paths:',
420
+ ...promptInfo.allowedPaths.map((item) => `- ${item}`),
421
+ 'Next safe commands:',
422
+ '- npx create-quiver ai prepare-context --with-planner --print-prompt',
423
+ '- npx create-quiver ai prepare-context --with-planner --dry-run --review',
424
+ '- npx create-quiver ai prepare-context --with-planner',
425
+ ];
426
+
427
+ return `${lines.join('\n')}\n`;
428
+ }
429
+
430
+ function serializeProposalForReview(proposal) {
431
+ return {
432
+ schema_version: proposal.schemaVersion,
433
+ kind: proposal.kind,
434
+ summary: proposal.summary,
435
+ assumptions: proposal.assumptions,
436
+ risks: proposal.risks,
437
+ docs: proposal.docs.map((doc) => ({
438
+ path: doc.path,
439
+ action: doc.action,
440
+ reason: doc.reason,
441
+ content: doc.content,
442
+ assumptions: doc.assumptions,
443
+ risks: doc.risks,
444
+ })),
445
+ omitted_paths: proposal.omittedPaths,
446
+ next_steps: proposal.nextSteps,
447
+ };
448
+ }
449
+
450
+ function createProposalReviewFile(proposal, options = {}) {
451
+ const reviewDir = options.reviewDir || fs.mkdtempSync(path.join(os.tmpdir(), 'quiver-context-review-'));
452
+ const reviewPath = path.join(reviewDir, 'context-proposal.json');
453
+ fs.mkdirSync(reviewDir, { recursive: true });
454
+ fs.writeFileSync(reviewPath, `${JSON.stringify(serializeProposalForReview(proposal), null, 2)}\n`);
455
+ return reviewPath;
456
+ }
457
+
458
+ function makeReviewError(message, reviewPath, cause) {
459
+ 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.`));
460
+ error.code = cause?.code || 'AI_CONTEXT_REVIEW_FAILED';
461
+ error.cause = cause;
462
+ error.reviewPath = reviewPath;
463
+ return error;
464
+ }
465
+
466
+ function createReviewTextFile(contents, options = {}) {
467
+ const reviewDir = options.reviewDir || fs.mkdtempSync(path.join(os.tmpdir(), 'quiver-review-'));
468
+ const filename = options.reviewFileName || 'review.md';
469
+ const reviewPath = path.join(reviewDir, filename);
470
+ fs.mkdirSync(reviewDir, { recursive: true });
471
+ fs.writeFileSync(reviewPath, String(contents || ''));
472
+ return reviewPath;
473
+ }
474
+
475
+ async function reviewTextWithEditor(repoRoot, contents, options = {}) {
476
+ const reviewPath = createReviewTextFile(contents, options);
477
+ const hasEditorRunner = typeof options.openEditorFn === 'function';
478
+ const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
479
+
480
+ if (!canOpenEditor) {
481
+ throw makeReviewError(`${options.reviewLabel || 'review'} requires an interactive terminal or an injected editor runner.`, reviewPath);
482
+ }
483
+
484
+ const editorResult = hasEditorRunner
485
+ ? options.openEditorFn(reviewPath, { cwd: repoRoot, env: options.env || process.env })
486
+ : openEditor(reviewPath, { cwd: repoRoot, env: options.env || process.env });
487
+
488
+ if (!editorResult || editorResult.ok !== true) {
489
+ throw makeReviewError(editorResult?.reason || `${options.reviewLabel || 'review'} was canceled.`, reviewPath);
490
+ }
491
+
492
+ return {
493
+ reviewPath,
494
+ text: fs.readFileSync(reviewPath, 'utf8'),
495
+ };
496
+ }
497
+
498
+ async function confirmInteractiveAction(message, options = {}) {
499
+ if (options.interactive !== true) {
500
+ return;
501
+ }
502
+
503
+ const ux = options.ux || createUx({
504
+ interactive: true,
505
+ promptConfirm: options.promptConfirm,
506
+ stdinIsTTY: options.stdinIsTTY,
507
+ stdoutIsTTY: options.stdoutIsTTY,
508
+ stderrIsTTY: options.stderrIsTTY,
509
+ write: options.write,
510
+ });
511
+ const confirmed = await ux.promptConfirm(message, {
512
+ initialValue: false,
513
+ });
514
+
515
+ if (!confirmed) {
516
+ const error = new Error(formatError('interactive approval declined. No files were written.'));
517
+ error.code = 'AI_INTERACTIVE_APPROVAL_DECLINED';
518
+ throw error;
519
+ }
520
+ }
521
+
522
+ async function reviewPlannerContextProposal(repoRoot, proposal, options = {}) {
523
+ const reviewPath = createProposalReviewFile(proposal, options);
524
+ const hasEditorRunner = typeof options.openEditorFn === 'function';
525
+ const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
526
+
527
+ if (!canOpenEditor) {
528
+ throw makeReviewError('ai prepare-context review requires an interactive terminal or an injected editor runner.', reviewPath);
529
+ }
530
+
531
+ const editorResult = hasEditorRunner
532
+ ? options.openEditorFn(reviewPath, { cwd: repoRoot, env: options.env || process.env })
533
+ : openEditor(reviewPath, { cwd: repoRoot, env: options.env || process.env });
534
+
535
+ if (!editorResult || editorResult.ok !== true) {
536
+ throw makeReviewError(editorResult?.reason || 'ai prepare-context review was canceled before applying docs.', reviewPath);
537
+ }
538
+
539
+ try {
540
+ return {
541
+ proposal: parseContextProposalOutput(fs.readFileSync(reviewPath, 'utf8')),
542
+ reviewPath,
543
+ };
544
+ } catch (error) {
545
+ throw makeReviewError('edited planner proposal is invalid after review.', reviewPath, error);
546
+ }
547
+ }
548
+
549
+ async function confirmPlannerContextWrites(writePlan, options = {}) {
550
+ const changed = writePlan.filter((item) => item.action !== 'skip').length;
551
+ await confirmInteractiveAction(`Apply ${changed} docs-only context update${changed === 1 ? '' : 's'}?`, options);
552
+ }
553
+
554
+ function buildPlannerContextWritePlan(repoRoot, proposal) {
555
+ const reasonByPath = new Map(proposal.docs.map((doc) => [doc.path, doc.reason]));
556
+ const draftDocs = proposal.docs
557
+ .filter((doc) => doc.action !== 'skip')
558
+ .map((doc) => ({
559
+ path: doc.path,
560
+ content: doc.content,
561
+ }));
562
+
563
+ return buildContextWritePlan(repoRoot, draftDocs).map((item) => ({
564
+ ...item,
565
+ reason: item.action === 'skip' ? item.reason : reasonByPath.get(item.path) || item.reason,
566
+ }));
567
+ }
568
+
299
569
  function writeProviderOutput(result) {
300
570
  if (result.stdout) {
301
571
  process.stdout.write(redactSecrets(result.stdout));
@@ -305,6 +575,14 @@ function writeProviderOutput(result) {
305
575
  }
306
576
  }
307
577
 
578
+ function writeCleanProviderOutput(clean) {
579
+ const output = String(clean?.cleanOutput || '');
580
+ if (!output) {
581
+ return;
582
+ }
583
+ process.stdout.write(output.endsWith('\n') ? output : `${output}\n`);
584
+ }
585
+
308
586
  function normalizeText(value) {
309
587
  return String(value || '').replace(/\r\n/g, '\n');
310
588
  }
@@ -548,10 +826,262 @@ function formatApprovalDryRunResult({ phase, input, version }) {
548
826
  return `${lines.join('\n')}\n`;
549
827
  }
550
828
 
829
+ function stripCreateQuiverPrefix(message) {
830
+ return String(message || '').replace(/^create-quiver:\s*/, '');
831
+ }
832
+
833
+ function readCurrentDraftForApproval(repoRoot, phase, version) {
834
+ const approval = readPhaseApproval(repoRoot, phase);
835
+ const selectedDraft = findDraftVersion(approval.meta, version);
836
+ if (!selectedDraft) {
837
+ throw new Error(formatError(`missing ${phase} draft version ${version}`));
838
+ }
839
+ const latestVersion = latestDraftVersion(approval.meta);
840
+ if (latestVersion && Number(selectedDraft.version) !== latestVersion) {
841
+ throw new Error(formatError(`${phase} draft version ${version} is not current; latest draft version is ${latestVersion}. Approve the latest version or revise again.`));
842
+ }
843
+ const draftPath = path.resolve(repoRoot, selectedDraft.path);
844
+ if (!fs.existsSync(draftPath)) {
845
+ throw new Error(formatError(`missing ${phase} draft artifact: ${selectedDraft.path}`));
846
+ }
847
+ return {
848
+ approval,
849
+ contents: fs.readFileSync(draftPath, 'utf8'),
850
+ draft: selectedDraft,
851
+ path: selectedDraft.path,
852
+ };
853
+ }
854
+
855
+ function assertTechnicalPlanDraftHasSpecContract(repoRoot, version) {
856
+ const draft = readCurrentDraftForApproval(repoRoot, 'technical-plan', version);
857
+ try {
858
+ validateTechnicalPlanSpecContract(repoRoot, {
859
+ inputPath: draft.path,
860
+ inputText: draft.contents,
861
+ });
862
+ } catch (error) {
863
+ throw new Error(formatError([
864
+ `technical-plan draft v${version} cannot be approved because it cannot create specs.`,
865
+ stripCreateQuiverPrefix(error.message || error),
866
+ 'Required contract: include a structured JSON block with `spec.slices[]` before approval.',
867
+ 'Next safe command: npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
868
+ ].join('\n')));
869
+ }
870
+ return draft;
871
+ }
872
+
873
+ function resolveApprovedTechnicalPlanForRepair(repoRoot, explicitInput = '') {
874
+ const approval = readPhaseApproval(repoRoot, 'technical-plan');
875
+ if (!approval.approved?.path) {
876
+ throw new Error(formatError('ai repair-plan requires an approved technical-plan artifact. Run `npx create-quiver ai approvals` to inspect planner state.'));
877
+ }
878
+
879
+ const approvedPath = approval.approved.path;
880
+ if (explicitInput) {
881
+ const explicit = path.resolve(repoRoot, explicitInput);
882
+ const approved = path.resolve(repoRoot, approvedPath);
883
+ if (explicit !== approved) {
884
+ throw new Error(formatError(`ai repair-plan input must match the approved technical-plan artifact: ${approvedPath}`));
885
+ }
886
+ }
887
+
888
+ const contents = readTextFile(approvedPath, repoRoot);
889
+ try {
890
+ validateTechnicalPlanSpecContract(repoRoot, {
891
+ inputPath: approvedPath,
892
+ inputText: contents,
893
+ });
894
+ } catch (error) {
895
+ return {
896
+ approval,
897
+ contents,
898
+ path: approvedPath,
899
+ validationError: stripCreateQuiverPrefix(error.message || error),
900
+ };
901
+ }
902
+
903
+ throw new Error(formatError('approved technical-plan already includes a valid structured `spec.slices[]` contract. No repair draft is needed.'));
904
+ }
905
+
906
+ function buildRepairPlanContext({ context, inputText, inputPath, repoRoot, role, validationError }) {
907
+ const pack = buildContextPackMetadata({
908
+ role,
909
+ packName: context,
910
+ repoRoot,
911
+ });
912
+ const prompt = [
913
+ pack.prompt,
914
+ 'Phase: technical-plan',
915
+ 'Task: repair the approved technical plan into a new draft only. Do not approve it, create specs, modify product code, or expand scope.',
916
+ 'Preserve the approved intent, scope, risks, and decisions.',
917
+ 'Add the required Quiver structured JSON contract in a fenced json block.',
918
+ 'The JSON must include `{ "spec": { "slug": "...", "title": "...", "objective": "...", "slices": [...] } }`.',
919
+ 'Each item in `spec.slices[]` must include at least `slice_id`, `title`, `objective`, and `files`.',
920
+ `Validation failure to repair: ${validationError}`,
921
+ `Approved technical-plan artifact: ${inputPath}`,
922
+ 'Approved technical-plan contents:',
923
+ inputText.trimEnd(),
924
+ ].join('\n\n');
925
+
926
+ return {
927
+ pack,
928
+ prompt,
929
+ };
930
+ }
931
+
932
+ function formatRepairPlanResult(result, repoRoot) {
933
+ const relativePath = path.relative(repoRoot, result.filePath).split(path.sep).join('/');
934
+ return [
935
+ 'AI technical-plan repair draft saved',
936
+ `Draft: ${relativePath}`,
937
+ `Version: v${result.version}`,
938
+ `Source approved artifact: ${result.sourcePath}`,
939
+ 'Original approved artifact: preserved',
940
+ 'Next safe commands:',
941
+ '- npx create-quiver ai review-plan --dry-run',
942
+ '- npx create-quiver ai review-plan',
943
+ `- npx create-quiver ai approve --phase technical-plan --version ${result.version}`,
944
+ ].join('\n').concat('\n');
945
+ }
946
+
947
+ function formatActiveSliceReconciliationReport(report, options = {}) {
948
+ const lines = [
949
+ 'AI active-slice reconciliation',
950
+ `Mode: ${options.dryRun ? 'dry-run' : 'read-only'}`,
951
+ `Decision: ${report.reconciliation.decision}`,
952
+ `Reason: ${report.reconciliation.reason}`,
953
+ '',
954
+ 'Supported sources:',
955
+ ];
956
+
957
+ for (const source of report.supported_sources) {
958
+ lines.push(`- ${source.path}: ${source.exists ? 'exists' : 'missing'}`);
959
+ }
960
+
961
+ lines.push('', 'Detected sources:');
962
+ if (report.sources.length === 0) {
963
+ lines.push('- none');
964
+ } else {
965
+ for (const source of report.sources) {
966
+ const ref = source.ref || '(unresolved)';
967
+ const status = source.status ? ` status=${source.status}` : '';
968
+ const issue = source.issue ? ` issue=${source.issue}` : '';
969
+ lines.push(`- ${source.source_id}: ${ref}${status}${issue}`);
970
+ }
971
+ }
972
+
973
+ lines.push('', 'Planned changes:');
974
+ if (report.reconciliation.planned_changes.length === 0) {
975
+ lines.push('- none');
976
+ } else {
977
+ for (const change of report.reconciliation.planned_changes) {
978
+ lines.push(`- ${change}`);
979
+ }
980
+ }
981
+
982
+ lines.push('', 'Risks:');
983
+ if (report.reconciliation.risks.length === 0) {
984
+ lines.push('- none');
985
+ } else {
986
+ for (const risk of report.reconciliation.risks) {
987
+ lines.push(`- ${risk}`);
988
+ }
989
+ }
990
+
991
+ lines.push('', options.dryRun ? 'No files were changed.' : 'This command is read-only; use start-slice or cleanup-slice for intentional writes.');
992
+ return `${lines.join('\n')}\n`;
993
+ }
994
+
995
+ function readRunApprovals(repoRoot, run) {
996
+ if (!run?.approvals_path) {
997
+ return [];
998
+ }
999
+ const filePath = path.resolve(repoRoot, run.approvals_path);
1000
+ if (!fs.existsSync(filePath)) {
1001
+ return [];
1002
+ }
1003
+ try {
1004
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
1005
+ return Array.isArray(parsed.approvals) ? parsed.approvals : [];
1006
+ } catch {
1007
+ return [];
1008
+ }
1009
+ }
1010
+
1011
+ function collectRunApprovalRows(repoRoot) {
1012
+ const activeRun = resolveAiRun(repoRoot, '');
1013
+ return listAiRuns(repoRoot)
1014
+ .flatMap((run) => readRunApprovals(repoRoot, run).map((approval) => ({
1015
+ run,
1016
+ approval,
1017
+ relation: activeRun && run.run_id === activeRun.run_id
1018
+ ? 'active'
1019
+ : run.status === 'closed'
1020
+ ? 'historical'
1021
+ : 'other-open',
1022
+ })));
1023
+ }
1024
+
1025
+ function approvalArtifactForRelation(report) {
1026
+ return report?.approved?.path || report?.draft?.path || '';
1027
+ }
1028
+
1029
+ function classifyGlobalApprovalRelation(report, runApprovalRows) {
1030
+ const artifact = approvalArtifactForRelation(report);
1031
+ if (!artifact || report.status === 'missing') {
1032
+ return 'none';
1033
+ }
1034
+ const matches = runApprovalRows.filter((row) => row.approval?.artifact === artifact);
1035
+ if (matches.some((row) => row.relation === 'active')) {
1036
+ return 'active';
1037
+ }
1038
+ if (matches.length > 0) {
1039
+ return 'historical';
1040
+ }
1041
+ return 'orphaned';
1042
+ }
1043
+
1044
+ function formatRunScopedApprovals(repoRoot, runApprovalRows) {
1045
+ const runs = listAiRuns(repoRoot);
1046
+ const activeRun = resolveAiRun(repoRoot, '');
1047
+ const lines = [
1048
+ 'Run-scoped approvals',
1049
+ `Active run: ${activeRun ? activeRun.run_id : '(none)'}`,
1050
+ ];
1051
+
1052
+ if (runs.length === 0) {
1053
+ lines.push('- no AI runs found');
1054
+ return `${lines.join('\n')}\n`;
1055
+ }
1056
+
1057
+ for (const run of runs.slice().reverse()) {
1058
+ const relation = activeRun && run.run_id === activeRun.run_id
1059
+ ? 'active'
1060
+ : run.status === 'closed'
1061
+ ? 'historical'
1062
+ : 'other-open';
1063
+ const approvals = runApprovalRows.filter((row) => row.run.run_id === run.run_id);
1064
+ lines.push(`Run: ${run.run_id} (${relation}, phase: ${run.phase}, status: ${run.status})`);
1065
+ if (approvals.length === 0) {
1066
+ lines.push('- no run-scoped approvals');
1067
+ continue;
1068
+ }
1069
+ for (const row of approvals) {
1070
+ const version = row.approval.version ? ` v${row.approval.version}` : '';
1071
+ lines.push(`- ${row.approval.phase || 'unknown'}${version}: ${row.approval.artifact || '(missing artifact)'}`);
1072
+ }
1073
+ }
1074
+
1075
+ return `${lines.join('\n')}\n`;
1076
+ }
1077
+
551
1078
  function formatApprovalStatusReport(repoRoot) {
552
- const sections = ['AI approvals status'];
1079
+ const runApprovalRows = collectRunApprovalRows(repoRoot);
1080
+ const sections = ['AI approvals status', formatRunScopedApprovals(repoRoot, runApprovalRows).trimEnd(), 'Global planner approvals'];
553
1081
  for (const phase of PLANNER_APPROVAL_PHASES) {
554
- sections.push(summarizePlannerApproval(repoRoot, phase).trimEnd());
1082
+ const summary = summarizePlannerApproval(repoRoot, phase).trimEnd();
1083
+ const relation = classifyGlobalApprovalRelation(readPhaseApproval(repoRoot, phase), runApprovalRows);
1084
+ sections.push(`${summary}\nRun relation: ${relation}`);
555
1085
  }
556
1086
  sections.push(summarizePlanReview(repoRoot).trimEnd());
557
1087
  return `${sections.join('\n\n')}\n`;
@@ -640,12 +1170,14 @@ async function runOnboard(repoRoot, options = {}) {
640
1170
  throw annotateProviderError(error, 'onboard');
641
1171
  }
642
1172
 
643
- writeProviderOutput(result);
644
-
645
1173
  if (!result.ok) {
1174
+ writeProviderOutput(result);
646
1175
  throw annotateProviderError(result.error || new Error('provider run failed'), 'onboard');
647
1176
  }
648
1177
 
1178
+ const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
1179
+ writeCleanProviderOutput(clean);
1180
+
649
1181
  return {
650
1182
  task: 'onboard',
651
1183
  provider,
@@ -658,6 +1190,10 @@ async function runOnboard(repoRoot, options = {}) {
658
1190
  }
659
1191
 
660
1192
  async function runPrepareContext(repoRoot, options = {}) {
1193
+ if (options.withPlanner === true) {
1194
+ return runPrepareContextWithPlanner(repoRoot, options);
1195
+ }
1196
+
661
1197
  const draftPack = buildContextPreparationDrafts(repoRoot);
662
1198
  const writePlan = buildContextWritePlan(repoRoot, draftPack.docs);
663
1199
  const report = {
@@ -719,6 +1255,152 @@ async function runPrepareContext(repoRoot, options = {}) {
719
1255
  };
720
1256
  }
721
1257
 
1258
+ async function runPrepareContextWithPlanner(repoRoot, options = {}) {
1259
+ const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
1260
+ const provider = resolveProviderForProfile(repoRoot, role, options.provider, options.providerExplicit, DEFAULT_PLAN_PROVIDER);
1261
+ const context = options.context || DEFAULT_PLAN_CONTEXT;
1262
+ const timeoutMs = normalizeTimeout(options.timeout);
1263
+ const draftPack = buildContextPreparationDrafts(repoRoot);
1264
+ const pack = buildContextPackMetadata({
1265
+ role,
1266
+ packName: context,
1267
+ repoRoot,
1268
+ });
1269
+ const promptInfo = buildPrepareContextPlannerPrompt({ pack, draftPack });
1270
+ const prompt = promptInfo.prompt;
1271
+ assertProviderPromptWithinLimit(prompt, options.promptLimitOptions || {});
1272
+ let invocation;
1273
+
1274
+ try {
1275
+ invocation = buildProviderInvocation(provider, {
1276
+ prompt,
1277
+ cwd: repoRoot,
1278
+ timeoutMs,
1279
+ });
1280
+ } catch (error) {
1281
+ throw annotateProviderError(error, 'prepare-context');
1282
+ }
1283
+
1284
+ if (options.dryRun) {
1285
+ const report = {
1286
+ task: 'prepare-context',
1287
+ mode: 'planner',
1288
+ dryRun: true,
1289
+ provider,
1290
+ role,
1291
+ contextPack: context,
1292
+ invocation,
1293
+ candidateDocs: promptInfo.allowedPaths,
1294
+ plan: draftPack.plan,
1295
+ };
1296
+ process.stdout.write(formatPrepareContextPlannerDryRunReport({
1297
+ provider,
1298
+ role,
1299
+ context,
1300
+ invocation,
1301
+ promptInfo,
1302
+ review: options.review === true,
1303
+ interactive: options.interactive === true,
1304
+ }));
1305
+ return report;
1306
+ }
1307
+
1308
+ if (options.printPrompt) {
1309
+ const report = {
1310
+ task: 'prepare-context',
1311
+ provider,
1312
+ role,
1313
+ contextPack: context,
1314
+ invocation,
1315
+ prompt,
1316
+ promptSource: promptInfo.promptSource,
1317
+ };
1318
+ process.stdout.write(formatPromptOnlyReport(report));
1319
+ return report;
1320
+ }
1321
+
1322
+ let result;
1323
+ try {
1324
+ result = await (options.runProviderFn || runProvider)(provider, {
1325
+ prompt,
1326
+ cwd: repoRoot,
1327
+ timeoutMs,
1328
+ dryRun: false,
1329
+ probe: options.probe,
1330
+ spawn: options.spawn,
1331
+ tempRoot: options.tempRoot,
1332
+ tempFileName: options.tempFileName,
1333
+ tempFilePrefix: options.tempFilePrefix,
1334
+ });
1335
+ } catch (error) {
1336
+ throw annotateProviderError(error, 'prepare-context');
1337
+ }
1338
+
1339
+ if (!result.ok) {
1340
+ writeProviderOutput(result);
1341
+ throw annotateProviderError(result.error || new Error('provider run failed'), 'prepare-context');
1342
+ }
1343
+
1344
+ const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
1345
+ let proposal = parseContextProposalOutput(clean.cleanOutput);
1346
+ let reviewPath = '';
1347
+
1348
+ if (options.review === true) {
1349
+ const reviewed = await reviewPlannerContextProposal(repoRoot, proposal, options);
1350
+ proposal = reviewed.proposal;
1351
+ reviewPath = reviewed.reviewPath;
1352
+ }
1353
+
1354
+ const writePlan = buildPlannerContextWritePlan(repoRoot, proposal);
1355
+ await confirmPlannerContextWrites(writePlan, options);
1356
+
1357
+ const lifecycleRun = ensureAiRun(repoRoot, {
1358
+ command: 'ai prepare-context --with-planner',
1359
+ input: options.input || '',
1360
+ runId: options.runId,
1361
+ phase: 'created',
1362
+ });
1363
+ const snapshot = createContextSnapshots(repoRoot, lifecycleRun, writePlan, options.now || new Date());
1364
+ const plannedDocs = writePlan.filter((item) => item.action !== 'skip').map((item) => item.path);
1365
+
1366
+ process.stdout.write(formatContextPreparationReport({
1367
+ dryRun: false,
1368
+ plan: draftPack.plan,
1369
+ writePlan,
1370
+ writtenDocs: plannedDocs,
1371
+ snapshot,
1372
+ }));
1373
+
1374
+ const writtenDocs = writeDraftDocs(writePlan);
1375
+ updateAiRunPhase(repoRoot, lifecycleRun.run_id, 'onboarding-ready', {
1376
+ artifact: snapshot.manifestPath,
1377
+ command: 'ai prepare-context --with-planner',
1378
+ });
1379
+ process.stdout.write(formatContextPreparationReport({
1380
+ dryRun: false,
1381
+ plan: draftPack.plan,
1382
+ writePlan,
1383
+ writtenDocs,
1384
+ snapshot,
1385
+ completed: true,
1386
+ }));
1387
+
1388
+ return {
1389
+ task: 'prepare-context',
1390
+ mode: 'planner',
1391
+ dryRun: false,
1392
+ provider,
1393
+ role,
1394
+ contextPack: context,
1395
+ invocation,
1396
+ proposal,
1397
+ reviewPath,
1398
+ runId: lifecycleRun.run_id,
1399
+ snapshot,
1400
+ writtenDocs,
1401
+ };
1402
+ }
1403
+
722
1404
  async function runPlan(repoRoot, options = {}) {
723
1405
  const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
724
1406
  const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
@@ -839,6 +1521,15 @@ async function runPlan(repoRoot, options = {}) {
839
1521
  invocation,
840
1522
  };
841
1523
  process.stdout.write(formatDryRunReport(report));
1524
+ if (options.withPlanner === true) {
1525
+ process.stdout.write('Planner mode: already active for ai plan; --with-planner is accepted for UX consistency.\n');
1526
+ }
1527
+ if (options.review === true) {
1528
+ process.stdout.write('Review requested: provider output will be opened for review before saving the draft in live mode.\n');
1529
+ }
1530
+ if (options.interactive === true) {
1531
+ process.stdout.write('Interactive requested: live mode will ask before saving the draft.\n');
1532
+ }
842
1533
  return report;
843
1534
  }
844
1535
 
@@ -873,18 +1564,32 @@ async function runPlan(repoRoot, options = {}) {
873
1564
  throw annotateProviderError(error, 'plan', phase);
874
1565
  }
875
1566
 
876
- writeProviderOutput(result);
877
-
878
1567
  if (!result.ok) {
1568
+ writeProviderOutput(result);
879
1569
  throw annotateProviderError(result.error || new Error('provider run failed'), 'plan', phase);
880
1570
  }
881
1571
 
1572
+ const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
1573
+ let cleanOutput = clean.cleanOutput;
1574
+ let reviewPath = '';
1575
+
1576
+ if (options.review === true) {
1577
+ const reviewed = await reviewTextWithEditor(repoRoot, cleanOutput, {
1578
+ ...options,
1579
+ reviewFileName: `ai-plan-${phase}-draft.md`,
1580
+ reviewLabel: `ai plan --phase ${phase} review`,
1581
+ });
1582
+ cleanOutput = reviewed.text;
1583
+ reviewPath = reviewed.reviewPath;
1584
+ }
1585
+
1586
+ await confirmInteractiveAction(`Save ${phase} planner draft?`, options);
882
1587
  const lifecycleRun = ensureAiRun(repoRoot, {
883
1588
  command: `ai plan --phase ${phase}`,
884
1589
  input: inputPath,
885
1590
  runId: options.runId,
886
1591
  });
887
- const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
1592
+ writeCleanProviderOutput({ cleanOutput });
888
1593
  const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, `ai-plan-${phase}`, result, {
889
1594
  metadata: {
890
1595
  phase,
@@ -895,10 +1600,11 @@ async function runPlan(repoRoot, options = {}) {
895
1600
  input_compaction: inputCompaction,
896
1601
  },
897
1602
  });
898
- const draft = savePlannerDraft(repoRoot, phase, inputPath, clean.cleanOutput, {
1603
+ const draft = savePlannerDraft(repoRoot, phase, inputPath, cleanOutput, {
899
1604
  rawArtifactPath: rawArtifact.path,
900
1605
  outputSource: clean.source,
901
1606
  inputCompaction,
1607
+ reviewPath,
902
1608
  });
903
1609
  updateAiRunPhase(repoRoot, lifecycleRun.run_id, phase === 'acceptance' ? 'acceptance-draft' : 'technical-plan-draft', {
904
1610
  artifact: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
@@ -913,6 +1619,7 @@ async function runPlan(repoRoot, options = {}) {
913
1619
  phase,
914
1620
  invocation,
915
1621
  result,
1622
+ reviewPath,
916
1623
  };
917
1624
  }
918
1625
 
@@ -1011,9 +1718,8 @@ async function runReviewPlan(repoRoot, options = {}) {
1011
1718
  throw annotateProviderError(error, 'review-plan');
1012
1719
  }
1013
1720
 
1014
- writeProviderOutput(result);
1015
-
1016
1721
  if (!result.ok) {
1722
+ writeProviderOutput(result);
1017
1723
  throw annotateProviderError(result.error || new Error('provider run failed'), 'review-plan');
1018
1724
  }
1019
1725
 
@@ -1035,6 +1741,7 @@ async function runReviewPlan(repoRoot, options = {}) {
1035
1741
  stripped_prompt_echo: clean.strippedPromptEcho,
1036
1742
  },
1037
1743
  });
1744
+ writeCleanProviderOutput(clean);
1038
1745
  const saved = savePlanReview(repoRoot, {
1039
1746
  contents: clean.cleanOutput,
1040
1747
  inputPath,
@@ -1044,7 +1751,8 @@ async function runReviewPlan(repoRoot, options = {}) {
1044
1751
  rawArtifactPath: rawArtifact.path,
1045
1752
  });
1046
1753
  const relativePath = path.relative(repoRoot, saved.filePath).split(path.sep).join('/');
1047
- process.stdout.write(`AI plan review saved\nArtifact: ${relativePath}\nPrompt source: ${PLAN_REVIEW_PROMPT_SOURCE}\n`);
1754
+ const summary = summarizePlanReview(repoRoot).trimEnd();
1755
+ process.stdout.write(`AI plan review saved\nArtifact: ${relativePath}\nPrompt source: ${PLAN_REVIEW_PROMPT_SOURCE}\n${summary}\n`);
1048
1756
 
1049
1757
  return {
1050
1758
  task: 'review-plan',
@@ -1060,6 +1768,146 @@ async function runReviewPlan(repoRoot, options = {}) {
1060
1768
  };
1061
1769
  }
1062
1770
 
1771
+ async function runRepairPlan(repoRoot, options = {}) {
1772
+ const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
1773
+ const provider = resolveProviderForProfile(repoRoot, role, options.provider, options.providerExplicit, DEFAULT_PLAN_PROVIDER);
1774
+ const context = options.context || DEFAULT_PLAN_CONTEXT;
1775
+ const timeoutMs = normalizeTimeout(options.timeout);
1776
+ const source = resolveApprovedTechnicalPlanForRepair(repoRoot, options.input || '');
1777
+ const built = buildRepairPlanContext({
1778
+ context,
1779
+ inputText: source.contents,
1780
+ inputPath: source.path,
1781
+ repoRoot,
1782
+ role,
1783
+ validationError: source.validationError,
1784
+ });
1785
+ assertProviderPromptWithinLimit(built.prompt, options);
1786
+ let invocation;
1787
+
1788
+ try {
1789
+ invocation = buildProviderInvocation(provider, {
1790
+ prompt: built.prompt,
1791
+ cwd: repoRoot,
1792
+ timeoutMs,
1793
+ });
1794
+ } catch (error) {
1795
+ throw annotateProviderError(error, 'repair-plan');
1796
+ }
1797
+
1798
+ if (options.dryRun) {
1799
+ const report = {
1800
+ task: 'repair-plan',
1801
+ provider,
1802
+ role,
1803
+ contextPack: built.pack.packName,
1804
+ phase: 'technical-plan',
1805
+ invocation,
1806
+ };
1807
+ process.stdout.write(formatDryRunReport(report));
1808
+ process.stdout.write(`Source approved artifact: ${source.path}\n`);
1809
+ process.stdout.write(`Validation failure: ${source.validationError}\n`);
1810
+ return report;
1811
+ }
1812
+
1813
+ if (options.printPrompt) {
1814
+ const report = {
1815
+ task: 'repair-plan',
1816
+ provider,
1817
+ role,
1818
+ contextPack: built.pack.packName,
1819
+ phase: 'technical-plan',
1820
+ invocation,
1821
+ prompt: built.prompt,
1822
+ inputPath: source.path,
1823
+ inputKind: 'approved',
1824
+ inputVersion: source.approval.meta?.approved?.version || null,
1825
+ };
1826
+ process.stdout.write(formatPromptOnlyReport(report));
1827
+ return report;
1828
+ }
1829
+
1830
+ let providerResult;
1831
+ try {
1832
+ providerResult = await (options.runProviderFn || runProvider)(provider, {
1833
+ prompt: built.prompt,
1834
+ cwd: repoRoot,
1835
+ timeoutMs,
1836
+ dryRun: false,
1837
+ probe: options.probe,
1838
+ spawn: options.spawn,
1839
+ tempRoot: options.tempRoot,
1840
+ tempFileName: options.tempFileName,
1841
+ tempFilePrefix: options.tempFilePrefix,
1842
+ });
1843
+ } catch (error) {
1844
+ throw annotateProviderError(error, 'repair-plan');
1845
+ }
1846
+
1847
+ if (!providerResult.ok) {
1848
+ writeProviderOutput(providerResult);
1849
+ throw annotateProviderError(providerResult.error || new Error('provider run failed'), 'repair-plan');
1850
+ }
1851
+
1852
+ const lifecycleRun = ensureAiRun(repoRoot, {
1853
+ command: 'ai repair-plan',
1854
+ input: source.path,
1855
+ runId: options.runId,
1856
+ });
1857
+ const clean = extractCleanProviderOutput(providerResult, { prompt: built.prompt, projectRoot: repoRoot });
1858
+ const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, 'ai-repair-plan', providerResult, {
1859
+ metadata: {
1860
+ phase: 'technical-plan-repair',
1861
+ input_path: source.path,
1862
+ prompt_bytes: invocation.promptLength,
1863
+ clean_output_source: clean.source,
1864
+ stripped_prompt_echo: clean.strippedPromptEcho,
1865
+ validation_failure: source.validationError,
1866
+ },
1867
+ });
1868
+
1869
+ try {
1870
+ validateTechnicalPlanSpecContract(repoRoot, {
1871
+ inputPath: source.path,
1872
+ inputText: clean.cleanOutput,
1873
+ });
1874
+ } catch (error) {
1875
+ throw new Error(formatError([
1876
+ 'ai repair-plan provider output is still missing the required structured `spec.slices[]` contract.',
1877
+ stripCreateQuiverPrefix(error.message || error),
1878
+ `Raw provider artifact: ${rawArtifact.path}`,
1879
+ 'No technical-plan draft was written.',
1880
+ ].join('\n')));
1881
+ }
1882
+
1883
+ writeCleanProviderOutput(clean);
1884
+ const draft = savePlannerDraft(repoRoot, 'technical-plan', source.path, clean.cleanOutput, {
1885
+ rawArtifactPath: rawArtifact.path,
1886
+ outputSource: clean.source,
1887
+ });
1888
+ updateAiRunPhase(repoRoot, lifecycleRun.run_id, 'technical-plan-draft', {
1889
+ artifact: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
1890
+ command: 'ai repair-plan',
1891
+ });
1892
+ process.stdout.write(formatRepairPlanResult({
1893
+ ...draft,
1894
+ sourcePath: source.path,
1895
+ }, repoRoot));
1896
+
1897
+ return {
1898
+ task: 'repair-plan',
1899
+ provider,
1900
+ role,
1901
+ contextPack: built.pack.packName,
1902
+ phase: 'technical-plan',
1903
+ inputPath: source.path,
1904
+ filePath: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
1905
+ version: draft.version || null,
1906
+ invocation,
1907
+ result: providerResult,
1908
+ };
1909
+ }
1910
+
1063
1911
  async function runRevise(repoRoot, options = {}) {
1064
1912
  const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
1065
1913
  if (phase === 'spec') {
@@ -1095,8 +1943,14 @@ async function runApprove(repoRoot, options = {}) {
1095
1943
  if (phase === 'technical-plan') {
1096
1944
  const review = readPlanReview(repoRoot);
1097
1945
  if (review.status !== 'unapproved' && review.status !== 'reviewed') {
1098
- throw new Error(formatError(`ai approve --phase technical-plan requires a production review for the current draft; current review status is ${review.status}. Run \`npx create-quiver ai review-plan\`.`));
1946
+ throw new Error(formatError(`ai approve --phase technical-plan requires a production review for the current draft; current review status is ${review.status}. Run \`npx create-quiver ai review-plan --dry-run\`, then \`npx create-quiver ai review-plan\`.`));
1947
+ }
1948
+ if (reviewBlocksApproval(review)) {
1949
+ const result = review.meta.review_result;
1950
+ const requiredFixes = Array.isArray(result.required_fixes) ? result.required_fixes.length : 0;
1951
+ 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}`));
1099
1952
  }
1953
+ assertTechnicalPlanDraftHasSpecContract(repoRoot, options.version);
1100
1954
  }
1101
1955
 
1102
1956
  const inputText = '';
@@ -1255,14 +2109,53 @@ function runTraceReport(repoRoot, options = {}) {
1255
2109
  };
1256
2110
  }
1257
2111
 
2112
+ function runActiveSlice(repoRoot, options = {}) {
2113
+ const command = String(options.command || 'status').trim().toLowerCase();
2114
+ if (command !== 'status' && command !== 'reconcile') {
2115
+ throw new Error(formatError(`unsupported ai active-slice subcommand: ${command}. Supported tasks: status, reconcile`));
2116
+ }
2117
+ if (command === 'reconcile' && options.dryRun !== true) {
2118
+ throw new Error(formatError('ai active-slice reconcile is dry-run first. Run `npx create-quiver ai active-slice reconcile --dry-run`.'));
2119
+ }
2120
+
2121
+ const state = resolveProjectState(repoRoot, { allowGraphErrors: true });
2122
+ const report = collectActiveSliceState(repoRoot, { slices: state.graph.nodes });
2123
+ process.stdout.write(formatActiveSliceReconciliationReport(report, {
2124
+ dryRun: options.dryRun === true,
2125
+ }));
2126
+ return {
2127
+ task: 'active-slice',
2128
+ command,
2129
+ dryRun: options.dryRun === true,
2130
+ report,
2131
+ };
2132
+ }
2133
+
1258
2134
  function runLifecycleRun(repoRoot, options = {}) {
1259
2135
  const command = String(options.command || '').trim().toLowerCase();
1260
- if (command !== 'create') {
1261
- throw new Error(formatError(`unsupported ai run subcommand: ${command}. Supported tasks: create`));
2136
+ if (command !== 'create' && command !== 'close') {
2137
+ throw new Error(formatError(`unsupported ai run subcommand: ${command}. Supported tasks: create, close`));
1262
2138
  }
1263
- if (!options.input) {
2139
+ if (command === 'create' && !options.input) {
1264
2140
  throw new Error(formatError('ai run create requires --input <requirements.md>'));
1265
2141
  }
2142
+ if (command === 'close') {
2143
+ const current = resolveAiRun(repoRoot, options.runId || '');
2144
+ if (!current) {
2145
+ throw new Error(formatError('ai run close requires an active run or --run <id>'));
2146
+ }
2147
+ const run = updateAiRunPhase(repoRoot, current.run_id, 'closed', {
2148
+ command: 'ai run close',
2149
+ });
2150
+ const report = `AI run closed\n${formatAiRunStatus(repoRoot, run)}`;
2151
+ process.stdout.write(report);
2152
+ return {
2153
+ task: 'run',
2154
+ command,
2155
+ run,
2156
+ report,
2157
+ };
2158
+ }
1266
2159
  const run = createAiRun(repoRoot, {
1267
2160
  command: 'ai run create',
1268
2161
  input: options.input,
@@ -1463,6 +2356,31 @@ async function runPr(repoRoot, options = {}) {
1463
2356
  throw annotateGitHubError(error, 'pr');
1464
2357
  }
1465
2358
 
2359
+ if (options.review === true) {
2360
+ const hasEditorRunner = typeof options.openEditorFn === 'function';
2361
+ const canOpenEditor = hasEditorRunner || options.stdinIsTTY === true || (options.stdinIsTTY !== false && Boolean(process.stdin.isTTY));
2362
+ if (!canOpenEditor) {
2363
+ throw annotateGitHubError(makeReviewError('ai pr --review requires an interactive terminal or an injected editor runner.', plan.prBodyPath), 'pr');
2364
+ }
2365
+ const editorResult = hasEditorRunner
2366
+ ? options.openEditorFn(plan.prBodyPath, { cwd: repoRoot, env: options.env || process.env })
2367
+ : openEditor(plan.prBodyPath, { cwd: repoRoot, env: options.env || process.env });
2368
+ if (!editorResult || editorResult.ok !== true) {
2369
+ throw annotateGitHubError(makeReviewError(editorResult?.reason || 'ai pr review was canceled.', plan.prBodyPath), 'pr');
2370
+ }
2371
+ try {
2372
+ plan = buildPrCreatePlan(repoRoot, preflight, {
2373
+ baseBranch: options.baseBranch,
2374
+ ghCommand: options.ghCommand,
2375
+ input: options.input,
2376
+ prBodyPath: options.prBodyPath,
2377
+ title: options.title,
2378
+ });
2379
+ } catch (error) {
2380
+ throw annotateGitHubError(error, 'pr');
2381
+ }
2382
+ }
2383
+
1466
2384
  if (dryRun || !create) {
1467
2385
  process.stdout.write(formatPrCreateReport({ preflight, plan }, { dryRun, create }));
1468
2386
  return {
@@ -1474,6 +2392,8 @@ async function runPr(repoRoot, options = {}) {
1474
2392
  };
1475
2393
  }
1476
2394
 
2395
+ await confirmInteractiveAction(`Create GitHub PR '${plan.title}'?`, options);
2396
+
1477
2397
  let result;
1478
2398
  try {
1479
2399
  result = runGhPrCreate(plan, {
@@ -1516,6 +2436,7 @@ module.exports = {
1516
2436
  normalizeTimeout,
1517
2437
  readTextFile,
1518
2438
  runAgent,
2439
+ runActiveSlice,
1519
2440
  runDoctor,
1520
2441
  runExecutePlan,
1521
2442
  runExecuteSlice,
@@ -1528,6 +2449,7 @@ module.exports = {
1528
2449
  runApprove,
1529
2450
  runApprovalStatus,
1530
2451
  runPrepareContext,
2452
+ runRepairPlan,
1531
2453
  runReviewPlan,
1532
2454
  runRevise,
1533
2455
  runPr,