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.
Files changed (46) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +9 -2
  3. package/README_FOR_AI.md +4 -0
  4. package/ROADMAP.md +6 -0
  5. package/docs/COMMANDS.md.template +3 -1
  6. package/docs/TROUBLESHOOTING.md.template +29 -0
  7. package/docs/WORKFLOW.md.template +13 -12
  8. package/package.json +1 -1
  9. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
  10. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
  11. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
  12. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
  13. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
  14. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
  15. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
  16. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
  17. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
  18. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
  19. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
  20. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
  21. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
  22. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
  23. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
  24. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
  25. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
  26. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
  27. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
  28. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
  29. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
  30. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
  31. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
  32. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
  33. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
  34. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  35. package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
  36. package/src/create-quiver/commands/ai.js +481 -14
  37. package/src/create-quiver/commands/spec.js +10 -0
  38. package/src/create-quiver/index.js +42 -4
  39. package/src/create-quiver/lib/ai/context-packs.js +2 -2
  40. package/src/create-quiver/lib/ai/export-state.js +52 -5
  41. package/src/create-quiver/lib/ai/github.js +14 -2
  42. package/src/create-quiver/lib/ai/plan-review.js +159 -0
  43. package/src/create-quiver/lib/ai/run-state.js +17 -2
  44. package/src/create-quiver/lib/ai/spec-generator.js +15 -0
  45. package/src/create-quiver/lib/project-state-resolver.js +195 -1
  46. 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 worktreeMissing = Boolean(existingWorktree && (!fs.existsSync(existingWorktree) || !isGitWorktree(existingWorktree)));
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('current checkout is not clean. Commit or stash before starting a spec worktree.'));
303
+ throw new Error(formatError(formatDirtyCheckoutRecovery(repoRoot)));
256
304
  }
257
305
 
258
306
  if (options.dryRun === true) {