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
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
const cp = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
2
4
|
|
|
3
5
|
function runGit(args, cwd, options = {}) {
|
|
4
6
|
return cp.execFileSync('git', args, {
|
|
@@ -98,6 +100,9 @@ function currentBranch(repoRoot) {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
function statusPorcelain(repoRoot) {
|
|
103
|
+
if (!repoRoot || !fs.existsSync(repoRoot)) {
|
|
104
|
+
return '__MISSING_WORKTREE__';
|
|
105
|
+
}
|
|
101
106
|
return tryGit(['status', '--porcelain'], repoRoot);
|
|
102
107
|
}
|
|
103
108
|
|
|
@@ -111,7 +116,37 @@ function hasRemote(repoRoot, remoteName = 'origin') {
|
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
function isCleanWorktree(repoRoot) {
|
|
114
|
-
return statusPorcelain(repoRoot) === '';
|
|
119
|
+
return Boolean(repoRoot && fs.existsSync(repoRoot) && isGitWorktree(repoRoot) && statusPorcelain(repoRoot) === '');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function isGitWorktree(repoRoot) {
|
|
123
|
+
return tryGit(['rev-parse', '--is-inside-work-tree'], repoRoot) === 'true';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function absoluteGitDir(repoRoot) {
|
|
127
|
+
return tryGit(['rev-parse', '--absolute-git-dir'], repoRoot);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function gitCommonDir(repoRoot) {
|
|
131
|
+
const value = tryGit(['rev-parse', '--git-common-dir'], repoRoot);
|
|
132
|
+
if (!value) {
|
|
133
|
+
return '';
|
|
134
|
+
}
|
|
135
|
+
return path.isAbsolute(value) ? path.resolve(value) : path.resolve(repoRoot, value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function realpathOrResolve(value) {
|
|
139
|
+
try {
|
|
140
|
+
return fs.realpathSync(value);
|
|
141
|
+
} catch {
|
|
142
|
+
return path.resolve(value);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isLinkedWorktree(repoRoot) {
|
|
147
|
+
const gitDir = absoluteGitDir(repoRoot);
|
|
148
|
+
const commonDir = gitCommonDir(repoRoot);
|
|
149
|
+
return Boolean(gitDir && commonDir && realpathOrResolve(gitDir) !== realpathOrResolve(commonDir));
|
|
115
150
|
}
|
|
116
151
|
|
|
117
152
|
function isDetachedHead(repoRoot) {
|
|
@@ -161,8 +196,12 @@ module.exports = {
|
|
|
161
196
|
lsRemoteHeads,
|
|
162
197
|
mergeBaseIsAncestor,
|
|
163
198
|
hasRemote,
|
|
199
|
+
absoluteGitDir,
|
|
200
|
+
gitCommonDir,
|
|
164
201
|
isCleanWorktree,
|
|
165
202
|
isDetachedHead,
|
|
203
|
+
isGitWorktree,
|
|
204
|
+
isLinkedWorktree,
|
|
166
205
|
revListCount,
|
|
167
206
|
remoteList,
|
|
168
207
|
runGit,
|
|
@@ -113,6 +113,46 @@ function validateBriefSections(text, kind) {
|
|
|
113
113
|
.map((group) => group.label);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
function headingGroupsForKind(kind) {
|
|
117
|
+
if (kind === 'handoff') {
|
|
118
|
+
return REQUIRED_HEADINGS.map((heading) => ({
|
|
119
|
+
label: heading.replace(/^##\s+/, '').toLowerCase(),
|
|
120
|
+
alternatives: [heading],
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
return kind === 'closure-brief' ? CLOSURE_BRIEF_REQUIRED_HEADINGS : EXECUTION_BRIEF_REQUIRED_HEADINGS;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatAliasGuidance(kind) {
|
|
127
|
+
return headingGroupsForKind(kind)
|
|
128
|
+
.map((group) => `- ${group.label}: ${group.alternatives.join(' | ')}`)
|
|
129
|
+
.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function canonicalHeadingForGroup(group) {
|
|
133
|
+
return group.alternatives[0] || `## ${group.label}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatMinimalTemplate(kind) {
|
|
137
|
+
const lines = [];
|
|
138
|
+
for (const group of headingGroupsForKind(kind)) {
|
|
139
|
+
lines.push(canonicalHeadingForGroup(group), '', 'TODO', '');
|
|
140
|
+
}
|
|
141
|
+
return lines.join('\n').trimEnd();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function formatMissingSectionsError(resolved, missingSections) {
|
|
145
|
+
return [
|
|
146
|
+
`create-quiver: ${resolved.label.toLowerCase()} is missing required sections: ${missingSections.join(', ')}`,
|
|
147
|
+
'',
|
|
148
|
+
'Accepted headings/aliases:',
|
|
149
|
+
formatAliasGuidance(resolved.kind),
|
|
150
|
+
'',
|
|
151
|
+
'Minimal template:',
|
|
152
|
+
formatMinimalTemplate(resolved.kind),
|
|
153
|
+
].join('\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
116
156
|
function checkHandoff(handoffInput, repoRoot = process.cwd()) {
|
|
117
157
|
const resolved = resolveHandoffPath(repoRoot, handoffInput);
|
|
118
158
|
|
|
@@ -125,7 +165,7 @@ function checkHandoff(handoffInput, repoRoot = process.cwd()) {
|
|
|
125
165
|
? validateHandoffSections(text)
|
|
126
166
|
: validateBriefSections(text, resolved.kind);
|
|
127
167
|
if (missingSections.length > 0) {
|
|
128
|
-
throw new Error(
|
|
168
|
+
throw new Error(formatMissingSectionsError(resolved, missingSections));
|
|
129
169
|
}
|
|
130
170
|
|
|
131
171
|
return resolved;
|
|
@@ -165,6 +205,8 @@ module.exports = {
|
|
|
165
205
|
EXECUTION_BRIEF_REQUIRED_HEADINGS,
|
|
166
206
|
REQUIRED_HEADINGS,
|
|
167
207
|
checkHandoff,
|
|
208
|
+
formatAliasGuidance,
|
|
209
|
+
formatMinimalTemplate,
|
|
168
210
|
readHandoffSections,
|
|
169
211
|
scaffoldHandoff,
|
|
170
212
|
resolveHandoffPath,
|
|
@@ -1092,6 +1092,15 @@ function installSelfAsDevDep(projectRoot, version) {
|
|
|
1092
1092
|
return 'skipped-already-present';
|
|
1093
1093
|
}
|
|
1094
1094
|
|
|
1095
|
+
try {
|
|
1096
|
+
execSync(formatInstallSelfCommand(projectRoot, version), { cwd: projectRoot, stdio: 'inherit' });
|
|
1097
|
+
return 'installed';
|
|
1098
|
+
} catch {
|
|
1099
|
+
return 'failed';
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function formatInstallSelfCommand(projectRoot, version) {
|
|
1095
1104
|
const pm = detectPackageManager(projectRoot);
|
|
1096
1105
|
const commands = {
|
|
1097
1106
|
npm: `npm install -D create-quiver@${version}`,
|
|
@@ -1099,13 +1108,7 @@ function installSelfAsDevDep(projectRoot, version) {
|
|
|
1099
1108
|
pnpm: `pnpm add -D create-quiver@${version}`,
|
|
1100
1109
|
bun: `bun add -d create-quiver@${version}`,
|
|
1101
1110
|
};
|
|
1102
|
-
|
|
1103
|
-
try {
|
|
1104
|
-
execSync(commands[pm], { cwd: projectRoot, stdio: 'inherit' });
|
|
1105
|
-
return 'installed';
|
|
1106
|
-
} catch {
|
|
1107
|
-
return 'failed';
|
|
1108
|
-
}
|
|
1111
|
+
return commands[pm] || commands.npm;
|
|
1109
1112
|
}
|
|
1110
1113
|
|
|
1111
1114
|
function normalizeSkippedReason(reason) {
|
|
@@ -1253,5 +1256,6 @@ module.exports = {
|
|
|
1253
1256
|
writeFrontMatter,
|
|
1254
1257
|
toProjectSlug,
|
|
1255
1258
|
detectPackageManager,
|
|
1259
|
+
formatInstallSelfCommand,
|
|
1256
1260
|
installSelfAsDevDep,
|
|
1257
1261
|
};
|
|
@@ -209,6 +209,7 @@ function resolveInitPackageScripts(profile, options = {}) {
|
|
|
209
209
|
'quiver:spec:create': 'npx create-quiver spec create',
|
|
210
210
|
'quiver:spec:start': 'npx create-quiver spec start',
|
|
211
211
|
'quiver:spec:status': 'npx create-quiver spec status',
|
|
212
|
+
'quiver:spec:validate': 'npx create-quiver spec validate',
|
|
212
213
|
'quiver:spec:close': 'npx create-quiver spec close',
|
|
213
214
|
'quiver:start-slice': 'npx create-quiver start-slice',
|
|
214
215
|
'quiver:check-slice': 'npx create-quiver check-slice',
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const { branchDelete, catFileExists, currentBranch, fetchBranch, fetchRemote, hasLocalBranch, hasRemoteBranch, lsRemoteHeads, mergeBaseIsAncestor, revListCount, runGit, statusPorcelain, worktreeAdd, worktreeList, worktreePrune, worktreeRemove } = require('./git');
|
|
3
|
+
const { branchDelete, catFileExists, currentBranch, fetchBranch, fetchRemote, hasLocalBranch, hasRemoteBranch, isGitWorktree, isLinkedWorktree, lsRemoteHeads, mergeBaseIsAncestor, revListCount, runGit, statusPorcelain, worktreeAdd, worktreeList, worktreePrune, worktreeRemove } = require('./git');
|
|
4
4
|
const { parseJsonWithComments } = require('./json');
|
|
5
5
|
const { writeFrontMatter } = require('./init-docs');
|
|
6
|
+
const { withLockSync } = require('./locks');
|
|
6
7
|
const { relativePosixPath, resolveTargetRoot } = require('./paths');
|
|
7
8
|
const { ensureSpecSliceZeroComplete } = require('./spec-worktrees');
|
|
8
9
|
const { activeSlicePath, renderActiveSlice, resolveSliceContext, safeBranchName, toAlias, validateSliceMetaForStart, worktreesRootForRepo } = require('./slice');
|
|
@@ -295,6 +296,35 @@ function findExistingWorktreeForBranch(repoRoot, branchName) {
|
|
|
295
296
|
return '';
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
function sameRealPath(left, right) {
|
|
300
|
+
try {
|
|
301
|
+
return fs.realpathSync(left) === fs.realpathSync(right);
|
|
302
|
+
} catch {
|
|
303
|
+
return path.resolve(left) === path.resolve(right);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function formatMissingSliceWorktree(branchName, worktreePath) {
|
|
308
|
+
return [
|
|
309
|
+
`create-quiver: registered slice worktree is missing or stale for ${branchName}: ${worktreePath}`,
|
|
310
|
+
'Recovery:',
|
|
311
|
+
'- Run `git worktree prune` from the main checkout, then retry the slice command.',
|
|
312
|
+
'- If the directory was moved manually, restore it or remove the stale git worktree registration intentionally.',
|
|
313
|
+
'- Do not create a nested replacement worktree from inside another worktree.',
|
|
314
|
+
].join('\n');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatNestedSliceWorktree(branchName, existingWorktreePath = '') {
|
|
318
|
+
return [
|
|
319
|
+
`create-quiver: refusing to create a slice worktree from inside a linked worktree for ${branchName}.`,
|
|
320
|
+
'Recovery:',
|
|
321
|
+
existingWorktreePath
|
|
322
|
+
? `- Use the existing worktree: ${existingWorktreePath}`
|
|
323
|
+
: '- Return to the main checkout and rerun the command.',
|
|
324
|
+
'- This prevents nested .worktrees paths and conflicting slice worktrees.',
|
|
325
|
+
].join('\n');
|
|
326
|
+
}
|
|
327
|
+
|
|
298
328
|
function startSlice(sliceInput, options = {}) {
|
|
299
329
|
const allowDraft = options.allowDraft === true || process.env.ALLOW_DRAFT_SLICE === '1';
|
|
300
330
|
const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
|
|
@@ -323,13 +353,25 @@ function startSlice(sliceInput, options = {}) {
|
|
|
323
353
|
console.log('WARN: bootstrap intencional para un slice en draft.');
|
|
324
354
|
}
|
|
325
355
|
|
|
356
|
+
return withLockSync(repoRoot, `slice-worktree-${slice.branchName}`, {
|
|
357
|
+
command: 'start-slice',
|
|
358
|
+
metadata: {
|
|
359
|
+
branch: slice.branchName,
|
|
360
|
+
slice: slice.sliceRel,
|
|
361
|
+
},
|
|
362
|
+
}, () => {
|
|
326
363
|
const worktreesRoot = worktreesRootForRepo(repoRoot, slice.branchName);
|
|
327
364
|
const worktreePath = path.join(worktreesRoot, safeBranchName(slice.branchName));
|
|
328
365
|
const existingWorktreePath = findExistingWorktreeForBranch(repoRoot, slice.branchName);
|
|
329
366
|
|
|
330
|
-
|
|
367
|
+
if (existingWorktreePath && (!fs.existsSync(existingWorktreePath) || !isGitWorktree(existingWorktreePath))) {
|
|
368
|
+
throw new Error(formatMissingSliceWorktree(slice.branchName, existingWorktreePath));
|
|
369
|
+
}
|
|
331
370
|
|
|
332
|
-
if (existingWorktreePath
|
|
371
|
+
if (existingWorktreePath) {
|
|
372
|
+
if (isLinkedWorktree(repoRoot) && !sameRealPath(repoRoot, existingWorktreePath)) {
|
|
373
|
+
throw new Error(formatNestedSliceWorktree(slice.branchName, existingWorktreePath));
|
|
374
|
+
}
|
|
333
375
|
writeWorktreeContext(existingWorktreePath, slice, slice.branchName);
|
|
334
376
|
const activeSlice = writeActiveSlice(repoRoot, slice);
|
|
335
377
|
if (activeSlice.replaced) {
|
|
@@ -349,6 +391,12 @@ function startSlice(sliceInput, options = {}) {
|
|
|
349
391
|
return { worktreePath: existingWorktreePath, reused: true };
|
|
350
392
|
}
|
|
351
393
|
|
|
394
|
+
if (isLinkedWorktree(repoRoot)) {
|
|
395
|
+
throw new Error(formatNestedSliceWorktree(slice.branchName));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
worktreePrune(repoRoot);
|
|
399
|
+
|
|
352
400
|
if (fs.existsSync(worktreePath) && !fs.existsSync(path.join(worktreePath, '.git'))) {
|
|
353
401
|
throw new Error(`create-quiver: la ruta '${worktreePath}' ya existe y no parece un worktree git.`);
|
|
354
402
|
}
|
|
@@ -395,6 +443,7 @@ function startSlice(sliceInput, options = {}) {
|
|
|
395
443
|
console.log(`Worktree: ${worktreePath}`);
|
|
396
444
|
console.log(`Contexto: ${worktreePath}/WORKTREE_CONTEXT.md`);
|
|
397
445
|
return { worktreePath, reused: false };
|
|
446
|
+
});
|
|
398
447
|
}
|
|
399
448
|
|
|
400
449
|
function cleanupSlice(sliceInput, options = {}) {
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const { execFileSync } = require('node:child_process');
|
|
5
|
+
|
|
6
|
+
const { quiverInternalPaths } = require('./init-layout');
|
|
7
|
+
|
|
8
|
+
function formatError(message) {
|
|
9
|
+
return `create-quiver: ${message}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toRelativePosix(root, filePath) {
|
|
13
|
+
return path.relative(root, filePath).split(path.sep).join('/');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sanitizeLockName(value) {
|
|
17
|
+
return String(value || '')
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
20
|
+
.replace(/^-+|-+$/g, '') || 'operation';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function lockPath(projectRoot, lockName) {
|
|
24
|
+
return path.join(quiverInternalPaths(projectRoot).locksDir, `${sanitizeLockName(lockName)}.lock`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readLock(projectRoot, lockName) {
|
|
28
|
+
const filePath = lockPath(projectRoot, lockName);
|
|
29
|
+
if (!fs.existsSync(filePath)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
34
|
+
} catch {
|
|
35
|
+
return {
|
|
36
|
+
schema_version: 1,
|
|
37
|
+
lock_name: sanitizeLockName(lockName),
|
|
38
|
+
command: 'unknown',
|
|
39
|
+
created_at: 'unknown',
|
|
40
|
+
pid: 'unknown',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function appendUniqueLine(filePath, line) {
|
|
46
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
47
|
+
const current = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '';
|
|
48
|
+
const lines = current.split(/\r?\n/);
|
|
49
|
+
if (!lines.includes(line)) {
|
|
50
|
+
const prefix = current.endsWith('\n') || current.length === 0 ? current : `${current}\n`;
|
|
51
|
+
fs.writeFileSync(filePath, `${prefix}${line}\n`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureQuiverStateIgnored(projectRoot) {
|
|
56
|
+
try {
|
|
57
|
+
const gitDir = execFileSync('git', ['rev-parse', '--absolute-git-dir'], {
|
|
58
|
+
cwd: projectRoot,
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
61
|
+
}).trim();
|
|
62
|
+
if (gitDir) {
|
|
63
|
+
appendUniqueLine(path.join(gitDir, 'info', 'exclude'), '.quiver/');
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// Non-git fixtures can still use filesystem locks.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function acquireLock(projectRoot, lockName, options = {}) {
|
|
71
|
+
const filePath = lockPath(projectRoot, lockName);
|
|
72
|
+
const payload = {
|
|
73
|
+
schema_version: 1,
|
|
74
|
+
lock_name: sanitizeLockName(lockName),
|
|
75
|
+
pid: process.pid,
|
|
76
|
+
hostname: os.hostname(),
|
|
77
|
+
command: options.command || 'unknown',
|
|
78
|
+
created_at: (options.now || new Date()).toISOString(),
|
|
79
|
+
metadata: options.metadata || {},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
ensureQuiverStateIgnored(projectRoot);
|
|
83
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, { flag: 'wx' });
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (error.code === 'EEXIST') {
|
|
89
|
+
const existing = readLock(projectRoot, lockName);
|
|
90
|
+
throw new Error(formatError(`operation is locked: ${toRelativePosix(projectRoot, filePath)}\nLock owner: pid=${existing?.pid || 'unknown'} command=${existing?.command || 'unknown'} created_at=${existing?.created_at || 'unknown'}\nIf this process is gone, inspect the lock and remove it intentionally.`));
|
|
91
|
+
}
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
filePath,
|
|
97
|
+
lock: payload,
|
|
98
|
+
lockName: sanitizeLockName(lockName),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function releaseLock(handle) {
|
|
103
|
+
if (handle?.filePath && fs.existsSync(handle.filePath)) {
|
|
104
|
+
fs.rmSync(handle.filePath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function withLockSync(projectRoot, lockName, options, callback) {
|
|
109
|
+
const handle = acquireLock(projectRoot, lockName, options);
|
|
110
|
+
try {
|
|
111
|
+
return callback(handle);
|
|
112
|
+
} finally {
|
|
113
|
+
releaseLock(handle);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function withLock(projectRoot, lockName, options, callback) {
|
|
118
|
+
const handle = acquireLock(projectRoot, lockName, options);
|
|
119
|
+
try {
|
|
120
|
+
return await callback(handle);
|
|
121
|
+
} finally {
|
|
122
|
+
releaseLock(handle);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = {
|
|
127
|
+
acquireLock,
|
|
128
|
+
lockPath,
|
|
129
|
+
readLock,
|
|
130
|
+
releaseLock,
|
|
131
|
+
sanitizeLockName,
|
|
132
|
+
withLock,
|
|
133
|
+
withLockSync,
|
|
134
|
+
};
|
|
@@ -13,6 +13,12 @@ const SAFETY_RULES = [
|
|
|
13
13
|
return /(^|\/)\.npmrc$/.test(relativePath) || /(^|\/)\.npm(\/|$)/.test(relativePath);
|
|
14
14
|
},
|
|
15
15
|
},
|
|
16
|
+
{
|
|
17
|
+
code: 'ai-raw-artifact',
|
|
18
|
+
match(relativePath) {
|
|
19
|
+
return /(^|\/)\.quiver\/runs\/[^/]+\/raw(\/|$)/.test(relativePath);
|
|
20
|
+
},
|
|
21
|
+
},
|
|
16
22
|
{
|
|
17
23
|
code: 'ai-tool-state',
|
|
18
24
|
match(relativePath) {
|
|
@@ -80,6 +86,7 @@ function collectPackageSafetyViolations(paths) {
|
|
|
80
86
|
code: rule.code,
|
|
81
87
|
path: normalizedPath,
|
|
82
88
|
});
|
|
89
|
+
break;
|
|
83
90
|
}
|
|
84
91
|
}
|
|
85
92
|
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
function formatError(message) {
|
|
5
|
+
return `create-quiver: ${message}`;
|
|
6
|
+
}
|
|
2
7
|
|
|
3
8
|
function resolveTargetRoot(cwd, targetDir, pathLib = path) {
|
|
4
9
|
return pathLib.resolve(cwd, targetDir);
|
|
@@ -71,10 +76,79 @@ function specRelativePathFromPath(filePath, pathLib = path) {
|
|
|
71
76
|
return parts.slice(specIndex).join('/');
|
|
72
77
|
}
|
|
73
78
|
|
|
79
|
+
function realpathOrResolve(filePath, pathLib = path) {
|
|
80
|
+
try {
|
|
81
|
+
return pathLib.resolve(fs.realpathSync(filePath));
|
|
82
|
+
} catch {
|
|
83
|
+
return pathLib.resolve(normalizeGitBashDrivePath(filePath, pathLib));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isPathInsideRoot(root, target, pathLib = path) {
|
|
88
|
+
const rootPath = realpathOrResolve(root, pathLib);
|
|
89
|
+
const targetPath = realpathOrResolve(target, pathLib);
|
|
90
|
+
const windowsPath = pathLib === path.win32 || process.platform === 'win32';
|
|
91
|
+
const comparableRoot = windowsPath ? rootPath.toLowerCase() : rootPath;
|
|
92
|
+
const comparableTarget = windowsPath ? targetPath.toLowerCase() : targetPath;
|
|
93
|
+
|
|
94
|
+
if (comparableTarget === comparableRoot) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const relative = pathLib.relative(comparableRoot, comparableTarget);
|
|
99
|
+
return Boolean(relative && !relative.startsWith('..') && !pathLib.isAbsolute(relative));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assertPathInsideRoot(root, target, label = 'path', pathLib = path) {
|
|
103
|
+
if (!isPathInsideRoot(root, target, pathLib)) {
|
|
104
|
+
throw new Error(formatError(`${label} must stay inside the project root: ${toPosixPath(target, pathLib)}`));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getProjectRelativePathIssue(filePath, pathLib = path) {
|
|
109
|
+
const original = String(filePath || '').trim();
|
|
110
|
+
if (!original) {
|
|
111
|
+
return 'empty-path';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (/^file:/i.test(original)) {
|
|
115
|
+
return 'file-url';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const normalized = toPosixPath(normalizeGitBashDrivePath(original, pathLib), pathLib);
|
|
119
|
+
if (normalized.startsWith('/') || /^[A-Za-z]:\//.test(normalized) || pathLib.isAbsolute(original)) {
|
|
120
|
+
return 'absolute-path';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const segments = normalized.split('/').filter(Boolean);
|
|
124
|
+
if (segments.some((segment) => segment === '..')) {
|
|
125
|
+
return 'path-traversal';
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validateProjectRelativePath(filePath, fieldName = 'path', pathLib = path) {
|
|
132
|
+
const issue = getProjectRelativePathIssue(filePath, pathLib);
|
|
133
|
+
if (issue) {
|
|
134
|
+
throw new Error(formatError(`${fieldName} must be a project-relative path without traversal (got ${String(filePath || '<empty>')}; issue=${issue}).`));
|
|
135
|
+
}
|
|
136
|
+
return toPosixPath(normalizeGitBashDrivePath(String(filePath).trim(), pathLib), pathLib);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateProjectRelativePaths(paths, fieldName = 'paths', pathLib = path) {
|
|
140
|
+
return (Array.isArray(paths) ? paths : []).map((filePath) => validateProjectRelativePath(filePath, fieldName, pathLib));
|
|
141
|
+
}
|
|
142
|
+
|
|
74
143
|
module.exports = {
|
|
144
|
+
assertPathInsideRoot,
|
|
145
|
+
getProjectRelativePathIssue,
|
|
146
|
+
isPathInsideRoot,
|
|
75
147
|
normalizeGitBashDrivePath,
|
|
76
148
|
relativePosixPath,
|
|
77
149
|
resolveTargetRoot,
|
|
78
150
|
specRelativePathFromPath,
|
|
79
151
|
toPosixPath,
|
|
152
|
+
validateProjectRelativePath,
|
|
153
|
+
validateProjectRelativePaths,
|
|
80
154
|
};
|
|
@@ -49,6 +49,79 @@ function readProjectScanArtifact(projectRoot) {
|
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
function statIso(filePath) {
|
|
53
|
+
try {
|
|
54
|
+
return fs.statSync(filePath).mtime.toISOString();
|
|
55
|
+
} catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readProjectScanStatus(projectRoot) {
|
|
61
|
+
const { currentScanPath, legacyScanPath, projectMapPath } = projectScanPaths(projectRoot);
|
|
62
|
+
const projectMapExists = fs.existsSync(projectMapPath);
|
|
63
|
+
let artifact = null;
|
|
64
|
+
let artifactError = '';
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
artifact = readProjectScanArtifact(projectRoot);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
artifactError = error.message;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const scanPath = artifact?.path || (fs.existsSync(currentScanPath) ? currentScanPath : fs.existsSync(legacyScanPath) ? legacyScanPath : '');
|
|
73
|
+
const source = artifact?.source || (artifactError ? 'invalid' : 'missing');
|
|
74
|
+
const scanUpdatedAt = scanPath ? statIso(scanPath) : null;
|
|
75
|
+
const projectMapUpdatedAt = projectMapExists ? statIso(projectMapPath) : null;
|
|
76
|
+
const stale = Boolean(
|
|
77
|
+
scanUpdatedAt
|
|
78
|
+
&& projectMapUpdatedAt
|
|
79
|
+
&& Date.parse(projectMapUpdatedAt) + 1000 < Date.parse(scanUpdatedAt),
|
|
80
|
+
);
|
|
81
|
+
let status = 'missing';
|
|
82
|
+
|
|
83
|
+
if (artifactError) {
|
|
84
|
+
status = 'invalid';
|
|
85
|
+
} else if (artifact && projectMapExists && stale) {
|
|
86
|
+
status = 'stale';
|
|
87
|
+
} else if (artifact && projectMapExists && source === 'current') {
|
|
88
|
+
status = 'fresh';
|
|
89
|
+
} else if (artifact && projectMapExists && source === 'legacy') {
|
|
90
|
+
status = 'legacy';
|
|
91
|
+
} else if (artifact || projectMapExists) {
|
|
92
|
+
status = 'partial';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let summary;
|
|
96
|
+
if (status === 'fresh') {
|
|
97
|
+
summary = `${artifact.relativePath} (current, updated ${scanUpdatedAt})`;
|
|
98
|
+
} else if (status === 'legacy') {
|
|
99
|
+
summary = `${artifact.relativePath} (legacy scan, updated ${scanUpdatedAt})`;
|
|
100
|
+
} else if (status === 'stale') {
|
|
101
|
+
summary = `${artifact.relativePath} newer than docs/PROJECT_MAP.md; run analyze to refresh visible context`;
|
|
102
|
+
} else if (status === 'partial' && artifact && !projectMapExists) {
|
|
103
|
+
summary = `${artifact.relativePath} exists but docs/PROJECT_MAP.md is missing`;
|
|
104
|
+
} else if (status === 'partial' && !artifact && projectMapExists) {
|
|
105
|
+
summary = `docs/PROJECT_MAP.md exists but no scan artifact was found`;
|
|
106
|
+
} else if (status === 'invalid') {
|
|
107
|
+
summary = `scan artifact is invalid: ${artifactError}`;
|
|
108
|
+
} else {
|
|
109
|
+
summary = 'missing analysis artifacts; run npx create-quiver analyze';
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
artifactPath: artifact?.relativePath || (scanPath ? toRelativeScanPath(projectRoot, scanPath) : null),
|
|
114
|
+
error: artifactError || null,
|
|
115
|
+
projectMapPath: projectMapExists ? PROJECT_MAP_RELATIVE_PATH : null,
|
|
116
|
+
projectMapUpdatedAt,
|
|
117
|
+
scanUpdatedAt,
|
|
118
|
+
source,
|
|
119
|
+
status,
|
|
120
|
+
stale,
|
|
121
|
+
summary,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
52
125
|
function hasProjectScanArtifact(projectRoot) {
|
|
53
126
|
const { currentScanPath, legacyScanPath } = projectScanPaths(projectRoot);
|
|
54
127
|
return fs.existsSync(currentScanPath) || fs.existsSync(legacyScanPath);
|
|
@@ -61,6 +134,7 @@ module.exports = {
|
|
|
61
134
|
hasProjectScanArtifact,
|
|
62
135
|
projectScanPaths,
|
|
63
136
|
readProjectScanArtifact,
|
|
137
|
+
readProjectScanStatus,
|
|
64
138
|
toRelativeScanPath,
|
|
65
139
|
writeProjectScanJson,
|
|
66
140
|
};
|