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.
- package/CHANGELOG.md +27 -0
- package/README.md +24 -9
- package/README_FOR_AI.md +15 -6
- package/ROADMAP.md +15 -2
- package/docs/COMMANDS.md.template +12 -3
- package/docs/TROUBLESHOOTING.md.template +29 -0
- package/docs/WORKFLOW.md.template +13 -12
- package/package.json +2 -1
- package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +2 -2
- package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +5 -5
- package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
- package/src/create-quiver/commands/ai.js +563 -21
- package/src/create-quiver/commands/flow.js +52 -4
- package/src/create-quiver/commands/graph.js +7 -7
- package/src/create-quiver/commands/plan.js +6 -15
- package/src/create-quiver/commands/spec.js +292 -0
- package/src/create-quiver/index.js +125 -25
- package/src/create-quiver/lib/agent-profiles.js +15 -3
- package/src/create-quiver/lib/ai/artifacts.js +318 -0
- package/src/create-quiver/lib/ai/context-packs.js +2 -2
- package/src/create-quiver/lib/ai/execution-plan.js +9 -0
- package/src/create-quiver/lib/ai/executor.js +3 -2
- package/src/create-quiver/lib/ai/export-state.js +287 -95
- package/src/create-quiver/lib/ai/github.js +93 -4
- package/src/create-quiver/lib/ai/plan-review.js +161 -0
- package/src/create-quiver/lib/ai/run-state.js +17 -2
- package/src/create-quiver/lib/ai/spec-generator.js +87 -13
- package/src/create-quiver/lib/ai/spec-templates.js +72 -12
- package/src/create-quiver/lib/analyze.js +2 -2
- package/src/create-quiver/lib/approvals.js +14 -2
- package/src/create-quiver/lib/doctor.js +79 -0
- package/src/create-quiver/lib/git.js +40 -1
- package/src/create-quiver/lib/handoff.js +43 -1
- package/src/create-quiver/lib/init-docs.js +11 -7
- package/src/create-quiver/lib/init-layout.js +1 -0
- package/src/create-quiver/lib/lifecycle.js +52 -3
- package/src/create-quiver/lib/locks.js +134 -0
- package/src/create-quiver/lib/package-safety.js +7 -0
- package/src/create-quiver/lib/paths.js +74 -0
- package/src/create-quiver/lib/project-scan.js +74 -0
- package/src/create-quiver/lib/project-state-resolver.js +430 -0
- package/src/create-quiver/lib/readiness.js +48 -7
- package/src/create-quiver/lib/scope.js +2 -1
- package/src/create-quiver/lib/slice.js +8 -4
- package/src/create-quiver/lib/spec-worktrees.js +169 -38
- 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
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
294
|
+
if (currentIsLinkedWorktree) {
|
|
295
|
+
throw new Error(formatError(recoveryForNestedWorktree(identity.branchName)));
|
|
296
|
+
}
|
|
198
297
|
|
|
199
|
-
|
|
200
|
-
|
|
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:
|
|
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 (
|
|
218
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
|