create-quiver 0.12.1 → 0.14.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 (110) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +24 -9
  3. package/README_FOR_AI.md +15 -6
  4. package/ROADMAP.md +15 -2
  5. package/docs/COMMANDS.md.template +12 -3
  6. package/docs/TROUBLESHOOTING.md.template +29 -0
  7. package/docs/WORKFLOW.md.template +13 -12
  8. package/package.json +2 -1
  9. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +2 -2
  10. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +5 -5
  11. package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
  12. package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
  13. package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
  14. package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
  15. package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
  16. package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
  17. package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
  18. package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
  19. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
  20. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
  21. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
  22. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
  23. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
  24. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
  25. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
  26. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
  27. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
  28. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
  29. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
  30. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
  31. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
  32. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
  33. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
  34. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
  35. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
  36. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
  37. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
  38. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
  39. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
  40. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
  41. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
  42. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
  43. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
  44. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
  45. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
  46. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
  47. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  48. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
  49. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
  50. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
  51. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
  52. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
  53. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
  54. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
  55. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
  56. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
  57. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
  58. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
  59. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
  60. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
  61. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
  62. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
  63. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
  64. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
  65. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
  66. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
  67. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
  68. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
  69. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
  70. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
  71. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
  72. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
  73. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
  74. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  75. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
  76. package/src/create-quiver/commands/ai.js +563 -21
  77. package/src/create-quiver/commands/flow.js +52 -4
  78. package/src/create-quiver/commands/graph.js +7 -7
  79. package/src/create-quiver/commands/plan.js +6 -15
  80. package/src/create-quiver/commands/spec.js +292 -0
  81. package/src/create-quiver/index.js +125 -25
  82. package/src/create-quiver/lib/agent-profiles.js +15 -3
  83. package/src/create-quiver/lib/ai/artifacts.js +318 -0
  84. package/src/create-quiver/lib/ai/context-packs.js +2 -2
  85. package/src/create-quiver/lib/ai/execution-plan.js +9 -0
  86. package/src/create-quiver/lib/ai/executor.js +3 -2
  87. package/src/create-quiver/lib/ai/export-state.js +287 -95
  88. package/src/create-quiver/lib/ai/github.js +93 -4
  89. package/src/create-quiver/lib/ai/plan-review.js +161 -0
  90. package/src/create-quiver/lib/ai/run-state.js +17 -2
  91. package/src/create-quiver/lib/ai/spec-generator.js +87 -13
  92. package/src/create-quiver/lib/ai/spec-templates.js +72 -12
  93. package/src/create-quiver/lib/analyze.js +2 -2
  94. package/src/create-quiver/lib/approvals.js +14 -2
  95. package/src/create-quiver/lib/doctor.js +79 -0
  96. package/src/create-quiver/lib/git.js +40 -1
  97. package/src/create-quiver/lib/handoff.js +43 -1
  98. package/src/create-quiver/lib/init-docs.js +11 -7
  99. package/src/create-quiver/lib/init-layout.js +1 -0
  100. package/src/create-quiver/lib/lifecycle.js +52 -3
  101. package/src/create-quiver/lib/locks.js +134 -0
  102. package/src/create-quiver/lib/package-safety.js +7 -0
  103. package/src/create-quiver/lib/paths.js +74 -0
  104. package/src/create-quiver/lib/project-scan.js +74 -0
  105. package/src/create-quiver/lib/project-state-resolver.js +430 -0
  106. package/src/create-quiver/lib/readiness.js +48 -7
  107. package/src/create-quiver/lib/scope.js +2 -1
  108. package/src/create-quiver/lib/slice.js +8 -4
  109. package/src/create-quiver/lib/spec-worktrees.js +169 -38
  110. package/src/create-quiver/lib/statuses.js +115 -0
@@ -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 = {}) {
@@ -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
  };