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.
Files changed (110) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +24 -9
  3. package/README_FOR_AI.md +15 -6
  4. package/ROADMAP.md +15 -2
  5. package/docs/COMMANDS.md.template +12 -3
  6. package/docs/TROUBLESHOOTING.md.template +29 -0
  7. package/docs/WORKFLOW.md.template +13 -12
  8. package/package.json +2 -1
  9. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +2 -2
  10. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +5 -5
  11. package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
  12. package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
  13. package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
  14. package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
  15. package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
  16. package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
  17. package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
  18. package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
  19. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
  20. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
  21. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
  22. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
  23. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
  24. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
  25. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
  26. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
  27. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
  28. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
  29. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
  30. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
  31. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
  32. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
  33. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
  34. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
  35. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
  36. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
  37. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
  38. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
  39. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
  40. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
  41. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
  42. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
  43. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
  44. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
  45. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
  46. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
  47. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  48. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
  49. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
  50. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
  51. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
  52. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
  53. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
  54. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
  55. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
  56. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
  57. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
  58. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
  59. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
  60. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
  61. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
  62. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
  63. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
  64. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
  65. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
  66. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
  67. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
  68. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
  69. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
  70. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
  71. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
  72. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
  73. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
  74. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  75. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
  76. package/src/create-quiver/commands/ai.js +563 -21
  77. package/src/create-quiver/commands/flow.js +52 -4
  78. package/src/create-quiver/commands/graph.js +7 -7
  79. package/src/create-quiver/commands/plan.js +6 -15
  80. package/src/create-quiver/commands/spec.js +292 -0
  81. package/src/create-quiver/index.js +125 -25
  82. package/src/create-quiver/lib/agent-profiles.js +15 -3
  83. package/src/create-quiver/lib/ai/artifacts.js +318 -0
  84. package/src/create-quiver/lib/ai/context-packs.js +2 -2
  85. package/src/create-quiver/lib/ai/execution-plan.js +9 -0
  86. package/src/create-quiver/lib/ai/executor.js +3 -2
  87. package/src/create-quiver/lib/ai/export-state.js +287 -95
  88. package/src/create-quiver/lib/ai/github.js +93 -4
  89. package/src/create-quiver/lib/ai/plan-review.js +161 -0
  90. package/src/create-quiver/lib/ai/run-state.js +17 -2
  91. package/src/create-quiver/lib/ai/spec-generator.js +87 -13
  92. package/src/create-quiver/lib/ai/spec-templates.js +72 -12
  93. package/src/create-quiver/lib/analyze.js +2 -2
  94. package/src/create-quiver/lib/approvals.js +14 -2
  95. package/src/create-quiver/lib/doctor.js +79 -0
  96. package/src/create-quiver/lib/git.js +40 -1
  97. package/src/create-quiver/lib/handoff.js +43 -1
  98. package/src/create-quiver/lib/init-docs.js +11 -7
  99. package/src/create-quiver/lib/init-layout.js +1 -0
  100. package/src/create-quiver/lib/lifecycle.js +52 -3
  101. package/src/create-quiver/lib/locks.js +134 -0
  102. package/src/create-quiver/lib/package-safety.js +7 -0
  103. package/src/create-quiver/lib/paths.js +74 -0
  104. package/src/create-quiver/lib/project-scan.js +74 -0
  105. package/src/create-quiver/lib/project-state-resolver.js +430 -0
  106. package/src/create-quiver/lib/readiness.js +48 -7
  107. package/src/create-quiver/lib/scope.js +2 -1
  108. package/src/create-quiver/lib/slice.js +8 -4
  109. package/src/create-quiver/lib/spec-worktrees.js +169 -38
  110. 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 (hasRemoteBranch(repoRoot, 'develop')) {
380
- touchedRaw = runGit(['diff', '--name-only', 'origin/develop...HEAD'], repoRoot);
381
- } else if (hasLocalBranch(repoRoot, 'develop')) {
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('WARN: No se encontro rama origin/develop ni develop. Saltando check de scope.');
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('WARN: No se encontraron archivos modificados respecto de develop.');
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
- const allowedBaseBranches = allowedBaseByType[slice.branchType];
93
- if (!allowedBaseBranches.includes(slice.baseBranch)) {
94
- throw new Error(`create-quiver: git.base_branch invalido para ${slice.branchType}. Usa "${allowedBaseBranches.join('" o "')}".`);
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