create-quiver 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +183 -511
  3. package/README_FOR_AI.md +36 -26
  4. package/ROADMAP.md +11 -0
  5. package/assets/quiver-wordmark.svg +22 -0
  6. package/docs/AI_CONTEXT.md.template +2 -0
  7. package/docs/AI_ONBOARDING_PROMPT.md.template +9 -1
  8. package/docs/CLI_UX_GUIDE.md +125 -0
  9. package/docs/COMMANDS.md.template +19 -4
  10. package/docs/GITFLOW_PR_GUIDE.md +70 -0
  11. package/docs/TROUBLESHOOTING.md.template +29 -0
  12. package/docs/WORKFLOW.md.template +13 -12
  13. package/docs/getting-started/linux.md +84 -0
  14. package/docs/getting-started/macos.md +85 -0
  15. package/docs/getting-started/windows-git-bash-wsl.md +78 -0
  16. package/docs/getting-started/windows-powershell.md +96 -0
  17. package/docs/reference/commands.md +94 -0
  18. package/docs/workflows/existing-project.md +131 -0
  19. package/docs/workflows/full-ai-spec-to-pr.md +311 -0
  20. package/docs/workflows/legacy-quiver-project.md +102 -0
  21. package/docs/workflows/new-project.md +76 -0
  22. package/package.json +5 -1
  23. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
  24. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
  25. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
  26. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
  27. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
  28. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
  29. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
  30. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
  31. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
  32. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
  33. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
  34. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
  35. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
  36. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
  37. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
  38. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
  39. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
  40. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
  41. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
  42. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
  43. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
  44. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
  45. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
  46. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
  47. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
  48. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  49. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
  50. package/specs/quiver-v29-planner-prepare-context-cli-ux/EVIDENCE_REPORT.md +163 -0
  51. package/specs/quiver-v29-planner-prepare-context-cli-ux/EXECUTION_PLAN.md +72 -0
  52. package/specs/quiver-v29-planner-prepare-context-cli-ux/SPEC.md +173 -0
  53. package/specs/quiver-v29-planner-prepare-context-cli-ux/STATUS.md +34 -0
  54. package/specs/quiver-v29-planner-prepare-context-cli-ux/pr.md +95 -0
  55. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/CLOSURE_BRIEF.md +32 -0
  56. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/EXECUTION_BRIEF.md +45 -0
  57. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-00-cli-ux-spec-foundation/slice.json +58 -0
  58. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/CLOSURE_BRIEF.md +33 -0
  59. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/EXECUTION_BRIEF.md +49 -0
  60. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-01-cli-ux-primitives-theme/slice.json +75 -0
  61. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/CLOSURE_BRIEF.md +32 -0
  62. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/EXECUTION_BRIEF.md +47 -0
  63. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-02-planner-context-proposal-contract/slice.json +71 -0
  64. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/CLOSURE_BRIEF.md +33 -0
  65. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/EXECUTION_BRIEF.md +52 -0
  66. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-03-prepare-context-planner-review-flow/slice.json +82 -0
  67. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/CLOSURE_BRIEF.md +34 -0
  68. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/EXECUTION_BRIEF.md +46 -0
  69. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-04-ux-flag-matrix-compatibility/slice.json +73 -0
  70. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/CLOSURE_BRIEF.md +34 -0
  71. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/EXECUTION_BRIEF.md +46 -0
  72. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-05-progressive-command-adoption/slice.json +83 -0
  73. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/CLOSURE_BRIEF.md +31 -0
  74. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/EXECUTION_BRIEF.md +50 -0
  75. package/specs/quiver-v29-planner-prepare-context-cli-ux/slices/slice-06-docs-tests-smoke-readiness/slice.json +95 -0
  76. package/src/create-quiver/commands/ai.js +938 -16
  77. package/src/create-quiver/commands/spec.js +74 -2
  78. package/src/create-quiver/index.js +91 -5
  79. package/src/create-quiver/lib/ai/context-packs.js +2 -2
  80. package/src/create-quiver/lib/ai/context-proposal.js +389 -0
  81. package/src/create-quiver/lib/ai/context-proposal.schema.js +31 -0
  82. package/src/create-quiver/lib/ai/export-state.js +52 -5
  83. package/src/create-quiver/lib/ai/github.js +14 -2
  84. package/src/create-quiver/lib/ai/plan-review.js +159 -0
  85. package/src/create-quiver/lib/ai/run-state.js +17 -2
  86. package/src/create-quiver/lib/ai/spec-generator.js +15 -0
  87. package/src/create-quiver/lib/cli/editor.js +118 -0
  88. package/src/create-quiver/lib/cli/theme.js +100 -0
  89. package/src/create-quiver/lib/cli/ux-flags.js +151 -0
  90. package/src/create-quiver/lib/cli/ux.js +130 -0
  91. package/src/create-quiver/lib/project-state-resolver.js +195 -1
  92. package/src/create-quiver/lib/spec-worktrees.js +50 -2
@@ -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
- return [
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
- ].join('\n');
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
+ };