create-quiver 0.6.0 → 0.8.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 (120) hide show
  1. package/.claude/settings.local.json +45 -0
  2. package/.github/workflows/ci.yml +9 -32
  3. package/AGENTS.md.template +41 -0
  4. package/BACKLOG.md +139 -0
  5. package/CHANGELOG.md +17 -0
  6. package/README.md +68 -14
  7. package/README_FOR_AI.md +48 -16
  8. package/ROADMAP.md +100 -0
  9. package/docs/AI_CONTEXT.md.template +19 -26
  10. package/docs/AI_ONBOARDING_PROMPT.md.template +16 -0
  11. package/docs/COMMANDS.md.template +25 -0
  12. package/docs/CONTEXTO.md.template +4 -17
  13. package/docs/DECISIONS.md.template +18 -0
  14. package/docs/DEEP.md.template +34 -0
  15. package/docs/DOCUMENTATION_GUIDE.md.template +9 -7
  16. package/docs/GITFLOW_PR_GUIDE.md.template +7 -0
  17. package/docs/INDEX.md.template +11 -0
  18. package/docs/QUICK.md.template +27 -0
  19. package/docs/STANDARD.md.template +49 -0
  20. package/docs/STATUS.md.template +2 -2
  21. package/docs/SUPPORT_MATRIX.md.template +16 -4
  22. package/docs/TESTING_GUIDE_FOR_AI.md.template +4 -3
  23. package/docs/TROUBLESHOOTING.md.template +14 -0
  24. package/docs/WORKFLOW.md.template +21 -4
  25. package/docs/examples/graph.md.template +62 -0
  26. package/docs/examples/next.md.template +27 -0
  27. package/docs/examples/plan.md.template +28 -0
  28. package/package.json +6 -2
  29. package/package.template.json +16 -0
  30. package/scripts/check-slice-readiness.sh +6 -4
  31. package/scripts/cleanup-slice.sh +2 -172
  32. package/scripts/init-docs.sh +147 -26
  33. package/scripts/package-quiver.sh +5 -0
  34. package/scripts/start-slice.sh +3 -425
  35. package/specs/[project-name]/EVIDENCE_REPORT.md.template +3 -1
  36. package/specs/[project-name]/HANDOFF.md.template +37 -0
  37. package/specs/[project-name]/slices/slice-template/slice.json +7 -2
  38. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-01-project-scan-command/slice.json +1 -1
  39. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-02-ai-onboarding-prompt/slice.json +1 -1
  40. package/specs/quiver-v08-agent-onboarding-analysis/slices/slice-03-doctor-readme-adoption-flow/slice.json +1 -1
  41. package/specs/quiver-v12-cross-platform-native-runtime/EVIDENCE_REPORT.md +30 -0
  42. package/specs/quiver-v12-cross-platform-native-runtime/SPEC.md +86 -0
  43. package/specs/quiver-v12-cross-platform-native-runtime/STATUS.md +29 -0
  44. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-01-cross-platform-support-contract/slice.json +69 -0
  45. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-02-node-init-docs-runtime/slice.json +76 -0
  46. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-03-node-migrate-analyze-doctor-flow/slice.json +74 -0
  47. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-04-node-slice-lifecycle-commands/slice.json +81 -0
  48. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-05-generated-project-scripts-and-migration/slice.json +78 -0
  49. package/specs/quiver-v12-cross-platform-native-runtime/slices/slice-06-cross-platform-ci-release-readiness/slice.json +74 -0
  50. package/specs/quiver-v13-token-efficient-ai-context/EVIDENCE_REPORT.md +28 -0
  51. package/specs/quiver-v13-token-efficient-ai-context/SPEC.md +68 -0
  52. package/specs/quiver-v13-token-efficient-ai-context/STATUS.md +26 -0
  53. package/specs/quiver-v13-token-efficient-ai-context/slices/slice-01-token-efficient-ai-modes-guidance/slice.json +65 -0
  54. package/specs/quiver-v13-token-efficient-ai-context/slices/slice-02-decision-log-context-checkpoint/slice.json +64 -0
  55. package/specs/quiver-v13-token-efficient-ai-context/slices/slice-03-project-map-reading-order/slice.json +66 -0
  56. package/specs/quiver-v14-tiered-context-pack/EVIDENCE_REPORT.md +42 -0
  57. package/specs/quiver-v14-tiered-context-pack/SPEC.md +116 -0
  58. package/specs/quiver-v14-tiered-context-pack/STATUS.md +35 -0
  59. package/specs/quiver-v14-tiered-context-pack/slices/slice-01-tiered-context-pack/slice.json +77 -0
  60. package/specs/quiver-v14-tiered-context-pack/slices/slice-02-agents-md-router/slice.json +74 -0
  61. package/specs/quiver-v14-tiered-context-pack/slices/slice-03-active-slice-lifecycle/slice.json +74 -0
  62. package/specs/quiver-v14-tiered-context-pack/slices/slice-04-dedup-frontmatter/slice.json +83 -0
  63. package/specs/quiver-v14-tiered-context-pack/slices/slice-05-doctor-smokes-tiered-pack/slice.json +84 -0
  64. package/specs/quiver-v15-init-required-before-migrate/EVIDENCE_REPORT.md +26 -0
  65. package/specs/quiver-v15-init-required-before-migrate/SPEC.md +66 -0
  66. package/specs/quiver-v15-init-required-before-migrate/STATUS.md +26 -0
  67. package/specs/quiver-v15-init-required-before-migrate/slices/slice-01-migrate-initialization-precondition/slice.json +65 -0
  68. package/specs/quiver-v15-init-required-before-migrate/slices/slice-02-doctor-not-initialized-guidance/slice.json +61 -0
  69. package/specs/quiver-v15-init-required-before-migrate/slices/slice-03-docs-smokes-init-before-migrate/slice.json +64 -0
  70. package/specs/quiver-v16-handoff-contract/EVIDENCE_REPORT.md +26 -0
  71. package/specs/quiver-v16-handoff-contract/SPEC.md +68 -0
  72. package/specs/quiver-v16-handoff-contract/STATUS.md +26 -0
  73. package/specs/quiver-v16-handoff-contract/slices/slice-01-handoff-template-and-contract/slice.json +66 -0
  74. package/specs/quiver-v16-handoff-contract/slices/slice-02-check-handoff-command/slice.json +70 -0
  75. package/specs/quiver-v16-handoff-contract/slices/slice-03-handoff-scaffold-optional/slice.json +67 -0
  76. package/specs/quiver-v17-orchestration-foundation/EVIDENCE_REPORT.md +32 -0
  77. package/specs/quiver-v17-orchestration-foundation/SPEC.md +79 -0
  78. package/specs/quiver-v17-orchestration-foundation/STATUS.md +31 -0
  79. package/specs/quiver-v17-orchestration-foundation/slices/slice-01-ci-matrix-verified/slice.json +68 -0
  80. package/specs/quiver-v17-orchestration-foundation/slices/slice-02-slice-graph-library/slice.json +65 -0
  81. package/specs/quiver-v17-orchestration-foundation/slices/slice-03-depends-on-validation/slice.json +72 -0
  82. package/specs/quiver-v18-slice-orchestration/EVIDENCE_REPORT.md +38 -0
  83. package/specs/quiver-v18-slice-orchestration/SPEC.md +91 -0
  84. package/specs/quiver-v18-slice-orchestration/STATUS.md +33 -0
  85. package/specs/quiver-v18-slice-orchestration/slices/slice-01-plan-command/slice.json +79 -0
  86. package/specs/quiver-v18-slice-orchestration/slices/slice-02-graph-mvp-tree/slice.json +75 -0
  87. package/specs/quiver-v18-slice-orchestration/slices/slice-03-graph-extended-formats/slice.json +70 -0
  88. package/specs/quiver-v18-slice-orchestration/slices/slice-04-next-command/slice.json +73 -0
  89. package/specs/quiver-v18-stabilization/EVIDENCE_REPORT.md +26 -0
  90. package/specs/quiver-v18-stabilization/SPEC.md +62 -0
  91. package/specs/quiver-v18-stabilization/STATUS.md +30 -0
  92. package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/CLOSURE_BRIEF.md +29 -0
  93. package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/EXECUTION_BRIEF.md +134 -0
  94. package/specs/quiver-v18-stabilization/slices/slice-01-fix-legacy-dependency-resolution/slice.json +56 -0
  95. package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/CLOSURE_BRIEF.md +29 -0
  96. package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/EXECUTION_BRIEF.md +118 -0
  97. package/specs/quiver-v18-stabilization/slices/slice-02-roadmap-and-branch-cleanup/slice.json +57 -0
  98. package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/CLOSURE_BRIEF.md +23 -0
  99. package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/EXECUTION_BRIEF.md +73 -0
  100. package/specs/quiver-v18-stabilization/slices/slice-03-publish-drafts-branch/slice.json +49 -0
  101. package/src/create-quiver/commands/graph.js +97 -0
  102. package/src/create-quiver/commands/next.js +134 -0
  103. package/src/create-quiver/commands/plan.js +205 -0
  104. package/src/create-quiver/index.js +476 -123
  105. package/src/create-quiver/lib/analyze.js +9 -0
  106. package/src/create-quiver/lib/doctor.js +212 -0
  107. package/src/create-quiver/lib/git.js +154 -0
  108. package/src/create-quiver/lib/handoff.js +104 -0
  109. package/src/create-quiver/lib/init-docs.js +674 -0
  110. package/src/create-quiver/lib/json.js +14 -0
  111. package/src/create-quiver/lib/lifecycle.js +479 -0
  112. package/src/create-quiver/lib/paths.js +19 -0
  113. package/src/create-quiver/lib/readiness.js +354 -0
  114. package/src/create-quiver/lib/renderers/dot.js +129 -0
  115. package/src/create-quiver/lib/renderers/mermaid.js +119 -0
  116. package/src/create-quiver/lib/renderers/tree.js +116 -0
  117. package/src/create-quiver/lib/scope.js +5 -0
  118. package/src/create-quiver/lib/slice-graph.js +453 -0
  119. package/src/create-quiver/lib/slice.js +195 -0
  120. package/src/create-quiver/lib/state.js +139 -0
@@ -0,0 +1,9 @@
1
+ const { runAnalyze } = require('../index');
2
+
3
+ function analyzeProject(targetDir) {
4
+ return runAnalyze(targetDir);
5
+ }
6
+
7
+ module.exports = {
8
+ analyzeProject,
9
+ };
@@ -0,0 +1,212 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { worktreeList } = require('./git');
4
+
5
+ function readTextIfExists(filePath) {
6
+ if (!fs.existsSync(filePath)) {
7
+ return null;
8
+ }
9
+
10
+ return fs.readFileSync(filePath, 'utf8');
11
+ }
12
+
13
+ function countNonEmptyLines(text) {
14
+ return String(text || '')
15
+ .split(/\r?\n/)
16
+ .filter((line) => line.trim().length > 0)
17
+ .length;
18
+ }
19
+
20
+ function hasFrontMatter(text) {
21
+ const value = String(text || '');
22
+ if (!value.startsWith('---\n')) {
23
+ return false;
24
+ }
25
+
26
+ return value.indexOf('\n---\n', 4) !== -1;
27
+ }
28
+
29
+ function normalizeRelativePath(root, absolutePath) {
30
+ return path.relative(root, absolutePath).split(path.sep).join('/');
31
+ }
32
+
33
+ function collectAiMarkdownFiles(projectRoot) {
34
+ const aiDir = path.join(projectRoot, 'docs', 'ai');
35
+ if (!fs.existsSync(aiDir)) {
36
+ return [];
37
+ }
38
+
39
+ const files = [];
40
+
41
+ const walk = (dirPath) => {
42
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
43
+ const fullPath = path.join(dirPath, entry.name);
44
+ if (entry.isDirectory()) {
45
+ walk(fullPath);
46
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
47
+ files.push(fullPath);
48
+ }
49
+ }
50
+ };
51
+
52
+ walk(aiDir);
53
+
54
+ return files;
55
+ }
56
+
57
+ function countDocsAiFrontMatterIssues(projectRoot) {
58
+ const files = collectAiMarkdownFiles(projectRoot);
59
+ const missing = [];
60
+
61
+ for (const filePath of files) {
62
+ const text = readTextIfExists(filePath);
63
+ if (!text || !hasFrontMatter(text)) {
64
+ missing.push(normalizeRelativePath(projectRoot, filePath));
65
+ }
66
+ }
67
+
68
+ return missing;
69
+ }
70
+
71
+ function countAgentsSections(projectRoot) {
72
+ const agentsPath = path.join(projectRoot, 'AGENTS.md');
73
+ const text = readTextIfExists(agentsPath);
74
+ if (!text) {
75
+ return ['AGENTS.md'];
76
+ }
77
+
78
+ const requiredSections = [
79
+ [/^Purpose$/m, 'Purpose'],
80
+ [/^## Reading Budget$/m, 'Reading Budget'],
81
+ [/^## Reading Order$/m, 'Reading Order'],
82
+ [/^## Output Policy$/m, 'Output Policy'],
83
+ [/^## Slice Execution Rules$/m, 'Slice Execution Rules'],
84
+ [/^## Links$/m, 'Links'],
85
+ ];
86
+
87
+ const missing = requiredSections
88
+ .filter(([pattern]) => !pattern.test(text))
89
+ .map(([, label]) => label);
90
+ return missing;
91
+ }
92
+
93
+ function countTieredPackSizeWarnings(projectRoot) {
94
+ const warnings = [];
95
+
96
+ const quickPath = path.join(projectRoot, 'docs', 'ai', 'QUICK.md');
97
+ const quickText = readTextIfExists(quickPath);
98
+ if (quickText && countNonEmptyLines(quickText) > 50) {
99
+ warnings.push(`docs/ai/QUICK.md exceeds the 50 non-empty line budget (${countNonEmptyLines(quickText)})`);
100
+ }
101
+
102
+ const standardPath = path.join(projectRoot, 'docs', 'ai', 'STANDARD.md');
103
+ const standardText = readTextIfExists(standardPath);
104
+ if (standardText && countNonEmptyLines(standardText) > 300) {
105
+ warnings.push(`docs/ai/STANDARD.md exceeds the 300 non-empty line budget (${countNonEmptyLines(standardText)})`);
106
+ }
107
+
108
+ return warnings;
109
+ }
110
+
111
+ function countActiveSliceOrphans(projectRoot) {
112
+ const activeSlicePath = path.join(projectRoot, 'docs', 'ai', 'ACTIVE_SLICE.md');
113
+ if (!fs.existsSync(activeSlicePath)) {
114
+ return [];
115
+ }
116
+
117
+ const activeWorktrees = worktreeList(projectRoot).filter((entry) => {
118
+ const worktreePath = entry.worktree || '';
119
+ if (!worktreePath || worktreePath === projectRoot) {
120
+ return false;
121
+ }
122
+
123
+ return fs.existsSync(path.join(worktreePath, 'WORKTREE_CONTEXT.md'));
124
+ });
125
+
126
+ if (activeWorktrees.length === 0) {
127
+ return ['docs/ai/ACTIVE_SLICE.md exists without an active slice worktree'];
128
+ }
129
+
130
+ return [];
131
+ }
132
+
133
+ function countStackInfoLeaks(projectRoot) {
134
+ const leakPatterns = [
135
+ /Package manager:/i,
136
+ /Detected package manager:/i,
137
+ /Detected primary stack:/i,
138
+ /Primary install:/i,
139
+ /Primary test:/i,
140
+ /Stack summary:/i,
141
+ ];
142
+
143
+ const generatedFiles = [];
144
+ const docsDir = path.join(projectRoot, 'docs');
145
+
146
+ if (fs.existsSync(docsDir)) {
147
+ const walk = (dirPath) => {
148
+ for (const entry of fs.readdirSync(dirPath, { withFileTypes: true })) {
149
+ const fullPath = path.join(dirPath, entry.name);
150
+ if (entry.isDirectory()) {
151
+ walk(fullPath);
152
+ } else if (entry.isFile() && entry.name.endsWith('.md') && path.relative(docsDir, fullPath) !== 'PROJECT_MAP.md') {
153
+ generatedFiles.push(fullPath);
154
+ }
155
+ }
156
+ };
157
+
158
+ walk(docsDir);
159
+ }
160
+
161
+ const leaks = [];
162
+
163
+ for (const filePath of generatedFiles) {
164
+ const text = readTextIfExists(filePath);
165
+ if (!text) {
166
+ continue;
167
+ }
168
+
169
+ if (leakPatterns.some((pattern) => pattern.test(text))) {
170
+ leaks.push(normalizeRelativePath(projectRoot, filePath));
171
+ }
172
+ }
173
+
174
+ return leaks;
175
+ }
176
+
177
+ function collectDoctorWarnings(projectRoot) {
178
+ const warnings = [];
179
+
180
+ const agentsMissing = countAgentsSections(projectRoot);
181
+ if (agentsMissing.length > 0) {
182
+ if (agentsMissing.includes('AGENTS.md')) {
183
+ warnings.push('missing AGENTS.md');
184
+ } else {
185
+ warnings.push(`AGENTS.md is missing required sections: ${agentsMissing.join(', ')}`);
186
+ }
187
+ }
188
+
189
+ for (const issue of countTieredPackSizeWarnings(projectRoot)) {
190
+ warnings.push(issue);
191
+ }
192
+
193
+ const frontMatterIssues = countDocsAiFrontMatterIssues(projectRoot);
194
+ for (const issue of frontMatterIssues) {
195
+ warnings.push(`${issue} is missing YAML front matter`);
196
+ }
197
+
198
+ for (const issue of countActiveSliceOrphans(projectRoot)) {
199
+ warnings.push(issue);
200
+ }
201
+
202
+ const leakIssues = countStackInfoLeaks(projectRoot);
203
+ if (leakIssues.length > 0) {
204
+ warnings.push(`stack information appears outside docs/PROJECT_MAP.md: ${leakIssues.join(', ')}`);
205
+ }
206
+
207
+ return warnings;
208
+ }
209
+
210
+ module.exports = {
211
+ collectDoctorWarnings,
212
+ };
@@ -0,0 +1,154 @@
1
+ const cp = require('child_process');
2
+
3
+ function runGit(args, cwd, options = {}) {
4
+ return cp.execFileSync('git', args, {
5
+ cwd,
6
+ encoding: 'utf8',
7
+ stdio: ['ignore', 'pipe', 'pipe'],
8
+ ...options,
9
+ }).trim();
10
+ }
11
+
12
+ function tryGit(args, cwd, options = {}) {
13
+ try {
14
+ return runGit(args, cwd, options);
15
+ } catch {
16
+ return '';
17
+ }
18
+ }
19
+
20
+ function hasRef(repoRoot, ref) {
21
+ try {
22
+ runGit(['show-ref', '--verify', '--quiet', ref], repoRoot);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ function hasLocalBranch(repoRoot, branchName) {
30
+ return hasRef(repoRoot, `refs/heads/${branchName}`);
31
+ }
32
+
33
+ function hasRemoteBranch(repoRoot, branchName, remote = 'origin') {
34
+ return hasRef(repoRoot, `refs/remotes/${remote}/${branchName}`);
35
+ }
36
+
37
+ function fetchBranch(repoRoot, branchName, remote = 'origin') {
38
+ return runGit(['fetch', remote, `${branchName}:${branchName}`], repoRoot);
39
+ }
40
+
41
+ function fetchRemote(repoRoot, remote = 'origin', args = ['--prune']) {
42
+ return runGit(['fetch', remote, ...args], repoRoot);
43
+ }
44
+
45
+ function worktreePrune(repoRoot) {
46
+ tryGit(['worktree', 'prune'], repoRoot);
47
+ }
48
+
49
+ function worktreeList(repoRoot) {
50
+ const text = tryGit(['worktree', 'list', '--porcelain'], repoRoot);
51
+ const entries = [];
52
+ const chunks = text.trim().split('\n\n').filter(Boolean);
53
+
54
+ for (const chunk of chunks) {
55
+ const entry = {};
56
+ for (const line of chunk.split('\n')) {
57
+ const idx = line.indexOf(' ');
58
+ if (idx === -1) {
59
+ continue;
60
+ }
61
+ entry[line.slice(0, idx)] = line.slice(idx + 1);
62
+ }
63
+ if (entry.worktree) {
64
+ entries.push(entry);
65
+ }
66
+ }
67
+
68
+ return entries;
69
+ }
70
+
71
+ function worktreeAdd(repoRoot, worktreePath, ref, options = {}) {
72
+ const args = ['worktree', 'add'];
73
+ if (options.branch) {
74
+ args.push('-b', options.branch);
75
+ }
76
+ if (options.force) {
77
+ args.push('--force');
78
+ }
79
+ args.push(worktreePath, ref);
80
+ return runGit(args, repoRoot);
81
+ }
82
+
83
+ function worktreeRemove(repoRoot, worktreePath, force = false) {
84
+ const args = ['worktree', 'remove'];
85
+ if (force) {
86
+ args.push('--force');
87
+ }
88
+ args.push(worktreePath);
89
+ return runGit(args, repoRoot);
90
+ }
91
+
92
+ function branchDelete(repoRoot, branchName, force = false) {
93
+ return runGit(['branch', force ? '-D' : '-d', branchName], repoRoot);
94
+ }
95
+
96
+ function currentBranch(repoRoot) {
97
+ return tryGit(['branch', '--show-current'], repoRoot);
98
+ }
99
+
100
+ function statusPorcelain(repoRoot) {
101
+ return tryGit(['status', '--porcelain'], repoRoot);
102
+ }
103
+
104
+ function revListCount(repoRoot, range) {
105
+ const output = tryGit(['rev-list', '--count', range], repoRoot);
106
+ return Number(output || '0');
107
+ }
108
+
109
+ function mergeBaseIsAncestor(repoRoot, maybeAncestor, ref) {
110
+ try {
111
+ runGit(['merge-base', '--is-ancestor', maybeAncestor, ref], repoRoot);
112
+ return true;
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ function lsRemoteHeads(repoRoot, branchName, remote = 'origin') {
119
+ try {
120
+ runGit(['ls-remote', '--exit-code', '--heads', remote, branchName], repoRoot);
121
+ return true;
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ function catFileExists(repoRoot, specRef) {
128
+ try {
129
+ runGit(['cat-file', '-e', specRef], repoRoot);
130
+ return true;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ module.exports = {
137
+ branchDelete,
138
+ catFileExists,
139
+ currentBranch,
140
+ fetchBranch,
141
+ fetchRemote,
142
+ hasLocalBranch,
143
+ hasRemoteBranch,
144
+ lsRemoteHeads,
145
+ mergeBaseIsAncestor,
146
+ revListCount,
147
+ runGit,
148
+ statusPorcelain,
149
+ tryGit,
150
+ worktreeAdd,
151
+ worktreeList,
152
+ worktreePrune,
153
+ worktreeRemove,
154
+ };
@@ -0,0 +1,104 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const REQUIRED_HEADINGS = [
5
+ '## Background',
6
+ '## What you will change',
7
+ '## Validation checklist',
8
+ '## Out of scope',
9
+ '## Expected deliverable',
10
+ '## Constraints',
11
+ ];
12
+
13
+ const HANDOFF_TEMPLATE_PATH = path.resolve(__dirname, '..', '..', '..', 'specs', '[project-name]', 'HANDOFF.md.template');
14
+
15
+ function normalizePosixPath(filePath, pathLib = path) {
16
+ return filePath.split(pathLib.sep).join('/');
17
+ }
18
+
19
+ function resolveHandoffPath(repoRoot, handoffInput, pathLib = path) {
20
+ const absolutePath = pathLib.resolve(repoRoot, handoffInput);
21
+ const relativePath = normalizePosixPath(pathLib.relative(repoRoot, absolutePath), pathLib);
22
+
23
+ if (relativePath.startsWith('..') || pathLib.isAbsolute(relativePath)) {
24
+ throw new Error(`create-quiver: handoff must live at specs/<spec-slug>/HANDOFF.md (got ${normalizePosixPath(handoffInput, pathLib)})`);
25
+ }
26
+
27
+ const match = relativePath.match(/^specs\/([^/]+)\/HANDOFF\.md$/);
28
+ if (!match) {
29
+ throw new Error(`create-quiver: handoff must live at specs/<spec-slug>/HANDOFF.md (got ${relativePath})`);
30
+ }
31
+
32
+ return {
33
+ absolutePath,
34
+ relativePath,
35
+ specSlug: match[1],
36
+ };
37
+ }
38
+
39
+ function readHandoffSections(text) {
40
+ return String(text || '')
41
+ .split(/\r?\n/)
42
+ .map((line) => line.trim())
43
+ .filter((line) => line.startsWith('## '))
44
+ .map((line) => line.replace(/\s+$/, ''));
45
+ }
46
+
47
+ function validateHandoffSections(text) {
48
+ const sections = new Set(readHandoffSections(text));
49
+ return REQUIRED_HEADINGS.filter((heading) => !sections.has(heading));
50
+ }
51
+
52
+ function checkHandoff(handoffInput, repoRoot = process.cwd()) {
53
+ const resolved = resolveHandoffPath(repoRoot, handoffInput);
54
+
55
+ if (!fs.existsSync(resolved.absolutePath)) {
56
+ throw new Error(`create-quiver: missing handoff file: ${resolved.relativePath}`);
57
+ }
58
+
59
+ const text = fs.readFileSync(resolved.absolutePath, 'utf8');
60
+ const missingSections = validateHandoffSections(text);
61
+ if (missingSections.length > 0) {
62
+ throw new Error(`create-quiver: handoff is missing required sections: ${missingSections.join(', ')}`);
63
+ }
64
+
65
+ return resolved;
66
+ }
67
+
68
+ function scaffoldHandoff(specSlug, repoRoot = process.cwd()) {
69
+ const trimmedSlug = String(specSlug || '').trim();
70
+ if (!trimmedSlug) {
71
+ throw new Error('create-quiver: missing handoff slug. Use: npx create-quiver new-handoff <spec-slug>');
72
+ }
73
+
74
+ if (!fs.existsSync(HANDOFF_TEMPLATE_PATH)) {
75
+ throw new Error('create-quiver: missing handoff template at specs/[project-name]/HANDOFF.md.template');
76
+ }
77
+
78
+ const resolved = resolveHandoffPath(repoRoot, path.join('specs', trimmedSlug, 'HANDOFF.md'));
79
+ if (fs.existsSync(resolved.absolutePath)) {
80
+ throw new Error(`create-quiver: handoff already exists at ${resolved.relativePath}`);
81
+ }
82
+
83
+ const templateText = fs.readFileSync(HANDOFF_TEMPLATE_PATH, 'utf8');
84
+ const projectName = trimmedSlug.replace(/-/g, ' ');
85
+ const currentDate = new Date().toISOString().slice(0, 10);
86
+ const renderedText = templateText
87
+ .replace(/{{PROJECT_NAME}}/g, projectName)
88
+ .replace(/{{PROJECT_SLUG}}/g, trimmedSlug)
89
+ .replace(/{{FECHA}}/g, currentDate);
90
+
91
+ fs.mkdirSync(path.dirname(resolved.absolutePath), { recursive: true });
92
+ fs.writeFileSync(resolved.absolutePath, renderedText);
93
+
94
+ return checkHandoff(resolved.relativePath, repoRoot);
95
+ }
96
+
97
+ module.exports = {
98
+ REQUIRED_HEADINGS,
99
+ checkHandoff,
100
+ readHandoffSections,
101
+ scaffoldHandoff,
102
+ resolveHandoffPath,
103
+ validateHandoffSections,
104
+ };