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
@@ -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,47 @@ 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
+
91
135
  function resolveBaseRef(repoRoot, preferred = '') {
92
136
  const candidates = [preferred, 'main', 'develop'].filter(Boolean);
93
137
  for (const candidate of candidates) {
@@ -133,7 +177,8 @@ function buildSpecStatus(repoRoot, specInput) {
133
177
  const pendingSlices = slices.filter((slice) => slice.status !== 'completed');
134
178
  const laterSlicesBlocked = !slice00 || slice00.status !== 'completed';
135
179
  const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
136
- const worktreeDirty = existingWorktree ? !isCleanWorktree(existingWorktree) : false;
180
+ const worktreeMissing = Boolean(existingWorktree && (!fs.existsSync(existingWorktree) || !isGitWorktree(existingWorktree)));
181
+ const worktreeDirty = existingWorktree && !worktreeMissing ? !isCleanWorktree(existingWorktree) : false;
137
182
 
138
183
  return {
139
184
  ...identity,
@@ -144,6 +189,7 @@ function buildSpecStatus(repoRoot, specInput) {
144
189
  slices,
145
190
  specDir,
146
191
  worktreeDirty,
192
+ worktreeMissing,
147
193
  };
148
194
  }
149
195
 
@@ -153,6 +199,7 @@ function formatSpecStatus(status) {
153
199
  `Spec: ${status.relativeSpecDir}`,
154
200
  `Branch: ${status.branchName}`,
155
201
  `Worktree: ${status.existingWorktree || status.worktreePath}`,
202
+ `Worktree missing/stale: ${status.worktreeMissing ? 'yes' : 'no'}`,
156
203
  `Worktree dirty: ${status.worktreeDirty ? 'yes' : 'no'}`,
157
204
  `slice-00: ${status.slice00 ? status.slice00.status : 'missing'}`,
158
205
  `Later slices blocked: ${status.laterSlicesBlocked ? 'yes' : 'no'}`,
@@ -173,64 +220,107 @@ function formatSpecStatus(status) {
173
220
  function startSpecWorktree(repoRoot, specInput, options = {}) {
174
221
  const specDir = findSpecDir(repoRoot, specInput);
175
222
  const identity = resolveSpecIdentity(repoRoot, specDir);
176
- const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
177
223
  const slices = listSpecSlices(specDir);
178
224
  const slice00 = slices.find((slice) => slice.id.startsWith('slice-00')) || null;
179
225
  const baseRef = resolveBaseRef(repoRoot, options.baseBranch);
180
226
 
181
- if (existingWorktree) {
182
- if (!isCleanWorktree(existingWorktree)) {
183
- throw new Error(formatError(`existing spec worktree is dirty: ${existingWorktree}`));
227
+ const run = () => {
228
+ const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
229
+ const currentIsLinkedWorktree = isLinkedWorktree(repoRoot);
230
+
231
+ if (existingWorktree) {
232
+ assertExistingWorktreeUsable(identity.branchName, existingWorktree);
233
+ if (currentIsLinkedWorktree && !sameRealPath(repoRoot, existingWorktree)) {
234
+ throw new Error(formatError(recoveryForNestedWorktree(identity.branchName, existingWorktree)));
235
+ }
236
+ return {
237
+ ...identity,
238
+ baseRef,
239
+ dryRun: options.dryRun === true,
240
+ reused: true,
241
+ slice00,
242
+ worktreePath: existingWorktree,
243
+ };
244
+ }
245
+
246
+ if (currentIsLinkedWorktree) {
247
+ throw new Error(formatError(recoveryForNestedWorktree(identity.branchName)));
248
+ }
249
+
250
+ if (fs.existsSync(identity.worktreePath)) {
251
+ 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.`));
184
252
  }
253
+
254
+ if (!isCleanWorktree(repoRoot)) {
255
+ throw new Error(formatError('current checkout is not clean. Commit or stash before starting a spec worktree.'));
256
+ }
257
+
258
+ if (options.dryRun === true) {
259
+ return {
260
+ ...identity,
261
+ baseRef,
262
+ currentBranch: currentBranch(repoRoot),
263
+ dryRun: true,
264
+ reused: false,
265
+ slice00,
266
+ };
267
+ }
268
+
269
+ worktreePrune(repoRoot);
270
+ fs.mkdirSync(path.dirname(identity.worktreePath), { recursive: true });
271
+
272
+ if (hasLocalBranch(repoRoot, identity.branchName)) {
273
+ worktreeAdd(repoRoot, identity.worktreePath, identity.branchName);
274
+ } else {
275
+ worktreeAdd(repoRoot, identity.worktreePath, baseRef, { branch: identity.branchName });
276
+ }
277
+
185
278
  return {
186
279
  ...identity,
187
280
  baseRef,
188
- reused: true,
281
+ currentBranch: currentBranch(repoRoot),
282
+ dryRun: false,
283
+ reused: false,
189
284
  slice00,
190
- worktreePath: existingWorktree,
191
285
  };
192
- }
193
-
194
- if (fs.existsSync(identity.worktreePath)) {
195
- throw new Error(formatError(`worktree path already exists and is not registered for ${identity.branchName}: ${identity.worktreePath}`));
196
- }
197
-
198
- if (!isCleanWorktree(repoRoot)) {
199
- throw new Error(formatError('current checkout is not clean. Commit or stash before starting a spec worktree.'));
200
- }
201
-
202
- worktreePrune(repoRoot);
203
- fs.mkdirSync(path.dirname(identity.worktreePath), { recursive: true });
286
+ };
204
287
 
205
- if (hasLocalBranch(repoRoot, identity.branchName)) {
206
- worktreeAdd(repoRoot, identity.worktreePath, identity.branchName);
207
- } else {
208
- worktreeAdd(repoRoot, identity.worktreePath, baseRef, { branch: identity.branchName });
288
+ if (options.dryRun === true) {
289
+ return run();
209
290
  }
210
291
 
211
- return {
212
- ...identity,
213
- baseRef,
214
- currentBranch: currentBranch(repoRoot),
215
- reused: false,
216
- slice00,
217
- };
292
+ return withLockSync(repoRoot, `spec-worktree-${identity.specSlug}`, {
293
+ command: 'spec start',
294
+ metadata: {
295
+ branch: identity.branchName,
296
+ spec: identity.relativeSpecDir,
297
+ },
298
+ }, run);
218
299
  }
219
300
 
220
301
  function formatSpecStartResult(result) {
221
302
  return `${[
222
- 'Spec worktree ready',
303
+ result.dryRun ? 'Spec worktree start dry-run' : 'Spec worktree ready',
223
304
  `Branch: ${result.branchName}`,
224
305
  `Base: ${result.baseRef}`,
225
306
  `Worktree: ${result.worktreePath}`,
226
307
  `Reused: ${result.reused ? 'yes' : 'no'}`,
227
308
  `slice-00: ${result.slice00 ? result.slice00.status : 'missing'}`,
228
- ].join('\n')}\n`;
309
+ result.dryRun && !result.reused ? `Would create worktree: ${result.worktreePath}` : '',
310
+ ].filter(Boolean).join('\n')}\n`;
229
311
  }
230
312
 
231
313
  function closeSpecWorktree(repoRoot, specInput, options = {}) {
232
314
  const specDir = findSpecDir(repoRoot, specInput);
233
315
  const identity = resolveSpecIdentity(repoRoot, specDir);
316
+ const lock = options.dryRun === true ? null : acquireLock(repoRoot, `spec-worktree-${identity.specSlug}`, {
317
+ command: 'spec close',
318
+ metadata: {
319
+ branch: identity.branchName,
320
+ spec: identity.relativeSpecDir,
321
+ },
322
+ });
323
+ try {
234
324
  const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
235
325
  const discard = options.discard === true;
236
326
  const dryRun = options.dryRun === true;
@@ -242,6 +332,10 @@ function closeSpecWorktree(repoRoot, specInput, options = {}) {
242
332
  throw new Error(formatError(`missing spec worktree for branch ${identity.branchName}.`));
243
333
  }
244
334
 
335
+ if (!fs.existsSync(existingWorktree) || !isGitWorktree(existingWorktree)) {
336
+ throw new Error(formatError(recoveryForMissingWorktree(identity.branchName, existingWorktree)));
337
+ }
338
+
245
339
  if (!discard && !isCleanWorktree(existingWorktree)) {
246
340
  throw new Error(formatError(`spec worktree is dirty: ${existingWorktree}. Commit or stash before closing, or pass --discard intentionally.`));
247
341
  }
@@ -296,6 +390,9 @@ function closeSpecWorktree(repoRoot, specInput, options = {}) {
296
390
  removed: true,
297
391
  worktreePath: existingWorktree,
298
392
  };
393
+ } finally {
394
+ releaseLock(lock);
395
+ }
299
396
  }
300
397
 
301
398
  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
+