create-quiver 0.12.1 → 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.
- package/CHANGELOG.md +8 -0
- package/README.md +16 -8
- package/README_FOR_AI.md +11 -6
- package/ROADMAP.md +9 -2
- package/docs/COMMANDS.md.template +9 -2
- 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/src/create-quiver/commands/ai.js +84 -9
- 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 +282 -0
- package/src/create-quiver/index.js +83 -21
- 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/execution-plan.js +9 -0
- package/src/create-quiver/lib/ai/executor.js +3 -2
- package/src/create-quiver/lib/ai/export-state.js +242 -97
- package/src/create-quiver/lib/ai/github.js +80 -3
- package/src/create-quiver/lib/ai/plan-review.js +2 -0
- package/src/create-quiver/lib/ai/spec-generator.js +72 -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 +236 -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 +121 -38
- package/src/create-quiver/lib/statuses.js +115 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { parseJsonWithComments } = require('./json');
|
|
4
|
-
const { normalizeGitBashDrivePath, relativePosixPath, resolveTargetRoot, specRelativePathFromPath, toPosixPath } = require('./paths');
|
|
4
|
+
const { assertPathInsideRoot, normalizeGitBashDrivePath, relativePosixPath, resolveTargetRoot, specRelativePathFromPath, toPosixPath } = require('./paths');
|
|
5
5
|
|
|
6
6
|
function readJson(filePath) {
|
|
7
7
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -89,9 +89,12 @@ function validateSliceMetaForStart(slice) {
|
|
|
89
89
|
throw new Error(`create-quiver: git.branch_type invalido: "${slice.branchType}". Usa "feature", "bugfix" o "hotfix".`);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(slice.baseBranch)
|
|
93
|
+
|| slice.baseBranch.includes('..')
|
|
94
|
+
|| slice.baseBranch.startsWith('/')
|
|
95
|
+
|| slice.baseBranch.endsWith('/')
|
|
96
|
+
|| slice.baseBranch.includes('\\')) {
|
|
97
|
+
throw new Error('create-quiver: git.base_branch invalido. Usa una rama base valida como "main", "develop", "master" o "release/2026".');
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
const expectedBranchName = `${slice.branchType}/${slice.ticket}-${slice.branchSlug}`;
|
|
@@ -116,6 +119,7 @@ function resolveRepoSlicePath(repoRoot, relSlicePath) {
|
|
|
116
119
|
function resolveSliceContext(repoRoot, slicePath) {
|
|
117
120
|
const canonicalRepoRoot = canonicalizePath(repoRoot);
|
|
118
121
|
let absSlicePath = resolveSlicePath(slicePath);
|
|
122
|
+
assertPathInsideRoot(canonicalRepoRoot, absSlicePath, 'slice path');
|
|
119
123
|
let relSlicePath = relativePosixPath(canonicalRepoRoot, absSlicePath);
|
|
120
124
|
let parts = relSlicePath.split('/');
|
|
121
125
|
|
|
@@ -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
|
|
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,61 +220,82 @@ 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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
+
};
|
|
184
244
|
}
|
|
185
|
-
return {
|
|
186
|
-
...identity,
|
|
187
|
-
baseRef,
|
|
188
|
-
dryRun: options.dryRun === true,
|
|
189
|
-
reused: true,
|
|
190
|
-
slice00,
|
|
191
|
-
worktreePath: existingWorktree,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
245
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
246
|
+
if (currentIsLinkedWorktree) {
|
|
247
|
+
throw new Error(formatError(recoveryForNestedWorktree(identity.branchName)));
|
|
248
|
+
}
|
|
198
249
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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.`));
|
|
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
|
+
}
|
|
202
277
|
|
|
203
|
-
if (options.dryRun === true) {
|
|
204
278
|
return {
|
|
205
279
|
...identity,
|
|
206
280
|
baseRef,
|
|
207
281
|
currentBranch: currentBranch(repoRoot),
|
|
208
|
-
dryRun:
|
|
282
|
+
dryRun: false,
|
|
209
283
|
reused: false,
|
|
210
284
|
slice00,
|
|
211
285
|
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
worktreePrune(repoRoot);
|
|
215
|
-
fs.mkdirSync(path.dirname(identity.worktreePath), { recursive: true });
|
|
286
|
+
};
|
|
216
287
|
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
} else {
|
|
220
|
-
worktreeAdd(repoRoot, identity.worktreePath, baseRef, { branch: identity.branchName });
|
|
288
|
+
if (options.dryRun === true) {
|
|
289
|
+
return run();
|
|
221
290
|
}
|
|
222
291
|
|
|
223
|
-
return {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
};
|
|
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);
|
|
231
299
|
}
|
|
232
300
|
|
|
233
301
|
function formatSpecStartResult(result) {
|
|
@@ -245,6 +313,14 @@ function formatSpecStartResult(result) {
|
|
|
245
313
|
function closeSpecWorktree(repoRoot, specInput, options = {}) {
|
|
246
314
|
const specDir = findSpecDir(repoRoot, specInput);
|
|
247
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 {
|
|
248
324
|
const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
|
|
249
325
|
const discard = options.discard === true;
|
|
250
326
|
const dryRun = options.dryRun === true;
|
|
@@ -256,6 +332,10 @@ function closeSpecWorktree(repoRoot, specInput, options = {}) {
|
|
|
256
332
|
throw new Error(formatError(`missing spec worktree for branch ${identity.branchName}.`));
|
|
257
333
|
}
|
|
258
334
|
|
|
335
|
+
if (!fs.existsSync(existingWorktree) || !isGitWorktree(existingWorktree)) {
|
|
336
|
+
throw new Error(formatError(recoveryForMissingWorktree(identity.branchName, existingWorktree)));
|
|
337
|
+
}
|
|
338
|
+
|
|
259
339
|
if (!discard && !isCleanWorktree(existingWorktree)) {
|
|
260
340
|
throw new Error(formatError(`spec worktree is dirty: ${existingWorktree}. Commit or stash before closing, or pass --discard intentionally.`));
|
|
261
341
|
}
|
|
@@ -310,6 +390,9 @@ function closeSpecWorktree(repoRoot, specInput, options = {}) {
|
|
|
310
390
|
removed: true,
|
|
311
391
|
worktreePath: existingWorktree,
|
|
312
392
|
};
|
|
393
|
+
} finally {
|
|
394
|
+
releaseLock(lock);
|
|
395
|
+
}
|
|
313
396
|
}
|
|
314
397
|
|
|
315
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
|
+
|