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
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const {
|
|
5
|
+
SliceGraphError,
|
|
6
|
+
buildGraph,
|
|
7
|
+
computeLevels,
|
|
8
|
+
detectFileConflicts,
|
|
9
|
+
inferDependencies,
|
|
10
|
+
readAllSlices,
|
|
11
|
+
readSlicesForSpec,
|
|
12
|
+
topoSort,
|
|
13
|
+
} = require('./slice-graph');
|
|
14
|
+
const {
|
|
15
|
+
CANONICAL_STATUSES,
|
|
16
|
+
isBlockedStatus,
|
|
17
|
+
isCompletedStatus,
|
|
18
|
+
normalizeStatus,
|
|
19
|
+
} = require('./statuses');
|
|
20
|
+
|
|
21
|
+
const DEFAULT_SLICE_STATUS = 'planned';
|
|
22
|
+
const DEFAULT_SPEC_STATUS = 'planned';
|
|
23
|
+
const DEFAULT_RUN_STATUS = 'draft';
|
|
24
|
+
const DEFAULT_AGENT_STATUS = 'idle';
|
|
25
|
+
|
|
26
|
+
const CLOSED_SLICE_STATUSES = new Set(['completed', 'skipped']);
|
|
27
|
+
const HISTORY_CLOSED_SLICE_STATUSES = new Set(['skipped']);
|
|
28
|
+
|
|
29
|
+
function toPosix(relativePath) {
|
|
30
|
+
return String(relativePath || '').split(path.sep).join('/');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function compareRefs(left, right) {
|
|
34
|
+
return String(left || '').localeCompare(String(right || ''));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeSliceRecord(slice) {
|
|
38
|
+
const rawStatus = String(slice?.status || slice?.json?.status || 'draft').trim() || 'draft';
|
|
39
|
+
const canonicalStatus = normalizeStatus('slice', rawStatus, DEFAULT_SLICE_STATUS);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
...slice,
|
|
43
|
+
raw_status: rawStatus,
|
|
44
|
+
canonical_status: canonicalStatus,
|
|
45
|
+
status: rawStatus,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readResolverSlices(projectRoot, specSlug = '') {
|
|
50
|
+
const targetSpec = String(specSlug || '').trim();
|
|
51
|
+
const slices = targetSpec ? readSlicesForSpec(projectRoot, targetSpec) : readAllSlices(projectRoot);
|
|
52
|
+
return slices.map(normalizeSliceRecord);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function safeBuildGraph(slices, allowGraphErrors) {
|
|
56
|
+
try {
|
|
57
|
+
const graph = buildGraph(slices);
|
|
58
|
+
return {
|
|
59
|
+
ok: true,
|
|
60
|
+
nodes: graph.nodes.map(normalizeSliceRecord),
|
|
61
|
+
edges: graph.edges,
|
|
62
|
+
cycles: graph.cycles,
|
|
63
|
+
error: null,
|
|
64
|
+
};
|
|
65
|
+
} catch (error) {
|
|
66
|
+
if (!allowGraphErrors || !(error instanceof SliceGraphError)) {
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
ok: false,
|
|
72
|
+
nodes: inferDependencies(slices).map(normalizeSliceRecord),
|
|
73
|
+
edges: [],
|
|
74
|
+
cycles: [],
|
|
75
|
+
error: {
|
|
76
|
+
code: error.code,
|
|
77
|
+
message: error.message,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolveProjectState(projectRoot, options = {}) {
|
|
84
|
+
const specSlug = options.specSlug ? String(options.specSlug).trim() : '';
|
|
85
|
+
const rawSlices = readResolverSlices(projectRoot, specSlug);
|
|
86
|
+
const graph = safeBuildGraph(rawSlices, options.allowGraphErrors === true);
|
|
87
|
+
const orderedSlices = graph.ok ? topoSort(graph).map(normalizeSliceRecord) : graph.nodes.slice().sort((left, right) => compareRefs(left.ref, right.ref));
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
graph,
|
|
91
|
+
orderedSlices,
|
|
92
|
+
projectRoot,
|
|
93
|
+
rawSlices,
|
|
94
|
+
specSlug,
|
|
95
|
+
specs: groupSlicesBySpec(graph.nodes),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function filterSlicesForExecution(slices, options = {}) {
|
|
100
|
+
const includeCompleted = options.includeCompleted === true;
|
|
101
|
+
const excluded = includeCompleted ? HISTORY_CLOSED_SLICE_STATUSES : CLOSED_SLICE_STATUSES;
|
|
102
|
+
|
|
103
|
+
return (Array.isArray(slices) ? slices : [])
|
|
104
|
+
.filter((slice) => !excluded.has(normalizeStatus('slice', slice?.canonical_status || slice?.status, DEFAULT_SLICE_STATUS)))
|
|
105
|
+
.sort((left, right) => compareRefs(left.ref, right.ref));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function progressForSlice(slice) {
|
|
109
|
+
const explicit = Number(slice?.json?.progress);
|
|
110
|
+
if (Number.isFinite(explicit)) {
|
|
111
|
+
return Math.max(0, Math.min(100, explicit));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const status = normalizeStatus('slice', slice?.canonical_status || slice?.status, DEFAULT_SLICE_STATUS);
|
|
115
|
+
if (status === 'completed') {
|
|
116
|
+
return 100;
|
|
117
|
+
}
|
|
118
|
+
if (status === 'in-progress' || status === 'review') {
|
|
119
|
+
return 50;
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function summarizeSliceProgress(items) {
|
|
125
|
+
const slices = Array.isArray(items) ? items : [];
|
|
126
|
+
const total = slices.length;
|
|
127
|
+
const completed = slices.filter((item) => isCompletedStatus('slice', item.canonical_status || item.status)).length;
|
|
128
|
+
const blocked = slices.filter((item) => isBlockedStatus('slice', item.canonical_status || item.status, item)).length;
|
|
129
|
+
const open = Math.max(0, total - completed);
|
|
130
|
+
const percent = total === 0 ? 0 : Math.round((completed / total) * 100);
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
total,
|
|
134
|
+
completed,
|
|
135
|
+
open,
|
|
136
|
+
blocked,
|
|
137
|
+
percent,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function statusForSpec(specSlices) {
|
|
142
|
+
const slices = Array.isArray(specSlices) ? specSlices : [];
|
|
143
|
+
if (slices.length === 0) {
|
|
144
|
+
return 'draft';
|
|
145
|
+
}
|
|
146
|
+
if (slices.some((slice) => isBlockedStatus('slice', slice.canonical_status || slice.status, slice))) {
|
|
147
|
+
return 'blocked';
|
|
148
|
+
}
|
|
149
|
+
if (slices.every((slice) => isCompletedStatus('slice', slice.canonical_status || slice.status))) {
|
|
150
|
+
return 'done';
|
|
151
|
+
}
|
|
152
|
+
if (slices.some((slice) => progressForSlice(slice) > 0)) {
|
|
153
|
+
return 'in-progress';
|
|
154
|
+
}
|
|
155
|
+
return DEFAULT_SPEC_STATUS;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function groupSlicesBySpec(slices) {
|
|
159
|
+
const groups = new Map();
|
|
160
|
+
|
|
161
|
+
for (const slice of Array.isArray(slices) ? slices : []) {
|
|
162
|
+
const key = `${slice.specFamily || 'specs'}/${slice.specSlug || ''}`;
|
|
163
|
+
if (!groups.has(key)) {
|
|
164
|
+
groups.set(key, []);
|
|
165
|
+
}
|
|
166
|
+
groups.get(key).push(slice);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return Array.from(groups.entries())
|
|
170
|
+
.map(([key, specSlices]) => {
|
|
171
|
+
const [specFamily, specSlug] = key.split('/');
|
|
172
|
+
const ordered = specSlices.slice().sort((left, right) => compareRefs(left.ref, right.ref));
|
|
173
|
+
const status = statusForSpec(ordered);
|
|
174
|
+
return {
|
|
175
|
+
canonical_status: normalizeStatus('spec', status, DEFAULT_SPEC_STATUS),
|
|
176
|
+
specFamily,
|
|
177
|
+
specSlug,
|
|
178
|
+
status,
|
|
179
|
+
slices: ordered,
|
|
180
|
+
};
|
|
181
|
+
})
|
|
182
|
+
.sort((left, right) => left.specSlug.localeCompare(right.specSlug));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function summarizeGraph(graph) {
|
|
186
|
+
if (!graph?.ok) {
|
|
187
|
+
return {
|
|
188
|
+
ok: false,
|
|
189
|
+
edges: [],
|
|
190
|
+
levels: [],
|
|
191
|
+
conflicts: [],
|
|
192
|
+
error: graph?.error || null,
|
|
193
|
+
nodes: Array.isArray(graph?.nodes) ? graph.nodes : [],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const levels = computeLevels(graph).map((level, index) => ({
|
|
198
|
+
level: index,
|
|
199
|
+
slices: level.map((slice) => slice.ref),
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
ok: true,
|
|
204
|
+
edges: graph.edges.map((edge) => ({ from: edge.from, to: edge.to })),
|
|
205
|
+
levels,
|
|
206
|
+
conflicts: detectFileConflicts(graph.nodes).map((conflict) => ({
|
|
207
|
+
files: conflict.files,
|
|
208
|
+
slices: conflict.slices,
|
|
209
|
+
})),
|
|
210
|
+
error: null,
|
|
211
|
+
nodes: graph.nodes,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function relativeProjectPath(projectRoot, filePath) {
|
|
216
|
+
return toPosix(path.relative(projectRoot, filePath));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function readMarkdownSectionValue(text, heading) {
|
|
220
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
221
|
+
const normalizedHeading = String(heading || '').trim().toLowerCase();
|
|
222
|
+
let capture = false;
|
|
223
|
+
|
|
224
|
+
for (const rawLine of lines) {
|
|
225
|
+
const line = rawLine.trim();
|
|
226
|
+
const match = line.match(/^##\s+(.+)$/);
|
|
227
|
+
if (match) {
|
|
228
|
+
capture = match[1].trim().toLowerCase() === normalizedHeading;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (capture && line && !line.startsWith('---')) {
|
|
232
|
+
return line.replace(/^[-*]\s+/, '').trim();
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return '';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function buildSliceLookup(slices) {
|
|
240
|
+
const byRef = new Map();
|
|
241
|
+
const bySliceId = new Map();
|
|
242
|
+
|
|
243
|
+
for (const slice of Array.isArray(slices) ? slices : []) {
|
|
244
|
+
if (slice.ref) {
|
|
245
|
+
byRef.set(slice.ref, slice);
|
|
246
|
+
}
|
|
247
|
+
const sliceId = slice.sliceId || slice.json?.slice_id || '';
|
|
248
|
+
if (!sliceId) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if (!bySliceId.has(sliceId)) {
|
|
252
|
+
bySliceId.set(sliceId, []);
|
|
253
|
+
}
|
|
254
|
+
bySliceId.get(sliceId).push(slice);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { byRef, bySliceId };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeActiveSliceSource(source, lookup) {
|
|
261
|
+
const ref = source.ref || (source.spec_slug && source.slice_id ? `${source.spec_slug}/${source.slice_id}` : '');
|
|
262
|
+
let resolved = ref ? lookup.byRef.get(ref) : null;
|
|
263
|
+
let issue = '';
|
|
264
|
+
|
|
265
|
+
if (!resolved && source.slice_id && !source.spec_slug) {
|
|
266
|
+
const matches = lookup.bySliceId.get(source.slice_id) || [];
|
|
267
|
+
if (matches.length === 1) {
|
|
268
|
+
resolved = matches[0];
|
|
269
|
+
} else if (matches.length > 1) {
|
|
270
|
+
issue = `ambiguous slice id '${source.slice_id}' appears in multiple specs`;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!resolved && !issue) {
|
|
275
|
+
issue = source.slice_id ? `slice '${source.slice_id}' was not found` : 'active slice source did not declare a slice id';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
...source,
|
|
280
|
+
ref: resolved?.ref || ref || null,
|
|
281
|
+
spec_slug: source.spec_slug || resolved?.specSlug || null,
|
|
282
|
+
slice_id: source.slice_id || resolved?.sliceId || null,
|
|
283
|
+
status: resolved?.status || source.status || null,
|
|
284
|
+
canonical_status: resolved ? normalizeStatus('slice', resolved.canonical_status || resolved.status, DEFAULT_SLICE_STATUS) : null,
|
|
285
|
+
title: resolved?.title || source.title || null,
|
|
286
|
+
valid: Boolean(resolved),
|
|
287
|
+
issue: resolved ? null : issue,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseActiveSliceDoc(projectRoot, lookup) {
|
|
292
|
+
const relativePath = 'docs/ai/ACTIVE_SLICE.md';
|
|
293
|
+
const filePath = path.join(projectRoot, relativePath);
|
|
294
|
+
if (!fs.existsSync(filePath)) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
const text = fs.readFileSync(filePath, 'utf8');
|
|
298
|
+
return [normalizeActiveSliceSource({
|
|
299
|
+
kind: 'active-doc',
|
|
300
|
+
path: relativePath,
|
|
301
|
+
source_id: relativePath,
|
|
302
|
+
slice_id: readMarkdownSectionValue(text, 'Slice ID'),
|
|
303
|
+
title: readMarkdownSectionValue(text, 'Title'),
|
|
304
|
+
}, lookup)];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function isMarkdownSeparatorCell(value) {
|
|
308
|
+
return /^:?-{3,}:?$/.test(String(value || '').trim());
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parseActiveSlicesBoard(projectRoot, lookup) {
|
|
312
|
+
const relativePath = 'ACTIVE_SLICES.md';
|
|
313
|
+
const filePath = path.join(projectRoot, relativePath);
|
|
314
|
+
if (!fs.existsSync(filePath)) {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const sources = [];
|
|
319
|
+
const lines = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
|
|
320
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
321
|
+
const line = lines[index].trim();
|
|
322
|
+
if (!line.startsWith('|') || !line.endsWith('|')) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const cells = line.split('|').slice(1, -1).map((cell) => cell.trim());
|
|
326
|
+
if (cells.length < 6 || cells[1] === 'Spec' || cells.every(isMarkdownSeparatorCell)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const specSlug = cells[1];
|
|
330
|
+
const sliceId = cells[2];
|
|
331
|
+
if (!specSlug || !sliceId || specSlug === '-' || sliceId === '-') {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
sources.push(normalizeActiveSliceSource({
|
|
335
|
+
branch: cells[3] || null,
|
|
336
|
+
kind: 'active-board',
|
|
337
|
+
path: relativePath,
|
|
338
|
+
row: index + 1,
|
|
339
|
+
source_id: `${relativePath}:${index + 1}`,
|
|
340
|
+
spec_slug: specSlug,
|
|
341
|
+
slice_id: sliceId,
|
|
342
|
+
status: cells[4] || null,
|
|
343
|
+
worktree_path: cells[5] || null,
|
|
344
|
+
}, lookup));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return sources;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function buildActiveSliceReconciliation(sources) {
|
|
351
|
+
const activeSources = Array.isArray(sources) ? sources : [];
|
|
352
|
+
const existingRefs = Array.from(new Set(activeSources.filter((source) => source.valid && source.ref).map((source) => source.ref)));
|
|
353
|
+
const invalidSources = activeSources.filter((source) => !source.valid);
|
|
354
|
+
const hasActiveDoc = activeSources.some((source) => source.kind === 'active-doc');
|
|
355
|
+
const hasBoard = activeSources.some((source) => source.kind === 'active-board');
|
|
356
|
+
const planned_changes = [];
|
|
357
|
+
const risks = [];
|
|
358
|
+
let decision = 'preserve';
|
|
359
|
+
let reason = 'No active-slice source needs changes.';
|
|
360
|
+
|
|
361
|
+
if (activeSources.length === 0) {
|
|
362
|
+
reason = 'No active-slice source exists.';
|
|
363
|
+
} else if (invalidSources.length > 0) {
|
|
364
|
+
decision = 'blocked';
|
|
365
|
+
reason = 'One or more active-slice sources reference missing or ambiguous slices.';
|
|
366
|
+
risks.push(...invalidSources.map((source) => `${source.source_id}: ${source.issue}`));
|
|
367
|
+
} else if (existingRefs.length > 1) {
|
|
368
|
+
decision = 'blocked';
|
|
369
|
+
reason = 'Active-slice sources disagree about the active slice.';
|
|
370
|
+
risks.push(`Conflicting refs: ${existingRefs.join(', ')}`);
|
|
371
|
+
} else {
|
|
372
|
+
const active = activeSources.find((source) => source.ref === existingRefs[0]) || activeSources[0];
|
|
373
|
+
if (active && isCompletedStatus('slice', active.canonical_status || active.status)) {
|
|
374
|
+
decision = 'close';
|
|
375
|
+
reason = 'The active slice is already completed and local active-state files should be closed intentionally.';
|
|
376
|
+
planned_changes.push('remove docs/ai/ACTIVE_SLICE.md if it exists');
|
|
377
|
+
planned_changes.push('refresh ACTIVE_SLICES.md from current worktrees');
|
|
378
|
+
} else if (!hasActiveDoc && hasBoard) {
|
|
379
|
+
decision = 'replace';
|
|
380
|
+
reason = 'ACTIVE_SLICES.md reports an active slice but docs/ai/ACTIVE_SLICE.md is missing.';
|
|
381
|
+
planned_changes.push(`recreate docs/ai/ACTIVE_SLICE.md from ${active?.ref || 'the board source'}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
decision,
|
|
387
|
+
planned_changes,
|
|
388
|
+
possible_decisions: ['preserve', 'close', 'replace', 'blocked'],
|
|
389
|
+
reason,
|
|
390
|
+
risks,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function collectActiveSliceState(projectRoot, options = {}) {
|
|
395
|
+
const slices = Array.isArray(options.slices) ? options.slices : readResolverSlices(projectRoot, options.specSlug || '');
|
|
396
|
+
const lookup = buildSliceLookup(slices);
|
|
397
|
+
const sources = [
|
|
398
|
+
...parseActiveSliceDoc(projectRoot, lookup),
|
|
399
|
+
...parseActiveSlicesBoard(projectRoot, lookup),
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
supported_sources: [
|
|
404
|
+
{ path: 'docs/ai/ACTIVE_SLICE.md', kind: 'active-doc', exists: fs.existsSync(path.join(projectRoot, 'docs', 'ai', 'ACTIVE_SLICE.md')) },
|
|
405
|
+
{ path: 'ACTIVE_SLICES.md', kind: 'active-board', exists: fs.existsSync(path.join(projectRoot, 'ACTIVE_SLICES.md')) },
|
|
406
|
+
],
|
|
407
|
+
sources,
|
|
408
|
+
reconciliation: buildActiveSliceReconciliation(sources),
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
module.exports = {
|
|
413
|
+
CANONICAL_STATUSES,
|
|
414
|
+
DEFAULT_AGENT_STATUS,
|
|
415
|
+
DEFAULT_RUN_STATUS,
|
|
416
|
+
DEFAULT_SLICE_STATUS,
|
|
417
|
+
DEFAULT_SPEC_STATUS,
|
|
418
|
+
collectActiveSliceState,
|
|
419
|
+
filterSlicesForExecution,
|
|
420
|
+
groupSlicesBySpec,
|
|
421
|
+
isBlockedStatus,
|
|
422
|
+
isCompletedStatus,
|
|
423
|
+
normalizeStatus,
|
|
424
|
+
progressForSlice,
|
|
425
|
+
relativeProjectPath,
|
|
426
|
+
resolveProjectState,
|
|
427
|
+
summarizeGraph,
|
|
428
|
+
summarizeSliceProgress,
|
|
429
|
+
toPosix,
|
|
430
|
+
};
|
|
@@ -3,7 +3,8 @@ const path = require('path');
|
|
|
3
3
|
const { catFileExists, currentBranch, hasLocalBranch, hasRemoteBranch, mergeBaseIsAncestor, revListCount, runGit, statusPorcelain, worktreeList } = require('./git');
|
|
4
4
|
const { parseJsonWithComments } = require('./json');
|
|
5
5
|
const { buildGraph, normalizeDeclaredDependencies, readAllSlices, SliceGraphError, topoSort } = require('./slice-graph');
|
|
6
|
-
const { resolveSliceContext, toAlias } = require('./slice');
|
|
6
|
+
const { resolveSliceContext, toAlias, validateSliceMetaForStart } = require('./slice');
|
|
7
|
+
const { validateProjectRelativePaths } = require('./paths');
|
|
7
8
|
|
|
8
9
|
function ensureExists(filePath, message) {
|
|
9
10
|
if (!fs.existsSync(filePath)) {
|
|
@@ -111,6 +112,13 @@ function validateLocalSliceArtifacts(repoRoot, slice) {
|
|
|
111
112
|
throw new Error('create-quiver: slice.json.files contiene entradas invalidas.');
|
|
112
113
|
}
|
|
113
114
|
console.log('PASS: slice.json declara archivos de alcance.');
|
|
115
|
+
|
|
116
|
+
validateSliceMetaForStart(slice);
|
|
117
|
+
console.log('PASS: slice.json declara metadata git compatible con start-slice.');
|
|
118
|
+
|
|
119
|
+
validateProjectRelativePaths(slice.files, 'slice.json files/allowed_write_paths');
|
|
120
|
+
validateProjectRelativePaths(slice.expectedReadPaths, 'slice.json expected_read_paths');
|
|
121
|
+
console.log('PASS: slice.json declara rutas relativas seguras dentro del proyecto.');
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
function baseRecoveryMessage(remote, baseBranch) {
|
|
@@ -219,6 +227,11 @@ function validateDeclaredDependencyContract(repoRoot, slice) {
|
|
|
219
227
|
}
|
|
220
228
|
}
|
|
221
229
|
|
|
230
|
+
function localCheckSummary() {
|
|
231
|
+
console.log('INFO: Modo local: checks ejecutados: spec docs, briefs, metadata git, scope declarado, rutas seguras, dependencias y gate.');
|
|
232
|
+
console.log('INFO: Modo local: checks omitidos: existencia en base remota/local y overlap contra worktrees activos.');
|
|
233
|
+
}
|
|
234
|
+
|
|
222
235
|
function checkSliceReadiness(sliceInput, options = {}) {
|
|
223
236
|
const gate = options.gate || 'execution';
|
|
224
237
|
const localMode = options.local === true;
|
|
@@ -262,6 +275,9 @@ function checkSliceReadiness(sliceInput, options = {}) {
|
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
validateDeclaredDependencyContract(repoRoot, slice);
|
|
278
|
+
if (localMode) {
|
|
279
|
+
localCheckSummary();
|
|
280
|
+
}
|
|
265
281
|
|
|
266
282
|
switch (gate) {
|
|
267
283
|
case 'ready':
|
|
@@ -371,22 +387,47 @@ function checkPrReadiness(sliceInput) {
|
|
|
371
387
|
|
|
372
388
|
function checkScope(sliceInput, options = {}) {
|
|
373
389
|
const strict = options.strict === true;
|
|
390
|
+
const remote = options.remote || 'origin';
|
|
374
391
|
const repoRoot = runGit(['rev-parse', '--show-toplevel'], process.cwd());
|
|
375
392
|
const slice = resolveSliceContext(repoRoot, sliceInput);
|
|
376
393
|
const declared = slice.files;
|
|
394
|
+
validateProjectRelativePaths(declared, 'slice scope path');
|
|
395
|
+
|
|
396
|
+
const explicitBaseBranch = typeof options.baseBranch === 'string' ? options.baseBranch.trim() : '';
|
|
397
|
+
const candidateBaseBranches = Array.from(new Set([
|
|
398
|
+
explicitBaseBranch,
|
|
399
|
+
slice.baseBranch,
|
|
400
|
+
'main',
|
|
401
|
+
'develop',
|
|
402
|
+
'master',
|
|
403
|
+
].filter(Boolean)));
|
|
404
|
+
|
|
405
|
+
let baseRef = '';
|
|
406
|
+
let baseSource = '';
|
|
407
|
+
for (const candidate of candidateBaseBranches) {
|
|
408
|
+
if (hasRemoteBranch(repoRoot, candidate, remote)) {
|
|
409
|
+
baseRef = `${remote}/${candidate}`;
|
|
410
|
+
baseSource = explicitBaseBranch === candidate ? '--base' : candidate === slice.baseBranch ? 'slice.git.base_branch' : 'fallback';
|
|
411
|
+
break;
|
|
412
|
+
}
|
|
413
|
+
if (hasLocalBranch(repoRoot, candidate)) {
|
|
414
|
+
baseRef = candidate;
|
|
415
|
+
baseSource = explicitBaseBranch === candidate ? '--base' : candidate === slice.baseBranch ? 'slice.git.base_branch' : 'fallback';
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
377
419
|
|
|
378
420
|
let touchedRaw = '';
|
|
379
|
-
if (
|
|
380
|
-
touchedRaw = runGit(['diff', '--name-only',
|
|
381
|
-
|
|
382
|
-
touchedRaw = runGit(['diff', '--name-only', 'develop...HEAD'], repoRoot);
|
|
421
|
+
if (baseRef) {
|
|
422
|
+
touchedRaw = runGit(['diff', '--name-only', `${baseRef}...HEAD`], repoRoot);
|
|
423
|
+
console.log(`INFO: check-scope base: ${baseRef} (${baseSource}).`);
|
|
383
424
|
} else {
|
|
384
|
-
console.log(
|
|
425
|
+
console.log(`WARN: No se encontro base para check-scope. Probadas: ${candidateBaseBranches.join(', ')}. Usa --base <branch> o configura git.base_branch en slice.json.`);
|
|
385
426
|
return;
|
|
386
427
|
}
|
|
387
428
|
|
|
388
429
|
if (!touchedRaw) {
|
|
389
|
-
console.log(
|
|
430
|
+
console.log(`WARN: No se encontraron archivos modificados respecto de ${baseRef}.`);
|
|
390
431
|
return;
|
|
391
432
|
}
|
|
392
433
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const { statusPorcelain } = require('./git');
|
|
2
2
|
const { normalizeContextPath } = require('./ai/safety');
|
|
3
3
|
const { checkScope } = require('./readiness');
|
|
4
|
+
const { validateProjectRelativePaths } = require('./paths');
|
|
4
5
|
|
|
5
6
|
class ScopeValidationError extends Error {
|
|
6
7
|
constructor(code, message, details = {}) {
|
|
@@ -118,7 +119,7 @@ function diffWorktreeSnapshots(beforeSnapshot, afterSnapshot) {
|
|
|
118
119
|
function validateScopeSnapshot({ allowedFiles = [], beforeSnapshot, afterSnapshot, strict = true } = {}) {
|
|
119
120
|
const normalizedAllowedFiles = Array.from(new Set(
|
|
120
121
|
Array.isArray(allowedFiles)
|
|
121
|
-
? allowedFiles.map(normalizeScopePath).filter(Boolean)
|
|
122
|
+
? validateProjectRelativePaths(allowedFiles, 'allowed scope path').map(normalizeScopePath).filter(Boolean)
|
|
122
123
|
: [],
|
|
123
124
|
));
|
|
124
125
|
const changedFiles = diffWorktreeSnapshots(beforeSnapshot, afterSnapshot);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { parseJsonWithComments } = require('./json');
|
|
4
|
-
const { normalizeGitBashDrivePath, relativePosixPath, resolveTargetRoot, specRelativePathFromPath, toPosixPath } = require('./paths');
|
|
4
|
+
const { assertPathInsideRoot, normalizeGitBashDrivePath, relativePosixPath, resolveTargetRoot, specRelativePathFromPath, toPosixPath } = require('./paths');
|
|
5
5
|
|
|
6
6
|
function readJson(filePath) {
|
|
7
7
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
@@ -89,9 +89,12 @@ function validateSliceMetaForStart(slice) {
|
|
|
89
89
|
throw new Error(`create-quiver: git.branch_type invalido: "${slice.branchType}". Usa "feature", "bugfix" o "hotfix".`);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
92
|
+
if (!/^[A-Za-z0-9._/-]+$/.test(slice.baseBranch)
|
|
93
|
+
|| slice.baseBranch.includes('..')
|
|
94
|
+
|| slice.baseBranch.startsWith('/')
|
|
95
|
+
|| slice.baseBranch.endsWith('/')
|
|
96
|
+
|| slice.baseBranch.includes('\\')) {
|
|
97
|
+
throw new Error('create-quiver: git.base_branch invalido. Usa una rama base valida como "main", "develop", "master" o "release/2026".');
|
|
95
98
|
}
|
|
96
99
|
|
|
97
100
|
const expectedBranchName = `${slice.branchType}/${slice.ticket}-${slice.branchSlug}`;
|
|
@@ -116,6 +119,7 @@ function resolveRepoSlicePath(repoRoot, relSlicePath) {
|
|
|
116
119
|
function resolveSliceContext(repoRoot, slicePath) {
|
|
117
120
|
const canonicalRepoRoot = canonicalizePath(repoRoot);
|
|
118
121
|
let absSlicePath = resolveSlicePath(slicePath);
|
|
122
|
+
assertPathInsideRoot(canonicalRepoRoot, absSlicePath, 'slice path');
|
|
119
123
|
let relSlicePath = relativePosixPath(canonicalRepoRoot, absSlicePath);
|
|
120
124
|
let parts = relSlicePath.split('/');
|
|
121
125
|
|