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
|
@@ -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
|
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
SliceGraphError,
|
|
5
|
+
buildGraph,
|
|
6
|
+
computeLevels,
|
|
7
|
+
detectFileConflicts,
|
|
8
|
+
inferDependencies,
|
|
9
|
+
readAllSlices,
|
|
10
|
+
readSlicesForSpec,
|
|
11
|
+
topoSort,
|
|
12
|
+
} = require('./slice-graph');
|
|
13
|
+
const {
|
|
14
|
+
CANONICAL_STATUSES,
|
|
15
|
+
isBlockedStatus,
|
|
16
|
+
isCompletedStatus,
|
|
17
|
+
normalizeStatus,
|
|
18
|
+
} = require('./statuses');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SLICE_STATUS = 'planned';
|
|
21
|
+
const DEFAULT_SPEC_STATUS = 'planned';
|
|
22
|
+
const DEFAULT_RUN_STATUS = 'draft';
|
|
23
|
+
const DEFAULT_AGENT_STATUS = 'idle';
|
|
24
|
+
|
|
25
|
+
const CLOSED_SLICE_STATUSES = new Set(['completed', 'skipped']);
|
|
26
|
+
const HISTORY_CLOSED_SLICE_STATUSES = new Set(['skipped']);
|
|
27
|
+
|
|
28
|
+
function toPosix(relativePath) {
|
|
29
|
+
return String(relativePath || '').split(path.sep).join('/');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function compareRefs(left, right) {
|
|
33
|
+
return String(left || '').localeCompare(String(right || ''));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeSliceRecord(slice) {
|
|
37
|
+
const rawStatus = String(slice?.status || slice?.json?.status || 'draft').trim() || 'draft';
|
|
38
|
+
const canonicalStatus = normalizeStatus('slice', rawStatus, DEFAULT_SLICE_STATUS);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...slice,
|
|
42
|
+
raw_status: rawStatus,
|
|
43
|
+
canonical_status: canonicalStatus,
|
|
44
|
+
status: rawStatus,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readResolverSlices(projectRoot, specSlug = '') {
|
|
49
|
+
const targetSpec = String(specSlug || '').trim();
|
|
50
|
+
const slices = targetSpec ? readSlicesForSpec(projectRoot, targetSpec) : readAllSlices(projectRoot);
|
|
51
|
+
return slices.map(normalizeSliceRecord);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function safeBuildGraph(slices, allowGraphErrors) {
|
|
55
|
+
try {
|
|
56
|
+
const graph = buildGraph(slices);
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
nodes: graph.nodes.map(normalizeSliceRecord),
|
|
60
|
+
edges: graph.edges,
|
|
61
|
+
cycles: graph.cycles,
|
|
62
|
+
error: null,
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
if (!allowGraphErrors || !(error instanceof SliceGraphError)) {
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
nodes: inferDependencies(slices).map(normalizeSliceRecord),
|
|
72
|
+
edges: [],
|
|
73
|
+
cycles: [],
|
|
74
|
+
error: {
|
|
75
|
+
code: error.code,
|
|
76
|
+
message: error.message,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveProjectState(projectRoot, options = {}) {
|
|
83
|
+
const specSlug = options.specSlug ? String(options.specSlug).trim() : '';
|
|
84
|
+
const rawSlices = readResolverSlices(projectRoot, specSlug);
|
|
85
|
+
const graph = safeBuildGraph(rawSlices, options.allowGraphErrors === true);
|
|
86
|
+
const orderedSlices = graph.ok ? topoSort(graph).map(normalizeSliceRecord) : graph.nodes.slice().sort((left, right) => compareRefs(left.ref, right.ref));
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
graph,
|
|
90
|
+
orderedSlices,
|
|
91
|
+
projectRoot,
|
|
92
|
+
rawSlices,
|
|
93
|
+
specSlug,
|
|
94
|
+
specs: groupSlicesBySpec(graph.nodes),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function filterSlicesForExecution(slices, options = {}) {
|
|
99
|
+
const includeCompleted = options.includeCompleted === true;
|
|
100
|
+
const excluded = includeCompleted ? HISTORY_CLOSED_SLICE_STATUSES : CLOSED_SLICE_STATUSES;
|
|
101
|
+
|
|
102
|
+
return (Array.isArray(slices) ? slices : [])
|
|
103
|
+
.filter((slice) => !excluded.has(normalizeStatus('slice', slice?.canonical_status || slice?.status, DEFAULT_SLICE_STATUS)))
|
|
104
|
+
.sort((left, right) => compareRefs(left.ref, right.ref));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function progressForSlice(slice) {
|
|
108
|
+
const explicit = Number(slice?.json?.progress);
|
|
109
|
+
if (Number.isFinite(explicit)) {
|
|
110
|
+
return Math.max(0, Math.min(100, explicit));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const status = normalizeStatus('slice', slice?.canonical_status || slice?.status, DEFAULT_SLICE_STATUS);
|
|
114
|
+
if (status === 'completed') {
|
|
115
|
+
return 100;
|
|
116
|
+
}
|
|
117
|
+
if (status === 'in-progress' || status === 'review') {
|
|
118
|
+
return 50;
|
|
119
|
+
}
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function summarizeSliceProgress(items) {
|
|
124
|
+
const slices = Array.isArray(items) ? items : [];
|
|
125
|
+
const total = slices.length;
|
|
126
|
+
const completed = slices.filter((item) => isCompletedStatus('slice', item.canonical_status || item.status)).length;
|
|
127
|
+
const blocked = slices.filter((item) => isBlockedStatus('slice', item.canonical_status || item.status, item)).length;
|
|
128
|
+
const open = Math.max(0, total - completed);
|
|
129
|
+
const percent = total === 0 ? 0 : Math.round((completed / total) * 100);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
total,
|
|
133
|
+
completed,
|
|
134
|
+
open,
|
|
135
|
+
blocked,
|
|
136
|
+
percent,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function statusForSpec(specSlices) {
|
|
141
|
+
const slices = Array.isArray(specSlices) ? specSlices : [];
|
|
142
|
+
if (slices.length === 0) {
|
|
143
|
+
return 'draft';
|
|
144
|
+
}
|
|
145
|
+
if (slices.some((slice) => isBlockedStatus('slice', slice.canonical_status || slice.status, slice))) {
|
|
146
|
+
return 'blocked';
|
|
147
|
+
}
|
|
148
|
+
if (slices.every((slice) => isCompletedStatus('slice', slice.canonical_status || slice.status))) {
|
|
149
|
+
return 'done';
|
|
150
|
+
}
|
|
151
|
+
if (slices.some((slice) => progressForSlice(slice) > 0)) {
|
|
152
|
+
return 'in-progress';
|
|
153
|
+
}
|
|
154
|
+
return DEFAULT_SPEC_STATUS;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function groupSlicesBySpec(slices) {
|
|
158
|
+
const groups = new Map();
|
|
159
|
+
|
|
160
|
+
for (const slice of Array.isArray(slices) ? slices : []) {
|
|
161
|
+
const key = `${slice.specFamily || 'specs'}/${slice.specSlug || ''}`;
|
|
162
|
+
if (!groups.has(key)) {
|
|
163
|
+
groups.set(key, []);
|
|
164
|
+
}
|
|
165
|
+
groups.get(key).push(slice);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return Array.from(groups.entries())
|
|
169
|
+
.map(([key, specSlices]) => {
|
|
170
|
+
const [specFamily, specSlug] = key.split('/');
|
|
171
|
+
const ordered = specSlices.slice().sort((left, right) => compareRefs(left.ref, right.ref));
|
|
172
|
+
const status = statusForSpec(ordered);
|
|
173
|
+
return {
|
|
174
|
+
canonical_status: normalizeStatus('spec', status, DEFAULT_SPEC_STATUS),
|
|
175
|
+
specFamily,
|
|
176
|
+
specSlug,
|
|
177
|
+
status,
|
|
178
|
+
slices: ordered,
|
|
179
|
+
};
|
|
180
|
+
})
|
|
181
|
+
.sort((left, right) => left.specSlug.localeCompare(right.specSlug));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function summarizeGraph(graph) {
|
|
185
|
+
if (!graph?.ok) {
|
|
186
|
+
return {
|
|
187
|
+
ok: false,
|
|
188
|
+
edges: [],
|
|
189
|
+
levels: [],
|
|
190
|
+
conflicts: [],
|
|
191
|
+
error: graph?.error || null,
|
|
192
|
+
nodes: Array.isArray(graph?.nodes) ? graph.nodes : [],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const levels = computeLevels(graph).map((level, index) => ({
|
|
197
|
+
level: index,
|
|
198
|
+
slices: level.map((slice) => slice.ref),
|
|
199
|
+
}));
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
edges: graph.edges.map((edge) => ({ from: edge.from, to: edge.to })),
|
|
204
|
+
levels,
|
|
205
|
+
conflicts: detectFileConflicts(graph.nodes).map((conflict) => ({
|
|
206
|
+
files: conflict.files,
|
|
207
|
+
slices: conflict.slices,
|
|
208
|
+
})),
|
|
209
|
+
error: null,
|
|
210
|
+
nodes: graph.nodes,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function relativeProjectPath(projectRoot, filePath) {
|
|
215
|
+
return toPosix(path.relative(projectRoot, filePath));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
module.exports = {
|
|
219
|
+
CANONICAL_STATUSES,
|
|
220
|
+
DEFAULT_AGENT_STATUS,
|
|
221
|
+
DEFAULT_RUN_STATUS,
|
|
222
|
+
DEFAULT_SLICE_STATUS,
|
|
223
|
+
DEFAULT_SPEC_STATUS,
|
|
224
|
+
filterSlicesForExecution,
|
|
225
|
+
groupSlicesBySpec,
|
|
226
|
+
isBlockedStatus,
|
|
227
|
+
isCompletedStatus,
|
|
228
|
+
normalizeStatus,
|
|
229
|
+
progressForSlice,
|
|
230
|
+
relativeProjectPath,
|
|
231
|
+
resolveProjectState,
|
|
232
|
+
summarizeGraph,
|
|
233
|
+
summarizeSliceProgress,
|
|
234
|
+
toPosix,
|
|
235
|
+
};
|
|
236
|
+
|