create-quiver 0.13.0 → 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 +19 -0
- package/README.md +9 -2
- package/README_FOR_AI.md +4 -0
- package/ROADMAP.md +6 -0
- package/docs/COMMANDS.md.template +3 -1
- package/docs/TROUBLESHOOTING.md.template +29 -0
- package/docs/WORKFLOW.md.template +13 -12
- package/package.json +1 -1
- 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 +481 -14
- package/src/create-quiver/commands/spec.js +10 -0
- package/src/create-quiver/index.js +42 -4
- package/src/create-quiver/lib/ai/context-packs.js +2 -2
- package/src/create-quiver/lib/ai/export-state.js +52 -5
- package/src/create-quiver/lib/ai/github.js +14 -2
- package/src/create-quiver/lib/ai/plan-review.js +159 -0
- package/src/create-quiver/lib/ai/run-state.js +17 -2
- package/src/create-quiver/lib/ai/spec-generator.js +15 -0
- package/src/create-quiver/lib/project-state-resolver.js +195 -1
- package/src/create-quiver/lib/spec-worktrees.js +50 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
1
2
|
const path = require('path');
|
|
2
3
|
|
|
3
4
|
const {
|
|
@@ -215,12 +216,206 @@ function relativeProjectPath(projectRoot, filePath) {
|
|
|
215
216
|
return toPosix(path.relative(projectRoot, filePath));
|
|
216
217
|
}
|
|
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
|
+
|
|
218
412
|
module.exports = {
|
|
219
413
|
CANONICAL_STATUSES,
|
|
220
414
|
DEFAULT_AGENT_STATUS,
|
|
221
415
|
DEFAULT_RUN_STATUS,
|
|
222
416
|
DEFAULT_SLICE_STATUS,
|
|
223
417
|
DEFAULT_SPEC_STATUS,
|
|
418
|
+
collectActiveSliceState,
|
|
224
419
|
filterSlicesForExecution,
|
|
225
420
|
groupSlicesBySpec,
|
|
226
421
|
isBlockedStatus,
|
|
@@ -233,4 +428,3 @@ module.exports = {
|
|
|
233
428
|
summarizeSliceProgress,
|
|
234
429
|
toPosix,
|
|
235
430
|
};
|
|
236
|
-
|
|
@@ -132,6 +132,48 @@ function assertExistingWorktreeUsable(branchName, worktreePath) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function parseDirtyStatusFiles(rawStatus) {
|
|
136
|
+
return String(rawStatus || '')
|
|
137
|
+
.split('\n')
|
|
138
|
+
.map((line) => line.trimEnd())
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.map((line) => {
|
|
141
|
+
if (line.startsWith('?? ')) {
|
|
142
|
+
return line.slice(3).trim();
|
|
143
|
+
}
|
|
144
|
+
const entry = (line[2] === ' ' ? line.slice(3) : line[1] === ' ' ? line.slice(2) : line.slice(3)).trim();
|
|
145
|
+
return entry.includes(' -> ') ? entry.split(' -> ').pop().trim() : entry;
|
|
146
|
+
})
|
|
147
|
+
.filter(Boolean);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatDirtyCheckoutRecovery(repoRoot) {
|
|
151
|
+
const files = parseDirtyStatusFiles(statusPorcelain(repoRoot));
|
|
152
|
+
const lines = [
|
|
153
|
+
'current checkout is not clean. Starting a spec worktree needs a clean main checkout.',
|
|
154
|
+
];
|
|
155
|
+
|
|
156
|
+
if (files.length > 0) {
|
|
157
|
+
lines.push('Dirty files:');
|
|
158
|
+
for (const file of files.slice(0, 20)) {
|
|
159
|
+
lines.push(`- ${file}`);
|
|
160
|
+
}
|
|
161
|
+
if (files.length > 20) {
|
|
162
|
+
lines.push(`- ...and ${files.length - 20} more`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
lines.push(
|
|
167
|
+
'Safe options:',
|
|
168
|
+
'- Commit the current changes if they belong to the active slice.',
|
|
169
|
+
'- Stash changes manually after reviewing them.',
|
|
170
|
+
'- Move this work to a separate worktree before starting the spec.',
|
|
171
|
+
'- Abort and rerun from a clean checkout.',
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return lines.filter((line) => line !== '').join('\n');
|
|
175
|
+
}
|
|
176
|
+
|
|
135
177
|
function resolveBaseRef(repoRoot, preferred = '') {
|
|
136
178
|
const candidates = [preferred, 'main', 'develop'].filter(Boolean);
|
|
137
179
|
for (const candidate of candidates) {
|
|
@@ -177,7 +219,10 @@ function buildSpecStatus(repoRoot, specInput) {
|
|
|
177
219
|
const pendingSlices = slices.filter((slice) => slice.status !== 'completed');
|
|
178
220
|
const laterSlicesBlocked = !slice00 || slice00.status !== 'completed';
|
|
179
221
|
const existingWorktree = findExistingWorktree(repoRoot, identity.branchName);
|
|
180
|
-
const
|
|
222
|
+
const expectedPathExists = fs.existsSync(identity.worktreePath);
|
|
223
|
+
const expectedPathUnregistered = Boolean(!existingWorktree && expectedPathExists);
|
|
224
|
+
const worktreeMissing = Boolean(existingWorktree && (!fs.existsSync(existingWorktree) || !isGitWorktree(existingWorktree)))
|
|
225
|
+
|| expectedPathUnregistered;
|
|
181
226
|
const worktreeDirty = existingWorktree && !worktreeMissing ? !isCleanWorktree(existingWorktree) : false;
|
|
182
227
|
|
|
183
228
|
return {
|
|
@@ -189,6 +234,7 @@ function buildSpecStatus(repoRoot, specInput) {
|
|
|
189
234
|
slices,
|
|
190
235
|
specDir,
|
|
191
236
|
worktreeDirty,
|
|
237
|
+
worktreeExpectedPathUnregistered: expectedPathUnregistered,
|
|
192
238
|
worktreeMissing,
|
|
193
239
|
};
|
|
194
240
|
}
|
|
@@ -200,6 +246,8 @@ function formatSpecStatus(status) {
|
|
|
200
246
|
`Branch: ${status.branchName}`,
|
|
201
247
|
`Worktree: ${status.existingWorktree || status.worktreePath}`,
|
|
202
248
|
`Worktree missing/stale: ${status.worktreeMissing ? 'yes' : 'no'}`,
|
|
249
|
+
`Worktree registered: ${status.existingWorktree ? 'yes' : 'no'}`,
|
|
250
|
+
status.worktreeExpectedPathUnregistered ? 'Worktree note: expected path exists but is not registered in git worktree list.' : '',
|
|
203
251
|
`Worktree dirty: ${status.worktreeDirty ? 'yes' : 'no'}`,
|
|
204
252
|
`slice-00: ${status.slice00 ? status.slice00.status : 'missing'}`,
|
|
205
253
|
`Later slices blocked: ${status.laterSlicesBlocked ? 'yes' : 'no'}`,
|
|
@@ -252,7 +300,7 @@ function startSpecWorktree(repoRoot, specInput, options = {}) {
|
|
|
252
300
|
}
|
|
253
301
|
|
|
254
302
|
if (!isCleanWorktree(repoRoot)) {
|
|
255
|
-
throw new Error(formatError(
|
|
303
|
+
throw new Error(formatError(formatDirtyCheckoutRecovery(repoRoot)));
|
|
256
304
|
}
|
|
257
305
|
|
|
258
306
|
if (options.dryRun === true) {
|