create-quiver 0.13.0 → 0.14.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 (46) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +9 -2
  3. package/README_FOR_AI.md +4 -0
  4. package/ROADMAP.md +6 -0
  5. package/docs/COMMANDS.md.template +3 -1
  6. package/docs/TROUBLESHOOTING.md.template +29 -0
  7. package/docs/WORKFLOW.md.template +13 -12
  8. package/package.json +1 -1
  9. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
  10. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
  11. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
  12. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
  13. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
  14. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
  15. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
  16. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
  17. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
  18. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
  19. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
  20. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
  21. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
  22. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
  23. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
  24. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
  25. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
  26. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
  27. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
  28. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
  29. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
  30. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
  31. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
  32. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
  33. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
  34. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  35. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
  36. package/src/create-quiver/commands/ai.js +481 -14
  37. package/src/create-quiver/commands/spec.js +10 -0
  38. package/src/create-quiver/index.js +42 -4
  39. package/src/create-quiver/lib/ai/context-packs.js +2 -2
  40. package/src/create-quiver/lib/ai/export-state.js +52 -5
  41. package/src/create-quiver/lib/ai/github.js +14 -2
  42. package/src/create-quiver/lib/ai/plan-review.js +159 -0
  43. package/src/create-quiver/lib/ai/run-state.js +17 -2
  44. package/src/create-quiver/lib/ai/spec-generator.js +15 -0
  45. package/src/create-quiver/lib/project-state-resolver.js +195 -1
  46. package/src/create-quiver/lib/spec-worktrees.js +50 -2
@@ -26,18 +26,25 @@ const {
26
26
  PLAN_REVIEW_PROMPT_SOURCE,
27
27
  buildPlanReviewPrompt,
28
28
  readPlanReview,
29
+ reviewBlocksApproval,
29
30
  resolveReviewedTechnicalPlanInput,
30
31
  resolveTechnicalPlanReviewInput,
31
32
  savePlanReview,
32
33
  summarizePlanReview,
33
34
  } = require('../lib/ai/plan-review');
34
- const { buildSpecGenerationManifest, describeSpecGeneration, generateSpecArtifacts } = require('../lib/ai/spec-generator');
35
+ const {
36
+ buildSpecGenerationManifest,
37
+ describeSpecGeneration,
38
+ generateSpecArtifacts,
39
+ validateTechnicalPlanSpecContract,
40
+ } = require('../lib/ai/spec-generator');
35
41
  const { buildProviderInvocation, runProvider } = require('../lib/ai/providers');
36
42
  const {
37
43
  createAiRun,
38
44
  ensureAiRun,
39
45
  formatAiRunResume,
40
46
  formatAiRunStatus,
47
+ listAiRuns,
41
48
  recordAiRunApproval,
42
49
  resolveAiRun,
43
50
  updateAiRunPhase,
@@ -53,12 +60,15 @@ const {
53
60
  const {
54
61
  PLANNER_APPROVAL_PHASES,
55
62
  approvePlannerPhase,
63
+ findDraftVersion,
64
+ latestDraftVersion,
56
65
  readPhaseApproval,
57
66
  resolveApprovedPlannerInput,
58
67
  savePlannerDraft,
59
68
  summarizePlannerApproval,
60
69
  } = require('../lib/approvals');
61
70
  const { assertPlannerPhaseReady, getPlannerPhaseDetails, normalizePlannerPhase, PlannerPhaseError } = require('../lib/ai/phase-gates');
71
+ const { collectActiveSliceState, resolveProjectState } = require('../lib/project-state-resolver');
62
72
 
63
73
  const DEFAULT_ONBOARD_PROVIDER = 'codex';
64
74
  const DEFAULT_ONBOARD_ROLE = 'planner';
@@ -133,6 +143,13 @@ function buildPlanContext({ role, context, phase, inputText, inputPath, repoRoot
133
143
  : 'Task: produce a technical plan only. Do not create files or modify product code.',
134
144
  ];
135
145
 
146
+ if (phaseDetails.phase === 'technical-plan') {
147
+ sections.push(
148
+ 'Required output contract: include a fenced json block with `{ "spec": { "slices": [...] } }` so Quiver can create specs after review and approval.',
149
+ 'Each `spec.slices[]` item must include at least `slice_id`, `title`, `objective`, and `files`.',
150
+ );
151
+ }
152
+
136
153
  if (relativeInputPath) {
137
154
  sections.push(`Input file: ${relativeInputPath}`);
138
155
  }
@@ -305,6 +322,14 @@ function writeProviderOutput(result) {
305
322
  }
306
323
  }
307
324
 
325
+ function writeCleanProviderOutput(clean) {
326
+ const output = String(clean?.cleanOutput || '');
327
+ if (!output) {
328
+ return;
329
+ }
330
+ process.stdout.write(output.endsWith('\n') ? output : `${output}\n`);
331
+ }
332
+
308
333
  function normalizeText(value) {
309
334
  return String(value || '').replace(/\r\n/g, '\n');
310
335
  }
@@ -548,10 +573,262 @@ function formatApprovalDryRunResult({ phase, input, version }) {
548
573
  return `${lines.join('\n')}\n`;
549
574
  }
550
575
 
576
+ function stripCreateQuiverPrefix(message) {
577
+ return String(message || '').replace(/^create-quiver:\s*/, '');
578
+ }
579
+
580
+ function readCurrentDraftForApproval(repoRoot, phase, version) {
581
+ const approval = readPhaseApproval(repoRoot, phase);
582
+ const selectedDraft = findDraftVersion(approval.meta, version);
583
+ if (!selectedDraft) {
584
+ throw new Error(formatError(`missing ${phase} draft version ${version}`));
585
+ }
586
+ const latestVersion = latestDraftVersion(approval.meta);
587
+ if (latestVersion && Number(selectedDraft.version) !== latestVersion) {
588
+ throw new Error(formatError(`${phase} draft version ${version} is not current; latest draft version is ${latestVersion}. Approve the latest version or revise again.`));
589
+ }
590
+ const draftPath = path.resolve(repoRoot, selectedDraft.path);
591
+ if (!fs.existsSync(draftPath)) {
592
+ throw new Error(formatError(`missing ${phase} draft artifact: ${selectedDraft.path}`));
593
+ }
594
+ return {
595
+ approval,
596
+ contents: fs.readFileSync(draftPath, 'utf8'),
597
+ draft: selectedDraft,
598
+ path: selectedDraft.path,
599
+ };
600
+ }
601
+
602
+ function assertTechnicalPlanDraftHasSpecContract(repoRoot, version) {
603
+ const draft = readCurrentDraftForApproval(repoRoot, 'technical-plan', version);
604
+ try {
605
+ validateTechnicalPlanSpecContract(repoRoot, {
606
+ inputPath: draft.path,
607
+ inputText: draft.contents,
608
+ });
609
+ } catch (error) {
610
+ throw new Error(formatError([
611
+ `technical-plan draft v${version} cannot be approved because it cannot create specs.`,
612
+ stripCreateQuiverPrefix(error.message || error),
613
+ 'Required contract: include a structured JSON block with `spec.slices[]` before approval.',
614
+ 'Next safe command: npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run',
615
+ ].join('\n')));
616
+ }
617
+ return draft;
618
+ }
619
+
620
+ function resolveApprovedTechnicalPlanForRepair(repoRoot, explicitInput = '') {
621
+ const approval = readPhaseApproval(repoRoot, 'technical-plan');
622
+ if (!approval.approved?.path) {
623
+ throw new Error(formatError('ai repair-plan requires an approved technical-plan artifact. Run `npx create-quiver ai approvals` to inspect planner state.'));
624
+ }
625
+
626
+ const approvedPath = approval.approved.path;
627
+ if (explicitInput) {
628
+ const explicit = path.resolve(repoRoot, explicitInput);
629
+ const approved = path.resolve(repoRoot, approvedPath);
630
+ if (explicit !== approved) {
631
+ throw new Error(formatError(`ai repair-plan input must match the approved technical-plan artifact: ${approvedPath}`));
632
+ }
633
+ }
634
+
635
+ const contents = readTextFile(approvedPath, repoRoot);
636
+ try {
637
+ validateTechnicalPlanSpecContract(repoRoot, {
638
+ inputPath: approvedPath,
639
+ inputText: contents,
640
+ });
641
+ } catch (error) {
642
+ return {
643
+ approval,
644
+ contents,
645
+ path: approvedPath,
646
+ validationError: stripCreateQuiverPrefix(error.message || error),
647
+ };
648
+ }
649
+
650
+ throw new Error(formatError('approved technical-plan already includes a valid structured `spec.slices[]` contract. No repair draft is needed.'));
651
+ }
652
+
653
+ function buildRepairPlanContext({ context, inputText, inputPath, repoRoot, role, validationError }) {
654
+ const pack = buildContextPackMetadata({
655
+ role,
656
+ packName: context,
657
+ repoRoot,
658
+ });
659
+ const prompt = [
660
+ pack.prompt,
661
+ 'Phase: technical-plan',
662
+ 'Task: repair the approved technical plan into a new draft only. Do not approve it, create specs, modify product code, or expand scope.',
663
+ 'Preserve the approved intent, scope, risks, and decisions.',
664
+ 'Add the required Quiver structured JSON contract in a fenced json block.',
665
+ 'The JSON must include `{ "spec": { "slug": "...", "title": "...", "objective": "...", "slices": [...] } }`.',
666
+ 'Each item in `spec.slices[]` must include at least `slice_id`, `title`, `objective`, and `files`.',
667
+ `Validation failure to repair: ${validationError}`,
668
+ `Approved technical-plan artifact: ${inputPath}`,
669
+ 'Approved technical-plan contents:',
670
+ inputText.trimEnd(),
671
+ ].join('\n\n');
672
+
673
+ return {
674
+ pack,
675
+ prompt,
676
+ };
677
+ }
678
+
679
+ function formatRepairPlanResult(result, repoRoot) {
680
+ const relativePath = path.relative(repoRoot, result.filePath).split(path.sep).join('/');
681
+ return [
682
+ 'AI technical-plan repair draft saved',
683
+ `Draft: ${relativePath}`,
684
+ `Version: v${result.version}`,
685
+ `Source approved artifact: ${result.sourcePath}`,
686
+ 'Original approved artifact: preserved',
687
+ 'Next safe commands:',
688
+ '- npx create-quiver ai review-plan --dry-run',
689
+ '- npx create-quiver ai review-plan',
690
+ `- npx create-quiver ai approve --phase technical-plan --version ${result.version}`,
691
+ ].join('\n').concat('\n');
692
+ }
693
+
694
+ function formatActiveSliceReconciliationReport(report, options = {}) {
695
+ const lines = [
696
+ 'AI active-slice reconciliation',
697
+ `Mode: ${options.dryRun ? 'dry-run' : 'read-only'}`,
698
+ `Decision: ${report.reconciliation.decision}`,
699
+ `Reason: ${report.reconciliation.reason}`,
700
+ '',
701
+ 'Supported sources:',
702
+ ];
703
+
704
+ for (const source of report.supported_sources) {
705
+ lines.push(`- ${source.path}: ${source.exists ? 'exists' : 'missing'}`);
706
+ }
707
+
708
+ lines.push('', 'Detected sources:');
709
+ if (report.sources.length === 0) {
710
+ lines.push('- none');
711
+ } else {
712
+ for (const source of report.sources) {
713
+ const ref = source.ref || '(unresolved)';
714
+ const status = source.status ? ` status=${source.status}` : '';
715
+ const issue = source.issue ? ` issue=${source.issue}` : '';
716
+ lines.push(`- ${source.source_id}: ${ref}${status}${issue}`);
717
+ }
718
+ }
719
+
720
+ lines.push('', 'Planned changes:');
721
+ if (report.reconciliation.planned_changes.length === 0) {
722
+ lines.push('- none');
723
+ } else {
724
+ for (const change of report.reconciliation.planned_changes) {
725
+ lines.push(`- ${change}`);
726
+ }
727
+ }
728
+
729
+ lines.push('', 'Risks:');
730
+ if (report.reconciliation.risks.length === 0) {
731
+ lines.push('- none');
732
+ } else {
733
+ for (const risk of report.reconciliation.risks) {
734
+ lines.push(`- ${risk}`);
735
+ }
736
+ }
737
+
738
+ lines.push('', options.dryRun ? 'No files were changed.' : 'This command is read-only; use start-slice or cleanup-slice for intentional writes.');
739
+ return `${lines.join('\n')}\n`;
740
+ }
741
+
742
+ function readRunApprovals(repoRoot, run) {
743
+ if (!run?.approvals_path) {
744
+ return [];
745
+ }
746
+ const filePath = path.resolve(repoRoot, run.approvals_path);
747
+ if (!fs.existsSync(filePath)) {
748
+ return [];
749
+ }
750
+ try {
751
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
752
+ return Array.isArray(parsed.approvals) ? parsed.approvals : [];
753
+ } catch {
754
+ return [];
755
+ }
756
+ }
757
+
758
+ function collectRunApprovalRows(repoRoot) {
759
+ const activeRun = resolveAiRun(repoRoot, '');
760
+ return listAiRuns(repoRoot)
761
+ .flatMap((run) => readRunApprovals(repoRoot, run).map((approval) => ({
762
+ run,
763
+ approval,
764
+ relation: activeRun && run.run_id === activeRun.run_id
765
+ ? 'active'
766
+ : run.status === 'closed'
767
+ ? 'historical'
768
+ : 'other-open',
769
+ })));
770
+ }
771
+
772
+ function approvalArtifactForRelation(report) {
773
+ return report?.approved?.path || report?.draft?.path || '';
774
+ }
775
+
776
+ function classifyGlobalApprovalRelation(report, runApprovalRows) {
777
+ const artifact = approvalArtifactForRelation(report);
778
+ if (!artifact || report.status === 'missing') {
779
+ return 'none';
780
+ }
781
+ const matches = runApprovalRows.filter((row) => row.approval?.artifact === artifact);
782
+ if (matches.some((row) => row.relation === 'active')) {
783
+ return 'active';
784
+ }
785
+ if (matches.length > 0) {
786
+ return 'historical';
787
+ }
788
+ return 'orphaned';
789
+ }
790
+
791
+ function formatRunScopedApprovals(repoRoot, runApprovalRows) {
792
+ const runs = listAiRuns(repoRoot);
793
+ const activeRun = resolveAiRun(repoRoot, '');
794
+ const lines = [
795
+ 'Run-scoped approvals',
796
+ `Active run: ${activeRun ? activeRun.run_id : '(none)'}`,
797
+ ];
798
+
799
+ if (runs.length === 0) {
800
+ lines.push('- no AI runs found');
801
+ return `${lines.join('\n')}\n`;
802
+ }
803
+
804
+ for (const run of runs.slice().reverse()) {
805
+ const relation = activeRun && run.run_id === activeRun.run_id
806
+ ? 'active'
807
+ : run.status === 'closed'
808
+ ? 'historical'
809
+ : 'other-open';
810
+ const approvals = runApprovalRows.filter((row) => row.run.run_id === run.run_id);
811
+ lines.push(`Run: ${run.run_id} (${relation}, phase: ${run.phase}, status: ${run.status})`);
812
+ if (approvals.length === 0) {
813
+ lines.push('- no run-scoped approvals');
814
+ continue;
815
+ }
816
+ for (const row of approvals) {
817
+ const version = row.approval.version ? ` v${row.approval.version}` : '';
818
+ lines.push(`- ${row.approval.phase || 'unknown'}${version}: ${row.approval.artifact || '(missing artifact)'}`);
819
+ }
820
+ }
821
+
822
+ return `${lines.join('\n')}\n`;
823
+ }
824
+
551
825
  function formatApprovalStatusReport(repoRoot) {
552
- const sections = ['AI approvals status'];
826
+ const runApprovalRows = collectRunApprovalRows(repoRoot);
827
+ const sections = ['AI approvals status', formatRunScopedApprovals(repoRoot, runApprovalRows).trimEnd(), 'Global planner approvals'];
553
828
  for (const phase of PLANNER_APPROVAL_PHASES) {
554
- sections.push(summarizePlannerApproval(repoRoot, phase).trimEnd());
829
+ const summary = summarizePlannerApproval(repoRoot, phase).trimEnd();
830
+ const relation = classifyGlobalApprovalRelation(readPhaseApproval(repoRoot, phase), runApprovalRows);
831
+ sections.push(`${summary}\nRun relation: ${relation}`);
555
832
  }
556
833
  sections.push(summarizePlanReview(repoRoot).trimEnd());
557
834
  return `${sections.join('\n\n')}\n`;
@@ -640,12 +917,14 @@ async function runOnboard(repoRoot, options = {}) {
640
917
  throw annotateProviderError(error, 'onboard');
641
918
  }
642
919
 
643
- writeProviderOutput(result);
644
-
645
920
  if (!result.ok) {
921
+ writeProviderOutput(result);
646
922
  throw annotateProviderError(result.error || new Error('provider run failed'), 'onboard');
647
923
  }
648
924
 
925
+ const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
926
+ writeCleanProviderOutput(clean);
927
+
649
928
  return {
650
929
  task: 'onboard',
651
930
  provider,
@@ -873,9 +1152,8 @@ async function runPlan(repoRoot, options = {}) {
873
1152
  throw annotateProviderError(error, 'plan', phase);
874
1153
  }
875
1154
 
876
- writeProviderOutput(result);
877
-
878
1155
  if (!result.ok) {
1156
+ writeProviderOutput(result);
879
1157
  throw annotateProviderError(result.error || new Error('provider run failed'), 'plan', phase);
880
1158
  }
881
1159
 
@@ -885,6 +1163,7 @@ async function runPlan(repoRoot, options = {}) {
885
1163
  runId: options.runId,
886
1164
  });
887
1165
  const clean = extractCleanProviderOutput(result, { prompt, projectRoot: repoRoot });
1166
+ writeCleanProviderOutput(clean);
888
1167
  const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, `ai-plan-${phase}`, result, {
889
1168
  metadata: {
890
1169
  phase,
@@ -1011,9 +1290,8 @@ async function runReviewPlan(repoRoot, options = {}) {
1011
1290
  throw annotateProviderError(error, 'review-plan');
1012
1291
  }
1013
1292
 
1014
- writeProviderOutput(result);
1015
-
1016
1293
  if (!result.ok) {
1294
+ writeProviderOutput(result);
1017
1295
  throw annotateProviderError(result.error || new Error('provider run failed'), 'review-plan');
1018
1296
  }
1019
1297
 
@@ -1035,6 +1313,7 @@ async function runReviewPlan(repoRoot, options = {}) {
1035
1313
  stripped_prompt_echo: clean.strippedPromptEcho,
1036
1314
  },
1037
1315
  });
1316
+ writeCleanProviderOutput(clean);
1038
1317
  const saved = savePlanReview(repoRoot, {
1039
1318
  contents: clean.cleanOutput,
1040
1319
  inputPath,
@@ -1044,7 +1323,8 @@ async function runReviewPlan(repoRoot, options = {}) {
1044
1323
  rawArtifactPath: rawArtifact.path,
1045
1324
  });
1046
1325
  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`);
1326
+ const summary = summarizePlanReview(repoRoot).trimEnd();
1327
+ process.stdout.write(`AI plan review saved\nArtifact: ${relativePath}\nPrompt source: ${PLAN_REVIEW_PROMPT_SOURCE}\n${summary}\n`);
1048
1328
 
1049
1329
  return {
1050
1330
  task: 'review-plan',
@@ -1060,6 +1340,146 @@ async function runReviewPlan(repoRoot, options = {}) {
1060
1340
  };
1061
1341
  }
1062
1342
 
1343
+ async function runRepairPlan(repoRoot, options = {}) {
1344
+ const role = normalizeRole(options.role || DEFAULT_PLAN_ROLE);
1345
+ const provider = resolveProviderForProfile(repoRoot, role, options.provider, options.providerExplicit, DEFAULT_PLAN_PROVIDER);
1346
+ const context = options.context || DEFAULT_PLAN_CONTEXT;
1347
+ const timeoutMs = normalizeTimeout(options.timeout);
1348
+ const source = resolveApprovedTechnicalPlanForRepair(repoRoot, options.input || '');
1349
+ const built = buildRepairPlanContext({
1350
+ context,
1351
+ inputText: source.contents,
1352
+ inputPath: source.path,
1353
+ repoRoot,
1354
+ role,
1355
+ validationError: source.validationError,
1356
+ });
1357
+ assertProviderPromptWithinLimit(built.prompt, options);
1358
+ let invocation;
1359
+
1360
+ try {
1361
+ invocation = buildProviderInvocation(provider, {
1362
+ prompt: built.prompt,
1363
+ cwd: repoRoot,
1364
+ timeoutMs,
1365
+ });
1366
+ } catch (error) {
1367
+ throw annotateProviderError(error, 'repair-plan');
1368
+ }
1369
+
1370
+ if (options.dryRun) {
1371
+ const report = {
1372
+ task: 'repair-plan',
1373
+ provider,
1374
+ role,
1375
+ contextPack: built.pack.packName,
1376
+ phase: 'technical-plan',
1377
+ invocation,
1378
+ };
1379
+ process.stdout.write(formatDryRunReport(report));
1380
+ process.stdout.write(`Source approved artifact: ${source.path}\n`);
1381
+ process.stdout.write(`Validation failure: ${source.validationError}\n`);
1382
+ return report;
1383
+ }
1384
+
1385
+ if (options.printPrompt) {
1386
+ const report = {
1387
+ task: 'repair-plan',
1388
+ provider,
1389
+ role,
1390
+ contextPack: built.pack.packName,
1391
+ phase: 'technical-plan',
1392
+ invocation,
1393
+ prompt: built.prompt,
1394
+ inputPath: source.path,
1395
+ inputKind: 'approved',
1396
+ inputVersion: source.approval.meta?.approved?.version || null,
1397
+ };
1398
+ process.stdout.write(formatPromptOnlyReport(report));
1399
+ return report;
1400
+ }
1401
+
1402
+ let providerResult;
1403
+ 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,
1414
+ });
1415
+ } catch (error) {
1416
+ throw annotateProviderError(error, 'repair-plan');
1417
+ }
1418
+
1419
+ if (!providerResult.ok) {
1420
+ writeProviderOutput(providerResult);
1421
+ throw annotateProviderError(providerResult.error || new Error('provider run failed'), 'repair-plan');
1422
+ }
1423
+
1424
+ const lifecycleRun = ensureAiRun(repoRoot, {
1425
+ command: 'ai repair-plan',
1426
+ input: source.path,
1427
+ runId: options.runId,
1428
+ });
1429
+ const clean = extractCleanProviderOutput(providerResult, { prompt: built.prompt, projectRoot: repoRoot });
1430
+ const rawArtifact = writeRawProviderArtifact(repoRoot, lifecycleRun.run_id, 'ai-repair-plan', providerResult, {
1431
+ metadata: {
1432
+ phase: 'technical-plan-repair',
1433
+ input_path: source.path,
1434
+ prompt_bytes: invocation.promptLength,
1435
+ clean_output_source: clean.source,
1436
+ stripped_prompt_echo: clean.strippedPromptEcho,
1437
+ validation_failure: source.validationError,
1438
+ },
1439
+ });
1440
+
1441
+ try {
1442
+ validateTechnicalPlanSpecContract(repoRoot, {
1443
+ inputPath: source.path,
1444
+ inputText: clean.cleanOutput,
1445
+ });
1446
+ } catch (error) {
1447
+ throw new Error(formatError([
1448
+ 'ai repair-plan provider output is still missing the required structured `spec.slices[]` contract.',
1449
+ stripCreateQuiverPrefix(error.message || error),
1450
+ `Raw provider artifact: ${rawArtifact.path}`,
1451
+ 'No technical-plan draft was written.',
1452
+ ].join('\n')));
1453
+ }
1454
+
1455
+ writeCleanProviderOutput(clean);
1456
+ const draft = savePlannerDraft(repoRoot, 'technical-plan', source.path, clean.cleanOutput, {
1457
+ rawArtifactPath: rawArtifact.path,
1458
+ outputSource: clean.source,
1459
+ });
1460
+ updateAiRunPhase(repoRoot, lifecycleRun.run_id, 'technical-plan-draft', {
1461
+ artifact: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
1462
+ command: 'ai repair-plan',
1463
+ });
1464
+ process.stdout.write(formatRepairPlanResult({
1465
+ ...draft,
1466
+ sourcePath: source.path,
1467
+ }, repoRoot));
1468
+
1469
+ return {
1470
+ task: 'repair-plan',
1471
+ provider,
1472
+ role,
1473
+ contextPack: built.pack.packName,
1474
+ phase: 'technical-plan',
1475
+ inputPath: source.path,
1476
+ filePath: path.relative(repoRoot, draft.filePath).split(path.sep).join('/'),
1477
+ version: draft.version || null,
1478
+ invocation,
1479
+ result: providerResult,
1480
+ };
1481
+ }
1482
+
1063
1483
  async function runRevise(repoRoot, options = {}) {
1064
1484
  const phase = normalizePlannerPhase(options.phase || DEFAULT_PLAN_PHASE);
1065
1485
  if (phase === 'spec') {
@@ -1095,8 +1515,14 @@ async function runApprove(repoRoot, options = {}) {
1095
1515
  if (phase === 'technical-plan') {
1096
1516
  const review = readPlanReview(repoRoot);
1097
1517
  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\`.`));
1518
+ 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\`.`));
1519
+ }
1520
+ if (reviewBlocksApproval(review)) {
1521
+ const result = review.meta.review_result;
1522
+ const requiredFixes = Array.isArray(result.required_fixes) ? result.required_fixes.length : 0;
1523
+ 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
1524
  }
1525
+ assertTechnicalPlanDraftHasSpecContract(repoRoot, options.version);
1100
1526
  }
1101
1527
 
1102
1528
  const inputText = '';
@@ -1255,14 +1681,53 @@ function runTraceReport(repoRoot, options = {}) {
1255
1681
  };
1256
1682
  }
1257
1683
 
1684
+ function runActiveSlice(repoRoot, options = {}) {
1685
+ const command = String(options.command || 'status').trim().toLowerCase();
1686
+ if (command !== 'status' && command !== 'reconcile') {
1687
+ throw new Error(formatError(`unsupported ai active-slice subcommand: ${command}. Supported tasks: status, reconcile`));
1688
+ }
1689
+ if (command === 'reconcile' && options.dryRun !== true) {
1690
+ throw new Error(formatError('ai active-slice reconcile is dry-run first. Run `npx create-quiver ai active-slice reconcile --dry-run`.'));
1691
+ }
1692
+
1693
+ const state = resolveProjectState(repoRoot, { allowGraphErrors: true });
1694
+ const report = collectActiveSliceState(repoRoot, { slices: state.graph.nodes });
1695
+ process.stdout.write(formatActiveSliceReconciliationReport(report, {
1696
+ dryRun: options.dryRun === true,
1697
+ }));
1698
+ return {
1699
+ task: 'active-slice',
1700
+ command,
1701
+ dryRun: options.dryRun === true,
1702
+ report,
1703
+ };
1704
+ }
1705
+
1258
1706
  function runLifecycleRun(repoRoot, options = {}) {
1259
1707
  const command = String(options.command || '').trim().toLowerCase();
1260
- if (command !== 'create') {
1261
- throw new Error(formatError(`unsupported ai run subcommand: ${command}. Supported tasks: create`));
1708
+ if (command !== 'create' && command !== 'close') {
1709
+ throw new Error(formatError(`unsupported ai run subcommand: ${command}. Supported tasks: create, close`));
1262
1710
  }
1263
- if (!options.input) {
1711
+ if (command === 'create' && !options.input) {
1264
1712
  throw new Error(formatError('ai run create requires --input <requirements.md>'));
1265
1713
  }
1714
+ if (command === 'close') {
1715
+ const current = resolveAiRun(repoRoot, options.runId || '');
1716
+ if (!current) {
1717
+ throw new Error(formatError('ai run close requires an active run or --run <id>'));
1718
+ }
1719
+ const run = updateAiRunPhase(repoRoot, current.run_id, 'closed', {
1720
+ command: 'ai run close',
1721
+ });
1722
+ const report = `AI run closed\n${formatAiRunStatus(repoRoot, run)}`;
1723
+ process.stdout.write(report);
1724
+ return {
1725
+ task: 'run',
1726
+ command,
1727
+ run,
1728
+ report,
1729
+ };
1730
+ }
1266
1731
  const run = createAiRun(repoRoot, {
1267
1732
  command: 'ai run create',
1268
1733
  input: options.input,
@@ -1516,6 +1981,7 @@ module.exports = {
1516
1981
  normalizeTimeout,
1517
1982
  readTextFile,
1518
1983
  runAgent,
1984
+ runActiveSlice,
1519
1985
  runDoctor,
1520
1986
  runExecutePlan,
1521
1987
  runExecuteSlice,
@@ -1528,6 +1994,7 @@ module.exports = {
1528
1994
  runApprove,
1529
1995
  runApprovalStatus,
1530
1996
  runPrepareContext,
1997
+ runRepairPlan,
1531
1998
  runReviewPlan,
1532
1999
  runRevise,
1533
2000
  runPr,
@@ -129,6 +129,12 @@ function detectCycle(nodes) {
129
129
  return null;
130
130
  }
131
131
 
132
+ function missingGitFields(git) {
133
+ const data = git && typeof git === 'object' ? git : {};
134
+ return ['branch_type', 'base_branch', 'branch_slug', 'branch_name']
135
+ .filter((field) => !String(data[field] || '').trim());
136
+ }
137
+
132
138
  function buildSpecValidationReport(repoRoot, specInput, options = {}) {
133
139
  const strict = options.strict === true;
134
140
  const specDir = resolveSpecDir(repoRoot, specInput);
@@ -190,6 +196,10 @@ function buildSpecValidationReport(repoRoot, specInput, options = {}) {
190
196
  if (!Array.isArray(writeScope) || writeScope.length === 0) {
191
197
  errors.push(`${relativeSliceFile} must declare files or allowed_write_paths`);
192
198
  }
199
+ const missingGit = missingGitFields(json.git);
200
+ if (missingGit.length > 0) {
201
+ errors.push(`${relativeSliceFile} must declare git.branch_type, git.base_branch, git.branch_slug, and git.branch_name for local execution (missing: ${missingGit.join(', ')}).`);
202
+ }
193
203
  try {
194
204
  validateProjectRelativePaths(writeScope, `${relativeSliceFile} write scope`);
195
205
  validateProjectRelativePaths(json.expected_read_paths, `${relativeSliceFile} expected_read_paths`);