create-quiver 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +65 -25
  3. package/README_FOR_AI.md +36 -29
  4. package/ROADMAP.md +22 -3
  5. package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
  6. package/docs/COMMANDS.md.template +53 -20
  7. package/docs/STATUS.md.template +5 -1
  8. package/docs/WORKFLOW.md.template +13 -11
  9. package/package.json +10 -3
  10. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EVIDENCE_REPORT.md +293 -0
  11. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EXECUTION_PLAN.md +58 -0
  12. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/SPEC.md +242 -0
  13. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/STATUS.md +35 -0
  14. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/pr.md +77 -0
  15. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +34 -0
  16. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
  17. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/slice.json +52 -0
  18. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/CLOSURE_BRIEF.md +36 -0
  19. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/EXECUTION_BRIEF.md +52 -0
  20. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/slice.json +56 -0
  21. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/CLOSURE_BRIEF.md +43 -0
  22. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/EXECUTION_BRIEF.md +54 -0
  23. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/slice.json +52 -0
  24. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/CLOSURE_BRIEF.md +35 -0
  25. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/EXECUTION_BRIEF.md +53 -0
  26. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/slice.json +54 -0
  27. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/CLOSURE_BRIEF.md +34 -0
  28. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/EXECUTION_BRIEF.md +54 -0
  29. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/slice.json +52 -0
  30. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/CLOSURE_BRIEF.md +34 -0
  31. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/EXECUTION_BRIEF.md +54 -0
  32. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/slice.json +53 -0
  33. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/CLOSURE_BRIEF.md +33 -0
  34. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/EXECUTION_BRIEF.md +56 -0
  35. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/slice.json +55 -0
  36. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/CLOSURE_BRIEF.md +33 -0
  37. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/EXECUTION_BRIEF.md +54 -0
  38. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/slice.json +52 -0
  39. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/CLOSURE_BRIEF.md +39 -0
  40. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/EXECUTION_BRIEF.md +56 -0
  41. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/slice.json +53 -0
  42. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/CLOSURE_BRIEF.md +38 -0
  43. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/EXECUTION_BRIEF.md +57 -0
  44. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/slice.json +52 -0
  45. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/CLOSURE_BRIEF.md +39 -0
  46. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/EXECUTION_BRIEF.md +55 -0
  47. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/slice.json +56 -0
  48. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/CLOSURE_BRIEF.md +36 -0
  49. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/EXECUTION_BRIEF.md +54 -0
  50. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/slice.json +53 -0
  51. package/specs/quiver-v26-0121-smoke-hardening/EVIDENCE_REPORT.md +208 -0
  52. package/specs/quiver-v26-0121-smoke-hardening/EXECUTION_PLAN.md +57 -0
  53. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +137 -0
  54. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +32 -0
  55. package/specs/quiver-v26-0121-smoke-hardening/pr.md +96 -0
  56. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/CLOSURE_BRIEF.md +35 -0
  57. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/EXECUTION_BRIEF.md +55 -0
  58. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/slice.json +73 -0
  59. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/CLOSURE_BRIEF.md +38 -0
  60. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/EXECUTION_BRIEF.md +51 -0
  61. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/slice.json +76 -0
  62. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/CLOSURE_BRIEF.md +37 -0
  63. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/EXECUTION_BRIEF.md +52 -0
  64. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/slice.json +75 -0
  65. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/CLOSURE_BRIEF.md +37 -0
  66. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/EXECUTION_BRIEF.md +53 -0
  67. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/slice.json +77 -0
  68. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/CLOSURE_BRIEF.md +35 -0
  69. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/EXECUTION_BRIEF.md +52 -0
  70. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/slice.json +77 -0
  71. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/CLOSURE_BRIEF.md +34 -0
  72. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/EXECUTION_BRIEF.md +54 -0
  73. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/slice.json +84 -0
  74. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/CLOSURE_BRIEF.md +35 -0
  75. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/EXECUTION_BRIEF.md +53 -0
  76. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/slice.json +82 -0
  77. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/CLOSURE_BRIEF.md +35 -0
  78. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/EXECUTION_BRIEF.md +55 -0
  79. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/slice.json +92 -0
  80. package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
  81. package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
  82. package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
  83. package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
  84. package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
  85. package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
  86. package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
  87. package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
  88. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
  89. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
  90. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
  91. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
  92. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
  93. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
  94. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
  95. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
  96. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
  97. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
  98. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
  99. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
  100. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
  101. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
  102. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
  103. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
  104. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
  105. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
  106. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
  107. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
  108. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
  109. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
  110. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
  111. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
  112. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
  113. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
  114. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
  115. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
  116. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  117. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
  118. package/src/create-quiver/commands/ai.js +652 -27
  119. package/src/create-quiver/commands/flow.js +58 -9
  120. package/src/create-quiver/commands/graph.js +11 -9
  121. package/src/create-quiver/commands/plan.js +7 -16
  122. package/src/create-quiver/commands/spec.js +282 -0
  123. package/src/create-quiver/index.js +409 -31
  124. package/src/create-quiver/lib/actionable-error.js +27 -0
  125. package/src/create-quiver/lib/agent-profiles.js +16 -4
  126. package/src/create-quiver/lib/ai/artifacts.js +318 -0
  127. package/src/create-quiver/lib/ai/context-packs.js +4 -0
  128. package/src/create-quiver/lib/ai/execution-plan.js +16 -1
  129. package/src/create-quiver/lib/ai/executor.js +272 -21
  130. package/src/create-quiver/lib/ai/export-state.js +679 -0
  131. package/src/create-quiver/lib/ai/github.js +162 -2
  132. package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
  133. package/src/create-quiver/lib/ai/plan-review.js +7 -2
  134. package/src/create-quiver/lib/ai/providers.js +4 -3
  135. package/src/create-quiver/lib/ai/run-state.js +414 -0
  136. package/src/create-quiver/lib/ai/spec-generator.js +84 -13
  137. package/src/create-quiver/lib/ai/spec-templates.js +150 -21
  138. package/src/create-quiver/lib/analyze.js +2 -2
  139. package/src/create-quiver/lib/approvals.js +36 -5
  140. package/src/create-quiver/lib/demo.js +189 -14
  141. package/src/create-quiver/lib/doctor.js +154 -0
  142. package/src/create-quiver/lib/git.js +40 -1
  143. package/src/create-quiver/lib/handoff.js +123 -12
  144. package/src/create-quiver/lib/init-docs.js +35 -13
  145. package/src/create-quiver/lib/init-layout.js +9 -0
  146. package/src/create-quiver/lib/json.js +53 -3
  147. package/src/create-quiver/lib/lifecycle.js +52 -3
  148. package/src/create-quiver/lib/locks.js +134 -0
  149. package/src/create-quiver/lib/package-safety.js +7 -0
  150. package/src/create-quiver/lib/paths.js +74 -0
  151. package/src/create-quiver/lib/project-scan.js +74 -0
  152. package/src/create-quiver/lib/project-state-resolver.js +236 -0
  153. package/src/create-quiver/lib/readiness.js +66 -10
  154. package/src/create-quiver/lib/scope.js +52 -8
  155. package/src/create-quiver/lib/slice-graph.js +138 -38
  156. package/src/create-quiver/lib/slice.js +14 -5
  157. package/src/create-quiver/lib/spec-worktrees.js +129 -32
  158. package/src/create-quiver/lib/statuses.js +115 -0
@@ -0,0 +1,414 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+
5
+ const { quiverInternalPaths } = require('../init-layout');
6
+
7
+ const AI_RUN_PHASES = Object.freeze([
8
+ 'created',
9
+ 'onboarding-ready',
10
+ 'acceptance-draft',
11
+ 'acceptance-approved',
12
+ 'technical-plan-draft',
13
+ 'technical-plan-reviewed',
14
+ 'technical-plan-approved',
15
+ 'spec-generated',
16
+ 'execution-plan-generated',
17
+ 'slice-executing',
18
+ 'pr-ready',
19
+ 'closed',
20
+ ]);
21
+
22
+ const PHASE_NEXT_COMMAND = Object.freeze({
23
+ created: 'npx create-quiver ai plan --phase acceptance --input <requirements.md> --dry-run',
24
+ 'onboarding-ready': 'npx create-quiver ai plan --phase acceptance --input <requirements.md> --dry-run',
25
+ 'acceptance-draft': 'npx create-quiver ai approve --phase acceptance --version <n>',
26
+ 'acceptance-approved': 'npx create-quiver ai plan --phase technical-plan --dry-run',
27
+ 'technical-plan-draft': 'npx create-quiver ai review-plan --dry-run',
28
+ 'technical-plan-reviewed': 'npx create-quiver ai approve --phase technical-plan --version <n>',
29
+ 'technical-plan-approved': 'npx create-quiver spec create --dry-run',
30
+ 'spec-generated': 'npx create-quiver spec start specs/<spec-slug>',
31
+ 'execution-plan-generated': 'npx create-quiver ai execute-plan --dry-run --commit --mode manual',
32
+ 'slice-executing': 'npx create-quiver ai execute-plan --dry-run --commit --mode delegated',
33
+ 'pr-ready': 'npx create-quiver ai pr --dry-run --input specs/<spec-slug>/pr.md',
34
+ closed: 'No next command: lifecycle run is closed.',
35
+ });
36
+
37
+ function formatError(message) {
38
+ return `create-quiver: ${message}`;
39
+ }
40
+
41
+ function ensureDir(dirPath) {
42
+ fs.mkdirSync(dirPath, { recursive: true });
43
+ }
44
+
45
+ function toRelativePosix(root, filePath) {
46
+ return path.relative(root, filePath).split(path.sep).join('/');
47
+ }
48
+
49
+ function normalizeRunId(value) {
50
+ const normalized = String(value || '')
51
+ .trim()
52
+ .toLowerCase()
53
+ .replace(/[^a-z0-9._-]+/g, '-')
54
+ .replace(/^-+|-+$/g, '');
55
+
56
+ if (!normalized) {
57
+ throw new Error(formatError('invalid run id'));
58
+ }
59
+
60
+ return normalized;
61
+ }
62
+
63
+ function createRunId(now = new Date()) {
64
+ const stamp = now.toISOString()
65
+ .replace(/\.\d{3}Z$/, 'z')
66
+ .replace(/[^0-9a-z]+/gi, '-')
67
+ .toLowerCase()
68
+ .replace(/^-+|-+$/g, '');
69
+ return `run-${stamp}`;
70
+ }
71
+
72
+ function runsDir(projectRoot) {
73
+ return quiverInternalPaths(projectRoot).runsDir;
74
+ }
75
+
76
+ function locksDir(projectRoot) {
77
+ return quiverInternalPaths(projectRoot).locksDir || path.join(quiverInternalPaths(projectRoot).root, 'locks');
78
+ }
79
+
80
+ function runDir(projectRoot, runId) {
81
+ return path.join(runsDir(projectRoot), normalizeRunId(runId));
82
+ }
83
+
84
+ function runStatePath(projectRoot, runId) {
85
+ return path.join(runDir(projectRoot, runId), 'state.json');
86
+ }
87
+
88
+ function runApprovalsPath(projectRoot, runId) {
89
+ return path.join(runDir(projectRoot, runId), 'approvals.json');
90
+ }
91
+
92
+ function runRequirementPath(projectRoot, runId) {
93
+ return path.join(runDir(projectRoot, runId), 'requirement.md');
94
+ }
95
+
96
+ function assertKnownPhase(phase) {
97
+ if (!AI_RUN_PHASES.includes(phase)) {
98
+ throw new Error(formatError(`unsupported AI run phase '${phase}'`));
99
+ }
100
+ }
101
+
102
+ function phaseRank(phase) {
103
+ assertKnownPhase(phase);
104
+ return AI_RUN_PHASES.indexOf(phase);
105
+ }
106
+
107
+ function nextCommandForPhase(phase) {
108
+ assertKnownPhase(phase);
109
+ return PHASE_NEXT_COMMAND[phase];
110
+ }
111
+
112
+ function readJsonIfExists(filePath) {
113
+ if (!fs.existsSync(filePath)) {
114
+ return null;
115
+ }
116
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
117
+ }
118
+
119
+ function writeJson(filePath, value) {
120
+ ensureDir(path.dirname(filePath));
121
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
122
+ }
123
+
124
+ function listAiRuns(projectRoot) {
125
+ const root = runsDir(projectRoot);
126
+ if (!fs.existsSync(root)) {
127
+ return [];
128
+ }
129
+
130
+ return fs.readdirSync(root, { withFileTypes: true })
131
+ .filter((entry) => entry.isDirectory())
132
+ .map((entry) => readAiRun(projectRoot, entry.name))
133
+ .filter(Boolean)
134
+ .sort((a, b) => String(a.updated_at || a.created_at).localeCompare(String(b.updated_at || b.created_at)));
135
+ }
136
+
137
+ function latestAiRun(projectRoot) {
138
+ const runs = listAiRuns(projectRoot).filter((run) => run.status !== 'closed');
139
+ return runs.length > 0 ? runs[runs.length - 1] : null;
140
+ }
141
+
142
+ function readAiRun(projectRoot, runId) {
143
+ const statePath = runStatePath(projectRoot, runId);
144
+ if (!fs.existsSync(statePath)) {
145
+ return null;
146
+ }
147
+ return JSON.parse(fs.readFileSync(statePath, 'utf8'));
148
+ }
149
+
150
+ function resolveAiRun(projectRoot, runId = '') {
151
+ if (runId) {
152
+ const run = readAiRun(projectRoot, runId);
153
+ if (!run) {
154
+ throw new Error(formatError(`missing AI run: ${runId}`));
155
+ }
156
+ return run;
157
+ }
158
+
159
+ const latest = latestAiRun(projectRoot);
160
+ if (!latest) {
161
+ return null;
162
+ }
163
+ return latest;
164
+ }
165
+
166
+ function createAiRun(projectRoot, options = {}) {
167
+ const sourceInput = options.input ? path.resolve(projectRoot, options.input) : '';
168
+ if (sourceInput && !fs.existsSync(sourceInput)) {
169
+ throw new Error(formatError(`missing run requirement input file: ${options.input}`));
170
+ }
171
+
172
+ const runId = normalizeRunId(options.runId || createRunId(options.now || new Date()));
173
+ const targetDir = runDir(projectRoot, runId);
174
+ const now = (options.now || new Date()).toISOString();
175
+
176
+ if (fs.existsSync(runStatePath(projectRoot, runId))) {
177
+ throw new Error(formatError(`AI run already exists: ${runId}`));
178
+ }
179
+
180
+ ensureDir(targetDir);
181
+
182
+ const requirementTarget = runRequirementPath(projectRoot, runId);
183
+ if (sourceInput) {
184
+ fs.copyFileSync(sourceInput, requirementTarget);
185
+ } else {
186
+ fs.writeFileSync(requirementTarget, '');
187
+ }
188
+
189
+ const approvals = {
190
+ schema_version: 1,
191
+ run_id: runId,
192
+ approvals: [],
193
+ };
194
+
195
+ const state = {
196
+ schema_version: 1,
197
+ run_id: runId,
198
+ status: 'active',
199
+ phase: options.phase || 'created',
200
+ spec_slug: options.specSlug || null,
201
+ created_at: now,
202
+ updated_at: now,
203
+ requirement: {
204
+ source_file: sourceInput ? toRelativePosix(projectRoot, sourceInput) : null,
205
+ path: toRelativePosix(projectRoot, requirementTarget),
206
+ },
207
+ approvals_path: toRelativePosix(projectRoot, runApprovalsPath(projectRoot, runId)),
208
+ decisions_path: toRelativePosix(projectRoot, path.join(targetDir, 'decisions.md')),
209
+ history: [
210
+ {
211
+ phase: options.phase || 'created',
212
+ command: options.command || 'ai run create',
213
+ at: now,
214
+ },
215
+ ],
216
+ };
217
+
218
+ assertKnownPhase(state.phase);
219
+ writeJson(runApprovalsPath(projectRoot, runId), approvals);
220
+ writeJson(runStatePath(projectRoot, runId), state);
221
+ fs.writeFileSync(path.join(targetDir, 'decisions.md'), '# Decisions\n\n');
222
+ return state;
223
+ }
224
+
225
+ function ensureAiRun(projectRoot, options = {}) {
226
+ const existing = resolveAiRun(projectRoot, options.runId || '');
227
+ if (existing) {
228
+ return existing;
229
+ }
230
+ return createAiRun(projectRoot, options);
231
+ }
232
+
233
+ function updateAiRunPhase(projectRoot, runId, phase, options = {}) {
234
+ assertKnownPhase(phase);
235
+ const current = resolveAiRun(projectRoot, runId);
236
+ if (!current) {
237
+ throw new Error(formatError('missing AI run to update'));
238
+ }
239
+
240
+ if (phaseRank(phase) < phaseRank(current.phase)) {
241
+ throw new Error(formatError(`cannot move AI run ${current.run_id} backwards from ${current.phase} to ${phase}`));
242
+ }
243
+
244
+ const now = (options.now || new Date()).toISOString();
245
+ const next = {
246
+ ...current,
247
+ phase,
248
+ status: phase === 'closed' ? 'closed' : 'active',
249
+ spec_slug: options.specSlug || current.spec_slug || null,
250
+ updated_at: now,
251
+ history: (current.history || []).concat({
252
+ phase,
253
+ command: options.command || 'unknown',
254
+ artifact: options.artifact || null,
255
+ at: now,
256
+ }),
257
+ };
258
+
259
+ writeJson(runStatePath(projectRoot, current.run_id), next);
260
+ return next;
261
+ }
262
+
263
+ function recordAiRunApproval(projectRoot, runId, approval) {
264
+ const run = resolveAiRun(projectRoot, runId);
265
+ if (!run) {
266
+ throw new Error(formatError('missing AI run for approval metadata'));
267
+ }
268
+
269
+ const filePath = runApprovalsPath(projectRoot, run.run_id);
270
+ const current = readJsonIfExists(filePath) || { schema_version: 1, run_id: run.run_id, approvals: [] };
271
+ const next = {
272
+ ...current,
273
+ approvals: (current.approvals || []).concat({
274
+ ...approval,
275
+ at: approval.at || new Date().toISOString(),
276
+ }),
277
+ };
278
+ writeJson(filePath, next);
279
+ return next;
280
+ }
281
+
282
+ function assertAiRunPhaseAllows(run, requiredPhase, commandName) {
283
+ if (!run) {
284
+ throw new Error(formatError(`cannot run ${commandName}: no AI run exists. Next: npx create-quiver ai run create --input <requirements.md>`));
285
+ }
286
+ assertKnownPhase(requiredPhase);
287
+
288
+ if (phaseRank(run.phase) < phaseRank(requiredPhase)) {
289
+ throw new Error(formatError(`cannot run ${commandName}: AI run ${run.run_id} is at phase '${run.phase}' and requires '${requiredPhase}'. Next: ${nextCommandForPhase(run.phase)}`));
290
+ }
291
+
292
+ return true;
293
+ }
294
+
295
+ function sanitizeLockPart(value) {
296
+ return String(value || '')
297
+ .trim()
298
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
299
+ .replace(/^-+|-+$/g, '') || 'run';
300
+ }
301
+
302
+ function lockPath(projectRoot, runId, sliceId = '') {
303
+ const name = sliceId
304
+ ? `${sanitizeLockPart(runId)}--${sanitizeLockPart(sliceId)}.lock`
305
+ : `${sanitizeLockPart(runId)}.lock`;
306
+ return path.join(locksDir(projectRoot), name);
307
+ }
308
+
309
+ function readAiRunLock(projectRoot, runId, sliceId = '') {
310
+ return readJsonIfExists(lockPath(projectRoot, runId, sliceId));
311
+ }
312
+
313
+ function acquireAiRunLock(projectRoot, runId, options = {}) {
314
+ const filePath = lockPath(projectRoot, runId, options.sliceId || '');
315
+ const payload = {
316
+ schema_version: 1,
317
+ run_id: normalizeRunId(runId),
318
+ slice_id: options.sliceId || null,
319
+ pid: process.pid,
320
+ hostname: os.hostname(),
321
+ command: options.command || null,
322
+ created_at: (options.now || new Date()).toISOString(),
323
+ };
324
+ ensureDir(path.dirname(filePath));
325
+
326
+ try {
327
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, { flag: 'wx' });
328
+ } catch (error) {
329
+ if (error.code === 'EEXIST') {
330
+ const existing = readAiRunLock(projectRoot, runId, options.sliceId || '');
331
+ throw new Error(formatError(`AI run is locked: ${path.relative(projectRoot, filePath).split(path.sep).join('/')}\nLock owner: pid=${existing?.pid || 'unknown'} command=${existing?.command || 'unknown'} created_at=${existing?.created_at || 'unknown'}\nIf this process is gone, inspect the lock and remove it intentionally.`));
332
+ }
333
+ throw error;
334
+ }
335
+
336
+ return {
337
+ filePath,
338
+ lock: payload,
339
+ };
340
+ }
341
+
342
+ function releaseAiRunLock(projectRoot, runId, options = {}) {
343
+ const filePath = lockPath(projectRoot, runId, options.sliceId || '');
344
+ if (fs.existsSync(filePath)) {
345
+ fs.rmSync(filePath);
346
+ }
347
+ return filePath;
348
+ }
349
+
350
+ function formatAiRunStatus(projectRoot, run) {
351
+ if (!run) {
352
+ return [
353
+ 'AI run status',
354
+ 'Status: no active run',
355
+ 'Next safe command: npx create-quiver ai run create --input <requirements.md>',
356
+ '',
357
+ ].join('\n');
358
+ }
359
+
360
+ return [
361
+ 'AI run status',
362
+ `Run: ${run.run_id}`,
363
+ `Status: ${run.status}`,
364
+ `Phase: ${run.phase}`,
365
+ `Spec: ${run.spec_slug || '(not generated)'}`,
366
+ `Requirement: ${run.requirement?.path || '(missing)'}`,
367
+ `State: ${toRelativePosix(projectRoot, runStatePath(projectRoot, run.run_id))}`,
368
+ `Approvals: ${run.approvals_path}`,
369
+ `Next safe command: ${nextCommandForPhase(run.phase)}`,
370
+ '',
371
+ ].join('\n');
372
+ }
373
+
374
+ function formatAiRunResume(projectRoot, run) {
375
+ if (!run) {
376
+ return [
377
+ 'AI run resume',
378
+ 'No active run found.',
379
+ 'Next safe command: npx create-quiver ai run create --input <requirements.md>',
380
+ '',
381
+ ].join('\n');
382
+ }
383
+
384
+ return [
385
+ 'AI run resume',
386
+ `Run: ${run.run_id}`,
387
+ `Current phase: ${run.phase}`,
388
+ `Next safe command: ${nextCommandForPhase(run.phase)}`,
389
+ `State: ${toRelativePosix(projectRoot, runStatePath(projectRoot, run.run_id))}`,
390
+ '',
391
+ ].join('\n');
392
+ }
393
+
394
+ module.exports = {
395
+ AI_RUN_PHASES,
396
+ acquireAiRunLock,
397
+ assertAiRunPhaseAllows,
398
+ createAiRun,
399
+ ensureAiRun,
400
+ formatAiRunResume,
401
+ formatAiRunStatus,
402
+ latestAiRun,
403
+ listAiRuns,
404
+ nextCommandForPhase,
405
+ readAiRun,
406
+ readAiRunLock,
407
+ recordAiRunApproval,
408
+ releaseAiRunLock,
409
+ resolveAiRun,
410
+ runApprovalsPath,
411
+ runDir,
412
+ runStatePath,
413
+ updateAiRunPhase,
414
+ };
@@ -65,20 +65,73 @@ function parseApprovedManifest(sourceText, options = {}) {
65
65
  const acceptance = extractSectionBullets(normalizedText, ['Acceptance Criteria', 'Criterios de aceptacion', 'Criterios de aceptación']);
66
66
  const risks = extractSectionBullets(normalizedText, ['Risks', 'Riesgos']);
67
67
  const assumptions = extractSectionBullets(normalizedText, ['Assumptions', 'Suposiciones', 'Supuestos']);
68
+ const structuredBlock = extractStructuredJsonBlock(normalizedText);
69
+ const markdownSource = {
70
+ title: titleMatch ? titleMatch[1].trim() : options.fallbackTitle || '',
71
+ objective: objective || '',
72
+ scope,
73
+ acceptance,
74
+ risks,
75
+ assumptions,
76
+ };
77
+
78
+ if (structuredBlock) {
79
+ if (structuredBlock.spec && typeof structuredBlock.spec === 'object') {
80
+ return {
81
+ sourceText: normalizedText,
82
+ source: {
83
+ ...structuredBlock,
84
+ spec: {
85
+ ...markdownSource,
86
+ ...structuredBlock.spec,
87
+ },
88
+ },
89
+ };
90
+ }
91
+
92
+ return {
93
+ sourceText: normalizedText,
94
+ source: {
95
+ ...markdownSource,
96
+ ...structuredBlock,
97
+ },
98
+ };
99
+ }
68
100
 
69
101
  return {
70
102
  sourceText: normalizedText,
71
- source: {
72
- title: titleMatch ? titleMatch[1].trim() : options.fallbackTitle || '',
73
- objective: objective || '',
74
- scope,
75
- acceptance,
76
- risks,
77
- assumptions,
78
- },
103
+ source: markdownSource,
79
104
  };
80
105
  }
81
106
 
107
+ function hasStructuredSlices(value) {
108
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
109
+ return false;
110
+ }
111
+
112
+ return Array.isArray(value.slices) || Array.isArray(value.spec?.slices);
113
+ }
114
+
115
+ function extractStructuredJsonBlock(text) {
116
+ const fencePattern = /```(?:json|quiver|quiver-spec)?\s*\n([\s\S]*?)```/gi;
117
+ let match = fencePattern.exec(text);
118
+
119
+ while (match) {
120
+ const candidate = String(match[1] || '').trim();
121
+ try {
122
+ const parsed = parseJsonWithComments(candidate);
123
+ if (hasStructuredSlices(parsed)) {
124
+ return parsed;
125
+ }
126
+ } catch {
127
+ // Continue scanning other fenced blocks.
128
+ }
129
+ match = fencePattern.exec(text);
130
+ }
131
+
132
+ return null;
133
+ }
134
+
82
135
  function extractSectionText(text, headings) {
83
136
  const lines = String(text || '').split(/\r?\n/);
84
137
  const normalizedHeadings = new Set(headings.map((heading) => normalizeHeading(heading)));
@@ -182,10 +235,22 @@ function validateGeneratedSliceJson(filePath, expectedSliceId) {
182
235
  throw new Error(formatError(`slice.json files must be an array at ${filePath}`));
183
236
  }
184
237
 
238
+ if (!Array.isArray(parsed.expected_read_paths)) {
239
+ throw new Error(formatError(`slice.json expected_read_paths must be an array at ${filePath}`));
240
+ }
241
+
242
+ if (!Array.isArray(parsed.allowed_write_paths)) {
243
+ throw new Error(formatError(`slice.json allowed_write_paths must be an array at ${filePath}`));
244
+ }
245
+
185
246
  if (!Array.isArray(parsed.depends_on)) {
186
247
  throw new Error(formatError(`slice.json depends_on must be an array at ${filePath}`));
187
248
  }
188
249
 
250
+ if (!Array.isArray(parsed.validation_hints)) {
251
+ throw new Error(formatError(`slice.json validation_hints must be an array at ${filePath}`));
252
+ }
253
+
189
254
  return parsed;
190
255
  }
191
256
 
@@ -194,11 +259,17 @@ function buildSpecGenerationManifest({ inputText, inputPath, repoRoot, specSlug
194
259
  fallbackTitle: specSlug ? specSlug.replace(/-/g, ' ') : path.basename(inputPath || 'generated-spec.md', path.extname(inputPath || '.md')),
195
260
  });
196
261
 
197
- const manifest = buildManifest({
198
- ...source,
199
- sourcePath: path.relative(repoRoot, path.resolve(repoRoot, inputPath)).split(path.sep).join('/'),
200
- sourceText,
201
- }, { specSlug });
262
+ let manifest;
263
+ try {
264
+ manifest = buildManifest({
265
+ ...source,
266
+ sourcePath: path.relative(repoRoot, path.resolve(repoRoot, inputPath)).split(path.sep).join('/'),
267
+ sourceText,
268
+ }, { specSlug });
269
+ } catch (error) {
270
+ const message = String(error.message || error);
271
+ throw new Error(message.startsWith('create-quiver:') ? message : formatError(message));
272
+ }
202
273
 
203
274
  if (!manifest.slug) {
204
275
  throw new Error(formatError('unable to derive a spec slug from the approved input'));