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
@@ -6,6 +6,8 @@ const {
6
6
  fetchRemote,
7
7
  hasLocalBranch,
8
8
  hasRemoteBranch,
9
+ isGitWorktree,
10
+ isLinkedWorktree,
9
11
  isCleanWorktree,
10
12
  lsRemoteHeads,
11
13
  mergeBaseIsAncestor,
@@ -17,6 +19,7 @@ const {
17
19
  worktreeRemove,
18
20
  } = require('./git');
19
21
  const { parseJsonWithComments } = require('./json');
22
+ const { acquireLock, releaseLock, withLockSync } = require('./locks');
20
23
  const { safeBranchName, worktreesRootForRepo } = require('./slice');
21
24
 
22
25
  function formatError(message) {
@@ -88,6 +91,89 @@ function findExistingWorktree(repoRoot, branchName) {
88
91
  return '';
89
92
  }
90
93
 
94
+ function sameRealPath(left, right) {
95
+ try {
96
+ return fs.realpathSync(left) === fs.realpathSync(right);
97
+ } catch {
98
+ return path.resolve(left) === path.resolve(right);
99
+ }
100
+ }
101
+
102
+ function recoveryForMissingWorktree(branchName, worktreePath) {
103
+ return [
104
+ `registered spec worktree is missing or stale for ${branchName}: ${worktreePath}`,
105
+ 'Recovery:',
106
+ '- Run `git worktree prune` from the main checkout, then retry `npx create-quiver spec start specs/<spec-slug>`.',
107
+ '- If the directory was moved manually, restore it or remove the stale git worktree registration intentionally.',
108
+ '- Do not create a nested replacement worktree from inside another worktree.',
109
+ ].join('\n');
110
+ }
111
+
112
+ function recoveryForNestedWorktree(branchName, existingWorktree = '') {
113
+ return [
114
+ `refusing to create a spec worktree from inside a linked worktree for ${branchName}.`,
115
+ 'Recovery:',
116
+ existingWorktree
117
+ ? `- Use the existing spec worktree: ${existingWorktree}`
118
+ : '- Return to the main checkout and rerun the command.',
119
+ '- This prevents nested .worktrees paths and conflicting persistent spec worktrees.',
120
+ ].join('\n');
121
+ }
122
+
123
+ function assertExistingWorktreeUsable(branchName, worktreePath) {
124
+ if (!worktreePath) {
125
+ return;
126
+ }
127
+ if (!fs.existsSync(worktreePath) || !isGitWorktree(worktreePath)) {
128
+ throw new Error(formatError(recoveryForMissingWorktree(branchName, worktreePath)));
129
+ }
130
+ if (!isCleanWorktree(worktreePath)) {
131
+ throw new Error(formatError(`existing spec worktree is dirty: ${worktreePath}\nRecovery:\n- Commit or stash changes inside the spec worktree.\n- Then rerun the command.`));
132
+ }
133
+ }
134
+
135
+ function parseDirtyStatusFiles(rawStatus) {
136
+ return String(rawStatus || '')
137
+ .split('\n')
138
+ .map((line) => line.trimEnd())
139
+ .filter(Boolean)
140
+ .map((line) => {
141
+ if (line.startsWith('?? ')) {
142
+ return line.slice(3).trim();
143
+ }
144
+ const entry = (line[2] === ' ' ? line.slice(3) : line[1] === ' ' ? line.slice(2) : line.slice(3)).trim();
145
+ return entry.includes(' -> ') ? entry.split(' -> ').pop().trim() : entry;
146
+ })
147
+ .filter(Boolean);
148
+ }
149
+
150
+ function formatDirtyCheckoutRecovery(repoRoot) {
151
+ const files = parseDirtyStatusFiles(statusPorcelain(repoRoot));
152
+ const lines = [
153
+ 'current checkout is not clean. Starting a spec worktree needs a clean main checkout.',
154
+ ];
155
+
156
+ if (files.length > 0) {
157
+ lines.push('Dirty files:');
158
+ for (const file of files.slice(0, 20)) {
159
+ lines.push(`- ${file}`);
160
+ }
161
+ if (files.length > 20) {
162
+ lines.push(`- ...and ${files.length - 20} more`);
163
+ }
164
+ }
165
+
166
+ lines.push(
167
+ 'Safe options:',
168
+ '- Commit the current changes if they belong to the active slice.',
169
+ '- Stash changes manually after reviewing them.',
170
+ '- Move this work to a separate worktree before starting the spec.',
171
+ '- Abort and rerun from a clean checkout.',
172
+ );
173
+
174
+ return lines.filter((line) => line !== '').join('\n');
175
+ }
176
+
91
177
  function resolveBaseRef(repoRoot, preferred = '') {
92
178
  const candidates = [preferred, 'main', 'develop'].filter(Boolean);
93
179
  for (const candidate of candidates) {
@@ -133,7 +219,11 @@ function buildSpecStatus(repoRoot, specInput) {
133
219
  const pendingSlices = slices.filter((slice) => slice.status !== 'completed');
134
220
  const laterSlicesBlocked = !slice00 || slice00.status !== 'completed';
135
221
  const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
136
- const worktreeDirty = existingWorktree ? !isCleanWorktree(existingWorktree) : false;
222
+ const expectedPathExists = fs.existsSync(identity.worktreePath);
223
+ const expectedPathUnregistered = Boolean(!existingWorktree && expectedPathExists);
224
+ const worktreeMissing = Boolean(existingWorktree && (!fs.existsSync(existingWorktree) || !isGitWorktree(existingWorktree)))
225
+ || expectedPathUnregistered;
226
+ const worktreeDirty = existingWorktree && !worktreeMissing ? !isCleanWorktree(existingWorktree) : false;
137
227
 
138
228
  return {
139
229
  ...identity,
@@ -144,6 +234,8 @@ function buildSpecStatus(repoRoot, specInput) {
144
234
  slices,
145
235
  specDir,
146
236
  worktreeDirty,
237
+ worktreeExpectedPathUnregistered: expectedPathUnregistered,
238
+ worktreeMissing,
147
239
  };
148
240
  }
149
241
 
@@ -153,6 +245,9 @@ function formatSpecStatus(status) {
153
245
  `Spec: ${status.relativeSpecDir}`,
154
246
  `Branch: ${status.branchName}`,
155
247
  `Worktree: ${status.existingWorktree || status.worktreePath}`,
248
+ `Worktree missing/stale: ${status.worktreeMissing ? 'yes' : 'no'}`,
249
+ `Worktree registered: ${status.existingWorktree ? 'yes' : 'no'}`,
250
+ status.worktreeExpectedPathUnregistered ? 'Worktree note: expected path exists but is not registered in git worktree list.' : '',
156
251
  `Worktree dirty: ${status.worktreeDirty ? 'yes' : 'no'}`,
157
252
  `slice-00: ${status.slice00 ? status.slice00.status : 'missing'}`,
158
253
  `Later slices blocked: ${status.laterSlicesBlocked ? 'yes' : 'no'}`,
@@ -173,61 +268,82 @@ function formatSpecStatus(status) {
173
268
  function startSpecWorktree(repoRoot, specInput, options = {}) {
174
269
  const specDir = findSpecDir(repoRoot, specInput);
175
270
  const identity = resolveSpecIdentity(repoRoot, specDir);
176
- const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
177
271
  const slices = listSpecSlices(specDir);
178
272
  const slice00 = slices.find((slice) => slice.id.startsWith('slice-00')) || null;
179
273
  const baseRef = resolveBaseRef(repoRoot, options.baseBranch);
180
274
 
181
- if (existingWorktree) {
182
- if (!isCleanWorktree(existingWorktree)) {
183
- throw new Error(formatError(`existing spec worktree is dirty: ${existingWorktree}`));
275
+ const run = () => {
276
+ const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
277
+ const currentIsLinkedWorktree = isLinkedWorktree(repoRoot);
278
+
279
+ if (existingWorktree) {
280
+ assertExistingWorktreeUsable(identity.branchName, existingWorktree);
281
+ if (currentIsLinkedWorktree && !sameRealPath(repoRoot, existingWorktree)) {
282
+ throw new Error(formatError(recoveryForNestedWorktree(identity.branchName, existingWorktree)));
283
+ }
284
+ return {
285
+ ...identity,
286
+ baseRef,
287
+ dryRun: options.dryRun === true,
288
+ reused: true,
289
+ slice00,
290
+ worktreePath: existingWorktree,
291
+ };
184
292
  }
185
- return {
186
- ...identity,
187
- baseRef,
188
- dryRun: options.dryRun === true,
189
- reused: true,
190
- slice00,
191
- worktreePath: existingWorktree,
192
- };
193
- }
194
293
 
195
- if (fs.existsSync(identity.worktreePath)) {
196
- throw new Error(formatError(`worktree path already exists and is not registered for ${identity.branchName}: ${identity.worktreePath}`));
197
- }
294
+ if (currentIsLinkedWorktree) {
295
+ throw new Error(formatError(recoveryForNestedWorktree(identity.branchName)));
296
+ }
198
297
 
199
- if (!isCleanWorktree(repoRoot)) {
200
- throw new Error(formatError('current checkout is not clean. Commit or stash before starting a spec worktree.'));
201
- }
298
+ if (fs.existsSync(identity.worktreePath)) {
299
+ throw new Error(formatError(`worktree path already exists and is not registered for ${identity.branchName}: ${identity.worktreePath}\nRecovery:\n- Inspect the path and move or remove it intentionally before rerunning.\n- Run \`git worktree list\` to verify registered worktrees.`));
300
+ }
301
+
302
+ if (!isCleanWorktree(repoRoot)) {
303
+ throw new Error(formatError(formatDirtyCheckoutRecovery(repoRoot)));
304
+ }
305
+
306
+ if (options.dryRun === true) {
307
+ return {
308
+ ...identity,
309
+ baseRef,
310
+ currentBranch: currentBranch(repoRoot),
311
+ dryRun: true,
312
+ reused: false,
313
+ slice00,
314
+ };
315
+ }
316
+
317
+ worktreePrune(repoRoot);
318
+ fs.mkdirSync(path.dirname(identity.worktreePath), { recursive: true });
319
+
320
+ if (hasLocalBranch(repoRoot, identity.branchName)) {
321
+ worktreeAdd(repoRoot, identity.worktreePath, identity.branchName);
322
+ } else {
323
+ worktreeAdd(repoRoot, identity.worktreePath, baseRef, { branch: identity.branchName });
324
+ }
202
325
 
203
- if (options.dryRun === true) {
204
326
  return {
205
327
  ...identity,
206
328
  baseRef,
207
329
  currentBranch: currentBranch(repoRoot),
208
- dryRun: true,
330
+ dryRun: false,
209
331
  reused: false,
210
332
  slice00,
211
333
  };
212
- }
213
-
214
- worktreePrune(repoRoot);
215
- fs.mkdirSync(path.dirname(identity.worktreePath), { recursive: true });
334
+ };
216
335
 
217
- if (hasLocalBranch(repoRoot, identity.branchName)) {
218
- worktreeAdd(repoRoot, identity.worktreePath, identity.branchName);
219
- } else {
220
- worktreeAdd(repoRoot, identity.worktreePath, baseRef, { branch: identity.branchName });
336
+ if (options.dryRun === true) {
337
+ return run();
221
338
  }
222
339
 
223
- return {
224
- ...identity,
225
- baseRef,
226
- currentBranch: currentBranch(repoRoot),
227
- dryRun: false,
228
- reused: false,
229
- slice00,
230
- };
340
+ return withLockSync(repoRoot, `spec-worktree-${identity.specSlug}`, {
341
+ command: 'spec start',
342
+ metadata: {
343
+ branch: identity.branchName,
344
+ spec: identity.relativeSpecDir,
345
+ },
346
+ }, run);
231
347
  }
232
348
 
233
349
  function formatSpecStartResult(result) {
@@ -245,6 +361,14 @@ function formatSpecStartResult(result) {
245
361
  function closeSpecWorktree(repoRoot, specInput, options = {}) {
246
362
  const specDir = findSpecDir(repoRoot, specInput);
247
363
  const identity = resolveSpecIdentity(repoRoot, specDir);
364
+ const lock = options.dryRun === true ? null : acquireLock(repoRoot, `spec-worktree-${identity.specSlug}`, {
365
+ command: 'spec close',
366
+ metadata: {
367
+ branch: identity.branchName,
368
+ spec: identity.relativeSpecDir,
369
+ },
370
+ });
371
+ try {
248
372
  const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
249
373
  const discard = options.discard === true;
250
374
  const dryRun = options.dryRun === true;
@@ -256,6 +380,10 @@ function closeSpecWorktree(repoRoot, specInput, options = {}) {
256
380
  throw new Error(formatError(`missing spec worktree for branch ${identity.branchName}.`));
257
381
  }
258
382
 
383
+ if (!fs.existsSync(existingWorktree) || !isGitWorktree(existingWorktree)) {
384
+ throw new Error(formatError(recoveryForMissingWorktree(identity.branchName, existingWorktree)));
385
+ }
386
+
259
387
  if (!discard && !isCleanWorktree(existingWorktree)) {
260
388
  throw new Error(formatError(`spec worktree is dirty: ${existingWorktree}. Commit or stash before closing, or pass --discard intentionally.`));
261
389
  }
@@ -310,6 +438,9 @@ function closeSpecWorktree(repoRoot, specInput, options = {}) {
310
438
  removed: true,
311
439
  worktreePath: existingWorktree,
312
440
  };
441
+ } finally {
442
+ releaseLock(lock);
443
+ }
313
444
  }
314
445
 
315
446
  function formatSpecCloseResult(result) {
@@ -0,0 +1,115 @@
1
+ const CANONICAL_STATUSES = Object.freeze({
2
+ spec: Object.freeze(['draft', 'planned', 'approved', 'in-progress', 'blocked', 'review', 'done', 'archived']),
3
+ slice: Object.freeze(['planned', 'ready', 'in-progress', 'blocked', 'review', 'completed', 'skipped']),
4
+ run: Object.freeze(['draft', 'waiting-approval', 'approved', 'running', 'blocked', 'done', 'failed']),
5
+ approval: Object.freeze(['pending', 'approved', 'rejected', 'superseded']),
6
+ agent: Object.freeze(['idle', 'planning', 'reading', 'coding', 'reviewing', 'blocked', 'waiting-approval', 'done']),
7
+ dataset: Object.freeze(['ready', 'partial', 'empty', 'error']),
8
+ });
9
+
10
+ const STATUS_ALIASES = Object.freeze({
11
+ spec: Object.freeze({
12
+ active: 'in-progress',
13
+ complete: 'done',
14
+ completed: 'done',
15
+ closed: 'done',
16
+ done: 'done',
17
+ in_progress: 'in-progress',
18
+ pending: 'planned',
19
+ }),
20
+ slice: Object.freeze({
21
+ active: 'in-progress',
22
+ cancelled: 'skipped',
23
+ canceled: 'skipped',
24
+ closed: 'completed',
25
+ complete: 'completed',
26
+ done: 'completed',
27
+ draft: 'planned',
28
+ in_progress: 'in-progress',
29
+ pending: 'planned',
30
+ }),
31
+ run: Object.freeze({
32
+ active: 'running',
33
+ closed: 'done',
34
+ complete: 'done',
35
+ completed: 'done',
36
+ in_progress: 'running',
37
+ pending: 'draft',
38
+ stale: 'draft',
39
+ }),
40
+ approval: Object.freeze({
41
+ draft: 'pending',
42
+ review: 'pending',
43
+ reviewed: 'pending',
44
+ stale: 'pending',
45
+ unapproved: 'pending',
46
+ }),
47
+ agent: Object.freeze({
48
+ active: 'coding',
49
+ complete: 'done',
50
+ completed: 'done',
51
+ in_progress: 'coding',
52
+ waiting_approval: 'waiting-approval',
53
+ }),
54
+ dataset: Object.freeze({
55
+ ok: 'ready',
56
+ warning: 'partial',
57
+ missing: 'empty',
58
+ failed: 'error',
59
+ }),
60
+ });
61
+
62
+ function normalizeStatusToken(value) {
63
+ return String(value || '')
64
+ .trim()
65
+ .toLowerCase()
66
+ .replace(/\s+/g, '-')
67
+ .replace(/_/g, '-');
68
+ }
69
+
70
+ function normalizeStatus(kind, status, fallback = 'planned') {
71
+ const family = String(kind || '').trim().toLowerCase();
72
+ const catalog = CANONICAL_STATUSES[family];
73
+ if (!catalog) {
74
+ return normalizeStatusToken(status) || normalizeStatusToken(fallback);
75
+ }
76
+
77
+ const normalized = normalizeStatusToken(status) || normalizeStatusToken(fallback);
78
+ const aliasKey = normalized.replace(/-/g, '_');
79
+ const aliases = STATUS_ALIASES[family] || {};
80
+ const canonical = aliases[normalized] || aliases[aliasKey] || normalized;
81
+
82
+ if (catalog.includes(canonical)) {
83
+ return canonical;
84
+ }
85
+
86
+ const fallbackStatus = normalizeStatusToken(fallback);
87
+ return catalog.includes(fallbackStatus) ? fallbackStatus : catalog[0];
88
+ }
89
+
90
+ function isCompletedStatus(kind, status) {
91
+ const family = String(kind || '').trim().toLowerCase();
92
+ const canonical = normalizeStatus(family, status, family === 'slice' ? 'planned' : 'draft');
93
+
94
+ if (family === 'slice') {
95
+ return canonical === 'completed';
96
+ }
97
+
98
+ if (family === 'spec' || family === 'run' || family === 'agent') {
99
+ return canonical === 'done';
100
+ }
101
+
102
+ return canonical === 'approved';
103
+ }
104
+
105
+ function isBlockedStatus(kind, status, record = {}) {
106
+ return normalizeStatus(kind, status, 'planned') === 'blocked' || Boolean(record?.blocked_reason || record?.json?.blocked_reason);
107
+ }
108
+
109
+ module.exports = {
110
+ CANONICAL_STATUSES,
111
+ isBlockedStatus,
112
+ isCompletedStatus,
113
+ normalizeStatus,
114
+ };
115
+