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.
Files changed (79) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +16 -8
  3. package/README_FOR_AI.md +11 -6
  4. package/ROADMAP.md +9 -2
  5. package/docs/COMMANDS.md.template +9 -2
  6. package/package.json +2 -1
  7. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +2 -2
  8. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +5 -5
  9. package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
  10. package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
  11. package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
  12. package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
  13. package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
  14. package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
  15. package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
  16. package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
  17. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
  18. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
  19. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
  20. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
  21. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
  22. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
  23. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
  24. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
  25. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
  26. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
  27. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
  28. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
  29. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
  30. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
  31. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
  32. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
  33. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
  34. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
  35. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
  36. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
  37. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
  38. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
  39. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
  40. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
  41. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
  42. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
  43. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
  44. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
  45. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  46. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
  47. package/src/create-quiver/commands/ai.js +84 -9
  48. package/src/create-quiver/commands/flow.js +52 -4
  49. package/src/create-quiver/commands/graph.js +7 -7
  50. package/src/create-quiver/commands/plan.js +6 -15
  51. package/src/create-quiver/commands/spec.js +282 -0
  52. package/src/create-quiver/index.js +83 -21
  53. package/src/create-quiver/lib/agent-profiles.js +15 -3
  54. package/src/create-quiver/lib/ai/artifacts.js +318 -0
  55. package/src/create-quiver/lib/ai/execution-plan.js +9 -0
  56. package/src/create-quiver/lib/ai/executor.js +3 -2
  57. package/src/create-quiver/lib/ai/export-state.js +242 -97
  58. package/src/create-quiver/lib/ai/github.js +80 -3
  59. package/src/create-quiver/lib/ai/plan-review.js +2 -0
  60. package/src/create-quiver/lib/ai/spec-generator.js +72 -13
  61. package/src/create-quiver/lib/ai/spec-templates.js +72 -12
  62. package/src/create-quiver/lib/analyze.js +2 -2
  63. package/src/create-quiver/lib/approvals.js +14 -2
  64. package/src/create-quiver/lib/doctor.js +79 -0
  65. package/src/create-quiver/lib/git.js +40 -1
  66. package/src/create-quiver/lib/handoff.js +43 -1
  67. package/src/create-quiver/lib/init-docs.js +11 -7
  68. package/src/create-quiver/lib/init-layout.js +1 -0
  69. package/src/create-quiver/lib/lifecycle.js +52 -3
  70. package/src/create-quiver/lib/locks.js +134 -0
  71. package/src/create-quiver/lib/package-safety.js +7 -0
  72. package/src/create-quiver/lib/paths.js +74 -0
  73. package/src/create-quiver/lib/project-scan.js +74 -0
  74. package/src/create-quiver/lib/project-state-resolver.js +236 -0
  75. package/src/create-quiver/lib/readiness.js +48 -7
  76. package/src/create-quiver/lib/scope.js +2 -1
  77. package/src/create-quiver/lib/slice.js +8 -4
  78. package/src/create-quiver/lib/spec-worktrees.js +121 -38
  79. 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
- const manifest = buildManifest({
210
- ...source,
211
- sourcePath: path.relative(repoRoot, path.resolve(repoRoot, inputPath)).split(path.sep).join('/'),
212
- sourceText,
213
- }, { 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
+ }
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
- const normalizedSlices = rawSlices.length > 0
566
- ? rawSlices.map((slice) => normalizeImplementationSlice(slice, ticket))
567
- : [buildDefaultImplementationSlice({
568
- acceptance,
569
- assumptions,
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((dep) => String(dep).trim()).filter(Boolean) : [];
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]));
@@ -1,7 +1,7 @@
1
1
  const { runAnalyze } = require('../index');
2
2
 
3
- function analyzeProject(targetDir) {
4
- return runAnalyze(targetDir);
3
+ function analyzeProject(targetDir, options = {}) {
4
+ return runAnalyze(targetDir, options);
5
5
  }
6
6
 
7
7
  module.exports = {
@@ -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(`create-quiver: ${resolved.label.toLowerCase()} is missing required sections: ${missingSections.join(', ')}`);
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
- worktreePrune(repoRoot);
367
+ if (existingWorktreePath && (!fs.existsSync(existingWorktreePath) || !isGitWorktree(existingWorktreePath))) {
368
+ throw new Error(formatMissingSliceWorktree(slice.branchName, existingWorktreePath));
369
+ }
331
370
 
332
- if (existingWorktreePath && fs.existsSync(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 = {}) {