create-quiver 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +65 -25
  3. package/README_FOR_AI.md +36 -29
  4. package/ROADMAP.md +22 -3
  5. package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
  6. package/docs/COMMANDS.md.template +53 -20
  7. package/docs/STATUS.md.template +5 -1
  8. package/docs/WORKFLOW.md.template +13 -11
  9. package/package.json +10 -3
  10. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EVIDENCE_REPORT.md +293 -0
  11. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EXECUTION_PLAN.md +58 -0
  12. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/SPEC.md +242 -0
  13. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/STATUS.md +35 -0
  14. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/pr.md +77 -0
  15. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +34 -0
  16. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
  17. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/slice.json +52 -0
  18. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/CLOSURE_BRIEF.md +36 -0
  19. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/EXECUTION_BRIEF.md +52 -0
  20. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/slice.json +56 -0
  21. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/CLOSURE_BRIEF.md +43 -0
  22. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/EXECUTION_BRIEF.md +54 -0
  23. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/slice.json +52 -0
  24. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/CLOSURE_BRIEF.md +35 -0
  25. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/EXECUTION_BRIEF.md +53 -0
  26. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/slice.json +54 -0
  27. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/CLOSURE_BRIEF.md +34 -0
  28. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/EXECUTION_BRIEF.md +54 -0
  29. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/slice.json +52 -0
  30. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/CLOSURE_BRIEF.md +34 -0
  31. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/EXECUTION_BRIEF.md +54 -0
  32. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/slice.json +53 -0
  33. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/CLOSURE_BRIEF.md +33 -0
  34. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/EXECUTION_BRIEF.md +56 -0
  35. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/slice.json +55 -0
  36. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/CLOSURE_BRIEF.md +33 -0
  37. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/EXECUTION_BRIEF.md +54 -0
  38. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/slice.json +52 -0
  39. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/CLOSURE_BRIEF.md +39 -0
  40. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/EXECUTION_BRIEF.md +56 -0
  41. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/slice.json +53 -0
  42. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/CLOSURE_BRIEF.md +38 -0
  43. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/EXECUTION_BRIEF.md +57 -0
  44. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/slice.json +52 -0
  45. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/CLOSURE_BRIEF.md +39 -0
  46. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/EXECUTION_BRIEF.md +55 -0
  47. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/slice.json +56 -0
  48. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/CLOSURE_BRIEF.md +36 -0
  49. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/EXECUTION_BRIEF.md +54 -0
  50. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/slice.json +53 -0
  51. package/specs/quiver-v26-0121-smoke-hardening/EVIDENCE_REPORT.md +208 -0
  52. package/specs/quiver-v26-0121-smoke-hardening/EXECUTION_PLAN.md +57 -0
  53. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +137 -0
  54. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +32 -0
  55. package/specs/quiver-v26-0121-smoke-hardening/pr.md +96 -0
  56. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/CLOSURE_BRIEF.md +35 -0
  57. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/EXECUTION_BRIEF.md +55 -0
  58. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/slice.json +73 -0
  59. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/CLOSURE_BRIEF.md +38 -0
  60. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/EXECUTION_BRIEF.md +51 -0
  61. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/slice.json +76 -0
  62. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/CLOSURE_BRIEF.md +37 -0
  63. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/EXECUTION_BRIEF.md +52 -0
  64. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/slice.json +75 -0
  65. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/CLOSURE_BRIEF.md +37 -0
  66. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/EXECUTION_BRIEF.md +53 -0
  67. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/slice.json +77 -0
  68. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/CLOSURE_BRIEF.md +35 -0
  69. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/EXECUTION_BRIEF.md +52 -0
  70. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/slice.json +77 -0
  71. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/CLOSURE_BRIEF.md +34 -0
  72. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/EXECUTION_BRIEF.md +54 -0
  73. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/slice.json +84 -0
  74. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/CLOSURE_BRIEF.md +35 -0
  75. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/EXECUTION_BRIEF.md +53 -0
  76. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/slice.json +82 -0
  77. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/CLOSURE_BRIEF.md +35 -0
  78. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/EXECUTION_BRIEF.md +55 -0
  79. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/slice.json +92 -0
  80. package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
  81. package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
  82. package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
  83. package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
  84. package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
  85. package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
  86. package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
  87. package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
  88. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
  89. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
  90. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
  91. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
  92. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
  93. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
  94. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
  95. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
  96. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
  97. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
  98. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
  99. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
  100. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
  101. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
  102. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
  103. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
  104. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
  105. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
  106. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
  107. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
  108. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
  109. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
  110. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
  111. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
  112. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
  113. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
  114. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
  115. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
  116. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  117. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
  118. package/src/create-quiver/commands/ai.js +652 -27
  119. package/src/create-quiver/commands/flow.js +58 -9
  120. package/src/create-quiver/commands/graph.js +11 -9
  121. package/src/create-quiver/commands/plan.js +7 -16
  122. package/src/create-quiver/commands/spec.js +282 -0
  123. package/src/create-quiver/index.js +409 -31
  124. package/src/create-quiver/lib/actionable-error.js +27 -0
  125. package/src/create-quiver/lib/agent-profiles.js +16 -4
  126. package/src/create-quiver/lib/ai/artifacts.js +318 -0
  127. package/src/create-quiver/lib/ai/context-packs.js +4 -0
  128. package/src/create-quiver/lib/ai/execution-plan.js +16 -1
  129. package/src/create-quiver/lib/ai/executor.js +272 -21
  130. package/src/create-quiver/lib/ai/export-state.js +679 -0
  131. package/src/create-quiver/lib/ai/github.js +162 -2
  132. package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
  133. package/src/create-quiver/lib/ai/plan-review.js +7 -2
  134. package/src/create-quiver/lib/ai/providers.js +4 -3
  135. package/src/create-quiver/lib/ai/run-state.js +414 -0
  136. package/src/create-quiver/lib/ai/spec-generator.js +84 -13
  137. package/src/create-quiver/lib/ai/spec-templates.js +150 -21
  138. package/src/create-quiver/lib/analyze.js +2 -2
  139. package/src/create-quiver/lib/approvals.js +36 -5
  140. package/src/create-quiver/lib/demo.js +189 -14
  141. package/src/create-quiver/lib/doctor.js +154 -0
  142. package/src/create-quiver/lib/git.js +40 -1
  143. package/src/create-quiver/lib/handoff.js +123 -12
  144. package/src/create-quiver/lib/init-docs.js +35 -13
  145. package/src/create-quiver/lib/init-layout.js +9 -0
  146. package/src/create-quiver/lib/json.js +53 -3
  147. package/src/create-quiver/lib/lifecycle.js +52 -3
  148. package/src/create-quiver/lib/locks.js +134 -0
  149. package/src/create-quiver/lib/package-safety.js +7 -0
  150. package/src/create-quiver/lib/paths.js +74 -0
  151. package/src/create-quiver/lib/project-scan.js +74 -0
  152. package/src/create-quiver/lib/project-state-resolver.js +236 -0
  153. package/src/create-quiver/lib/readiness.js +66 -10
  154. package/src/create-quiver/lib/scope.js +52 -8
  155. package/src/create-quiver/lib/slice-graph.js +138 -38
  156. package/src/create-quiver/lib/slice.js +14 -5
  157. package/src/create-quiver/lib/spec-worktrees.js +129 -32
  158. package/src/create-quiver/lib/statuses.js +115 -0
@@ -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 = {}) {
@@ -0,0 +1,134 @@
1
+ const fs = require('node:fs');
2
+ const os = require('node:os');
3
+ const path = require('node:path');
4
+ const { execFileSync } = require('node:child_process');
5
+
6
+ const { quiverInternalPaths } = require('./init-layout');
7
+
8
+ function formatError(message) {
9
+ return `create-quiver: ${message}`;
10
+ }
11
+
12
+ function toRelativePosix(root, filePath) {
13
+ return path.relative(root, filePath).split(path.sep).join('/');
14
+ }
15
+
16
+ function sanitizeLockName(value) {
17
+ return String(value || '')
18
+ .trim()
19
+ .replace(/[^a-zA-Z0-9._-]+/g, '-')
20
+ .replace(/^-+|-+$/g, '') || 'operation';
21
+ }
22
+
23
+ function lockPath(projectRoot, lockName) {
24
+ return path.join(quiverInternalPaths(projectRoot).locksDir, `${sanitizeLockName(lockName)}.lock`);
25
+ }
26
+
27
+ function readLock(projectRoot, lockName) {
28
+ const filePath = lockPath(projectRoot, lockName);
29
+ if (!fs.existsSync(filePath)) {
30
+ return null;
31
+ }
32
+ try {
33
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
34
+ } catch {
35
+ return {
36
+ schema_version: 1,
37
+ lock_name: sanitizeLockName(lockName),
38
+ command: 'unknown',
39
+ created_at: 'unknown',
40
+ pid: 'unknown',
41
+ };
42
+ }
43
+ }
44
+
45
+ function appendUniqueLine(filePath, line) {
46
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
47
+ const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
48
+ const lines = current.split(/\r?\n/);
49
+ if (!lines.includes(line)) {
50
+ const prefix = current.endsWith('\n') || current.length === 0 ? current : `${current}\n`;
51
+ fs.writeFileSync(filePath, `${prefix}${line}\n`);
52
+ }
53
+ }
54
+
55
+ function ensureQuiverStateIgnored(projectRoot) {
56
+ try {
57
+ const gitDir = execFileSync('git', ['rev-parse', '--absolute-git-dir'], {
58
+ cwd: projectRoot,
59
+ encoding: 'utf8',
60
+ stdio: ['ignore', 'pipe', 'ignore'],
61
+ }).trim();
62
+ if (gitDir) {
63
+ appendUniqueLine(path.join(gitDir, 'info', 'exclude'), '.quiver/');
64
+ }
65
+ } catch {
66
+ // Non-git fixtures can still use filesystem locks.
67
+ }
68
+ }
69
+
70
+ function acquireLock(projectRoot, lockName, options = {}) {
71
+ const filePath = lockPath(projectRoot, lockName);
72
+ const payload = {
73
+ schema_version: 1,
74
+ lock_name: sanitizeLockName(lockName),
75
+ pid: process.pid,
76
+ hostname: os.hostname(),
77
+ command: options.command || 'unknown',
78
+ created_at: (options.now || new Date()).toISOString(),
79
+ metadata: options.metadata || {},
80
+ };
81
+
82
+ ensureQuiverStateIgnored(projectRoot);
83
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
84
+
85
+ try {
86
+ fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, { flag: 'wx' });
87
+ } catch (error) {
88
+ if (error.code === 'EEXIST') {
89
+ const existing = readLock(projectRoot, lockName);
90
+ throw new Error(formatError(`operation is locked: ${toRelativePosix(projectRoot, filePath)}\nLock owner: pid=${existing?.pid || 'unknown'} command=${existing?.command || 'unknown'} created_at=${existing?.created_at || 'unknown'}\nIf this process is gone, inspect the lock and remove it intentionally.`));
91
+ }
92
+ throw error;
93
+ }
94
+
95
+ return {
96
+ filePath,
97
+ lock: payload,
98
+ lockName: sanitizeLockName(lockName),
99
+ };
100
+ }
101
+
102
+ function releaseLock(handle) {
103
+ if (handle?.filePath && fs.existsSync(handle.filePath)) {
104
+ fs.rmSync(handle.filePath);
105
+ }
106
+ }
107
+
108
+ function withLockSync(projectRoot, lockName, options, callback) {
109
+ const handle = acquireLock(projectRoot, lockName, options);
110
+ try {
111
+ return callback(handle);
112
+ } finally {
113
+ releaseLock(handle);
114
+ }
115
+ }
116
+
117
+ async function withLock(projectRoot, lockName, options, callback) {
118
+ const handle = acquireLock(projectRoot, lockName, options);
119
+ try {
120
+ return await callback(handle);
121
+ } finally {
122
+ releaseLock(handle);
123
+ }
124
+ }
125
+
126
+ module.exports = {
127
+ acquireLock,
128
+ lockPath,
129
+ readLock,
130
+ releaseLock,
131
+ sanitizeLockName,
132
+ withLock,
133
+ withLockSync,
134
+ };
@@ -13,6 +13,12 @@ const SAFETY_RULES = [
13
13
  return /(^|\/)\.npmrc$/.test(relativePath) || /(^|\/)\.npm(\/|$)/.test(relativePath);
14
14
  },
15
15
  },
16
+ {
17
+ code: 'ai-raw-artifact',
18
+ match(relativePath) {
19
+ return /(^|\/)\.quiver\/runs\/[^/]+\/raw(\/|$)/.test(relativePath);
20
+ },
21
+ },
16
22
  {
17
23
  code: 'ai-tool-state',
18
24
  match(relativePath) {
@@ -80,6 +86,7 @@ function collectPackageSafetyViolations(paths) {
80
86
  code: rule.code,
81
87
  path: normalizedPath,
82
88
  });
89
+ break;
83
90
  }
84
91
  }
85
92
 
@@ -1,4 +1,9 @@
1
1
  const path = require('path');
2
+ const fs = require('fs');
3
+
4
+ function formatError(message) {
5
+ return `create-quiver: ${message}`;
6
+ }
2
7
 
3
8
  function resolveTargetRoot(cwd, targetDir, pathLib = path) {
4
9
  return pathLib.resolve(cwd, targetDir);
@@ -71,10 +76,79 @@ function specRelativePathFromPath(filePath, pathLib = path) {
71
76
  return parts.slice(specIndex).join('/');
72
77
  }
73
78
 
79
+ function realpathOrResolve(filePath, pathLib = path) {
80
+ try {
81
+ return pathLib.resolve(fs.realpathSync(filePath));
82
+ } catch {
83
+ return pathLib.resolve(normalizeGitBashDrivePath(filePath, pathLib));
84
+ }
85
+ }
86
+
87
+ function isPathInsideRoot(root, target, pathLib = path) {
88
+ const rootPath = realpathOrResolve(root, pathLib);
89
+ const targetPath = realpathOrResolve(target, pathLib);
90
+ const windowsPath = pathLib === path.win32 || process.platform === 'win32';
91
+ const comparableRoot = windowsPath ? rootPath.toLowerCase() : rootPath;
92
+ const comparableTarget = windowsPath ? targetPath.toLowerCase() : targetPath;
93
+
94
+ if (comparableTarget === comparableRoot) {
95
+ return true;
96
+ }
97
+
98
+ const relative = pathLib.relative(comparableRoot, comparableTarget);
99
+ return Boolean(relative && !relative.startsWith('..') && !pathLib.isAbsolute(relative));
100
+ }
101
+
102
+ function assertPathInsideRoot(root, target, label = 'path', pathLib = path) {
103
+ if (!isPathInsideRoot(root, target, pathLib)) {
104
+ throw new Error(formatError(`${label} must stay inside the project root: ${toPosixPath(target, pathLib)}`));
105
+ }
106
+ }
107
+
108
+ function getProjectRelativePathIssue(filePath, pathLib = path) {
109
+ const original = String(filePath || '').trim();
110
+ if (!original) {
111
+ return 'empty-path';
112
+ }
113
+
114
+ if (/^file:/i.test(original)) {
115
+ return 'file-url';
116
+ }
117
+
118
+ const normalized = toPosixPath(normalizeGitBashDrivePath(original, pathLib), pathLib);
119
+ if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized) || pathLib.isAbsolute(original)) {
120
+ return 'absolute-path';
121
+ }
122
+
123
+ const segments = normalized.split('/').filter(Boolean);
124
+ if (segments.some((segment) => segment === '..')) {
125
+ return 'path-traversal';
126
+ }
127
+
128
+ return null;
129
+ }
130
+
131
+ function validateProjectRelativePath(filePath, fieldName = 'path', pathLib = path) {
132
+ const issue = getProjectRelativePathIssue(filePath, pathLib);
133
+ if (issue) {
134
+ throw new Error(formatError(`${fieldName} must be a project-relative path without traversal (got ${String(filePath || '<empty>')}; issue=${issue}).`));
135
+ }
136
+ return toPosixPath(normalizeGitBashDrivePath(String(filePath).trim(), pathLib), pathLib);
137
+ }
138
+
139
+ function validateProjectRelativePaths(paths, fieldName = 'paths', pathLib = path) {
140
+ return (Array.isArray(paths) ? paths : []).map((filePath) => validateProjectRelativePath(filePath, fieldName, pathLib));
141
+ }
142
+
74
143
  module.exports = {
144
+ assertPathInsideRoot,
145
+ getProjectRelativePathIssue,
146
+ isPathInsideRoot,
75
147
  normalizeGitBashDrivePath,
76
148
  relativePosixPath,
77
149
  resolveTargetRoot,
78
150
  specRelativePathFromPath,
79
151
  toPosixPath,
152
+ validateProjectRelativePath,
153
+ validateProjectRelativePaths,
80
154
  };
@@ -49,6 +49,79 @@ function readProjectScanArtifact(projectRoot) {
49
49
  return null;
50
50
  }
51
51
 
52
+ function statIso(filePath) {
53
+ try {
54
+ return fs.statSync(filePath).mtime.toISOString();
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function readProjectScanStatus(projectRoot) {
61
+ const { currentScanPath, legacyScanPath, projectMapPath } = projectScanPaths(projectRoot);
62
+ const projectMapExists = fs.existsSync(projectMapPath);
63
+ let artifact = null;
64
+ let artifactError = '';
65
+
66
+ try {
67
+ artifact = readProjectScanArtifact(projectRoot);
68
+ } catch (error) {
69
+ artifactError = error.message;
70
+ }
71
+
72
+ const scanPath = artifact?.path || (fs.existsSync(currentScanPath) ? currentScanPath : fs.existsSync(legacyScanPath) ? legacyScanPath : '');
73
+ const source = artifact?.source || (artifactError ? 'invalid' : 'missing');
74
+ const scanUpdatedAt = scanPath ? statIso(scanPath) : null;
75
+ const projectMapUpdatedAt = projectMapExists ? statIso(projectMapPath) : null;
76
+ const stale = Boolean(
77
+ scanUpdatedAt
78
+ && projectMapUpdatedAt
79
+ && Date.parse(projectMapUpdatedAt) + 1000 < Date.parse(scanUpdatedAt),
80
+ );
81
+ let status = 'missing';
82
+
83
+ if (artifactError) {
84
+ status = 'invalid';
85
+ } else if (artifact && projectMapExists && stale) {
86
+ status = 'stale';
87
+ } else if (artifact && projectMapExists && source === 'current') {
88
+ status = 'fresh';
89
+ } else if (artifact && projectMapExists && source === 'legacy') {
90
+ status = 'legacy';
91
+ } else if (artifact || projectMapExists) {
92
+ status = 'partial';
93
+ }
94
+
95
+ let summary;
96
+ if (status === 'fresh') {
97
+ summary = `${artifact.relativePath} (current, updated ${scanUpdatedAt})`;
98
+ } else if (status === 'legacy') {
99
+ summary = `${artifact.relativePath} (legacy scan, updated ${scanUpdatedAt})`;
100
+ } else if (status === 'stale') {
101
+ summary = `${artifact.relativePath} newer than docs/PROJECT_MAP.md; run analyze to refresh visible context`;
102
+ } else if (status === 'partial' && artifact && !projectMapExists) {
103
+ summary = `${artifact.relativePath} exists but docs/PROJECT_MAP.md is missing`;
104
+ } else if (status === 'partial' && !artifact && projectMapExists) {
105
+ summary = `docs/PROJECT_MAP.md exists but no scan artifact was found`;
106
+ } else if (status === 'invalid') {
107
+ summary = `scan artifact is invalid: ${artifactError}`;
108
+ } else {
109
+ summary = 'missing analysis artifacts; run npx create-quiver analyze';
110
+ }
111
+
112
+ return {
113
+ artifactPath: artifact?.relativePath || (scanPath ? toRelativeScanPath(projectRoot, scanPath) : null),
114
+ error: artifactError || null,
115
+ projectMapPath: projectMapExists ? PROJECT_MAP_RELATIVE_PATH : null,
116
+ projectMapUpdatedAt,
117
+ scanUpdatedAt,
118
+ source,
119
+ status,
120
+ stale,
121
+ summary,
122
+ };
123
+ }
124
+
52
125
  function hasProjectScanArtifact(projectRoot) {
53
126
  const { currentScanPath, legacyScanPath } = projectScanPaths(projectRoot);
54
127
  return fs.existsSync(currentScanPath) || fs.existsSync(legacyScanPath);
@@ -61,6 +134,7 @@ module.exports = {
61
134
  hasProjectScanArtifact,
62
135
  projectScanPaths,
63
136
  readProjectScanArtifact,
137
+ readProjectScanStatus,
64
138
  toRelativeScanPath,
65
139
  writeProjectScanJson,
66
140
  };
@@ -0,0 +1,236 @@
1
+ const path = require('path');
2
+
3
+ const {
4
+ SliceGraphError,
5
+ buildGraph,
6
+ computeLevels,
7
+ detectFileConflicts,
8
+ inferDependencies,
9
+ readAllSlices,
10
+ readSlicesForSpec,
11
+ topoSort,
12
+ } = require('./slice-graph');
13
+ const {
14
+ CANONICAL_STATUSES,
15
+ isBlockedStatus,
16
+ isCompletedStatus,
17
+ normalizeStatus,
18
+ } = require('./statuses');
19
+
20
+ const DEFAULT_SLICE_STATUS = 'planned';
21
+ const DEFAULT_SPEC_STATUS = 'planned';
22
+ const DEFAULT_RUN_STATUS = 'draft';
23
+ const DEFAULT_AGENT_STATUS = 'idle';
24
+
25
+ const CLOSED_SLICE_STATUSES = new Set(['completed', 'skipped']);
26
+ const HISTORY_CLOSED_SLICE_STATUSES = new Set(['skipped']);
27
+
28
+ function toPosix(relativePath) {
29
+ return String(relativePath || '').split(path.sep).join('/');
30
+ }
31
+
32
+ function compareRefs(left, right) {
33
+ return String(left || '').localeCompare(String(right || ''));
34
+ }
35
+
36
+ function normalizeSliceRecord(slice) {
37
+ const rawStatus = String(slice?.status || slice?.json?.status || 'draft').trim() || 'draft';
38
+ const canonicalStatus = normalizeStatus('slice', rawStatus, DEFAULT_SLICE_STATUS);
39
+
40
+ return {
41
+ ...slice,
42
+ raw_status: rawStatus,
43
+ canonical_status: canonicalStatus,
44
+ status: rawStatus,
45
+ };
46
+ }
47
+
48
+ function readResolverSlices(projectRoot, specSlug = '') {
49
+ const targetSpec = String(specSlug || '').trim();
50
+ const slices = targetSpec ? readSlicesForSpec(projectRoot, targetSpec) : readAllSlices(projectRoot);
51
+ return slices.map(normalizeSliceRecord);
52
+ }
53
+
54
+ function safeBuildGraph(slices, allowGraphErrors) {
55
+ try {
56
+ const graph = buildGraph(slices);
57
+ return {
58
+ ok: true,
59
+ nodes: graph.nodes.map(normalizeSliceRecord),
60
+ edges: graph.edges,
61
+ cycles: graph.cycles,
62
+ error: null,
63
+ };
64
+ } catch (error) {
65
+ if (!allowGraphErrors || !(error instanceof SliceGraphError)) {
66
+ throw error;
67
+ }
68
+
69
+ return {
70
+ ok: false,
71
+ nodes: inferDependencies(slices).map(normalizeSliceRecord),
72
+ edges: [],
73
+ cycles: [],
74
+ error: {
75
+ code: error.code,
76
+ message: error.message,
77
+ },
78
+ };
79
+ }
80
+ }
81
+
82
+ function resolveProjectState(projectRoot, options = {}) {
83
+ const specSlug = options.specSlug ? String(options.specSlug).trim() : '';
84
+ const rawSlices = readResolverSlices(projectRoot, specSlug);
85
+ const graph = safeBuildGraph(rawSlices, options.allowGraphErrors === true);
86
+ const orderedSlices = graph.ok ? topoSort(graph).map(normalizeSliceRecord) : graph.nodes.slice().sort((left, right) => compareRefs(left.ref, right.ref));
87
+
88
+ return {
89
+ graph,
90
+ orderedSlices,
91
+ projectRoot,
92
+ rawSlices,
93
+ specSlug,
94
+ specs: groupSlicesBySpec(graph.nodes),
95
+ };
96
+ }
97
+
98
+ function filterSlicesForExecution(slices, options = {}) {
99
+ const includeCompleted = options.includeCompleted === true;
100
+ const excluded = includeCompleted ? HISTORY_CLOSED_SLICE_STATUSES : CLOSED_SLICE_STATUSES;
101
+
102
+ return (Array.isArray(slices) ? slices : [])
103
+ .filter((slice) => !excluded.has(normalizeStatus('slice', slice?.canonical_status || slice?.status, DEFAULT_SLICE_STATUS)))
104
+ .sort((left, right) => compareRefs(left.ref, right.ref));
105
+ }
106
+
107
+ function progressForSlice(slice) {
108
+ const explicit = Number(slice?.json?.progress);
109
+ if (Number.isFinite(explicit)) {
110
+ return Math.max(0, Math.min(100, explicit));
111
+ }
112
+
113
+ const status = normalizeStatus('slice', slice?.canonical_status || slice?.status, DEFAULT_SLICE_STATUS);
114
+ if (status === 'completed') {
115
+ return 100;
116
+ }
117
+ if (status === 'in-progress' || status === 'review') {
118
+ return 50;
119
+ }
120
+ return 0;
121
+ }
122
+
123
+ function summarizeSliceProgress(items) {
124
+ const slices = Array.isArray(items) ? items : [];
125
+ const total = slices.length;
126
+ const completed = slices.filter((item) => isCompletedStatus('slice', item.canonical_status || item.status)).length;
127
+ const blocked = slices.filter((item) => isBlockedStatus('slice', item.canonical_status || item.status, item)).length;
128
+ const open = Math.max(0, total - completed);
129
+ const percent = total === 0 ? 0 : Math.round((completed / total) * 100);
130
+
131
+ return {
132
+ total,
133
+ completed,
134
+ open,
135
+ blocked,
136
+ percent,
137
+ };
138
+ }
139
+
140
+ function statusForSpec(specSlices) {
141
+ const slices = Array.isArray(specSlices) ? specSlices : [];
142
+ if (slices.length === 0) {
143
+ return 'draft';
144
+ }
145
+ if (slices.some((slice) => isBlockedStatus('slice', slice.canonical_status || slice.status, slice))) {
146
+ return 'blocked';
147
+ }
148
+ if (slices.every((slice) => isCompletedStatus('slice', slice.canonical_status || slice.status))) {
149
+ return 'done';
150
+ }
151
+ if (slices.some((slice) => progressForSlice(slice) > 0)) {
152
+ return 'in-progress';
153
+ }
154
+ return DEFAULT_SPEC_STATUS;
155
+ }
156
+
157
+ function groupSlicesBySpec(slices) {
158
+ const groups = new Map();
159
+
160
+ for (const slice of Array.isArray(slices) ? slices : []) {
161
+ const key = `${slice.specFamily || 'specs'}/${slice.specSlug || ''}`;
162
+ if (!groups.has(key)) {
163
+ groups.set(key, []);
164
+ }
165
+ groups.get(key).push(slice);
166
+ }
167
+
168
+ return Array.from(groups.entries())
169
+ .map(([key, specSlices]) => {
170
+ const [specFamily, specSlug] = key.split('/');
171
+ const ordered = specSlices.slice().sort((left, right) => compareRefs(left.ref, right.ref));
172
+ const status = statusForSpec(ordered);
173
+ return {
174
+ canonical_status: normalizeStatus('spec', status, DEFAULT_SPEC_STATUS),
175
+ specFamily,
176
+ specSlug,
177
+ status,
178
+ slices: ordered,
179
+ };
180
+ })
181
+ .sort((left, right) => left.specSlug.localeCompare(right.specSlug));
182
+ }
183
+
184
+ function summarizeGraph(graph) {
185
+ if (!graph?.ok) {
186
+ return {
187
+ ok: false,
188
+ edges: [],
189
+ levels: [],
190
+ conflicts: [],
191
+ error: graph?.error || null,
192
+ nodes: Array.isArray(graph?.nodes) ? graph.nodes : [],
193
+ };
194
+ }
195
+
196
+ const levels = computeLevels(graph).map((level, index) => ({
197
+ level: index,
198
+ slices: level.map((slice) => slice.ref),
199
+ }));
200
+
201
+ return {
202
+ ok: true,
203
+ edges: graph.edges.map((edge) => ({ from: edge.from, to: edge.to })),
204
+ levels,
205
+ conflicts: detectFileConflicts(graph.nodes).map((conflict) => ({
206
+ files: conflict.files,
207
+ slices: conflict.slices,
208
+ })),
209
+ error: null,
210
+ nodes: graph.nodes,
211
+ };
212
+ }
213
+
214
+ function relativeProjectPath(projectRoot, filePath) {
215
+ return toPosix(path.relative(projectRoot, filePath));
216
+ }
217
+
218
+ module.exports = {
219
+ CANONICAL_STATUSES,
220
+ DEFAULT_AGENT_STATUS,
221
+ DEFAULT_RUN_STATUS,
222
+ DEFAULT_SLICE_STATUS,
223
+ DEFAULT_SPEC_STATUS,
224
+ filterSlicesForExecution,
225
+ groupSlicesBySpec,
226
+ isBlockedStatus,
227
+ isCompletedStatus,
228
+ normalizeStatus,
229
+ progressForSlice,
230
+ relativeProjectPath,
231
+ resolveProjectState,
232
+ summarizeGraph,
233
+ summarizeSliceProgress,
234
+ toPosix,
235
+ };
236
+