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
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const os = require('node:os');
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const { quiverInternalPaths } = require('../init-layout');
|
|
6
|
+
|
|
7
|
+
const AI_RUN_PHASES = Object.freeze([
|
|
8
|
+
'created',
|
|
9
|
+
'onboarding-ready',
|
|
10
|
+
'acceptance-draft',
|
|
11
|
+
'acceptance-approved',
|
|
12
|
+
'technical-plan-draft',
|
|
13
|
+
'technical-plan-reviewed',
|
|
14
|
+
'technical-plan-approved',
|
|
15
|
+
'spec-generated',
|
|
16
|
+
'execution-plan-generated',
|
|
17
|
+
'slice-executing',
|
|
18
|
+
'pr-ready',
|
|
19
|
+
'closed',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const PHASE_NEXT_COMMAND = Object.freeze({
|
|
23
|
+
created: 'npx create-quiver ai plan --phase acceptance --input <requirements.md> --dry-run',
|
|
24
|
+
'onboarding-ready': 'npx create-quiver ai plan --phase acceptance --input <requirements.md> --dry-run',
|
|
25
|
+
'acceptance-draft': 'npx create-quiver ai approve --phase acceptance --version <n>',
|
|
26
|
+
'acceptance-approved': 'npx create-quiver ai plan --phase technical-plan --dry-run',
|
|
27
|
+
'technical-plan-draft': 'npx create-quiver ai review-plan --dry-run',
|
|
28
|
+
'technical-plan-reviewed': 'npx create-quiver ai approve --phase technical-plan --version <n>',
|
|
29
|
+
'technical-plan-approved': 'npx create-quiver spec create --dry-run',
|
|
30
|
+
'spec-generated': 'npx create-quiver spec start specs/<spec-slug>',
|
|
31
|
+
'execution-plan-generated': 'npx create-quiver ai execute-plan --dry-run --commit --mode manual',
|
|
32
|
+
'slice-executing': 'npx create-quiver ai execute-plan --dry-run --commit --mode delegated',
|
|
33
|
+
'pr-ready': 'npx create-quiver ai pr --dry-run --input specs/<spec-slug>/pr.md',
|
|
34
|
+
closed: 'No next command: lifecycle run is closed.',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function formatError(message) {
|
|
38
|
+
return `create-quiver: ${message}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureDir(dirPath) {
|
|
42
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toRelativePosix(root, filePath) {
|
|
46
|
+
return path.relative(root, filePath).split(path.sep).join('/');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeRunId(value) {
|
|
50
|
+
const normalized = String(value || '')
|
|
51
|
+
.trim()
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
54
|
+
.replace(/^-+|-+$/g, '');
|
|
55
|
+
|
|
56
|
+
if (!normalized) {
|
|
57
|
+
throw new Error(formatError('invalid run id'));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createRunId(now = new Date()) {
|
|
64
|
+
const stamp = now.toISOString()
|
|
65
|
+
.replace(/\.\d{3}Z$/, 'z')
|
|
66
|
+
.replace(/[^0-9a-z]+/gi, '-')
|
|
67
|
+
.toLowerCase()
|
|
68
|
+
.replace(/^-+|-+$/g, '');
|
|
69
|
+
return `run-${stamp}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runsDir(projectRoot) {
|
|
73
|
+
return quiverInternalPaths(projectRoot).runsDir;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function locksDir(projectRoot) {
|
|
77
|
+
return quiverInternalPaths(projectRoot).locksDir || path.join(quiverInternalPaths(projectRoot).root, 'locks');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runDir(projectRoot, runId) {
|
|
81
|
+
return path.join(runsDir(projectRoot), normalizeRunId(runId));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function runStatePath(projectRoot, runId) {
|
|
85
|
+
return path.join(runDir(projectRoot, runId), 'state.json');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runApprovalsPath(projectRoot, runId) {
|
|
89
|
+
return path.join(runDir(projectRoot, runId), 'approvals.json');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runRequirementPath(projectRoot, runId) {
|
|
93
|
+
return path.join(runDir(projectRoot, runId), 'requirement.md');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function assertKnownPhase(phase) {
|
|
97
|
+
if (!AI_RUN_PHASES.includes(phase)) {
|
|
98
|
+
throw new Error(formatError(`unsupported AI run phase '${phase}'`));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function phaseRank(phase) {
|
|
103
|
+
assertKnownPhase(phase);
|
|
104
|
+
return AI_RUN_PHASES.indexOf(phase);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function nextCommandForPhase(phase) {
|
|
108
|
+
assertKnownPhase(phase);
|
|
109
|
+
return PHASE_NEXT_COMMAND[phase];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readJsonIfExists(filePath) {
|
|
113
|
+
if (!fs.existsSync(filePath)) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function writeJson(filePath, value) {
|
|
120
|
+
ensureDir(path.dirname(filePath));
|
|
121
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function listAiRuns(projectRoot) {
|
|
125
|
+
const root = runsDir(projectRoot);
|
|
126
|
+
if (!fs.existsSync(root)) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return fs.readdirSync(root, { withFileTypes: true })
|
|
131
|
+
.filter((entry) => entry.isDirectory())
|
|
132
|
+
.map((entry) => readAiRun(projectRoot, entry.name))
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.sort((a, b) => String(a.updated_at || a.created_at).localeCompare(String(b.updated_at || b.created_at)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function latestAiRun(projectRoot) {
|
|
138
|
+
const runs = listAiRuns(projectRoot).filter((run) => run.status !== 'closed');
|
|
139
|
+
return runs.length > 0 ? runs[runs.length - 1] : null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function readAiRun(projectRoot, runId) {
|
|
143
|
+
const statePath = runStatePath(projectRoot, runId);
|
|
144
|
+
if (!fs.existsSync(statePath)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf8'));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function resolveAiRun(projectRoot, runId = '') {
|
|
151
|
+
if (runId) {
|
|
152
|
+
const run = readAiRun(projectRoot, runId);
|
|
153
|
+
if (!run) {
|
|
154
|
+
throw new Error(formatError(`missing AI run: ${runId}`));
|
|
155
|
+
}
|
|
156
|
+
return run;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const latest = latestAiRun(projectRoot);
|
|
160
|
+
if (!latest) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return latest;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function createAiRun(projectRoot, options = {}) {
|
|
167
|
+
const sourceInput = options.input ? path.resolve(projectRoot, options.input) : '';
|
|
168
|
+
if (sourceInput && !fs.existsSync(sourceInput)) {
|
|
169
|
+
throw new Error(formatError(`missing run requirement input file: ${options.input}`));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const runId = normalizeRunId(options.runId || createRunId(options.now || new Date()));
|
|
173
|
+
const targetDir = runDir(projectRoot, runId);
|
|
174
|
+
const now = (options.now || new Date()).toISOString();
|
|
175
|
+
|
|
176
|
+
if (fs.existsSync(runStatePath(projectRoot, runId))) {
|
|
177
|
+
throw new Error(formatError(`AI run already exists: ${runId}`));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
ensureDir(targetDir);
|
|
181
|
+
|
|
182
|
+
const requirementTarget = runRequirementPath(projectRoot, runId);
|
|
183
|
+
if (sourceInput) {
|
|
184
|
+
fs.copyFileSync(sourceInput, requirementTarget);
|
|
185
|
+
} else {
|
|
186
|
+
fs.writeFileSync(requirementTarget, '');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const approvals = {
|
|
190
|
+
schema_version: 1,
|
|
191
|
+
run_id: runId,
|
|
192
|
+
approvals: [],
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const state = {
|
|
196
|
+
schema_version: 1,
|
|
197
|
+
run_id: runId,
|
|
198
|
+
status: 'active',
|
|
199
|
+
phase: options.phase || 'created',
|
|
200
|
+
spec_slug: options.specSlug || null,
|
|
201
|
+
created_at: now,
|
|
202
|
+
updated_at: now,
|
|
203
|
+
requirement: {
|
|
204
|
+
source_file: sourceInput ? toRelativePosix(projectRoot, sourceInput) : null,
|
|
205
|
+
path: toRelativePosix(projectRoot, requirementTarget),
|
|
206
|
+
},
|
|
207
|
+
approvals_path: toRelativePosix(projectRoot, runApprovalsPath(projectRoot, runId)),
|
|
208
|
+
decisions_path: toRelativePosix(projectRoot, path.join(targetDir, 'decisions.md')),
|
|
209
|
+
history: [
|
|
210
|
+
{
|
|
211
|
+
phase: options.phase || 'created',
|
|
212
|
+
command: options.command || 'ai run create',
|
|
213
|
+
at: now,
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
assertKnownPhase(state.phase);
|
|
219
|
+
writeJson(runApprovalsPath(projectRoot, runId), approvals);
|
|
220
|
+
writeJson(runStatePath(projectRoot, runId), state);
|
|
221
|
+
fs.writeFileSync(path.join(targetDir, 'decisions.md'), '# Decisions\n\n');
|
|
222
|
+
return state;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function ensureAiRun(projectRoot, options = {}) {
|
|
226
|
+
const existing = resolveAiRun(projectRoot, options.runId || '');
|
|
227
|
+
if (existing) {
|
|
228
|
+
return existing;
|
|
229
|
+
}
|
|
230
|
+
return createAiRun(projectRoot, options);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function updateAiRunPhase(projectRoot, runId, phase, options = {}) {
|
|
234
|
+
assertKnownPhase(phase);
|
|
235
|
+
const current = resolveAiRun(projectRoot, runId);
|
|
236
|
+
if (!current) {
|
|
237
|
+
throw new Error(formatError('missing AI run to update'));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (phaseRank(phase) < phaseRank(current.phase)) {
|
|
241
|
+
throw new Error(formatError(`cannot move AI run ${current.run_id} backwards from ${current.phase} to ${phase}`));
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const now = (options.now || new Date()).toISOString();
|
|
245
|
+
const next = {
|
|
246
|
+
...current,
|
|
247
|
+
phase,
|
|
248
|
+
status: phase === 'closed' ? 'closed' : 'active',
|
|
249
|
+
spec_slug: options.specSlug || current.spec_slug || null,
|
|
250
|
+
updated_at: now,
|
|
251
|
+
history: (current.history || []).concat({
|
|
252
|
+
phase,
|
|
253
|
+
command: options.command || 'unknown',
|
|
254
|
+
artifact: options.artifact || null,
|
|
255
|
+
at: now,
|
|
256
|
+
}),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
writeJson(runStatePath(projectRoot, current.run_id), next);
|
|
260
|
+
return next;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function recordAiRunApproval(projectRoot, runId, approval) {
|
|
264
|
+
const run = resolveAiRun(projectRoot, runId);
|
|
265
|
+
if (!run) {
|
|
266
|
+
throw new Error(formatError('missing AI run for approval metadata'));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const filePath = runApprovalsPath(projectRoot, run.run_id);
|
|
270
|
+
const current = readJsonIfExists(filePath) || { schema_version: 1, run_id: run.run_id, approvals: [] };
|
|
271
|
+
const next = {
|
|
272
|
+
...current,
|
|
273
|
+
approvals: (current.approvals || []).concat({
|
|
274
|
+
...approval,
|
|
275
|
+
at: approval.at || new Date().toISOString(),
|
|
276
|
+
}),
|
|
277
|
+
};
|
|
278
|
+
writeJson(filePath, next);
|
|
279
|
+
return next;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function assertAiRunPhaseAllows(run, requiredPhase, commandName) {
|
|
283
|
+
if (!run) {
|
|
284
|
+
throw new Error(formatError(`cannot run ${commandName}: no AI run exists. Next: npx create-quiver ai run create --input <requirements.md>`));
|
|
285
|
+
}
|
|
286
|
+
assertKnownPhase(requiredPhase);
|
|
287
|
+
|
|
288
|
+
if (phaseRank(run.phase) < phaseRank(requiredPhase)) {
|
|
289
|
+
throw new Error(formatError(`cannot run ${commandName}: AI run ${run.run_id} is at phase '${run.phase}' and requires '${requiredPhase}'. Next: ${nextCommandForPhase(run.phase)}`));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function sanitizeLockPart(value) {
|
|
296
|
+
return String(value || '')
|
|
297
|
+
.trim()
|
|
298
|
+
.replace(/[^a-zA-Z0-9._-]+/g, '-')
|
|
299
|
+
.replace(/^-+|-+$/g, '') || 'run';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function lockPath(projectRoot, runId, sliceId = '') {
|
|
303
|
+
const name = sliceId
|
|
304
|
+
? `${sanitizeLockPart(runId)}--${sanitizeLockPart(sliceId)}.lock`
|
|
305
|
+
: `${sanitizeLockPart(runId)}.lock`;
|
|
306
|
+
return path.join(locksDir(projectRoot), name);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function readAiRunLock(projectRoot, runId, sliceId = '') {
|
|
310
|
+
return readJsonIfExists(lockPath(projectRoot, runId, sliceId));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function acquireAiRunLock(projectRoot, runId, options = {}) {
|
|
314
|
+
const filePath = lockPath(projectRoot, runId, options.sliceId || '');
|
|
315
|
+
const payload = {
|
|
316
|
+
schema_version: 1,
|
|
317
|
+
run_id: normalizeRunId(runId),
|
|
318
|
+
slice_id: options.sliceId || null,
|
|
319
|
+
pid: process.pid,
|
|
320
|
+
hostname: os.hostname(),
|
|
321
|
+
command: options.command || null,
|
|
322
|
+
created_at: (options.now || new Date()).toISOString(),
|
|
323
|
+
};
|
|
324
|
+
ensureDir(path.dirname(filePath));
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
fs.writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, { flag: 'wx' });
|
|
328
|
+
} catch (error) {
|
|
329
|
+
if (error.code === 'EEXIST') {
|
|
330
|
+
const existing = readAiRunLock(projectRoot, runId, options.sliceId || '');
|
|
331
|
+
throw new Error(formatError(`AI run is locked: ${path.relative(projectRoot, filePath).split(path.sep).join('/')}\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.`));
|
|
332
|
+
}
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
filePath,
|
|
338
|
+
lock: payload,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function releaseAiRunLock(projectRoot, runId, options = {}) {
|
|
343
|
+
const filePath = lockPath(projectRoot, runId, options.sliceId || '');
|
|
344
|
+
if (fs.existsSync(filePath)) {
|
|
345
|
+
fs.rmSync(filePath);
|
|
346
|
+
}
|
|
347
|
+
return filePath;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function formatAiRunStatus(projectRoot, run) {
|
|
351
|
+
if (!run) {
|
|
352
|
+
return [
|
|
353
|
+
'AI run status',
|
|
354
|
+
'Status: no active run',
|
|
355
|
+
'Next safe command: npx create-quiver ai run create --input <requirements.md>',
|
|
356
|
+
'',
|
|
357
|
+
].join('\n');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return [
|
|
361
|
+
'AI run status',
|
|
362
|
+
`Run: ${run.run_id}`,
|
|
363
|
+
`Status: ${run.status}`,
|
|
364
|
+
`Phase: ${run.phase}`,
|
|
365
|
+
`Spec: ${run.spec_slug || '(not generated)'}`,
|
|
366
|
+
`Requirement: ${run.requirement?.path || '(missing)'}`,
|
|
367
|
+
`State: ${toRelativePosix(projectRoot, runStatePath(projectRoot, run.run_id))}`,
|
|
368
|
+
`Approvals: ${run.approvals_path}`,
|
|
369
|
+
`Next safe command: ${nextCommandForPhase(run.phase)}`,
|
|
370
|
+
'',
|
|
371
|
+
].join('\n');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function formatAiRunResume(projectRoot, run) {
|
|
375
|
+
if (!run) {
|
|
376
|
+
return [
|
|
377
|
+
'AI run resume',
|
|
378
|
+
'No active run found.',
|
|
379
|
+
'Next safe command: npx create-quiver ai run create --input <requirements.md>',
|
|
380
|
+
'',
|
|
381
|
+
].join('\n');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return [
|
|
385
|
+
'AI run resume',
|
|
386
|
+
`Run: ${run.run_id}`,
|
|
387
|
+
`Current phase: ${run.phase}`,
|
|
388
|
+
`Next safe command: ${nextCommandForPhase(run.phase)}`,
|
|
389
|
+
`State: ${toRelativePosix(projectRoot, runStatePath(projectRoot, run.run_id))}`,
|
|
390
|
+
'',
|
|
391
|
+
].join('\n');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
module.exports = {
|
|
395
|
+
AI_RUN_PHASES,
|
|
396
|
+
acquireAiRunLock,
|
|
397
|
+
assertAiRunPhaseAllows,
|
|
398
|
+
createAiRun,
|
|
399
|
+
ensureAiRun,
|
|
400
|
+
formatAiRunResume,
|
|
401
|
+
formatAiRunStatus,
|
|
402
|
+
latestAiRun,
|
|
403
|
+
listAiRuns,
|
|
404
|
+
nextCommandForPhase,
|
|
405
|
+
readAiRun,
|
|
406
|
+
readAiRunLock,
|
|
407
|
+
recordAiRunApproval,
|
|
408
|
+
releaseAiRunLock,
|
|
409
|
+
resolveAiRun,
|
|
410
|
+
runApprovalsPath,
|
|
411
|
+
runDir,
|
|
412
|
+
runStatePath,
|
|
413
|
+
updateAiRunPhase,
|
|
414
|
+
};
|
|
@@ -65,20 +65,73 @@ function parseApprovedManifest(sourceText, options = {}) {
|
|
|
65
65
|
const acceptance = extractSectionBullets(normalizedText, ['Acceptance Criteria', 'Criterios de aceptacion', 'Criterios de aceptación']);
|
|
66
66
|
const risks = extractSectionBullets(normalizedText, ['Risks', 'Riesgos']);
|
|
67
67
|
const assumptions = extractSectionBullets(normalizedText, ['Assumptions', 'Suposiciones', 'Supuestos']);
|
|
68
|
+
const structuredBlock = extractStructuredJsonBlock(normalizedText);
|
|
69
|
+
const markdownSource = {
|
|
70
|
+
title: titleMatch ? titleMatch[1].trim() : options.fallbackTitle || '',
|
|
71
|
+
objective: objective || '',
|
|
72
|
+
scope,
|
|
73
|
+
acceptance,
|
|
74
|
+
risks,
|
|
75
|
+
assumptions,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
if (structuredBlock) {
|
|
79
|
+
if (structuredBlock.spec && typeof structuredBlock.spec === 'object') {
|
|
80
|
+
return {
|
|
81
|
+
sourceText: normalizedText,
|
|
82
|
+
source: {
|
|
83
|
+
...structuredBlock,
|
|
84
|
+
spec: {
|
|
85
|
+
...markdownSource,
|
|
86
|
+
...structuredBlock.spec,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
sourceText: normalizedText,
|
|
94
|
+
source: {
|
|
95
|
+
...markdownSource,
|
|
96
|
+
...structuredBlock,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
68
100
|
|
|
69
101
|
return {
|
|
70
102
|
sourceText: normalizedText,
|
|
71
|
-
source:
|
|
72
|
-
title: titleMatch ? titleMatch[1].trim() : options.fallbackTitle || '',
|
|
73
|
-
objective: objective || '',
|
|
74
|
-
scope,
|
|
75
|
-
acceptance,
|
|
76
|
-
risks,
|
|
77
|
-
assumptions,
|
|
78
|
-
},
|
|
103
|
+
source: markdownSource,
|
|
79
104
|
};
|
|
80
105
|
}
|
|
81
106
|
|
|
107
|
+
function hasStructuredSlices(value) {
|
|
108
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return Array.isArray(value.slices) || Array.isArray(value.spec?.slices);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function extractStructuredJsonBlock(text) {
|
|
116
|
+
const fencePattern = /```(?:json|quiver|quiver-spec)?\s*\n([\s\S]*?)```/gi;
|
|
117
|
+
let match = fencePattern.exec(text);
|
|
118
|
+
|
|
119
|
+
while (match) {
|
|
120
|
+
const candidate = String(match[1] || '').trim();
|
|
121
|
+
try {
|
|
122
|
+
const parsed = parseJsonWithComments(candidate);
|
|
123
|
+
if (hasStructuredSlices(parsed)) {
|
|
124
|
+
return parsed;
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Continue scanning other fenced blocks.
|
|
128
|
+
}
|
|
129
|
+
match = fencePattern.exec(text);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
82
135
|
function extractSectionText(text, headings) {
|
|
83
136
|
const lines = String(text || '').split(/\r?\n/);
|
|
84
137
|
const normalizedHeadings = new Set(headings.map((heading) => normalizeHeading(heading)));
|
|
@@ -182,10 +235,22 @@ function validateGeneratedSliceJson(filePath, expectedSliceId) {
|
|
|
182
235
|
throw new Error(formatError(`slice.json files must be an array at ${filePath}`));
|
|
183
236
|
}
|
|
184
237
|
|
|
238
|
+
if (!Array.isArray(parsed.expected_read_paths)) {
|
|
239
|
+
throw new Error(formatError(`slice.json expected_read_paths must be an array at ${filePath}`));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (!Array.isArray(parsed.allowed_write_paths)) {
|
|
243
|
+
throw new Error(formatError(`slice.json allowed_write_paths must be an array at ${filePath}`));
|
|
244
|
+
}
|
|
245
|
+
|
|
185
246
|
if (!Array.isArray(parsed.depends_on)) {
|
|
186
247
|
throw new Error(formatError(`slice.json depends_on must be an array at ${filePath}`));
|
|
187
248
|
}
|
|
188
249
|
|
|
250
|
+
if (!Array.isArray(parsed.validation_hints)) {
|
|
251
|
+
throw new Error(formatError(`slice.json validation_hints must be an array at ${filePath}`));
|
|
252
|
+
}
|
|
253
|
+
|
|
189
254
|
return parsed;
|
|
190
255
|
}
|
|
191
256
|
|
|
@@ -194,11 +259,17 @@ function buildSpecGenerationManifest({ inputText, inputPath, repoRoot, specSlug
|
|
|
194
259
|
fallbackTitle: specSlug ? specSlug.replace(/-/g, ' ') : path.basename(inputPath || 'generated-spec.md', path.extname(inputPath || '.md')),
|
|
195
260
|
});
|
|
196
261
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
262
|
+
let manifest;
|
|
263
|
+
try {
|
|
264
|
+
manifest = buildManifest({
|
|
265
|
+
...source,
|
|
266
|
+
sourcePath: path.relative(repoRoot, path.resolve(repoRoot, inputPath)).split(path.sep).join('/'),
|
|
267
|
+
sourceText,
|
|
268
|
+
}, { specSlug });
|
|
269
|
+
} catch (error) {
|
|
270
|
+
const message = String(error.message || error);
|
|
271
|
+
throw new Error(message.startsWith('create-quiver:') ? message : formatError(message));
|
|
272
|
+
}
|
|
202
273
|
|
|
203
274
|
if (!manifest.slug) {
|
|
204
275
|
throw new Error(formatError('unable to derive a spec slug from the approved input'));
|