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.
- package/CHANGELOG.md +19 -0
- package/README.md +9 -2
- package/README_FOR_AI.md +4 -0
- package/ROADMAP.md +6 -0
- package/docs/COMMANDS.md.template +3 -1
- package/docs/TROUBLESHOOTING.md.template +29 -0
- package/docs/WORKFLOW.md.template +13 -12
- package/package.json +1 -1
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
- package/src/create-quiver/commands/ai.js +481 -14
- package/src/create-quiver/commands/spec.js +10 -0
- package/src/create-quiver/index.js +42 -4
- package/src/create-quiver/lib/ai/context-packs.js +2 -2
- package/src/create-quiver/lib/ai/export-state.js +52 -5
- package/src/create-quiver/lib/ai/github.js +14 -2
- package/src/create-quiver/lib/ai/plan-review.js +159 -0
- package/src/create-quiver/lib/ai/run-state.js +17 -2
- package/src/create-quiver/lib/ai/spec-generator.js +15 -0
- package/src/create-quiver/lib/project-state-resolver.js +195 -1
- 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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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`);
|