create-quiver 0.13.0 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/README.md +183 -511
- package/README_FOR_AI.md +36 -26
- package/ROADMAP.md +11 -0
- package/assets/quiver-wordmark.svg +22 -0
- package/docs/AI_CONTEXT.md.template +2 -0
- package/docs/AI_ONBOARDING_PROMPT.md.template +9 -1
- package/docs/CLI_UX_GUIDE.md +125 -0
- package/docs/COMMANDS.md.template +19 -4
- package/docs/GITFLOW_PR_GUIDE.md +70 -0
- package/docs/TROUBLESHOOTING.md.template +29 -0
- package/docs/WORKFLOW.md.template +13 -12
- package/docs/getting-started/linux.md +84 -0
- package/docs/getting-started/macos.md +85 -0
- package/docs/getting-started/windows-git-bash-wsl.md +78 -0
- package/docs/getting-started/windows-powershell.md +96 -0
- package/docs/reference/commands.md +94 -0
- package/docs/workflows/existing-project.md +131 -0
- package/docs/workflows/full-ai-spec-to-pr.md +311 -0
- package/docs/workflows/legacy-quiver-project.md +102 -0
- package/docs/workflows/new-project.md +76 -0
- package/package.json +5 -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/specs/quiver-v29-planner-prepare-context-cli-ux/EVIDENCE_REPORT.md +163 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/EXECUTION_PLAN.md +72 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/SPEC.md +173 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/STATUS.md +34 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/pr.md +95 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/EXECUTION_BRIEF.md +45 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/slice.json +58 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/EXECUTION_BRIEF.md +49 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/slice.json +75 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/EXECUTION_BRIEF.md +47 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/slice.json +71 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/slice.json +82 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/EXECUTION_BRIEF.md +46 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/slice.json +73 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/EXECUTION_BRIEF.md +46 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/slice.json +83 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/EXECUTION_BRIEF.md +50 -0
- package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/slice.json +95 -0
- package/src/create-quiver/commands/ai.js +938 -16
- package/src/create-quiver/commands/spec.js +74 -2
- package/src/create-quiver/index.js +91 -5
- package/src/create-quiver/lib/ai/context-packs.js +2 -2
- package/src/create-quiver/lib/ai/context-proposal.js +389 -0
- package/src/create-quiver/lib/ai/context-proposal.schema.js +31 -0
- 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/cli/editor.js +118 -0
- package/src/create-quiver/lib/cli/theme.js +100 -0
- package/src/create-quiver/lib/cli/ux-flags.js +151 -0
- package/src/create-quiver/lib/cli/ux.js +130 -0
- package/src/create-quiver/lib/project-state-resolver.js +195 -1
- package/src/create-quiver/lib/spec-worktrees.js +50 -2
|
@@ -5,6 +5,7 @@ const { readPhaseApproval, resolveApprovedPlannerInput } = require('../approvals
|
|
|
5
5
|
const { quiverInternalPaths } = require('../init-layout');
|
|
6
6
|
|
|
7
7
|
const PLAN_REVIEW_PROMPT_SOURCE = 'packaged production-readiness plan review template';
|
|
8
|
+
const PLAN_REVIEW_RECOMMENDATIONS = Object.freeze(['approve', 'approve-with-risk', 'revise']);
|
|
8
9
|
|
|
9
10
|
function formatError(message) {
|
|
10
11
|
return `create-quiver: ${message}`;
|
|
@@ -50,6 +51,141 @@ function samePath(projectRoot, left, right) {
|
|
|
50
51
|
return Boolean(left && right && resolvePath(projectRoot, left) === resolvePath(projectRoot, right));
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function normalizeList(value) {
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'string' && value.trim()) {
|
|
59
|
+
return value.split(/\r?\n/).map((item) => item.replace(/^[-*]\s+/, '').trim()).filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function normalizeRecommendation(value) {
|
|
65
|
+
const normalized = String(value || '').trim().toLowerCase().replace(/[_\s]+/g, '-');
|
|
66
|
+
if (PLAN_REVIEW_RECOMMENDATIONS.includes(normalized)) {
|
|
67
|
+
return normalized;
|
|
68
|
+
}
|
|
69
|
+
if (normalized === 'approved' || normalized === 'approvable') {
|
|
70
|
+
return 'approve';
|
|
71
|
+
}
|
|
72
|
+
if (normalized === 'approved-with-risk' || normalized === 'approvable-with-risk' || normalized === 'approve-with-risks') {
|
|
73
|
+
return 'approve-with-risk';
|
|
74
|
+
}
|
|
75
|
+
if (normalized === 'changes-required' || normalized === 'requires-revision' || normalized === 'needs-revision') {
|
|
76
|
+
return 'revise';
|
|
77
|
+
}
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function recommendedNextCommand(recommendation, sourceVersion) {
|
|
82
|
+
if (recommendation === 'revise') {
|
|
83
|
+
return 'npx create-quiver ai revise --phase technical-plan --input <feedback.md> --dry-run';
|
|
84
|
+
}
|
|
85
|
+
return `npx create-quiver ai approve --phase technical-plan --version ${sourceVersion || '<n>'}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseJsonObject(value) {
|
|
89
|
+
try {
|
|
90
|
+
const parsed = JSON.parse(value);
|
|
91
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractStructuredReview(text) {
|
|
98
|
+
const raw = String(text || '').trim();
|
|
99
|
+
if (!raw) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const direct = parseJsonObject(raw);
|
|
104
|
+
if (direct) {
|
|
105
|
+
return direct;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
109
|
+
if (!fenced) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return parseJsonObject(fenced[1].trim());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function normalizeStructuredReview(parsed, sourceVersion) {
|
|
116
|
+
const review = parsed?.review || parsed?.plan_review || parsed;
|
|
117
|
+
const requiredFixes = normalizeList(review.required_fixes || review.requiredFixes || review.blocking_issues || review.blockingIssues);
|
|
118
|
+
const optionalHardening = normalizeList(review.optional_hardening || review.optionalHardening || review.non_blocking_issues || review.nonBlockingIssues);
|
|
119
|
+
const risks = normalizeList(review.risks || review.remaining_risks || review.remainingRisks);
|
|
120
|
+
let approvalRecommendation = normalizeRecommendation(review.approval_recommendation || review.approvalRecommendation || review.recommendation);
|
|
121
|
+
const blocking = review.blocking === true || review.has_blockers === true || review.hasBlockers === true || requiredFixes.length > 0;
|
|
122
|
+
|
|
123
|
+
if (!approvalRecommendation) {
|
|
124
|
+
approvalRecommendation = blocking ? 'revise' : optionalHardening.length > 0 || risks.length > 0 ? 'approve-with-risk' : 'approve';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const normalizedBlocking = blocking || approvalRecommendation === 'revise';
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
schema_version: 1,
|
|
131
|
+
approval_recommendation: approvalRecommendation,
|
|
132
|
+
blocking: normalizedBlocking,
|
|
133
|
+
next_command: String(review.next_command || review.nextCommand || '').trim() || recommendedNextCommand(approvalRecommendation, sourceVersion),
|
|
134
|
+
optional_hardening: optionalHardening,
|
|
135
|
+
required_fixes: requiredFixes,
|
|
136
|
+
risks,
|
|
137
|
+
source: 'structured',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function classifyReviewText(text) {
|
|
142
|
+
const value = String(text || '').toLowerCase();
|
|
143
|
+
if (/\b(revise|revision|required fix|required fixes|blocking|blocker|not approvable|not ready)\b/.test(value)) {
|
|
144
|
+
return 'revise';
|
|
145
|
+
}
|
|
146
|
+
if (/\b(approve with risk|approvable with risk|non-blocking|optional hardening|p1|p2|risk)\b/.test(value)) {
|
|
147
|
+
return 'approve-with-risk';
|
|
148
|
+
}
|
|
149
|
+
if (/\b(approve|approved|approvable|no blockers|no blocking|production ready)\b/.test(value)) {
|
|
150
|
+
return 'approve';
|
|
151
|
+
}
|
|
152
|
+
return 'approve-with-risk';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function derivePlanReviewResult(contents, options = {}) {
|
|
156
|
+
const structured = extractStructuredReview(contents);
|
|
157
|
+
if (structured) {
|
|
158
|
+
return normalizeStructuredReview(structured, options.inputVersion);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const approvalRecommendation = classifyReviewText(contents);
|
|
162
|
+
const fallbackNote = approvalRecommendation === 'approve-with-risk'
|
|
163
|
+
? ['Review output did not include structured metadata; treat approval as risky and inspect the human review text before approving.']
|
|
164
|
+
: [];
|
|
165
|
+
const requiredFixes = approvalRecommendation === 'revise'
|
|
166
|
+
? ['Review output indicates the technical plan must be revised before approval.']
|
|
167
|
+
: [];
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
schema_version: 1,
|
|
171
|
+
approval_recommendation: approvalRecommendation,
|
|
172
|
+
blocking: approvalRecommendation === 'revise',
|
|
173
|
+
next_command: recommendedNextCommand(approvalRecommendation, options.inputVersion),
|
|
174
|
+
optional_hardening: fallbackNote,
|
|
175
|
+
required_fixes: requiredFixes,
|
|
176
|
+
risks: [],
|
|
177
|
+
source: 'heuristic',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function reviewBlocksApproval(review) {
|
|
182
|
+
const result = review?.meta?.review_result || review?.review_result || null;
|
|
183
|
+
if (!result) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
return result.blocking === true || result.approval_recommendation === 'revise';
|
|
187
|
+
}
|
|
188
|
+
|
|
53
189
|
function latestTechnicalPlanDraft(approval) {
|
|
54
190
|
const version = Number(approval.meta?.draft?.version || 0);
|
|
55
191
|
if (!version) {
|
|
@@ -184,6 +320,9 @@ function savePlanReview(projectRoot, options = {}) {
|
|
|
184
320
|
const now = new Date().toISOString();
|
|
185
321
|
const contents = String(options.contents || '');
|
|
186
322
|
const inputPath = options.inputPath || '';
|
|
323
|
+
const reviewResult = derivePlanReviewResult(contents, {
|
|
324
|
+
inputVersion: options.inputVersion,
|
|
325
|
+
});
|
|
187
326
|
|
|
188
327
|
fs.writeFileSync(reviewPath, contents);
|
|
189
328
|
const meta = {
|
|
@@ -194,6 +333,7 @@ function savePlanReview(projectRoot, options = {}) {
|
|
|
194
333
|
path: toRelativePosix(projectRoot, reviewPath),
|
|
195
334
|
raw_artifact_path: options.rawArtifactPath || null,
|
|
196
335
|
output_source: options.outputSource || null,
|
|
336
|
+
review_result: reviewResult,
|
|
197
337
|
reviewed_at: now,
|
|
198
338
|
};
|
|
199
339
|
fs.writeFileSync(planReviewMetaPath(projectRoot), `${JSON.stringify(meta, null, 2)}\n`);
|
|
@@ -216,6 +356,10 @@ function assertPlanReviewed(projectRoot) {
|
|
|
216
356
|
: ' Preview the review first, then run `npx create-quiver ai review-plan` to persist it.';
|
|
217
357
|
throw new Error(formatError(`ai plan phase 'spec' requires a reviewed and approved technical-plan input; current review status: ${review.status}. Run \`${nextCommand}\`.${followUp}`));
|
|
218
358
|
}
|
|
359
|
+
if (reviewBlocksApproval(review)) {
|
|
360
|
+
const result = review.meta.review_result;
|
|
361
|
+
throw new Error(formatError(`ai plan phase 'spec' requires an approvable production review; current approval recommendation is ${result.approval_recommendation}. Run \`${result.next_command || recommendedNextCommand('revise')}\`.`));
|
|
362
|
+
}
|
|
219
363
|
return review;
|
|
220
364
|
}
|
|
221
365
|
|
|
@@ -243,6 +387,8 @@ function buildPlanReviewPrompt({ pack, inputText, inputPath }) {
|
|
|
243
387
|
'- operational risks',
|
|
244
388
|
'- recommended fixes to the plan',
|
|
245
389
|
'If ambiguity is not blocking, state the safest assumption and continue.',
|
|
390
|
+
'Required output contract: include a fenced json block with `{ "review": { "blocking": boolean, "approvalRecommendation": "approve|approve-with-risk|revise", "requiredFixes": [], "optionalHardening": [], "risks": [], "nextCommand": "" } }`.',
|
|
391
|
+
'Use `approve` only when no required fixes remain. Use `approve-with-risk` when only optional hardening or accepted risks remain. Use `revise` when required fixes or blocking ambiguity remain.',
|
|
246
392
|
];
|
|
247
393
|
|
|
248
394
|
if (inputPath) {
|
|
@@ -261,6 +407,7 @@ function buildPlanReviewPrompt({ pack, inputText, inputPath }) {
|
|
|
261
407
|
|
|
262
408
|
function summarizePlanReview(projectRoot) {
|
|
263
409
|
const review = readPlanReview(projectRoot);
|
|
410
|
+
const result = review.meta?.review_result || null;
|
|
264
411
|
const lines = [
|
|
265
412
|
'Phase: plan-review',
|
|
266
413
|
`Status: ${review.status}`,
|
|
@@ -271,16 +418,28 @@ function summarizePlanReview(projectRoot) {
|
|
|
271
418
|
if (review.meta?.source_file) {
|
|
272
419
|
lines.push(`Source file: ${review.meta.source_file}`);
|
|
273
420
|
}
|
|
421
|
+
if (result) {
|
|
422
|
+
const requiredFixes = normalizeList(result.required_fixes);
|
|
423
|
+
const optionalHardening = normalizeList(result.optional_hardening);
|
|
424
|
+
lines.push(`Approval recommendation: ${result.approval_recommendation}`);
|
|
425
|
+
lines.push(`Blocking: ${result.blocking ? 'yes' : 'no'}`);
|
|
426
|
+
lines.push(`Required fixes: ${requiredFixes.length}`);
|
|
427
|
+
lines.push(`Optional hardening: ${optionalHardening.length}`);
|
|
428
|
+
lines.push(`Next command: ${result.next_command}`);
|
|
429
|
+
}
|
|
274
430
|
return `${lines.join('\n')}\n`;
|
|
275
431
|
}
|
|
276
432
|
|
|
277
433
|
module.exports = {
|
|
278
434
|
PLAN_REVIEW_PROMPT_SOURCE,
|
|
435
|
+
PLAN_REVIEW_RECOMMENDATIONS,
|
|
279
436
|
assertPlanReviewed,
|
|
280
437
|
buildPlanReviewPrompt,
|
|
438
|
+
derivePlanReviewResult,
|
|
281
439
|
planReviewMetaPath,
|
|
282
440
|
planReviewPath,
|
|
283
441
|
readPlanReview,
|
|
442
|
+
reviewBlocksApproval,
|
|
284
443
|
resolveTechnicalPlanReviewInput,
|
|
285
444
|
resolveReviewedTechnicalPlanInput,
|
|
286
445
|
savePlanReview,
|
|
@@ -357,7 +357,9 @@ function formatAiRunStatus(projectRoot, run) {
|
|
|
357
357
|
].join('\n');
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
const openRuns = listAiRuns(projectRoot).filter((item) => item.status !== 'closed');
|
|
361
|
+
const otherOpenRuns = openRuns.filter((item) => item.run_id !== run.run_id);
|
|
362
|
+
const lines = [
|
|
361
363
|
'AI run status',
|
|
362
364
|
`Run: ${run.run_id}`,
|
|
363
365
|
`Status: ${run.status}`,
|
|
@@ -366,9 +368,22 @@ function formatAiRunStatus(projectRoot, run) {
|
|
|
366
368
|
`Requirement: ${run.requirement?.path || '(missing)'}`,
|
|
367
369
|
`State: ${toRelativePosix(projectRoot, runStatePath(projectRoot, run.run_id))}`,
|
|
368
370
|
`Approvals: ${run.approvals_path}`,
|
|
371
|
+
`Open runs: ${openRuns.length}`,
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
if (otherOpenRuns.length > 0) {
|
|
375
|
+
lines.push('Other open runs:');
|
|
376
|
+
for (const item of otherOpenRuns) {
|
|
377
|
+
lines.push(`- ${item.run_id}: ${item.phase} (${item.status}) -> ${nextCommandForPhase(item.phase)}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
lines.push(
|
|
369
382
|
`Next safe command: ${nextCommandForPhase(run.phase)}`,
|
|
370
383
|
'',
|
|
371
|
-
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
return lines.join('\n');
|
|
372
387
|
}
|
|
373
388
|
|
|
374
389
|
function formatAiRunResume(projectRoot, run) {
|
|
@@ -278,6 +278,20 @@ function buildSpecGenerationManifest({ inputText, inputPath, repoRoot, specSlug
|
|
|
278
278
|
return manifest;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
function validateTechnicalPlanSpecContract(repoRoot, options = {}) {
|
|
282
|
+
const inputPath = options.inputPath || options.input;
|
|
283
|
+
const inputText = typeof options.inputText === 'string'
|
|
284
|
+
? options.inputText
|
|
285
|
+
: readSourceText(inputPath, repoRoot);
|
|
286
|
+
|
|
287
|
+
return buildSpecGenerationManifest({
|
|
288
|
+
inputPath,
|
|
289
|
+
inputText,
|
|
290
|
+
repoRoot,
|
|
291
|
+
specSlug: options.specSlug,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
281
295
|
function validateSpecCollision(specDir) {
|
|
282
296
|
if (fs.existsSync(specDir)) {
|
|
283
297
|
throw new Error(formatError(`spec directory already exists: ${path.relative(process.cwd(), specDir)}`));
|
|
@@ -381,5 +395,6 @@ module.exports = {
|
|
|
381
395
|
generateSpecArtifacts,
|
|
382
396
|
parseApprovedManifest,
|
|
383
397
|
readSourceText,
|
|
398
|
+
validateTechnicalPlanSpecContract,
|
|
384
399
|
validateGeneratedSliceJson,
|
|
385
400
|
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const { spawnSync } = require('child_process');
|
|
3
|
+
|
|
4
|
+
function splitEditorCommand(value) {
|
|
5
|
+
const input = String(value || '').trim();
|
|
6
|
+
if (!input) return null;
|
|
7
|
+
const parts = [];
|
|
8
|
+
let current = '';
|
|
9
|
+
let quote = null;
|
|
10
|
+
let escaping = false;
|
|
11
|
+
|
|
12
|
+
for (const char of input) {
|
|
13
|
+
if (escaping) {
|
|
14
|
+
current += char;
|
|
15
|
+
escaping = false;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (char === '\\') {
|
|
19
|
+
escaping = true;
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (quote) {
|
|
23
|
+
if (char === quote) {
|
|
24
|
+
quote = null;
|
|
25
|
+
} else {
|
|
26
|
+
current += char;
|
|
27
|
+
}
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (char === '"' || char === "'") {
|
|
31
|
+
quote = char;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (/\s/.test(char)) {
|
|
35
|
+
if (current) {
|
|
36
|
+
parts.push(current);
|
|
37
|
+
current = '';
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
current += char;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (current) parts.push(current);
|
|
45
|
+
return parts.length > 0 ? { command: parts[0], args: parts.slice(1) } : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function defaultEditorForPlatform(platform = os.platform()) {
|
|
49
|
+
return platform === 'win32' ? 'notepad' : 'vi';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveEditor(env = process.env, options = {}) {
|
|
53
|
+
const candidates = [
|
|
54
|
+
{ source: 'VISUAL', value: env.VISUAL },
|
|
55
|
+
{ source: 'EDITOR', value: env.EDITOR },
|
|
56
|
+
...((options.fallbacks || [defaultEditorForPlatform(options.platform)]).map((value) => ({
|
|
57
|
+
source: 'fallback',
|
|
58
|
+
value,
|
|
59
|
+
}))),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
for (const candidate of candidates) {
|
|
63
|
+
const parsed = splitEditorCommand(candidate.value);
|
|
64
|
+
if (parsed) {
|
|
65
|
+
return { ...parsed, source: candidate.source };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function openEditor(filePath, options = {}) {
|
|
73
|
+
const editor = options.editor || resolveEditor(options.env || process.env, options);
|
|
74
|
+
if (!editor) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
canceled: true,
|
|
78
|
+
filePath,
|
|
79
|
+
reason: 'No editor configured. Set VISUAL or EDITOR, then rerun the command.',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const runner = options.spawnSync || spawnSync;
|
|
84
|
+
const result = runner(editor.command, [...editor.args, filePath], {
|
|
85
|
+
cwd: options.cwd || process.cwd(),
|
|
86
|
+
env: options.env || process.env,
|
|
87
|
+
shell: false,
|
|
88
|
+
stdio: options.stdio || 'inherit',
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (result.error) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
canceled: true,
|
|
95
|
+
command: editor.command,
|
|
96
|
+
filePath,
|
|
97
|
+
reason: result.error.message,
|
|
98
|
+
source: editor.source,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const status = result.status ?? 0;
|
|
103
|
+
return {
|
|
104
|
+
ok: status === 0,
|
|
105
|
+
canceled: status !== 0,
|
|
106
|
+
command: editor.command,
|
|
107
|
+
exitCode: status,
|
|
108
|
+
filePath,
|
|
109
|
+
source: editor.source,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
defaultEditorForPlatform,
|
|
115
|
+
openEditor,
|
|
116
|
+
resolveEditor,
|
|
117
|
+
splitEditorCommand,
|
|
118
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const QUIVER_COLORS = Object.freeze({
|
|
2
|
+
sky: '#86C8F2',
|
|
3
|
+
blue: '#6BADEB',
|
|
4
|
+
periwinkle: '#7F9EE8',
|
|
5
|
+
violet: '#9B82E6',
|
|
6
|
+
magenta: '#D56AB0',
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const STATUS_COLORS = Object.freeze({
|
|
10
|
+
info: 'sky',
|
|
11
|
+
command: 'blue',
|
|
12
|
+
planner: 'violet',
|
|
13
|
+
review: 'magenta',
|
|
14
|
+
risk: 'magenta',
|
|
15
|
+
muted: 'periwinkle',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function isTruthy(value) {
|
|
19
|
+
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isAsciiLocale(env = process.env) {
|
|
23
|
+
const locale = `${env.LC_ALL || ''} ${env.LC_CTYPE || ''} ${env.LANG || ''}`;
|
|
24
|
+
return /\bC\b|ASCII/i.test(locale);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isDumbTerminal(env = process.env) {
|
|
28
|
+
return String(env.TERM || '').toLowerCase() === 'dumb';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function shouldUseColor(options = {}, env = process.env, tty = {}) {
|
|
32
|
+
if (options.json || options.noColor) return false;
|
|
33
|
+
if (env.NO_COLOR || env.FORCE_COLOR === '0') return false;
|
|
34
|
+
if (isDumbTerminal(env)) return false;
|
|
35
|
+
if (isTruthy(env.CI)) return false;
|
|
36
|
+
if (tty.stdout === false) return false;
|
|
37
|
+
if (options.color === true) return true;
|
|
38
|
+
return Boolean(tty.stdout);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function shouldUseUnicode(options = {}, env = process.env, tty = {}) {
|
|
42
|
+
if (options.ascii || options.json) return false;
|
|
43
|
+
if (isDumbTerminal(env) || isAsciiLocale(env)) return false;
|
|
44
|
+
if (tty.stdout === false) return false;
|
|
45
|
+
if (options.unicode === true) return true;
|
|
46
|
+
return Boolean(tty.stdout);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function hexToRgb(hex) {
|
|
50
|
+
const normalized = String(hex || '').replace(/^#/, '');
|
|
51
|
+
if (!/^[0-9a-f]{6}$/i.test(normalized)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
Number.parseInt(normalized.slice(0, 2), 16),
|
|
56
|
+
Number.parseInt(normalized.slice(2, 4), 16),
|
|
57
|
+
Number.parseInt(normalized.slice(4, 6), 16),
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function colorize(value, colorName, theme) {
|
|
62
|
+
const text = String(value ?? '');
|
|
63
|
+
if (!theme?.useColor) return text;
|
|
64
|
+
const rgb = hexToRgb(theme.colors[colorName] || colorName);
|
|
65
|
+
if (!rgb) return text;
|
|
66
|
+
return `\u001b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${text}\u001b[0m`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveTheme(options = {}, env = process.env, tty = {}) {
|
|
70
|
+
const useColor = shouldUseColor(options, env, tty);
|
|
71
|
+
const useUnicode = shouldUseUnicode(options, env, tty);
|
|
72
|
+
const symbols = useUnicode
|
|
73
|
+
? { start: '◇', success: '✓', warning: '!', error: '✖', bullet: '•' }
|
|
74
|
+
: { start: '*', success: 'OK', warning: 'WARN', error: 'ERR', bullet: '-' };
|
|
75
|
+
const theme = {
|
|
76
|
+
colors: QUIVER_COLORS,
|
|
77
|
+
statusColors: STATUS_COLORS,
|
|
78
|
+
symbols,
|
|
79
|
+
useColor,
|
|
80
|
+
useUnicode,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...theme,
|
|
85
|
+
colorize: (value, colorName) => colorize(value, colorName, theme),
|
|
86
|
+
status: (status, value) => colorize(value, STATUS_COLORS[status] || 'sky', theme),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
QUIVER_COLORS,
|
|
92
|
+
STATUS_COLORS,
|
|
93
|
+
colorize,
|
|
94
|
+
isAsciiLocale,
|
|
95
|
+
isDumbTerminal,
|
|
96
|
+
isTruthy,
|
|
97
|
+
resolveTheme,
|
|
98
|
+
shouldUseColor,
|
|
99
|
+
shouldUseUnicode,
|
|
100
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
const UX_FLAG_OPTIONS = Object.freeze({
|
|
2
|
+
withPlanner: '--with-planner',
|
|
3
|
+
interactive: '--interactive',
|
|
4
|
+
review: '--review',
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
const UX_FLAG_MATRIX = Object.freeze({
|
|
8
|
+
'ai prepare-context': Object.freeze({
|
|
9
|
+
withPlanner: true,
|
|
10
|
+
interactive: true,
|
|
11
|
+
review: true,
|
|
12
|
+
note: 'planner-assisted docs-only context preparation',
|
|
13
|
+
}),
|
|
14
|
+
'ai plan': Object.freeze({
|
|
15
|
+
withPlanner: true,
|
|
16
|
+
interactive: true,
|
|
17
|
+
review: true,
|
|
18
|
+
note: 'planner draft generation and human approval workflow',
|
|
19
|
+
}),
|
|
20
|
+
'spec create': Object.freeze({
|
|
21
|
+
withPlanner: true,
|
|
22
|
+
interactive: true,
|
|
23
|
+
review: true,
|
|
24
|
+
note: 'spec generation from approved planner output',
|
|
25
|
+
}),
|
|
26
|
+
'ai pr': Object.freeze({
|
|
27
|
+
withPlanner: false,
|
|
28
|
+
interactive: true,
|
|
29
|
+
review: true,
|
|
30
|
+
note: 'PR body review and interactive PR inputs',
|
|
31
|
+
}),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function formatError(message) {
|
|
35
|
+
return `create-quiver: ${message}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeCommandKey(value) {
|
|
39
|
+
return String(value || '').trim().replace(/\s+/g, ' ');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveUxCommandKey(args = {}) {
|
|
43
|
+
if (args.mode === 'ai') {
|
|
44
|
+
const command = args.aiCommand || '';
|
|
45
|
+
if (command === 'specs' || command === 'slices' || command === 'trace' || command === 'active-slice') {
|
|
46
|
+
return normalizeCommandKey(`ai ${command} ${args.aiSecondaryCommand || ''}`);
|
|
47
|
+
}
|
|
48
|
+
if (command === 'agent') {
|
|
49
|
+
return normalizeCommandKey(`ai ${command} ${args.aiAgentCommand || ''}`);
|
|
50
|
+
}
|
|
51
|
+
if (command === 'run') {
|
|
52
|
+
return normalizeCommandKey(`ai ${command} ${args.aiRunCommand || ''}`);
|
|
53
|
+
}
|
|
54
|
+
return normalizeCommandKey(`ai ${command}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (args.mode === 'spec') {
|
|
58
|
+
return normalizeCommandKey(`spec ${args.specCommand || ''}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (args.mode === 'demo') {
|
|
62
|
+
return normalizeCommandKey(`demo ${args.demoCommand || ''} ${args.demoName || ''}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (args.mode === 'evidence') {
|
|
66
|
+
return normalizeCommandKey(`evidence ${args.evidenceCommand || ''}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return normalizeCommandKey(args.mode);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getUxFlagSupport(commandKey) {
|
|
73
|
+
return UX_FLAG_MATRIX[normalizeCommandKey(commandKey)] || Object.freeze({
|
|
74
|
+
withPlanner: false,
|
|
75
|
+
interactive: false,
|
|
76
|
+
review: false,
|
|
77
|
+
note: 'no UX flag support',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getRequestedUxFlags(args = {}) {
|
|
82
|
+
return Object.keys(UX_FLAG_OPTIONS).filter((key) => args[key] === true);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function supportedFlagList(support) {
|
|
86
|
+
return Object.entries(UX_FLAG_OPTIONS)
|
|
87
|
+
.filter(([key]) => support[key] === true)
|
|
88
|
+
.map(([, flag]) => flag);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function explainAlternative(flag) {
|
|
92
|
+
if (flag === 'withPlanner') {
|
|
93
|
+
return 'Use a planner-capable command such as: npx create-quiver ai prepare-context --with-planner --dry-run';
|
|
94
|
+
}
|
|
95
|
+
if (flag === 'interactive') {
|
|
96
|
+
return 'Use explicit non-interactive flags for automation, or run a command that documents --interactive support.';
|
|
97
|
+
}
|
|
98
|
+
if (flag === 'review') {
|
|
99
|
+
return 'Use a command with persistent AI-generated output, then rerun with --review after inspecting the dry-run.';
|
|
100
|
+
}
|
|
101
|
+
return 'Run: npx create-quiver --help';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildUnsupportedFlagError(commandKey, flag, support) {
|
|
105
|
+
const flagName = UX_FLAG_OPTIONS[flag];
|
|
106
|
+
const supported = supportedFlagList(support);
|
|
107
|
+
const supportedText = supported.length > 0 ? supported.join(', ') : 'none';
|
|
108
|
+
|
|
109
|
+
return new Error(formatError([
|
|
110
|
+
`UX flag ${flagName} is not supported by '${commandKey}'.`,
|
|
111
|
+
`Supported UX flags for '${commandKey}': ${supportedText}.`,
|
|
112
|
+
explainAlternative(flag),
|
|
113
|
+
].join('\n')));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function validateUxFlagCombinations(args = {}, commandKey = resolveUxCommandKey(args)) {
|
|
117
|
+
if (args.json && args.interactive) {
|
|
118
|
+
throw new Error(formatError(`incompatible UX flags for '${commandKey}': --json cannot be combined with --interactive because JSON output must stay machine-readable.`));
|
|
119
|
+
}
|
|
120
|
+
if (args.json && args.review) {
|
|
121
|
+
throw new Error(formatError(`incompatible UX flags for '${commandKey}': --json cannot be combined with --review because editor review is a human-only flow.`));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateUxFlags(args = {}) {
|
|
126
|
+
const commandKey = resolveUxCommandKey(args);
|
|
127
|
+
validateUxFlagCombinations(args, commandKey);
|
|
128
|
+
|
|
129
|
+
const support = getUxFlagSupport(commandKey);
|
|
130
|
+
for (const flag of getRequestedUxFlags(args)) {
|
|
131
|
+
if (support[flag] !== true) {
|
|
132
|
+
throw buildUnsupportedFlagError(commandKey, flag, support);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
commandKey,
|
|
138
|
+
requested: getRequestedUxFlags(args).map((flag) => UX_FLAG_OPTIONS[flag]),
|
|
139
|
+
supported: supportedFlagList(support),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
UX_FLAG_MATRIX,
|
|
145
|
+
UX_FLAG_OPTIONS,
|
|
146
|
+
getRequestedUxFlags,
|
|
147
|
+
getUxFlagSupport,
|
|
148
|
+
resolveUxCommandKey,
|
|
149
|
+
validateUxFlagCombinations,
|
|
150
|
+
validateUxFlags,
|
|
151
|
+
};
|