create-quiver 0.12.1 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +16 -8
- package/README_FOR_AI.md +11 -6
- package/ROADMAP.md +9 -2
- package/docs/COMMANDS.md.template +9 -2
- package/package.json +2 -1
- package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +2 -2
- package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +5 -5
- package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
- package/src/create-quiver/commands/ai.js +84 -9
- package/src/create-quiver/commands/flow.js +52 -4
- package/src/create-quiver/commands/graph.js +7 -7
- package/src/create-quiver/commands/plan.js +6 -15
- package/src/create-quiver/commands/spec.js +282 -0
- package/src/create-quiver/index.js +83 -21
- package/src/create-quiver/lib/agent-profiles.js +15 -3
- package/src/create-quiver/lib/ai/artifacts.js +318 -0
- package/src/create-quiver/lib/ai/execution-plan.js +9 -0
- package/src/create-quiver/lib/ai/executor.js +3 -2
- package/src/create-quiver/lib/ai/export-state.js +242 -97
- package/src/create-quiver/lib/ai/github.js +80 -3
- package/src/create-quiver/lib/ai/plan-review.js +2 -0
- package/src/create-quiver/lib/ai/spec-generator.js +72 -13
- package/src/create-quiver/lib/ai/spec-templates.js +72 -12
- package/src/create-quiver/lib/analyze.js +2 -2
- package/src/create-quiver/lib/approvals.js +14 -2
- package/src/create-quiver/lib/doctor.js +79 -0
- package/src/create-quiver/lib/git.js +40 -1
- package/src/create-quiver/lib/handoff.js +43 -1
- package/src/create-quiver/lib/init-docs.js +11 -7
- package/src/create-quiver/lib/init-layout.js +1 -0
- package/src/create-quiver/lib/lifecycle.js +52 -3
- package/src/create-quiver/lib/locks.js +134 -0
- package/src/create-quiver/lib/package-safety.js +7 -0
- package/src/create-quiver/lib/paths.js +74 -0
- package/src/create-quiver/lib/project-scan.js +74 -0
- package/src/create-quiver/lib/project-state-resolver.js +236 -0
- package/src/create-quiver/lib/readiness.js +48 -7
- package/src/create-quiver/lib/scope.js +2 -1
- package/src/create-quiver/lib/slice.js +8 -4
- package/src/create-quiver/lib/spec-worktrees.js +121 -38
- package/src/create-quiver/lib/statuses.js +115 -0
|
@@ -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)));
|
|
@@ -206,11 +259,17 @@ function buildSpecGenerationManifest({ inputText, inputPath, repoRoot, specSlug
|
|
|
206
259
|
fallbackTitle: specSlug ? specSlug.replace(/-/g, ' ') : path.basename(inputPath || 'generated-spec.md', path.extname(inputPath || '.md')),
|
|
207
260
|
});
|
|
208
261
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
+
}
|
|
214
273
|
|
|
215
274
|
if (!manifest.slug) {
|
|
216
275
|
throw new Error(formatError('unable to derive a spec slug from the approved input'));
|
|
@@ -515,6 +515,16 @@ function normalizeSliceName(sliceId) {
|
|
|
515
515
|
return String(sliceId || '').trim() || SPEC_FOUNDATION_SLICE_ID;
|
|
516
516
|
}
|
|
517
517
|
|
|
518
|
+
function dependencySliceId(dependency) {
|
|
519
|
+
const value = String(dependency || '').trim();
|
|
520
|
+
if (!value) {
|
|
521
|
+
return '';
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const slashIndex = value.lastIndexOf('/');
|
|
525
|
+
return slashIndex === -1 ? value : value.slice(slashIndex + 1);
|
|
526
|
+
}
|
|
527
|
+
|
|
518
528
|
function buildDefaultImplementationSlice(manifest) {
|
|
519
529
|
const title = `${manifest.title} implementation`;
|
|
520
530
|
return {
|
|
@@ -562,17 +572,11 @@ function buildManifest(source, options = {}) {
|
|
|
562
572
|
const assumptions = Array.isArray(specSource.assumptions) ? specSource.assumptions.slice() : Array.isArray(source.assumptions) ? source.assumptions.slice() : [];
|
|
563
573
|
|
|
564
574
|
const rawSlices = Array.isArray(specSource.slices) ? specSource.slices : Array.isArray(source.slices) ? source.slices : [];
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
objective,
|
|
571
|
-
scope,
|
|
572
|
-
sourcePath,
|
|
573
|
-
ticket,
|
|
574
|
-
title,
|
|
575
|
-
})];
|
|
575
|
+
if (rawSlices.length === 0) {
|
|
576
|
+
throw new Error('approved technical plan must include a structured slices array. Expected JSON: { "spec": { "slices": [{ "slice_id": "slice-01-name", "title": "...", "objective": "...", "files": [] }] } }');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const normalizedSlices = rawSlices.map((slice) => normalizeImplementationSlice(slice, ticket));
|
|
576
580
|
|
|
577
581
|
const slices = [
|
|
578
582
|
{
|
|
@@ -622,6 +626,8 @@ function buildManifest(source, options = {}) {
|
|
|
622
626
|
...normalizedSlices,
|
|
623
627
|
].map((slice, index) => normalizeSliceManifest(slice, index, ticket));
|
|
624
628
|
|
|
629
|
+
validateSliceManifestGraph(slices);
|
|
630
|
+
|
|
625
631
|
const executionOrder = orderSlices(slices);
|
|
626
632
|
const executionGroups = buildExecutionGroups(slices);
|
|
627
633
|
|
|
@@ -661,7 +667,7 @@ function normalizeSliceManifest(slice, index, fallbackTicket) {
|
|
|
661
667
|
const description = String(slice.description || '').trim() || (index === 0
|
|
662
668
|
? 'Document the approved planning input.'
|
|
663
669
|
: `Implement the approved plan captured in the spec source.`);
|
|
664
|
-
const dependsOn = Array.isArray(slice.depends_on) ? slice.depends_on.map(
|
|
670
|
+
const dependsOn = Array.isArray(slice.depends_on) ? slice.depends_on.map(dependencySliceId).filter(Boolean) : [];
|
|
665
671
|
const explicitAllowedWritePaths = normalizeStringArray(slice.allowed_write_paths || slice.allowedWritePaths || slice.write_paths);
|
|
666
672
|
const declaredFiles = normalizeStringArray(slice.files);
|
|
667
673
|
const files = declaredFiles.length > 0 ? declaredFiles : explicitAllowedWritePaths;
|
|
@@ -711,6 +717,60 @@ function normalizeSliceManifest(slice, index, fallbackTicket) {
|
|
|
711
717
|
};
|
|
712
718
|
}
|
|
713
719
|
|
|
720
|
+
function validateSliceManifestGraph(slices) {
|
|
721
|
+
const ids = new Set();
|
|
722
|
+
|
|
723
|
+
for (const slice of slices) {
|
|
724
|
+
if (!String(slice.slice_id || '').startsWith('slice-')) {
|
|
725
|
+
throw new Error(`invalid slice_id '${slice.slice_id}'. Slice ids must start with 'slice-'.`);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (ids.has(slice.slice_id)) {
|
|
729
|
+
throw new Error(`duplicate slice_id '${slice.slice_id}' in approved technical plan.`);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
ids.add(slice.slice_id);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
for (const slice of slices) {
|
|
736
|
+
for (const dependency of slice.depends_on || []) {
|
|
737
|
+
if (!ids.has(dependency)) {
|
|
738
|
+
throw new Error(`slice '${slice.slice_id}' depends on missing slice '${dependency}'.`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const visiting = new Set();
|
|
744
|
+
const visited = new Set();
|
|
745
|
+
const stack = [];
|
|
746
|
+
|
|
747
|
+
function visit(sliceId) {
|
|
748
|
+
if (visited.has(sliceId)) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
if (visiting.has(sliceId)) {
|
|
753
|
+
const start = stack.indexOf(sliceId);
|
|
754
|
+
const cycle = start === -1 ? [sliceId] : [...stack.slice(start), sliceId];
|
|
755
|
+
throw new Error(`approved technical plan contains a dependency cycle: ${cycle.join(' -> ')}.`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
visiting.add(sliceId);
|
|
759
|
+
stack.push(sliceId);
|
|
760
|
+
const slice = slices.find((item) => item.slice_id === sliceId);
|
|
761
|
+
for (const dependency of slice?.depends_on || []) {
|
|
762
|
+
visit(dependency);
|
|
763
|
+
}
|
|
764
|
+
stack.pop();
|
|
765
|
+
visiting.delete(sliceId);
|
|
766
|
+
visited.add(sliceId);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
for (const slice of slices) {
|
|
770
|
+
visit(slice.slice_id);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
714
774
|
function orderSlices(slices) {
|
|
715
775
|
const ordered = [];
|
|
716
776
|
const remaining = new Map(slices.map((slice) => [slice.slice_id, slice]));
|
|
@@ -203,6 +203,9 @@ function writeApprovalArtifacts(projectRoot, phase, kind, sourceFile, contents,
|
|
|
203
203
|
};
|
|
204
204
|
let finalContents = `${contents}`;
|
|
205
205
|
let version = null;
|
|
206
|
+
let rawArtifactPath = options.rawArtifactPath || null;
|
|
207
|
+
let outputSource = options.outputSource || null;
|
|
208
|
+
let inputCompaction = options.inputCompaction || null;
|
|
206
209
|
|
|
207
210
|
if (kind === 'approved' && !options.version) {
|
|
208
211
|
throw new Error(formatError(`${normalizedPhase} approval requires a concrete draft version. Use --version <n>.`));
|
|
@@ -224,6 +227,9 @@ function writeApprovalArtifacts(projectRoot, phase, kind, sourceFile, contents,
|
|
|
224
227
|
finalContents = fs.readFileSync(draftPath, 'utf8');
|
|
225
228
|
sourceFile = selectedDraft.path;
|
|
226
229
|
version = Number(selectedDraft.version);
|
|
230
|
+
rawArtifactPath = rawArtifactPath || selectedDraft.raw_artifact_path || null;
|
|
231
|
+
outputSource = outputSource || selectedDraft.output_source || null;
|
|
232
|
+
inputCompaction = inputCompaction || selectedDraft.input_compaction || null;
|
|
227
233
|
}
|
|
228
234
|
|
|
229
235
|
if (kind === 'draft') {
|
|
@@ -238,6 +244,9 @@ function writeApprovalArtifacts(projectRoot, phase, kind, sourceFile, contents,
|
|
|
238
244
|
source_file: toRelativePosix(projectRoot, path.resolve(projectRoot, sourceFile)),
|
|
239
245
|
path: toRelativePosix(projectRoot, versionPath),
|
|
240
246
|
created_at: now,
|
|
247
|
+
raw_artifact_path: rawArtifactPath,
|
|
248
|
+
output_source: outputSource,
|
|
249
|
+
input_compaction: inputCompaction,
|
|
241
250
|
});
|
|
242
251
|
}
|
|
243
252
|
|
|
@@ -249,6 +258,9 @@ function writeApprovalArtifacts(projectRoot, phase, kind, sourceFile, contents,
|
|
|
249
258
|
path: toRelativePosix(projectRoot, filePath),
|
|
250
259
|
version,
|
|
251
260
|
created_at: now,
|
|
261
|
+
raw_artifact_path: rawArtifactPath,
|
|
262
|
+
output_source: outputSource,
|
|
263
|
+
input_compaction: inputCompaction,
|
|
252
264
|
...(kind === 'approved' ? { approved_at: now } : {}),
|
|
253
265
|
};
|
|
254
266
|
|
|
@@ -270,8 +282,8 @@ function writeApprovalArtifacts(projectRoot, phase, kind, sourceFile, contents,
|
|
|
270
282
|
};
|
|
271
283
|
}
|
|
272
284
|
|
|
273
|
-
function savePlannerDraft(projectRoot, phase, sourceFile, contents) {
|
|
274
|
-
return writeApprovalArtifacts(projectRoot, phase, 'draft', sourceFile, contents);
|
|
285
|
+
function savePlannerDraft(projectRoot, phase, sourceFile, contents, options = {}) {
|
|
286
|
+
return writeApprovalArtifacts(projectRoot, phase, 'draft', sourceFile, contents, options);
|
|
275
287
|
}
|
|
276
288
|
|
|
277
289
|
function approvePlannerPhase(projectRoot, phase, sourceFile, contents, options = {}) {
|
|
@@ -331,6 +331,7 @@ function collectLayoutReport(projectRoot) {
|
|
|
331
331
|
const hasStateMetadata = hasInitializedStateMetadata(readState(projectRoot));
|
|
332
332
|
const realSlices = readAllSlices(projectRoot);
|
|
333
333
|
const specSlugs = Array.from(new Set(realSlices.map((slice) => slice.specSlug))).sort((left, right) => left.localeCompare(right));
|
|
334
|
+
const exampleTarget = selectDoctorExampleTarget(realSlices, specSlugs);
|
|
334
335
|
const newLayoutFiles = collectPresentPaths(projectRoot, NEW_LAYOUT_REQUIRED_PATHS);
|
|
335
336
|
const missingNewLayoutFiles = collectMissingPaths(projectRoot, NEW_LAYOUT_REQUIRED_PATHS);
|
|
336
337
|
const legacySignals = collectPresentPaths(projectRoot, LEGACY_LAYOUT_PROBES);
|
|
@@ -379,7 +380,14 @@ function collectLayoutReport(projectRoot) {
|
|
|
379
380
|
}
|
|
380
381
|
}
|
|
381
382
|
|
|
383
|
+
if (exampleTarget.source === 'generic-multiple-specs') {
|
|
384
|
+
recommendations.push('Multiple specs were found and no active slice is obvious. Doctor examples use placeholders so they do not point to the wrong spec.');
|
|
385
|
+
} else if (exampleTarget.source === 'active-slice') {
|
|
386
|
+
recommendations.push(`Doctor examples target the active slice candidate ${exampleTarget.specSlug}/${exampleTarget.sliceId} (${exampleTarget.status}).`);
|
|
387
|
+
}
|
|
388
|
+
|
|
382
389
|
return {
|
|
390
|
+
exampleTarget,
|
|
383
391
|
hasLegacyLayout,
|
|
384
392
|
hasNewLayout,
|
|
385
393
|
hasStateMetadata,
|
|
@@ -393,6 +401,76 @@ function collectLayoutReport(projectRoot) {
|
|
|
393
401
|
};
|
|
394
402
|
}
|
|
395
403
|
|
|
404
|
+
function normalizeStatus(value) {
|
|
405
|
+
return String(value || '').trim().toLowerCase().replace(/_/g, '-');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function statusRank(status) {
|
|
409
|
+
const normalized = normalizeStatus(status);
|
|
410
|
+
const ranks = new Map([
|
|
411
|
+
['in-progress', 0],
|
|
412
|
+
['review', 1],
|
|
413
|
+
['ready', 2],
|
|
414
|
+
['planned', 3],
|
|
415
|
+
['approved', 4],
|
|
416
|
+
['blocked', 5],
|
|
417
|
+
['draft', 6],
|
|
418
|
+
['completed', 99],
|
|
419
|
+
['done', 99],
|
|
420
|
+
]);
|
|
421
|
+
|
|
422
|
+
return ranks.has(normalized) ? ranks.get(normalized) : 20;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function selectDoctorExampleTarget(realSlices, specSlugs) {
|
|
426
|
+
if (!Array.isArray(specSlugs) || specSlugs.length === 0) {
|
|
427
|
+
return {
|
|
428
|
+
sliceId: '<slice-id>',
|
|
429
|
+
source: 'no-specs',
|
|
430
|
+
specSlug: '<spec-slug>',
|
|
431
|
+
status: '',
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const rankedSlices = (Array.isArray(realSlices) ? realSlices : [])
|
|
436
|
+
.filter((slice) => specSlugs.includes(slice.specSlug))
|
|
437
|
+
.slice()
|
|
438
|
+
.sort((left, right) => {
|
|
439
|
+
const rankDelta = statusRank(left.status) - statusRank(right.status);
|
|
440
|
+
if (rankDelta !== 0) {
|
|
441
|
+
return rankDelta;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return left.ref.localeCompare(right.ref);
|
|
445
|
+
});
|
|
446
|
+
const activeSlice = rankedSlices.find((slice) => statusRank(slice.status) < 99);
|
|
447
|
+
|
|
448
|
+
if (activeSlice) {
|
|
449
|
+
return {
|
|
450
|
+
sliceId: activeSlice.sliceId,
|
|
451
|
+
source: 'active-slice',
|
|
452
|
+
specSlug: activeSlice.specSlug,
|
|
453
|
+
status: activeSlice.status || '',
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (specSlugs.length === 1) {
|
|
458
|
+
return {
|
|
459
|
+
sliceId: '<slice-id>',
|
|
460
|
+
source: 'single-spec',
|
|
461
|
+
specSlug: specSlugs[0],
|
|
462
|
+
status: '',
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
sliceId: '<slice-id>',
|
|
468
|
+
source: 'generic-multiple-specs',
|
|
469
|
+
specSlug: '<spec-slug>',
|
|
470
|
+
status: '',
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
396
474
|
function buildDoctorFixPlan(projectRoot) {
|
|
397
475
|
const fixes = [];
|
|
398
476
|
const rootGitignorePath = path.join(projectRoot, '.gitignore');
|
|
@@ -632,4 +710,5 @@ module.exports = {
|
|
|
632
710
|
collectDoctorWarnings,
|
|
633
711
|
collectLayoutReport,
|
|
634
712
|
formatDoctorFixPlan,
|
|
713
|
+
selectDoctorExampleTarget,
|
|
635
714
|
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const cp = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
2
4
|
|
|
3
5
|
function runGit(args, cwd, options = {}) {
|
|
4
6
|
return cp.execFileSync('git', args, {
|
|
@@ -98,6 +100,9 @@ function currentBranch(repoRoot) {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
function statusPorcelain(repoRoot) {
|
|
103
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) {
|
|
104
|
+
return '__MISSING_WORKTREE__';
|
|
105
|
+
}
|
|
101
106
|
return tryGit(['status', '--porcelain'], repoRoot);
|
|
102
107
|
}
|
|
103
108
|
|
|
@@ -111,7 +116,37 @@ function hasRemote(repoRoot, remoteName = 'origin') {
|
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
function isCleanWorktree(repoRoot) {
|
|
114
|
-
return statusPorcelain(repoRoot) === '';
|
|
119
|
+
return Boolean(repoRoot && fs.existsSync(repoRoot) && isGitWorktree(repoRoot) && statusPorcelain(repoRoot) === '');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isGitWorktree(repoRoot) {
|
|
123
|
+
return tryGit(['rev-parse', '--is-inside-work-tree'], repoRoot) === 'true';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function absoluteGitDir(repoRoot) {
|
|
127
|
+
return tryGit(['rev-parse', '--absolute-git-dir'], repoRoot);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function gitCommonDir(repoRoot) {
|
|
131
|
+
const value = tryGit(['rev-parse', '--git-common-dir'], repoRoot);
|
|
132
|
+
if (!value) {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
return path.isAbsolute(value) ? path.resolve(value) : path.resolve(repoRoot, value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function realpathOrResolve(value) {
|
|
139
|
+
try {
|
|
140
|
+
return fs.realpathSync(value);
|
|
141
|
+
} catch {
|
|
142
|
+
return path.resolve(value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isLinkedWorktree(repoRoot) {
|
|
147
|
+
const gitDir = absoluteGitDir(repoRoot);
|
|
148
|
+
const commonDir = gitCommonDir(repoRoot);
|
|
149
|
+
return Boolean(gitDir && commonDir && realpathOrResolve(gitDir) !== realpathOrResolve(commonDir));
|
|
115
150
|
}
|
|
116
151
|
|
|
117
152
|
function isDetachedHead(repoRoot) {
|
|
@@ -161,8 +196,12 @@ module.exports = {
|
|
|
161
196
|
lsRemoteHeads,
|
|
162
197
|
mergeBaseIsAncestor,
|
|
163
198
|
hasRemote,
|
|
199
|
+
absoluteGitDir,
|
|
200
|
+
gitCommonDir,
|
|
164
201
|
isCleanWorktree,
|
|
165
202
|
isDetachedHead,
|
|
203
|
+
isGitWorktree,
|
|
204
|
+
isLinkedWorktree,
|
|
166
205
|
revListCount,
|
|
167
206
|
remoteList,
|
|
168
207
|
runGit,
|
|
@@ -113,6 +113,46 @@ function validateBriefSections(text, kind) {
|
|
|
113
113
|
.map((group) => group.label);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function headingGroupsForKind(kind) {
|
|
117
|
+
if (kind === 'handoff') {
|
|
118
|
+
return REQUIRED_HEADINGS.map((heading) => ({
|
|
119
|
+
label: heading.replace(/^##\s+/, '').toLowerCase(),
|
|
120
|
+
alternatives: [heading],
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
return kind === 'closure-brief' ? CLOSURE_BRIEF_REQUIRED_HEADINGS : EXECUTION_BRIEF_REQUIRED_HEADINGS;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatAliasGuidance(kind) {
|
|
127
|
+
return headingGroupsForKind(kind)
|
|
128
|
+
.map((group) => `- ${group.label}: ${group.alternatives.join(' | ')}`)
|
|
129
|
+
.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function canonicalHeadingForGroup(group) {
|
|
133
|
+
return group.alternatives[0] || `## ${group.label}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatMinimalTemplate(kind) {
|
|
137
|
+
const lines = [];
|
|
138
|
+
for (const group of headingGroupsForKind(kind)) {
|
|
139
|
+
lines.push(canonicalHeadingForGroup(group), '', 'TODO', '');
|
|
140
|
+
}
|
|
141
|
+
return lines.join('\n').trimEnd();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatMissingSectionsError(resolved, missingSections) {
|
|
145
|
+
return [
|
|
146
|
+
`create-quiver: ${resolved.label.toLowerCase()} is missing required sections: ${missingSections.join(', ')}`,
|
|
147
|
+
'',
|
|
148
|
+
'Accepted headings/aliases:',
|
|
149
|
+
formatAliasGuidance(resolved.kind),
|
|
150
|
+
'',
|
|
151
|
+
'Minimal template:',
|
|
152
|
+
formatMinimalTemplate(resolved.kind),
|
|
153
|
+
].join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
116
156
|
function checkHandoff(handoffInput, repoRoot = process.cwd()) {
|
|
117
157
|
const resolved = resolveHandoffPath(repoRoot, handoffInput);
|
|
118
158
|
|
|
@@ -125,7 +165,7 @@ function checkHandoff(handoffInput, repoRoot = process.cwd()) {
|
|
|
125
165
|
? validateHandoffSections(text)
|
|
126
166
|
: validateBriefSections(text, resolved.kind);
|
|
127
167
|
if (missingSections.length > 0) {
|
|
128
|
-
throw new Error(
|
|
168
|
+
throw new Error(formatMissingSectionsError(resolved, missingSections));
|
|
129
169
|
}
|
|
130
170
|
|
|
131
171
|
return resolved;
|
|
@@ -165,6 +205,8 @@ module.exports = {
|
|
|
165
205
|
EXECUTION_BRIEF_REQUIRED_HEADINGS,
|
|
166
206
|
REQUIRED_HEADINGS,
|
|
167
207
|
checkHandoff,
|
|
208
|
+
formatAliasGuidance,
|
|
209
|
+
formatMinimalTemplate,
|
|
168
210
|
readHandoffSections,
|
|
169
211
|
scaffoldHandoff,
|
|
170
212
|
resolveHandoffPath,
|
|
@@ -1092,6 +1092,15 @@ function installSelfAsDevDep(projectRoot, version) {
|
|
|
1092
1092
|
return 'skipped-already-present';
|
|
1093
1093
|
}
|
|
1094
1094
|
|
|
1095
|
+
try {
|
|
1096
|
+
execSync(formatInstallSelfCommand(projectRoot, version), { cwd: projectRoot, stdio: 'inherit' });
|
|
1097
|
+
return 'installed';
|
|
1098
|
+
} catch {
|
|
1099
|
+
return 'failed';
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function formatInstallSelfCommand(projectRoot, version) {
|
|
1095
1104
|
const pm = detectPackageManager(projectRoot);
|
|
1096
1105
|
const commands = {
|
|
1097
1106
|
npm: `npm install -D create-quiver@${version}`,
|
|
@@ -1099,13 +1108,7 @@ function installSelfAsDevDep(projectRoot, version) {
|
|
|
1099
1108
|
pnpm: `pnpm add -D create-quiver@${version}`,
|
|
1100
1109
|
bun: `bun add -d create-quiver@${version}`,
|
|
1101
1110
|
};
|
|
1102
|
-
|
|
1103
|
-
try {
|
|
1104
|
-
execSync(commands[pm], { cwd: projectRoot, stdio: 'inherit' });
|
|
1105
|
-
return 'installed';
|
|
1106
|
-
} catch {
|
|
1107
|
-
return 'failed';
|
|
1108
|
-
}
|
|
1111
|
+
return commands[pm] || commands.npm;
|
|
1109
1112
|
}
|
|
1110
1113
|
|
|
1111
1114
|
function normalizeSkippedReason(reason) {
|
|
@@ -1253,5 +1256,6 @@ module.exports = {
|
|
|
1253
1256
|
writeFrontMatter,
|
|
1254
1257
|
toProjectSlug,
|
|
1255
1258
|
detectPackageManager,
|
|
1259
|
+
formatInstallSelfCommand,
|
|
1256
1260
|
installSelfAsDevDep,
|
|
1257
1261
|
};
|
|
@@ -209,6 +209,7 @@ function resolveInitPackageScripts(profile, options = {}) {
|
|
|
209
209
|
'quiver:spec:create': 'npx create-quiver spec create',
|
|
210
210
|
'quiver:spec:start': 'npx create-quiver spec start',
|
|
211
211
|
'quiver:spec:status': 'npx create-quiver spec status',
|
|
212
|
+
'quiver:spec:validate': 'npx create-quiver spec validate',
|
|
212
213
|
'quiver:spec:close': 'npx create-quiver spec close',
|
|
213
214
|
'quiver:start-slice': 'npx create-quiver start-slice',
|
|
214
215
|
'quiver:check-slice': 'npx create-quiver check-slice',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { branchDelete, catFileExists, currentBranch, fetchBranch, fetchRemote, hasLocalBranch, hasRemoteBranch, lsRemoteHeads, mergeBaseIsAncestor, revListCount, runGit, statusPorcelain, worktreeAdd, worktreeList, worktreePrune, worktreeRemove } = require('./git');
|
|
3
|
+
const { branchDelete, catFileExists, currentBranch, fetchBranch, fetchRemote, hasLocalBranch, hasRemoteBranch, isGitWorktree, isLinkedWorktree, lsRemoteHeads, mergeBaseIsAncestor, revListCount, runGit, statusPorcelain, worktreeAdd, worktreeList, worktreePrune, worktreeRemove } = require('./git');
|
|
4
4
|
const { parseJsonWithComments } = require('./json');
|
|
5
5
|
const { writeFrontMatter } = require('./init-docs');
|
|
6
|
+
const { withLockSync } = require('./locks');
|
|
6
7
|
const { relativePosixPath, resolveTargetRoot } = require('./paths');
|
|
7
8
|
const { ensureSpecSliceZeroComplete } = require('./spec-worktrees');
|
|
8
9
|
const { activeSlicePath, renderActiveSlice, resolveSliceContext, safeBranchName, toAlias, validateSliceMetaForStart, worktreesRootForRepo } = require('./slice');
|
|
@@ -295,6 +296,35 @@ function findExistingWorktreeForBranch(repoRoot, branchName) {
|
|
|
295
296
|
return '';
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
function sameRealPath(left, right) {
|
|
300
|
+
try {
|
|
301
|
+
return fs.realpathSync(left) === fs.realpathSync(right);
|
|
302
|
+
} catch {
|
|
303
|
+
return path.resolve(left) === path.resolve(right);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function formatMissingSliceWorktree(branchName, worktreePath) {
|
|
308
|
+
return [
|
|
309
|
+
`create-quiver: registered slice worktree is missing or stale for ${branchName}: ${worktreePath}`,
|
|
310
|
+
'Recovery:',
|
|
311
|
+
'- Run `git worktree prune` from the main checkout, then retry the slice command.',
|
|
312
|
+
'- If the directory was moved manually, restore it or remove the stale git worktree registration intentionally.',
|
|
313
|
+
'- Do not create a nested replacement worktree from inside another worktree.',
|
|
314
|
+
].join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatNestedSliceWorktree(branchName, existingWorktreePath = '') {
|
|
318
|
+
return [
|
|
319
|
+
`create-quiver: refusing to create a slice worktree from inside a linked worktree for ${branchName}.`,
|
|
320
|
+
'Recovery:',
|
|
321
|
+
existingWorktreePath
|
|
322
|
+
? `- Use the existing worktree: ${existingWorktreePath}`
|
|
323
|
+
: '- Return to the main checkout and rerun the command.',
|
|
324
|
+
'- This prevents nested .worktrees paths and conflicting slice worktrees.',
|
|
325
|
+
].join('\n');
|
|
326
|
+
}
|
|
327
|
+
|
|
298
328
|
function startSlice(sliceInput, options = {}) {
|
|
299
329
|
const allowDraft = options.allowDraft === true || process.env.ALLOW_DRAFT_SLICE === '1';
|
|
300
330
|
const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
|
|
@@ -323,13 +353,25 @@ function startSlice(sliceInput, options = {}) {
|
|
|
323
353
|
console.log('WARN: bootstrap intencional para un slice en draft.');
|
|
324
354
|
}
|
|
325
355
|
|
|
356
|
+
return withLockSync(repoRoot, `slice-worktree-${slice.branchName}`, {
|
|
357
|
+
command: 'start-slice',
|
|
358
|
+
metadata: {
|
|
359
|
+
branch: slice.branchName,
|
|
360
|
+
slice: slice.sliceRel,
|
|
361
|
+
},
|
|
362
|
+
}, () => {
|
|
326
363
|
const worktreesRoot = worktreesRootForRepo(repoRoot, slice.branchName);
|
|
327
364
|
const worktreePath = path.join(worktreesRoot, safeBranchName(slice.branchName));
|
|
328
365
|
const existingWorktreePath = findExistingWorktreeForBranch(repoRoot, slice.branchName);
|
|
329
366
|
|
|
330
|
-
|
|
367
|
+
if (existingWorktreePath && (!fs.existsSync(existingWorktreePath) || !isGitWorktree(existingWorktreePath))) {
|
|
368
|
+
throw new Error(formatMissingSliceWorktree(slice.branchName, existingWorktreePath));
|
|
369
|
+
}
|
|
331
370
|
|
|
332
|
-
if (existingWorktreePath
|
|
371
|
+
if (existingWorktreePath) {
|
|
372
|
+
if (isLinkedWorktree(repoRoot) && !sameRealPath(repoRoot, existingWorktreePath)) {
|
|
373
|
+
throw new Error(formatNestedSliceWorktree(slice.branchName, existingWorktreePath));
|
|
374
|
+
}
|
|
333
375
|
writeWorktreeContext(existingWorktreePath, slice, slice.branchName);
|
|
334
376
|
const activeSlice = writeActiveSlice(repoRoot, slice);
|
|
335
377
|
if (activeSlice.replaced) {
|
|
@@ -349,6 +391,12 @@ function startSlice(sliceInput, options = {}) {
|
|
|
349
391
|
return { worktreePath: existingWorktreePath, reused: true };
|
|
350
392
|
}
|
|
351
393
|
|
|
394
|
+
if (isLinkedWorktree(repoRoot)) {
|
|
395
|
+
throw new Error(formatNestedSliceWorktree(slice.branchName));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
worktreePrune(repoRoot);
|
|
399
|
+
|
|
352
400
|
if (fs.existsSync(worktreePath) && !fs.existsSync(path.join(worktreePath, '.git'))) {
|
|
353
401
|
throw new Error(`create-quiver: la ruta '${worktreePath}' ya existe y no parece un worktree git.`);
|
|
354
402
|
}
|
|
@@ -395,6 +443,7 @@ function startSlice(sliceInput, options = {}) {
|
|
|
395
443
|
console.log(`Worktree: ${worktreePath}`);
|
|
396
444
|
console.log(`Contexto: ${worktreePath}/WORKTREE_CONTEXT.md`);
|
|
397
445
|
return { worktreePath, reused: false };
|
|
446
|
+
});
|
|
398
447
|
}
|
|
399
448
|
|
|
400
449
|
function cleanupSlice(sliceInput, options = {}) {
|