create-quiver 0.10.0 → 0.12.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/BACKLOG.md +16 -17
- package/CHANGELOG.md +34 -0
- package/README.md +174 -39
- package/README_FOR_AI.md +48 -24
- package/ROADMAP.md +22 -11
- package/docs/AI_CONTEXT.md.template +2 -0
- package/docs/AI_ONBOARDING_PROMPT.md.template +25 -18
- package/docs/COMMANDS.md.template +59 -11
- package/docs/CONTEXTO.md.template +2 -0
- package/docs/DECISIONS.md.template +1 -0
- package/docs/INDEX.md.template +20 -18
- package/docs/STATUS.md.template +1 -0
- package/docs/SUPPORT_MATRIX.md.template +2 -2
- package/docs/TROUBLESHOOTING.md.template +50 -0
- package/docs/WORKFLOW.md.template +25 -17
- package/package.json +19 -2
- package/package.template.json +13 -1
- package/scripts/init-docs.sh +11 -4
- package/scripts/package-quiver.sh +18 -2
- package/specs/quiver-v22-guided-ai-workflow/EVIDENCE_REPORT.md +58 -0
- package/specs/quiver-v22-guided-ai-workflow/EXECUTION_PLAN.md +88 -0
- package/specs/quiver-v22-guided-ai-workflow/SPEC.md +228 -0
- package/specs/quiver-v22-guided-ai-workflow/STATUS.md +42 -0
- package/specs/quiver-v22-guided-ai-workflow/pr.md +104 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +61 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-00-spec-foundation/slice.json +51 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-01-docs-source-of-truth-sync/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-01-docs-source-of-truth-sync/EXECUTION_BRIEF.md +58 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-01-docs-source-of-truth-sync/slice.json +55 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-02-prepare-command-diagnostics/CLOSURE_BRIEF.md +30 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-02-prepare-command-diagnostics/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-02-prepare-command-diagnostics/slice.json +57 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-03-context-doc-refresh/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-03-context-doc-refresh/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-03-context-doc-refresh/slice.json +56 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-04-planner-approval-state/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-04-planner-approval-state/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-04-planner-approval-state/slice.json +58 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-05-spec-worktree-lifecycle/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-05-spec-worktree-lifecycle/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-05-spec-worktree-lifecycle/slice.json +54 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-06-executor-commit-recovery/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-06-executor-commit-recovery/EXECUTION_BRIEF.md +58 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-06-executor-commit-recovery/slice.json +57 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-07-execution-waves-delegation/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-07-execution-waves-delegation/EXECUTION_BRIEF.md +58 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-07-execution-waves-delegation/slice.json +55 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-08-pr-create-gh-ssh/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-08-pr-create-gh-ssh/EXECUTION_BRIEF.md +58 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-08-pr-create-gh-ssh/slice.json +53 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-09-post-merge-cleanup-release-safety/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-09-post-merge-cleanup-release-safety/EXECUTION_BRIEF.md +59 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-09-post-merge-cleanup-release-safety/slice.json +59 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-10-docs-smokes-release-readiness/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-10-docs-smokes-release-readiness/EXECUTION_BRIEF.md +58 -0
- package/specs/quiver-v22-guided-ai-workflow/slices/slice-10-docs-smokes-release-readiness/slice.json +60 -0
- package/specs/quiver-v23-guided-flow-productization/EVIDENCE_REPORT.md +80 -0
- package/specs/quiver-v23-guided-flow-productization/EXECUTION_PLAN.md +80 -0
- package/specs/quiver-v23-guided-flow-productization/SPEC.md +203 -0
- package/specs/quiver-v23-guided-flow-productization/STATUS.md +39 -0
- package/specs/quiver-v23-guided-flow-productization/pr.md +119 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +30 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +61 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-00-spec-foundation/slice.json +51 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-01-short-command-and-flow-entrypoint/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-01-short-command-and-flow-entrypoint/EXECUTION_BRIEF.md +35 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-01-short-command-and-flow-entrypoint/slice.json +56 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-02-flow-status-wizard/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-02-flow-status-wizard/EXECUTION_BRIEF.md +29 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-02-flow-status-wizard/slice.json +55 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-03-agent-profiles/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-03-agent-profiles/EXECUTION_BRIEF.md +29 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-03-agent-profiles/slice.json +54 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-04-context-preparation-onboarding/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-04-context-preparation-onboarding/EXECUTION_BRIEF.md +30 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-04-context-preparation-onboarding/slice.json +59 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-05-planner-iteration-history/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-05-planner-iteration-history/EXECUTION_BRIEF.md +29 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-05-planner-iteration-history/slice.json +53 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-06-production-plan-review/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-06-production-plan-review/EXECUTION_BRIEF.md +30 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-06-production-plan-review/slice.json +54 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-07-spec-create-experience/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-07-spec-create-experience/EXECUTION_BRIEF.md +30 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-07-spec-create-experience/slice.json +55 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-08-executor-prompt-generation/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-08-executor-prompt-generation/EXECUTION_BRIEF.md +30 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-08-executor-prompt-generation/slice.json +55 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-09-delegated-slice-execution/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-09-delegated-slice-execution/EXECUTION_BRIEF.md +34 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-09-delegated-slice-execution/slice.json +57 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-10-docs-smokes-release-readiness/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-10-docs-smokes-release-readiness/EXECUTION_BRIEF.md +32 -0
- package/specs/quiver-v23-guided-flow-productization/slices/slice-10-docs-smokes-release-readiness/slice.json +63 -0
- package/specs/quiver-v24-dx-onboarding-hardening/EVIDENCE_REPORT.md +55 -0
- package/specs/quiver-v24-dx-onboarding-hardening/EXECUTION_PLAN.md +43 -0
- package/specs/quiver-v24-dx-onboarding-hardening/SPEC.md +149 -0
- package/specs/quiver-v24-dx-onboarding-hardening/STATUS.md +31 -0
- package/specs/quiver-v24-dx-onboarding-hardening/pr.md +76 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-00-spec-foundation/slice.json +51 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-01-init-template-hygiene/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-01-init-template-hygiene/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-01-init-template-hygiene/slice.json +55 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-02-cli-command-routing-version-errors/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-02-cli-command-routing-version-errors/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-02-cli-command-routing-version-errors/slice.json +52 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-03-doctor-fix-doc-link-checks/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-03-doctor-fix-doc-link-checks/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-03-doctor-fix-doc-link-checks/slice.json +53 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-04-prepare-output-ai-context-drafts/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-04-prepare-output-ai-context-drafts/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-04-prepare-output-ai-context-drafts/slice.json +70 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-05-local-slice-validation-base-guidance/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-05-local-slice-validation-base-guidance/EXECUTION_BRIEF.md +49 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-05-local-slice-validation-base-guidance/slice.json +52 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-06-plan-graph-next-history-views/CLOSURE_BRIEF.md +43 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-06-plan-graph-next-history-views/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-06-plan-graph-next-history-views/slice.json +60 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-07-analyzer-command-map-hardening/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-07-analyzer-command-map-hardening/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-07-analyzer-command-map-hardening/slice.json +51 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-08-evidence-run-command/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-08-evidence-run-command/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-08-evidence-run-command/slice.json +54 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-09-spec-viewer-demo-scaffolding/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-09-spec-viewer-demo-scaffolding/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-09-spec-viewer-demo-scaffolding/slice.json +59 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-10-docs-smokes-release-readiness/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-10-docs-smokes-release-readiness/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v24-dx-onboarding-hardening/slices/slice-10-docs-smokes-release-readiness/slice.json +76 -0
- package/src/create-quiver/commands/ai.js +508 -35
- package/src/create-quiver/commands/demo.js +22 -0
- package/src/create-quiver/commands/evidence.js +37 -0
- package/src/create-quiver/commands/flow.js +561 -0
- package/src/create-quiver/commands/graph.js +14 -1
- package/src/create-quiver/commands/next.js +28 -0
- package/src/create-quiver/commands/plan.js +6 -3
- package/src/create-quiver/commands/prepare.js +236 -0
- package/src/create-quiver/commands/spec.js +133 -0
- package/src/create-quiver/index.js +688 -25
- package/src/create-quiver/lib/agent-profiles.js +148 -0
- package/src/create-quiver/lib/ai/context-packs.js +12 -0
- package/src/create-quiver/lib/ai/execution-plan.js +370 -10
- package/src/create-quiver/lib/ai/executor.js +376 -17
- package/src/create-quiver/lib/ai/github.js +196 -0
- package/src/create-quiver/lib/ai/onboarding-template.js +365 -0
- package/src/create-quiver/lib/ai/plan-review.js +283 -0
- package/src/create-quiver/lib/ai/providers.js +1 -0
- package/src/create-quiver/lib/ai/safety.js +5 -0
- package/src/create-quiver/lib/ai/spec-templates.js +2 -2
- package/src/create-quiver/lib/approvals.js +350 -0
- package/src/create-quiver/lib/demo.js +657 -0
- package/src/create-quiver/lib/doctor.js +234 -0
- package/src/create-quiver/lib/evidence.js +115 -0
- package/src/create-quiver/lib/init-docs.js +284 -17
- package/src/create-quiver/lib/init-layout.js +26 -1
- package/src/create-quiver/lib/lifecycle.js +6 -0
- package/src/create-quiver/lib/package-safety.js +117 -0
- package/src/create-quiver/lib/readiness.js +85 -18
- package/src/create-quiver/lib/slice-graph.js +1 -0
- package/src/create-quiver/lib/slice.js +8 -8
- package/src/create-quiver/lib/spec-worktrees.js +349 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { readPhaseApproval, resolveApprovedPlannerInput } = require('../approvals');
|
|
5
|
+
const { quiverInternalPaths } = require('../init-layout');
|
|
6
|
+
|
|
7
|
+
const PLAN_REVIEW_PROMPT_SOURCE = 'packaged production-readiness plan review template';
|
|
8
|
+
|
|
9
|
+
function formatError(message) {
|
|
10
|
+
return `create-quiver: ${message}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toRelativePosix(root, filePath) {
|
|
14
|
+
return path.relative(root, filePath).split(path.sep).join('/');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function planReviewRoot(projectRoot) {
|
|
18
|
+
return path.join(quiverInternalPaths(projectRoot).root, 'approvals', 'plan-review');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function planReviewPath(projectRoot) {
|
|
22
|
+
return path.join(planReviewRoot(projectRoot), 'review.md');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function planReviewMetaPath(projectRoot) {
|
|
26
|
+
return path.join(planReviewRoot(projectRoot), 'meta.json');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readPlanReviewMeta(projectRoot) {
|
|
30
|
+
const filePath = planReviewMetaPath(projectRoot);
|
|
31
|
+
if (!fs.existsSync(filePath)) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
throw new Error(formatError(`invalid plan-review metadata at ${toRelativePosix(projectRoot, filePath)}: ${error.message}`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeDrafts(meta) {
|
|
42
|
+
return Array.isArray(meta?.drafts) ? meta.drafts.filter((item) => item && typeof item === 'object') : [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolvePath(projectRoot, relativePath) {
|
|
46
|
+
return path.resolve(projectRoot, relativePath || '');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function samePath(projectRoot, left, right) {
|
|
50
|
+
return Boolean(left && right && resolvePath(projectRoot, left) === resolvePath(projectRoot, right));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function latestTechnicalPlanDraft(approval) {
|
|
54
|
+
const version = Number(approval.meta?.draft?.version || 0);
|
|
55
|
+
if (!version) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return normalizeDrafts(approval.meta).find((item) => Number(item.version) === version) || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function reviewMatchesTarget(projectRoot, review, target) {
|
|
62
|
+
if (review.version && target.version) {
|
|
63
|
+
return review.version === target.version;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!review.source) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return samePath(projectRoot, review.source, target.source)
|
|
71
|
+
|| samePath(projectRoot, review.source, target.artifact);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function resolveTechnicalPlanReviewInput(projectRoot, explicitInput) {
|
|
75
|
+
const approval = readPhaseApproval(projectRoot, 'technical-plan');
|
|
76
|
+
const latestDraft = latestTechnicalPlanDraft(approval);
|
|
77
|
+
const candidates = [];
|
|
78
|
+
|
|
79
|
+
if (latestDraft?.path) {
|
|
80
|
+
candidates.push({
|
|
81
|
+
kind: 'draft',
|
|
82
|
+
version: Number(latestDraft.version),
|
|
83
|
+
inputPath: latestDraft.path,
|
|
84
|
+
approval,
|
|
85
|
+
});
|
|
86
|
+
} else if (approval.draft?.path) {
|
|
87
|
+
candidates.push({
|
|
88
|
+
kind: 'draft',
|
|
89
|
+
version: Number(approval.meta?.draft?.version || 0) || null,
|
|
90
|
+
inputPath: approval.draft.path,
|
|
91
|
+
approval,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (approval.approved?.path) {
|
|
96
|
+
candidates.push({
|
|
97
|
+
kind: 'approved',
|
|
98
|
+
version: Number(approval.meta?.approved?.version || 0) || null,
|
|
99
|
+
inputPath: approval.approved.path,
|
|
100
|
+
approval,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (candidates.length === 0) {
|
|
105
|
+
throw new Error(formatError("ai review-plan requires a generated technical-plan draft. Run `npx create-quiver ai plan --phase technical-plan`."));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!explicitInput) {
|
|
109
|
+
return candidates[0];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const approvedSource = approval.meta?.approved?.source_file || '';
|
|
113
|
+
const draftSource = approval.meta?.draft?.source_file || '';
|
|
114
|
+
const matched = candidates.find((candidate) => samePath(projectRoot, explicitInput, candidate.inputPath))
|
|
115
|
+
|| candidates.find((candidate) => candidate.kind === 'approved' && samePath(projectRoot, explicitInput, approvedSource))
|
|
116
|
+
|| candidates.find((candidate) => candidate.kind === 'draft' && samePath(projectRoot, explicitInput, draftSource));
|
|
117
|
+
|
|
118
|
+
if (!matched) {
|
|
119
|
+
throw new Error(formatError(`ai review-plan input '${explicitInput}' must match the latest technical-plan draft or approved artifact.`));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return matched;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function readPlanReview(projectRoot) {
|
|
126
|
+
const reviewPath = planReviewPath(projectRoot);
|
|
127
|
+
const meta = readPlanReviewMeta(projectRoot);
|
|
128
|
+
if (!meta && !fs.existsSync(reviewPath)) {
|
|
129
|
+
return {
|
|
130
|
+
status: 'missing',
|
|
131
|
+
review: null,
|
|
132
|
+
meta: null,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const technicalPlan = readPhaseApproval(projectRoot, 'technical-plan');
|
|
137
|
+
const reviewedAt = meta?.reviewed_at ? new Date(meta.reviewed_at).getTime() : 0;
|
|
138
|
+
const approvedAt = technicalPlan.meta?.approved?.approved_at ? new Date(technicalPlan.meta.approved.approved_at).getTime() : 0;
|
|
139
|
+
const reviewedVersion = Number(meta?.source_version || 0) || null;
|
|
140
|
+
const approvedVersion = Number(technicalPlan.meta?.approved?.version || 0) || null;
|
|
141
|
+
const reviewedSource = meta?.source_file || '';
|
|
142
|
+
const approvedSource = technicalPlan.meta?.approved?.source_file || '';
|
|
143
|
+
const approvedArtifact = technicalPlan.approved?.path || '';
|
|
144
|
+
const reviewIdentity = {
|
|
145
|
+
source: reviewedSource,
|
|
146
|
+
version: reviewedVersion,
|
|
147
|
+
};
|
|
148
|
+
let status = 'unapproved';
|
|
149
|
+
|
|
150
|
+
if (technicalPlan.status === 'approved') {
|
|
151
|
+
const matchesApproved = reviewMatchesTarget(projectRoot, reviewIdentity, {
|
|
152
|
+
artifact: approvedArtifact,
|
|
153
|
+
source: approvedSource,
|
|
154
|
+
version: approvedVersion,
|
|
155
|
+
});
|
|
156
|
+
const staleByTime = !reviewedVersion && !matchesApproved && approvedAt > 0 && reviewedAt > 0 && approvedAt > reviewedAt;
|
|
157
|
+
status = matchesApproved && !staleByTime ? 'reviewed' : 'stale';
|
|
158
|
+
} else if (technicalPlan.status === 'draft' || technicalPlan.status === 'stale') {
|
|
159
|
+
const latestDraft = latestTechnicalPlanDraft(technicalPlan);
|
|
160
|
+
const matchesLatestDraft = reviewMatchesTarget(projectRoot, reviewIdentity, {
|
|
161
|
+
artifact: latestDraft?.path || technicalPlan.draft?.path || '',
|
|
162
|
+
source: technicalPlan.meta?.draft?.source_file || '',
|
|
163
|
+
version: Number(latestDraft?.version || technicalPlan.meta?.draft?.version || 0) || null,
|
|
164
|
+
});
|
|
165
|
+
status = matchesLatestDraft ? 'unapproved' : 'stale';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
status,
|
|
170
|
+
review: fs.existsSync(reviewPath)
|
|
171
|
+
? {
|
|
172
|
+
path: toRelativePosix(projectRoot, reviewPath),
|
|
173
|
+
contents: fs.readFileSync(reviewPath, 'utf8'),
|
|
174
|
+
}
|
|
175
|
+
: null,
|
|
176
|
+
meta,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function savePlanReview(projectRoot, options = {}) {
|
|
181
|
+
const root = planReviewRoot(projectRoot);
|
|
182
|
+
fs.mkdirSync(root, { recursive: true });
|
|
183
|
+
const reviewPath = planReviewPath(projectRoot);
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const contents = String(options.contents || '');
|
|
186
|
+
const inputPath = options.inputPath || '';
|
|
187
|
+
|
|
188
|
+
fs.writeFileSync(reviewPath, contents);
|
|
189
|
+
const meta = {
|
|
190
|
+
phase: 'plan-review',
|
|
191
|
+
source_file: inputPath,
|
|
192
|
+
source_kind: options.inputKind || null,
|
|
193
|
+
source_version: options.inputVersion || null,
|
|
194
|
+
path: toRelativePosix(projectRoot, reviewPath),
|
|
195
|
+
reviewed_at: now,
|
|
196
|
+
};
|
|
197
|
+
fs.writeFileSync(planReviewMetaPath(projectRoot), `${JSON.stringify(meta, null, 2)}\n`);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
filePath: reviewPath,
|
|
201
|
+
metaPath: planReviewMetaPath(projectRoot),
|
|
202
|
+
reviewedAt: now,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function assertPlanReviewed(projectRoot) {
|
|
207
|
+
const review = readPlanReview(projectRoot);
|
|
208
|
+
if (review.status !== 'reviewed') {
|
|
209
|
+
const nextCommand = review.status === 'unapproved'
|
|
210
|
+
? 'npx create-quiver ai approve --phase technical-plan --version <n>'
|
|
211
|
+
: 'npx create-quiver ai review-plan';
|
|
212
|
+
throw new Error(formatError(`ai plan phase 'spec' requires a reviewed and approved technical-plan input; current review status: ${review.status}. Run \`${nextCommand}\`.`));
|
|
213
|
+
}
|
|
214
|
+
return review;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function resolveReviewedTechnicalPlanInput(projectRoot, explicitInput) {
|
|
218
|
+
const resolved = resolveApprovedPlannerInput(projectRoot, 'spec', explicitInput);
|
|
219
|
+
const review = assertPlanReviewed(projectRoot);
|
|
220
|
+
return {
|
|
221
|
+
...resolved,
|
|
222
|
+
review,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function buildPlanReviewPrompt({ pack, inputText, inputPath }) {
|
|
227
|
+
const sections = [
|
|
228
|
+
pack.prompt,
|
|
229
|
+
'Task: review the technical plan as if it will be implemented and tested in production.',
|
|
230
|
+
'Do not question the approved scope.',
|
|
231
|
+
'Do not implement code, create specs, or modify files.',
|
|
232
|
+
'Focus on avoiding partial fixes.',
|
|
233
|
+
'Report:',
|
|
234
|
+
'- fragile assumptions',
|
|
235
|
+
'- uncovered cases',
|
|
236
|
+
'- ambiguous criteria',
|
|
237
|
+
'- validation gaps',
|
|
238
|
+
'- operational risks',
|
|
239
|
+
'- recommended fixes to the plan',
|
|
240
|
+
'If ambiguity is not blocking, state the safest assumption and continue.',
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
if (inputPath) {
|
|
244
|
+
sections.push(`Input file: ${inputPath}`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (inputText) {
|
|
248
|
+
sections.push('Technical plan:', inputText.trimEnd());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
promptSource: PLAN_REVIEW_PROMPT_SOURCE,
|
|
253
|
+
prompt: sections.join('\n\n'),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function summarizePlanReview(projectRoot) {
|
|
258
|
+
const review = readPlanReview(projectRoot);
|
|
259
|
+
const lines = [
|
|
260
|
+
'Phase: plan-review',
|
|
261
|
+
`Status: ${review.status}`,
|
|
262
|
+
];
|
|
263
|
+
if (review.review?.path) {
|
|
264
|
+
lines.push(`Review: ${review.review.path}`);
|
|
265
|
+
}
|
|
266
|
+
if (review.meta?.source_file) {
|
|
267
|
+
lines.push(`Source file: ${review.meta.source_file}`);
|
|
268
|
+
}
|
|
269
|
+
return `${lines.join('\n')}\n`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = {
|
|
273
|
+
PLAN_REVIEW_PROMPT_SOURCE,
|
|
274
|
+
assertPlanReviewed,
|
|
275
|
+
buildPlanReviewPrompt,
|
|
276
|
+
planReviewMetaPath,
|
|
277
|
+
planReviewPath,
|
|
278
|
+
readPlanReview,
|
|
279
|
+
resolveTechnicalPlanReviewInput,
|
|
280
|
+
resolveReviewedTechnicalPlanInput,
|
|
281
|
+
savePlanReview,
|
|
282
|
+
summarizePlanReview,
|
|
283
|
+
};
|
|
@@ -2,6 +2,7 @@ const path = require('node:path');
|
|
|
2
2
|
|
|
3
3
|
const SENSITIVE_SEGMENTS = [
|
|
4
4
|
'.ssh',
|
|
5
|
+
'.quiver',
|
|
5
6
|
'node_modules',
|
|
6
7
|
'dist',
|
|
7
8
|
'build',
|
|
@@ -95,6 +96,10 @@ function getContextPathExclusionReason(filePath) {
|
|
|
95
96
|
return 'empty-path';
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
if (normalized === '.quiver/scans/PROJECT_SCAN.json') {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
98
103
|
if (segments.some((segment) => SENSITIVE_SEGMENTS.includes(segment))) {
|
|
99
104
|
const matchedSegment = segments.find((segment) => SENSITIVE_SEGMENTS.includes(segment));
|
|
100
105
|
return `unsafe-segment:${matchedSegment}`;
|
|
@@ -273,7 +273,7 @@ function buildPrMarkdown(manifest) {
|
|
|
273
273
|
'',
|
|
274
274
|
'```bash',
|
|
275
275
|
`cd <repo-root>`,
|
|
276
|
-
`npx create-quiver
|
|
276
|
+
`npx create-quiver spec create --input <approved-input> --spec ${manifest.slug}`,
|
|
277
277
|
'```',
|
|
278
278
|
'',
|
|
279
279
|
'### Run the Project',
|
|
@@ -289,7 +289,7 @@ function buildPrMarkdown(manifest) {
|
|
|
289
289
|
'',
|
|
290
290
|
'**Prerequisite:** approved input is available.',
|
|
291
291
|
'',
|
|
292
|
-
'1. Run
|
|
292
|
+
'1. Run `spec create`.',
|
|
293
293
|
'2. Inspect the generated spec directory.',
|
|
294
294
|
'',
|
|
295
295
|
'**Expected result:** all generated files exist and every JSON file parses.',
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { quiverInternalPaths } = require('./init-layout');
|
|
5
|
+
|
|
6
|
+
const PLANNER_APPROVAL_PHASES = Object.freeze(['acceptance', 'technical-plan']);
|
|
7
|
+
const APPROVAL_DEPENDENCIES = Object.freeze({
|
|
8
|
+
acceptance: null,
|
|
9
|
+
'technical-plan': 'acceptance',
|
|
10
|
+
spec: 'technical-plan',
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function formatError(message) {
|
|
14
|
+
return `create-quiver: ${message}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function normalizePhase(phase) {
|
|
18
|
+
const normalized = String(phase || '').trim().toLowerCase();
|
|
19
|
+
if (!Object.prototype.hasOwnProperty.call(APPROVAL_DEPENDENCIES, normalized)) {
|
|
20
|
+
throw new Error(formatError(`unsupported approval phase '${phase}'`));
|
|
21
|
+
}
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function approvalRoot(projectRoot, phase) {
|
|
26
|
+
return path.join(quiverInternalPaths(projectRoot).root, 'approvals', normalizePhase(phase));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function approvalDraftPath(projectRoot, phase) {
|
|
30
|
+
return path.join(approvalRoot(projectRoot, phase), 'draft.md');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function approvalDraftsDir(projectRoot, phase) {
|
|
34
|
+
return path.join(approvalRoot(projectRoot, phase), 'drafts');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function approvalDraftVersionPath(projectRoot, phase, version) {
|
|
38
|
+
const padded = String(version).padStart(3, '0');
|
|
39
|
+
return path.join(approvalDraftsDir(projectRoot, phase), `${padded}.md`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function approvalApprovedPath(projectRoot, phase) {
|
|
43
|
+
return path.join(approvalRoot(projectRoot, phase), 'approved.md');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function approvalMetaPath(projectRoot, phase) {
|
|
47
|
+
return path.join(approvalRoot(projectRoot, phase), 'meta.json');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function ensureDir(dirPath) {
|
|
51
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toRelativePosix(root, filePath) {
|
|
55
|
+
return path.relative(root, filePath).split(path.sep).join('/');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readTextFile(filePath) {
|
|
59
|
+
if (!fs.existsSync(filePath)) {
|
|
60
|
+
throw new Error(formatError(`missing approval input file: ${filePath}`));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function readApprovalMeta(projectRoot, phase) {
|
|
67
|
+
const metaPath = approvalMetaPath(projectRoot, phase);
|
|
68
|
+
if (!fs.existsSync(metaPath)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(formatError(`invalid approval metadata at ${toRelativePosix(projectRoot, metaPath)}: ${error.message}`));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeDrafts(meta) {
|
|
80
|
+
return Array.isArray(meta?.drafts) ? meta.drafts.filter((item) => item && typeof item === 'object') : [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function nextDraftVersion(meta) {
|
|
84
|
+
const versions = normalizeDrafts(meta)
|
|
85
|
+
.map((item) => Number(item.version))
|
|
86
|
+
.filter((value) => Number.isInteger(value) && value > 0);
|
|
87
|
+
return versions.length > 0 ? Math.max(...versions) + 1 : 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function findDraftVersion(meta, version) {
|
|
91
|
+
const parsed = Number(version);
|
|
92
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
93
|
+
throw new Error(formatError(`invalid draft version: ${version}`));
|
|
94
|
+
}
|
|
95
|
+
return normalizeDrafts(meta).find((item) => Number(item.version) === parsed) || null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readPhaseApproval(projectRoot, phase) {
|
|
99
|
+
const normalizedPhase = normalizePhase(phase);
|
|
100
|
+
const draftPath = approvalDraftPath(projectRoot, normalizedPhase);
|
|
101
|
+
const approvedPath = approvalApprovedPath(projectRoot, normalizedPhase);
|
|
102
|
+
const meta = readApprovalMeta(projectRoot, normalizedPhase);
|
|
103
|
+
|
|
104
|
+
if (!meta && !fs.existsSync(draftPath) && !fs.existsSync(approvedPath)) {
|
|
105
|
+
return {
|
|
106
|
+
phase: normalizedPhase,
|
|
107
|
+
status: 'missing',
|
|
108
|
+
draft: null,
|
|
109
|
+
approved: null,
|
|
110
|
+
meta: null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const draft = fs.existsSync(draftPath)
|
|
115
|
+
? {
|
|
116
|
+
path: toRelativePosix(projectRoot, draftPath),
|
|
117
|
+
contents: fs.readFileSync(draftPath, 'utf8'),
|
|
118
|
+
}
|
|
119
|
+
: null;
|
|
120
|
+
const approved = fs.existsSync(approvedPath)
|
|
121
|
+
? {
|
|
122
|
+
path: toRelativePosix(projectRoot, approvedPath),
|
|
123
|
+
contents: fs.readFileSync(approvedPath, 'utf8'),
|
|
124
|
+
}
|
|
125
|
+
: null;
|
|
126
|
+
|
|
127
|
+
const approvedSource = meta?.approved || null;
|
|
128
|
+
const draftSource = meta?.draft || null;
|
|
129
|
+
const stale = Boolean(
|
|
130
|
+
approvedSource
|
|
131
|
+
&& approvedSource.source_file
|
|
132
|
+
&& !fs.existsSync(path.resolve(projectRoot, approvedSource.source_file))
|
|
133
|
+
&& !approvedSource.source_file.startsWith('.quiver/approvals/'),
|
|
134
|
+
) || Boolean(
|
|
135
|
+
draftSource?.created_at
|
|
136
|
+
&& approvedSource?.approved_at
|
|
137
|
+
&& new Date(draftSource.created_at).getTime() > new Date(approvedSource.approved_at).getTime(),
|
|
138
|
+
) || Boolean(
|
|
139
|
+
draftSource?.version
|
|
140
|
+
&& approvedSource?.version
|
|
141
|
+
&& Number(draftSource.version) > Number(approvedSource.version),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
let status = 'missing';
|
|
145
|
+
if (approved) {
|
|
146
|
+
status = stale ? 'stale' : 'approved';
|
|
147
|
+
} else if (draft) {
|
|
148
|
+
status = 'draft';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
phase: normalizedPhase,
|
|
153
|
+
status,
|
|
154
|
+
draft,
|
|
155
|
+
approved,
|
|
156
|
+
meta,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function renderApprovalStatus(report) {
|
|
161
|
+
if (!report || report.status === 'missing') {
|
|
162
|
+
return `missing ${report ? report.phase : 'approval'} approval`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (report.status === 'draft') {
|
|
166
|
+
return `draft ready for ${report.phase}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (report.status === 'stale') {
|
|
170
|
+
return `stale ${report.phase} approval`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return `approved ${report.phase}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function writeApprovalArtifacts(projectRoot, phase, kind, sourceFile, contents, options = {}) {
|
|
177
|
+
const normalizedPhase = normalizePhase(phase);
|
|
178
|
+
const root = approvalRoot(projectRoot, normalizedPhase);
|
|
179
|
+
ensureDir(root);
|
|
180
|
+
|
|
181
|
+
const filePath = kind === 'approved'
|
|
182
|
+
? approvalApprovedPath(projectRoot, normalizedPhase)
|
|
183
|
+
: approvalDraftPath(projectRoot, normalizedPhase);
|
|
184
|
+
const now = new Date().toISOString();
|
|
185
|
+
const current = readApprovalMeta(projectRoot, normalizedPhase) || {};
|
|
186
|
+
const nextMeta = {
|
|
187
|
+
phase: normalizedPhase,
|
|
188
|
+
drafts: normalizeDrafts(current),
|
|
189
|
+
draft: current.draft || null,
|
|
190
|
+
approved: current.approved || null,
|
|
191
|
+
};
|
|
192
|
+
let finalContents = `${contents}`;
|
|
193
|
+
let version = null;
|
|
194
|
+
|
|
195
|
+
if (kind === 'approved' && options.version) {
|
|
196
|
+
const selectedDraft = findDraftVersion(current, options.version);
|
|
197
|
+
if (!selectedDraft) {
|
|
198
|
+
throw new Error(formatError(`missing ${normalizedPhase} draft version ${options.version}`));
|
|
199
|
+
}
|
|
200
|
+
const draftPath = path.resolve(projectRoot, selectedDraft.path);
|
|
201
|
+
if (!fs.existsSync(draftPath)) {
|
|
202
|
+
throw new Error(formatError(`missing ${normalizedPhase} draft artifact: ${selectedDraft.path}`));
|
|
203
|
+
}
|
|
204
|
+
finalContents = fs.readFileSync(draftPath, 'utf8');
|
|
205
|
+
sourceFile = selectedDraft.path;
|
|
206
|
+
version = Number(selectedDraft.version);
|
|
207
|
+
} else if (kind === 'approved' && current.draft?.version) {
|
|
208
|
+
version = Number(current.draft.version);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (kind === 'draft') {
|
|
212
|
+
version = nextDraftVersion(current);
|
|
213
|
+
const versionPath = approvalDraftVersionPath(projectRoot, normalizedPhase, version);
|
|
214
|
+
ensureDir(path.dirname(versionPath));
|
|
215
|
+
fs.writeFileSync(versionPath, finalContents);
|
|
216
|
+
|
|
217
|
+
nextMeta.drafts = nextMeta.drafts.concat({
|
|
218
|
+
version,
|
|
219
|
+
phase: normalizedPhase,
|
|
220
|
+
source_file: toRelativePosix(projectRoot, path.resolve(projectRoot, sourceFile)),
|
|
221
|
+
path: toRelativePosix(projectRoot, versionPath),
|
|
222
|
+
created_at: now,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
fs.writeFileSync(filePath, finalContents);
|
|
227
|
+
|
|
228
|
+
nextMeta[kind] = {
|
|
229
|
+
phase: normalizedPhase,
|
|
230
|
+
source_file: toRelativePosix(projectRoot, path.resolve(projectRoot, sourceFile)),
|
|
231
|
+
path: toRelativePosix(projectRoot, filePath),
|
|
232
|
+
version,
|
|
233
|
+
created_at: now,
|
|
234
|
+
...(kind === 'approved' ? { approved_at: now } : {}),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if (kind === 'draft' && nextMeta.approved && nextMeta.approved.approved_at) {
|
|
238
|
+
nextMeta.approved = {
|
|
239
|
+
...nextMeta.approved,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fs.writeFileSync(approvalMetaPath(projectRoot, normalizedPhase), `${JSON.stringify(nextMeta, null, 2)}\n`);
|
|
244
|
+
|
|
245
|
+
return {
|
|
246
|
+
phase: normalizedPhase,
|
|
247
|
+
kind,
|
|
248
|
+
filePath,
|
|
249
|
+
metaPath: approvalMetaPath(projectRoot, normalizedPhase),
|
|
250
|
+
createdAt: now,
|
|
251
|
+
version,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function savePlannerDraft(projectRoot, phase, sourceFile, contents) {
|
|
256
|
+
return writeApprovalArtifacts(projectRoot, phase, 'draft', sourceFile, contents);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function approvePlannerPhase(projectRoot, phase, sourceFile, contents, options = {}) {
|
|
260
|
+
return writeApprovalArtifacts(projectRoot, phase, 'approved', sourceFile, contents, options);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function resolveApprovedPlannerInput(projectRoot, phase, explicitInput) {
|
|
264
|
+
const normalizedPhase = normalizePhase(phase);
|
|
265
|
+
const dependencyPhase = APPROVAL_DEPENDENCIES[normalizedPhase];
|
|
266
|
+
|
|
267
|
+
if (!dependencyPhase) {
|
|
268
|
+
return {
|
|
269
|
+
phase: normalizedPhase,
|
|
270
|
+
inputPath: explicitInput || null,
|
|
271
|
+
approval: null,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const approval = readPhaseApproval(projectRoot, dependencyPhase);
|
|
276
|
+
if (approval.status !== 'approved') {
|
|
277
|
+
throw new Error(formatError(`ai plan phase '${normalizedPhase}' requires approved ${dependencyPhase} input; current status: ${approval.status}. Run \`npx create-quiver ai approve --phase ${dependencyPhase} --input <file>\`.`));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const approvedPath = approval.approved?.path ? path.resolve(projectRoot, approval.approved.path) : '';
|
|
281
|
+
const approvedSource = approval.meta?.approved?.source_file ? path.resolve(projectRoot, approval.meta.approved.source_file) : '';
|
|
282
|
+
|
|
283
|
+
if (!explicitInput) {
|
|
284
|
+
return {
|
|
285
|
+
phase: normalizedPhase,
|
|
286
|
+
inputPath: approval.approved.path,
|
|
287
|
+
approval,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const resolvedExplicit = path.resolve(projectRoot, explicitInput);
|
|
292
|
+
const matchesApprovedArtifact = approvedPath && resolvedExplicit === approvedPath;
|
|
293
|
+
const matchesApprovedSource = approvedSource && resolvedExplicit === approvedSource;
|
|
294
|
+
|
|
295
|
+
if (!matchesApprovedArtifact && !matchesApprovedSource) {
|
|
296
|
+
throw new Error(formatError(`ai plan phase '${normalizedPhase}' requires approved ${dependencyPhase} input; '${explicitInput}' is not the approved source.`));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
phase: normalizedPhase,
|
|
301
|
+
inputPath: approval.approved.path,
|
|
302
|
+
approval,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function summarizePlannerApproval(projectRoot, phase) {
|
|
307
|
+
const report = readPhaseApproval(projectRoot, phase);
|
|
308
|
+
const lines = [`Phase: ${report.phase}`, `Status: ${report.status}`];
|
|
309
|
+
|
|
310
|
+
if (report.draft) {
|
|
311
|
+
const version = report.meta?.draft?.version ? ` v${report.meta.draft.version}` : '';
|
|
312
|
+
lines.push(`Draft${version}: ${report.draft.path}`);
|
|
313
|
+
}
|
|
314
|
+
const drafts = normalizeDrafts(report.meta);
|
|
315
|
+
if (drafts.length > 0) {
|
|
316
|
+
lines.push('Draft history:');
|
|
317
|
+
for (const draft of drafts) {
|
|
318
|
+
lines.push(`- v${draft.version}: ${draft.path}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (report.approved) {
|
|
322
|
+
const version = report.meta?.approved?.version ? ` v${report.meta.approved.version}` : '';
|
|
323
|
+
lines.push(`Approved${version}: ${report.approved.path}`);
|
|
324
|
+
}
|
|
325
|
+
if (report.meta?.approved?.source_file) {
|
|
326
|
+
lines.push(`Source file: ${report.meta.approved.source_file}`);
|
|
327
|
+
} else if (report.meta?.draft?.source_file) {
|
|
328
|
+
lines.push(`Source file: ${report.meta.draft.source_file}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return `${lines.join('\n')}\n`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
module.exports = {
|
|
335
|
+
APPROVAL_DEPENDENCIES,
|
|
336
|
+
PLANNER_APPROVAL_PHASES,
|
|
337
|
+
approvalApprovedPath,
|
|
338
|
+
approvalDraftPath,
|
|
339
|
+
approvalDraftsDir,
|
|
340
|
+
approvalDraftVersionPath,
|
|
341
|
+
approvalMetaPath,
|
|
342
|
+
approvePlannerPhase,
|
|
343
|
+
findDraftVersion,
|
|
344
|
+
normalizePhase,
|
|
345
|
+
readPhaseApproval,
|
|
346
|
+
renderApprovalStatus,
|
|
347
|
+
resolveApprovedPlannerInput,
|
|
348
|
+
savePlannerDraft,
|
|
349
|
+
summarizePlannerApproval,
|
|
350
|
+
};
|