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.
- package/CHANGELOG.md +52 -0
- package/README.md +65 -25
- package/README_FOR_AI.md +36 -29
- package/ROADMAP.md +22 -3
- package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
- package/docs/COMMANDS.md.template +53 -20
- package/docs/STATUS.md.template +5 -1
- package/docs/WORKFLOW.md.template +13 -11
- package/package.json +10 -3
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EVIDENCE_REPORT.md +293 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EXECUTION_PLAN.md +58 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/SPEC.md +242 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/STATUS.md +35 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/pr.md +77 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/slice.json +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/CLOSURE_BRIEF.md +43 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/slice.json +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/slice.json +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/slice.json +55 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/CLOSURE_BRIEF.md +39 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/slice.json +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/CLOSURE_BRIEF.md +39 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/slice.json +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/slice.json +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/EVIDENCE_REPORT.md +208 -0
- package/specs/quiver-v26-0121-smoke-hardening/EXECUTION_PLAN.md +57 -0
- package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +137 -0
- package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +32 -0
- package/specs/quiver-v26-0121-smoke-hardening/pr.md +96 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/slice.json +73 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/slice.json +76 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/slice.json +75 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/slice.json +77 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/slice.json +77 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/slice.json +84 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/slice.json +82 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/slice.json +92 -0
- 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 +652 -27
- package/src/create-quiver/commands/flow.js +58 -9
- package/src/create-quiver/commands/graph.js +11 -9
- package/src/create-quiver/commands/plan.js +7 -16
- package/src/create-quiver/commands/spec.js +282 -0
- package/src/create-quiver/index.js +409 -31
- package/src/create-quiver/lib/actionable-error.js +27 -0
- package/src/create-quiver/lib/agent-profiles.js +16 -4
- package/src/create-quiver/lib/ai/artifacts.js +318 -0
- package/src/create-quiver/lib/ai/context-packs.js +4 -0
- package/src/create-quiver/lib/ai/execution-plan.js +16 -1
- package/src/create-quiver/lib/ai/executor.js +272 -21
- package/src/create-quiver/lib/ai/export-state.js +679 -0
- package/src/create-quiver/lib/ai/github.js +162 -2
- package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
- package/src/create-quiver/lib/ai/plan-review.js +7 -2
- package/src/create-quiver/lib/ai/providers.js +4 -3
- package/src/create-quiver/lib/ai/run-state.js +414 -0
- package/src/create-quiver/lib/ai/spec-generator.js +84 -13
- package/src/create-quiver/lib/ai/spec-templates.js +150 -21
- package/src/create-quiver/lib/analyze.js +2 -2
- package/src/create-quiver/lib/approvals.js +36 -5
- package/src/create-quiver/lib/demo.js +189 -14
- package/src/create-quiver/lib/doctor.js +154 -0
- package/src/create-quiver/lib/git.js +40 -1
- package/src/create-quiver/lib/handoff.js +123 -12
- package/src/create-quiver/lib/init-docs.js +35 -13
- package/src/create-quiver/lib/init-layout.js +9 -0
- package/src/create-quiver/lib/json.js +53 -3
- 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 +66 -10
- package/src/create-quiver/lib/scope.js +52 -8
- package/src/create-quiver/lib/slice-graph.js +138 -38
- package/src/create-quiver/lib/slice.js +14 -5
- package/src/create-quiver/lib/spec-worktrees.js +129 -32
- 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
|
|
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
|
-
|
|
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
|
+
};
|
|
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
|
-
|
|
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 (
|
|
206
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
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
|
+
|