create-quiver 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +16 -8
  3. package/README_FOR_AI.md +11 -6
  4. package/ROADMAP.md +9 -2
  5. package/docs/COMMANDS.md.template +9 -2
  6. package/package.json +2 -1
  7. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +2 -2
  8. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +5 -5
  9. package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
  10. package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
  11. package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
  12. package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
  13. package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
  14. package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
  15. package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
  16. package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
  17. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
  18. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
  19. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
  20. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
  21. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
  22. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
  23. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
  24. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
  25. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
  26. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
  27. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
  28. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
  29. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
  30. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
  31. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
  32. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
  33. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
  34. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
  35. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
  36. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
  37. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
  38. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
  39. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
  40. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
  41. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
  42. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
  43. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
  44. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
  45. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
  46. package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
  47. package/src/create-quiver/commands/ai.js +84 -9
  48. package/src/create-quiver/commands/flow.js +52 -4
  49. package/src/create-quiver/commands/graph.js +7 -7
  50. package/src/create-quiver/commands/plan.js +6 -15
  51. package/src/create-quiver/commands/spec.js +282 -0
  52. package/src/create-quiver/index.js +83 -21
  53. package/src/create-quiver/lib/agent-profiles.js +15 -3
  54. package/src/create-quiver/lib/ai/artifacts.js +318 -0
  55. package/src/create-quiver/lib/ai/execution-plan.js +9 -0
  56. package/src/create-quiver/lib/ai/executor.js +3 -2
  57. package/src/create-quiver/lib/ai/export-state.js +242 -97
  58. package/src/create-quiver/lib/ai/github.js +80 -3
  59. package/src/create-quiver/lib/ai/plan-review.js +2 -0
  60. package/src/create-quiver/lib/ai/spec-generator.js +72 -13
  61. package/src/create-quiver/lib/ai/spec-templates.js +72 -12
  62. package/src/create-quiver/lib/analyze.js +2 -2
  63. package/src/create-quiver/lib/approvals.js +14 -2
  64. package/src/create-quiver/lib/doctor.js +79 -0
  65. package/src/create-quiver/lib/git.js +40 -1
  66. package/src/create-quiver/lib/handoff.js +43 -1
  67. package/src/create-quiver/lib/init-docs.js +11 -7
  68. package/src/create-quiver/lib/init-layout.js +1 -0
  69. package/src/create-quiver/lib/lifecycle.js +52 -3
  70. package/src/create-quiver/lib/locks.js +134 -0
  71. package/src/create-quiver/lib/package-safety.js +7 -0
  72. package/src/create-quiver/lib/paths.js +74 -0
  73. package/src/create-quiver/lib/project-scan.js +74 -0
  74. package/src/create-quiver/lib/project-state-resolver.js +236 -0
  75. package/src/create-quiver/lib/readiness.js +48 -7
  76. package/src/create-quiver/lib/scope.js +2 -1
  77. package/src/create-quiver/lib/slice.js +8 -4
  78. package/src/create-quiver/lib/spec-worktrees.js +121 -38
  79. package/src/create-quiver/lib/statuses.js +115 -0
@@ -9,6 +9,7 @@ const { currentBranch, runGit } = require('../git');
9
9
  const { redactSecrets, truncateText } = require('../evidence');
10
10
  const { captureWorktreeSnapshot, validateScopeSnapshot } = require('../scope');
11
11
  const { resolveSliceContext } = require('../slice');
12
+ const { validateProjectRelativePaths } = require('../paths');
12
13
 
13
14
  const DEFAULT_EXECUTE_PROVIDER = 'codex';
14
15
  const DEFAULT_EXECUTE_ROLE = 'executor';
@@ -303,8 +304,8 @@ function buildExecuteSliceContext({ repoRoot, slicePath, role, context }) {
303
304
  });
304
305
  const relativeSlicePath = toRelativePath(canonicalRepoRoot, slice.sliceAbs);
305
306
  const relativeBriefPath = toRelativePath(canonicalRepoRoot, briefPath);
306
- const allowedFiles = Array.isArray(slice.files) ? slice.files.map((file) => String(file)) : [];
307
- const expectedReadPaths = Array.isArray(slice.expectedReadPaths) ? slice.expectedReadPaths.map((file) => String(file)) : [];
307
+ const allowedFiles = validateProjectRelativePaths(Array.isArray(slice.files) ? slice.files.map((file) => String(file)) : [], 'slice write scope');
308
+ const expectedReadPaths = validateProjectRelativePaths(Array.isArray(slice.expectedReadPaths) ? slice.expectedReadPaths.map((file) => String(file)) : [], 'slice read scope');
308
309
  const acceptance = Array.isArray(slice.acceptance) ? slice.acceptance.map((item) => String(item)) : [];
309
310
  const validationCommands = Array.isArray(slice.tests) ? slice.tests.map((item) => String(item)) : [];
310
311
  const validationHints = Array.isArray(slice.validationHints) ? slice.validationHints.map((item) => String(item)) : [];
@@ -2,11 +2,24 @@ const fs = require('node:fs');
2
2
  const path = require('node:path');
3
3
 
4
4
  const { listAgentProfiles } = require('../agent-profiles');
5
+ const { PLANNER_APPROVAL_PHASES, readPhaseApproval } = require('../approvals');
5
6
  const { collectLayoutReport } = require('../doctor');
6
- const { buildGraph, computeLevels, detectFileConflicts, readAllSlices, SliceGraphError } = require('../slice-graph');
7
+ const {
8
+ filterSlicesForExecution,
9
+ groupSlicesBySpec: groupResolvedSlicesBySpec,
10
+ isBlockedStatus: isCanonicalBlockedStatus,
11
+ isCompletedStatus: isCanonicalCompletedStatus,
12
+ normalizeStatus,
13
+ progressForSlice: resolveProgressForSlice,
14
+ resolveProjectState,
15
+ summarizeGraph: summarizeResolvedGraph,
16
+ summarizeSliceProgress,
17
+ } = require('../project-state-resolver');
18
+ const { detectFileConflicts } = require('../slice-graph');
19
+ const { readPlanReview } = require('./plan-review');
7
20
  const { listAiRuns, nextCommandForPhase } = require('./run-state');
8
21
 
9
- const EXPORT_SCHEMA_VERSION = 1;
22
+ const EXPORT_SCHEMA_VERSION = 2;
10
23
 
11
24
  function toPosix(relativePath) {
12
25
  return String(relativePath || '').split(path.sep).join('/');
@@ -38,43 +51,19 @@ function readPackageSummary(projectRoot) {
38
51
  }
39
52
 
40
53
  function isCompletedStatus(status) {
41
- return ['closed', 'completed', 'done'].includes(String(status || '').toLowerCase());
54
+ return isCanonicalCompletedStatus('slice', status);
42
55
  }
43
56
 
44
57
  function isBlockedStatus(slice) {
45
- return String(slice?.status || '').toLowerCase() === 'blocked' || Boolean(slice?.json?.blocked_reason);
58
+ return isCanonicalBlockedStatus('slice', slice?.canonical_status || slice?.status, slice);
46
59
  }
47
60
 
48
61
  function progressForSlice(slice) {
49
- const explicit = Number(slice?.json?.progress);
50
- if (Number.isFinite(explicit)) {
51
- return Math.max(0, Math.min(100, explicit));
52
- }
53
-
54
- const status = String(slice?.status || '').toLowerCase();
55
- if (isCompletedStatus(status)) {
56
- return 100;
57
- }
58
- if (status === 'in-progress' || status === 'active' || status === 'review') {
59
- return 50;
60
- }
61
- return 0;
62
+ return resolveProgressForSlice(slice);
62
63
  }
63
64
 
64
65
  function summarizeProgress(items) {
65
- const total = items.length;
66
- const completed = items.filter((item) => isCompletedStatus(item.status)).length;
67
- const blocked = items.filter((item) => isBlockedStatus(item)).length;
68
- const open = Math.max(0, total - completed);
69
- const percent = total === 0 ? 0 : Math.round((completed / total) * 100);
70
-
71
- return {
72
- total,
73
- completed,
74
- open,
75
- blocked,
76
- percent,
77
- };
66
+ return summarizeSliceProgress(items);
78
67
  }
79
68
 
80
69
  function statusForSpec(specSlices) {
@@ -94,59 +83,11 @@ function statusForSpec(specSlices) {
94
83
  }
95
84
 
96
85
  function groupSlicesBySpec(slices) {
97
- const groups = new Map();
98
-
99
- for (const slice of slices) {
100
- const key = `${slice.specFamily}/${slice.specSlug}`;
101
- if (!groups.has(key)) {
102
- groups.set(key, []);
103
- }
104
- groups.get(key).push(slice);
105
- }
106
-
107
- return Array.from(groups.entries())
108
- .map(([key, specSlices]) => {
109
- const [specFamily, specSlug] = key.split('/');
110
- return { specFamily, specSlug, slices: specSlices };
111
- })
112
- .sort((left, right) => left.specSlug.localeCompare(right.specSlug));
86
+ return groupResolvedSlicesBySpec(slices);
113
87
  }
114
88
 
115
89
  function buildGraphSummary(slices) {
116
- try {
117
- const graph = buildGraph(slices);
118
- const levels = computeLevels(graph).map((level, index) => ({
119
- level: index,
120
- slices: level.map((slice) => slice.ref),
121
- }));
122
-
123
- return {
124
- ok: true,
125
- edges: graph.edges.map((edge) => ({ from: edge.from, to: edge.to })),
126
- levels,
127
- conflicts: detectFileConflicts(graph.nodes).map((conflict) => ({
128
- files: conflict.files,
129
- slices: conflict.slices,
130
- })),
131
- error: null,
132
- nodes: graph.nodes,
133
- };
134
- } catch (error) {
135
- if (error instanceof SliceGraphError) {
136
- return {
137
- ok: false,
138
- edges: [],
139
- levels: [],
140
- conflicts: [],
141
- error: {
142
- code: error.code,
143
- message: error.message,
144
- },
145
- nodes: slices,
146
- };
147
- }
148
- throw error;
149
- }
90
+ return summarizeResolvedGraph(slices);
150
91
  }
151
92
 
152
93
  function filterGraphSummary(graph, selectedRefs) {
@@ -187,6 +128,7 @@ function normalizeSlice(projectRoot, slice, dependencyMap) {
187
128
  id: slice.sliceId,
188
129
  title: slice.title,
189
130
  status: slice.status,
131
+ canonical_status: slice.canonical_status || normalizeStatus('slice', slice.status, 'planned'),
190
132
  progress: progressForSlice(slice),
191
133
  spec_slug: slice.specSlug,
192
134
  spec_family: slice.specFamily,
@@ -209,6 +151,7 @@ function normalizeRuns(projectRoot) {
209
151
  return listAiRuns(projectRoot).map((run) => ({
210
152
  run_id: run.run_id,
211
153
  status: run.status,
154
+ canonical_status: normalizeStatus('run', run.status, 'draft'),
212
155
  phase: run.phase,
213
156
  spec_slug: run.spec_slug || null,
214
157
  requirement_path: run.requirement?.path || null,
@@ -219,9 +162,73 @@ function normalizeRuns(projectRoot) {
219
162
  }));
220
163
  }
221
164
 
165
+ function safeReadApproval(projectRoot, phase) {
166
+ try {
167
+ return readPhaseApproval(projectRoot, phase);
168
+ } catch (error) {
169
+ return {
170
+ phase,
171
+ status: 'invalid',
172
+ draft: null,
173
+ approved: null,
174
+ meta: null,
175
+ error: error.message,
176
+ };
177
+ }
178
+ }
179
+
180
+ function safeReadPlanReview(projectRoot) {
181
+ try {
182
+ return readPlanReview(projectRoot);
183
+ } catch (error) {
184
+ return {
185
+ status: 'invalid',
186
+ review: null,
187
+ meta: null,
188
+ error: error.message,
189
+ };
190
+ }
191
+ }
192
+
193
+ function normalizeApproval(projectRoot, phase, approval) {
194
+ const drafts = Array.isArray(approval?.meta?.drafts) ? approval.meta.drafts : [];
195
+ return {
196
+ phase,
197
+ status: approval?.status || 'missing',
198
+ canonical_status: normalizeStatus('approval', approval?.status || 'missing', 'pending'),
199
+ draft_path: approval?.draft?.path || null,
200
+ approved_path: approval?.approved?.path || null,
201
+ latest_draft_version: Number(approval?.meta?.draft?.version || 0) || null,
202
+ approved_version: Number(approval?.meta?.approved?.version || 0) || null,
203
+ draft_count: drafts.length,
204
+ source_file: approval?.meta?.approved?.source_file || approval?.meta?.draft?.source_file || null,
205
+ error: approval?.error || null,
206
+ };
207
+ }
208
+
209
+ function normalizeApprovals(projectRoot) {
210
+ const plannerApprovals = PLANNER_APPROVAL_PHASES.map((phase) => normalizeApproval(projectRoot, phase, safeReadApproval(projectRoot, phase)));
211
+ const planReview = safeReadPlanReview(projectRoot);
212
+
213
+ return plannerApprovals.concat({
214
+ phase: 'plan-review',
215
+ status: planReview.status || 'missing',
216
+ canonical_status: normalizeStatus('approval', planReview.status || 'missing', 'pending'),
217
+ draft_path: null,
218
+ approved_path: planReview.review?.path || null,
219
+ latest_draft_version: Number(planReview.meta?.source_version || 0) || null,
220
+ approved_version: null,
221
+ draft_count: 0,
222
+ source_file: planReview.meta?.source_file || null,
223
+ error: planReview.error || null,
224
+ });
225
+ }
226
+
222
227
  function normalizeAgents(projectRoot) {
223
228
  return listAgentProfiles(projectRoot).map((item) => ({
224
229
  role: item.role,
230
+ status: 'idle',
231
+ canonical_status: normalizeStatus('agent', 'idle', 'idle'),
225
232
  configured: item.configured,
226
233
  provider: item.profile?.provider || null,
227
234
  model: item.profile?.model || null,
@@ -231,10 +238,111 @@ function normalizeAgents(projectRoot) {
231
238
  }));
232
239
  }
233
240
 
241
+ function collectEvidenceEntries(slices) {
242
+ return (Array.isArray(slices) ? slices : [])
243
+ .flatMap((slice) => {
244
+ const evidence = Array.isArray(slice.json?.evidence) ? slice.json.evidence : [];
245
+ return evidence.map((item, index) => ({
246
+ slice_ref: slice.ref,
247
+ index,
248
+ value: item,
249
+ }));
250
+ })
251
+ .sort((left, right) => left.slice_ref.localeCompare(right.slice_ref) || left.index - right.index);
252
+ }
253
+
254
+ function countByStatus(items, statusKey = 'canonical_status') {
255
+ return (Array.isArray(items) ? items : []).reduce((acc, item) => {
256
+ const key = item?.[statusKey] || item?.status || 'unknown';
257
+ acc[key] = (acc[key] || 0) + 1;
258
+ return acc;
259
+ }, {});
260
+ }
261
+
262
+ function collectWarnings({ graph, layout, specs, slices }) {
263
+ const warnings = [];
264
+
265
+ if (!graph.ok && graph.error?.message) {
266
+ warnings.push({
267
+ code: graph.error.code || 'GRAPH_ERROR',
268
+ message: graph.error.message,
269
+ });
270
+ }
271
+
272
+ if (layout.layout === 'legacy' || layout.layout === 'hybrid' || layout.layout === 'incomplete') {
273
+ warnings.push({
274
+ code: 'LAYOUT_REQUIRES_ATTENTION',
275
+ message: layout.recommendations.join(' '),
276
+ });
277
+ }
278
+
279
+ if (specs.length === 0) {
280
+ warnings.push({
281
+ code: 'NO_SPECS_FOUND',
282
+ message: 'No specs were found for the selected export filters.',
283
+ });
284
+ }
285
+
286
+ if (slices.length === 0) {
287
+ warnings.push({
288
+ code: 'NO_SLICES_FOUND',
289
+ message: 'No slices were found for the selected export filters.',
290
+ });
291
+ }
292
+
293
+ return warnings;
294
+ }
295
+
296
+ function collectNextSteps(data) {
297
+ const activeRun = [...data.runs].reverse().find((run) => run.status !== 'closed');
298
+ const commands = [];
299
+
300
+ commands.push({
301
+ id: activeRun ? 'continue-active-run' : 'create-ai-run',
302
+ command: activeRun ? activeRun.next_command : 'npx create-quiver ai run create --input <requirements.md>',
303
+ reason: activeRun ? `Continue AI run ${activeRun.run_id}.` : 'Start a new AI lifecycle run.',
304
+ });
305
+
306
+ if (data.summary.slices > 0) {
307
+ commands.push({
308
+ id: 'inspect-slices',
309
+ command: 'npx create-quiver ai slices list',
310
+ reason: 'Inspect current slice state.',
311
+ });
312
+ commands.push({
313
+ id: 'export-json',
314
+ command: 'npx create-quiver ai export --format json',
315
+ reason: 'Export machine-readable lifecycle state.',
316
+ });
317
+ } else {
318
+ commands.push({
319
+ id: 'draft-acceptance',
320
+ command: 'npx create-quiver ai plan --phase acceptance --input <requirements.md> --dry-run',
321
+ reason: 'Preview acceptance criteria generation.',
322
+ });
323
+ }
324
+
325
+ if (data.migration.layout === 'legacy' || data.migration.layout === 'hybrid' || data.migration.layout === 'incomplete') {
326
+ commands.push({
327
+ id: 'preview-migration',
328
+ command: 'npx create-quiver migrate --dry-run',
329
+ reason: 'Preview migration to the current layout.',
330
+ });
331
+ }
332
+
333
+ return commands;
334
+ }
335
+
234
336
  function collectLifecycleExport(projectRoot, options = {}) {
235
- const allSlices = readAllSlices(projectRoot);
236
- const slices = options.includeCompleted ? allSlices : allSlices.filter((slice) => !isCompletedStatus(slice.status));
237
- const fullGraph = buildGraphSummary(allSlices);
337
+ const state = resolveProjectState(projectRoot, {
338
+ allowGraphErrors: true,
339
+ specSlug: options.specSlug,
340
+ });
341
+ const allSlices = state.graph.nodes;
342
+ const slices = filterSlicesForExecution(allSlices, {
343
+ includeCompleted: options.includeCompleted === true,
344
+ });
345
+ const fullGraph = buildGraphSummary(state.graph);
238
346
  const selectedRefs = new Set(slices.map((slice) => slice.ref));
239
347
  const graph = filterGraphSummary(fullGraph, selectedRefs);
240
348
  if (graph.ok) {
@@ -255,7 +363,8 @@ function collectLifecycleExport(projectRoot, options = {}) {
255
363
  spec_path: fs.existsSync(path.join(projectRoot, specPath, 'SPEC.md')) ? toPosix(path.join(specPath, 'SPEC.md')) : null,
256
364
  status_path: fs.existsSync(path.join(projectRoot, specPath, 'STATUS.md')) ? toPosix(path.join(specPath, 'STATUS.md')) : null,
257
365
  pr_path: fs.existsSync(path.join(projectRoot, specPath, 'pr.md')) ? toPosix(path.join(specPath, 'pr.md')) : null,
258
- status: statusForSpec(spec.slices),
366
+ status: spec.status || statusForSpec(spec.slices),
367
+ canonical_status: spec.canonical_status || normalizeStatus('spec', spec.status || statusForSpec(spec.slices), 'planned'),
259
368
  progress,
260
369
  slices: spec.slices.map((slice) => slice.ref),
261
370
  blockers: spec.slices.filter((slice) => isBlockedStatus(slice)).map((slice) => ({
@@ -267,7 +376,9 @@ function collectLifecycleExport(projectRoot, options = {}) {
267
376
  const layout = collectLayoutReport(projectRoot);
268
377
  const runs = normalizeRuns(projectRoot);
269
378
  const agents = normalizeAgents(projectRoot);
379
+ const approvals = normalizeApprovals(projectRoot);
270
380
  const progress = summarizeProgress(slices);
381
+ const evidence = collectEvidenceEntries(slices);
271
382
  const blockers = normalizedSlices
272
383
  .filter((slice) => slice.blocked_reason || String(slice.status).toLowerCase() === 'blocked')
273
384
  .map((slice) => ({ ref: slice.ref, reason: slice.blocked_reason || 'blocked' }));
@@ -279,9 +390,18 @@ function collectLifecycleExport(projectRoot, options = {}) {
279
390
  blockers.push({ ref: 'migration', reason: layout.recommendations.join(' ') });
280
391
  }
281
392
 
282
- return {
393
+ const exportData = {
283
394
  schema_version: EXPORT_SCHEMA_VERSION,
284
395
  generated_at: new Date().toISOString(),
396
+ source_metadata: {
397
+ generator: 'create-quiver',
398
+ command: 'ai export',
399
+ resolver: 'project-state-resolver',
400
+ project_root_name: path.basename(projectRoot),
401
+ include_completed: options.includeCompleted === true,
402
+ spec_filter: options.specSlug || null,
403
+ families: Array.from(new Set(allSlices.map((slice) => slice.specFamily))).sort((left, right) => left.localeCompare(right)),
404
+ },
285
405
  project: readPackageSummary(projectRoot),
286
406
  summary: {
287
407
  specs: specs.length,
@@ -292,8 +412,11 @@ function collectLifecycleExport(projectRoot, options = {}) {
292
412
  progress_percent: progress.percent,
293
413
  runs: runs.length,
294
414
  configured_agents: agents.filter((agent) => agent.configured).length,
415
+ approvals: approvals.length,
416
+ warnings: 0,
295
417
  },
296
418
  agents,
419
+ approvals,
297
420
  runs,
298
421
  specs,
299
422
  slices: normalizedSlices,
@@ -313,6 +436,26 @@ function collectLifecycleExport(projectRoot, options = {}) {
313
436
  recommendations: layout.recommendations,
314
437
  dry_run_command: 'npx create-quiver migrate --dry-run',
315
438
  },
439
+ evidence,
440
+ warnings: [],
441
+ blockers,
442
+ next_steps: [],
443
+ lifecycle: {
444
+ phase: runs.length > 0 ? runs[runs.length - 1].phase : 'no-active-run',
445
+ active_run_id: (runs.length > 0 ? [...runs].reverse().find((run) => run.status !== 'closed') : null)?.run_id || null,
446
+ include_completed: options.includeCompleted === true,
447
+ spec_filter: options.specSlug || null,
448
+ levels: graph.levels,
449
+ },
450
+ aggregates: {
451
+ specs_by_status: countByStatus(specs),
452
+ slices_by_status: countByStatus(normalizedSlices),
453
+ runs_by_status: countByStatus(runs),
454
+ approvals_by_status: countByStatus(approvals),
455
+ blockers: blockers.length,
456
+ evidence: evidence.length,
457
+ progress_percent: progress.percent,
458
+ },
316
459
  dashboard: {
317
460
  progress,
318
461
  blockers,
@@ -339,6 +482,18 @@ function collectLifecycleExport(projectRoot, options = {}) {
339
482
  dependencies: graph.edges,
340
483
  },
341
484
  };
485
+
486
+ exportData.warnings = collectWarnings({
487
+ graph,
488
+ layout,
489
+ specs,
490
+ slices: normalizedSlices,
491
+ });
492
+ exportData.summary.warnings = exportData.warnings.length;
493
+ exportData.next_steps = collectNextSteps(exportData);
494
+ exportData.lifecycle.next_commands = exportData.next_steps.map((step) => step.command);
495
+
496
+ return exportData;
342
497
  }
343
498
 
344
499
  function formatLifecycleInspect(data) {
@@ -354,18 +509,8 @@ function formatLifecycleInspect(data) {
354
509
  'Next safe commands',
355
510
  ];
356
511
 
357
- const activeRun = [...data.runs].reverse().find((run) => run.status !== 'closed');
358
- lines.push(`- ${activeRun ? activeRun.next_command : 'npx create-quiver ai run create --input <requirements.md>'}`);
359
-
360
- if (data.summary.slices > 0) {
361
- lines.push('- npx create-quiver ai slices list');
362
- lines.push('- npx create-quiver ai export --format json');
363
- } else {
364
- lines.push('- npx create-quiver ai plan --phase acceptance --input <requirements.md> --dry-run');
365
- }
366
-
367
- if (data.migration.layout === 'legacy' || data.migration.layout === 'hybrid' || data.migration.layout === 'incomplete') {
368
- lines.push('- npx create-quiver migrate --dry-run');
512
+ for (const step of data.next_steps || collectNextSteps(data)) {
513
+ lines.push(`- ${step.command}`);
369
514
  }
370
515
 
371
516
  if (data.dashboard.blockers.length > 0) {
@@ -63,6 +63,60 @@ function formatGhInstallGuidance() {
63
63
  ].join('\n');
64
64
  }
65
65
 
66
+ function quotePosixArg(arg) {
67
+ const value = String(arg);
68
+ return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
69
+ }
70
+
71
+ function quotePowerShellArg(arg) {
72
+ const value = String(arg);
73
+ return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replace(/'/g, "''")}'`;
74
+ }
75
+
76
+ function hasShellSensitivePath(...values) {
77
+ return values.some((value) => /\s/.test(String(value || '')));
78
+ }
79
+
80
+ function formatShellPathGuidance(optionName, examplePath) {
81
+ const fallbackPath = examplePath || '~/ssh/github work';
82
+ const windowsFallback = examplePath || '$HOME\\ssh\\github work';
83
+ return [
84
+ 'Path guidance:',
85
+ `- macOS/Linux: ${optionName} ${quotePosixArg(fallbackPath)}`,
86
+ `- Windows PowerShell: ${optionName} ${quotePowerShellArg(windowsFallback)}`,
87
+ `- Git Bash/WSL: ${optionName} ${quotePosixArg(fallbackPath)}`,
88
+ '- Quote paths with spaces; do not remove spaces from real file names.',
89
+ ].join('\n');
90
+ }
91
+
92
+ function formatCommandForShell(command, args, quoter) {
93
+ return `${command} ${args.map(quoter).join(' ')}`;
94
+ }
95
+
96
+ function classifyGhAuthFailure(output) {
97
+ const text = String(output || '').toLowerCase();
98
+ const issues = [];
99
+
100
+ if (/not logged|not authenticated|authentication required|no account/.test(text)) {
101
+ issues.push('no GitHub account is authenticated for this host');
102
+ }
103
+ if (/scope|permission|forbidden|403|oauth/.test(text)) {
104
+ issues.push('the active token may be missing repo/org scopes');
105
+ }
106
+ if (/account|user|login|host/.test(text) && !issues.some((issue) => issue.includes('account'))) {
107
+ issues.push('the active GitHub account or host may not match this repository');
108
+ }
109
+ if (/ssh|identity|alias|public key|permission denied/.test(text)) {
110
+ issues.push('the SSH alias or identity may not match the authenticated GitHub account');
111
+ }
112
+
113
+ if (issues.length === 0) {
114
+ issues.push('GitHub CLI authentication is not usable for this repository yet');
115
+ }
116
+
117
+ return `Likely issue: ${issues.join('; ')}.`;
118
+ }
119
+
66
120
  function createError(code, message, details = {}) {
67
121
  return new GitHubPreflightError(code, message, details);
68
122
  }
@@ -150,7 +204,15 @@ function ensureGhAuthenticated(options = {}) {
150
204
  const details = [stderr, stdout].filter(Boolean).join('\n');
151
205
  throw createError(
152
206
  'GH_NOT_AUTHENTICATED',
153
- `${formatError('gh auth status failed. Run gh auth login and then re-run the preflight.')}${details ? `\n${details}` : ''}`,
207
+ `${formatActionableError({
208
+ failure: 'gh auth status failed. GitHub CLI is not authenticated or the active account/scopes are not usable.',
209
+ impact: 'Quiver cannot verify the GitHub account, repository permissions, or PR readiness.',
210
+ fix: [
211
+ classifyGhAuthFailure(details),
212
+ 'Run `gh auth login`, confirm the expected GitHub account and host, verify repo/org scopes, and if you use --ssh-host-alias run `ssh -T <alias>` to confirm the SSH identity.',
213
+ ].join(' '),
214
+ nextCommand: 'gh auth status',
215
+ })}${details ? `\nDetails:\n${details}` : ''}`,
154
216
  {
155
217
  command,
156
218
  authArgs,
@@ -240,7 +302,12 @@ function ensureIdentityFile(repoRoot, identityFile) {
240
302
  if (!fs.existsSync(resolved)) {
241
303
  throw createError(
242
304
  'MISSING_IDENTITY_FILE',
243
- formatError(`missing SSH identity file at ${resolved}. Check the path you passed as identityFile.`),
305
+ formatActionableError({
306
+ failure: `missing SSH identity file at ${resolved}.`,
307
+ impact: 'Quiver cannot verify the SSH identity that should be used for GitHub PR commands.',
308
+ fix: `Check the path passed with --identity-file and quote it for your shell when it contains spaces.\n${formatShellPathGuidance('--identity-file', normalized)}`,
309
+ nextCommand: 'npx create-quiver ai doctor --dry-run --ssh-host-alias <alias> --identity-file <path>',
310
+ }),
244
311
  {
245
312
  identityFile: normalized,
246
313
  resolvedIdentityFile: resolved,
@@ -259,7 +326,7 @@ function ensureSshHostAlias(sshHostAlias) {
259
326
  formatActionableError({
260
327
  failure: 'missing SSH host alias. Pass --ssh-host-alias <alias> before opening the PR.',
261
328
  impact: 'Quiver cannot verify which GitHub SSH identity should be used for this PR flow.',
262
- fix: 'macOS/Linux: add a Host entry in ~/.ssh/config, for example `Host github-work`. Windows: add the Host entry in %USERPROFILE%\\.ssh\\config.',
329
+ fix: 'macOS/Linux/Git Bash/WSL: add a Host entry in ~/.ssh/config, for example `Host github-work`. Windows PowerShell: add the Host entry in $HOME\\.ssh\\config.',
263
330
  nextCommand: 'ssh -T <alias>',
264
331
  }),
265
332
  );
@@ -531,6 +598,10 @@ function formatPreflightReport(report, options = {}) {
531
598
  lines.push(`Identity file: ${report.identityFile}`);
532
599
  }
533
600
 
601
+ if (hasShellSensitivePath(report.repoRoot, report.guidePath, report.identityFile)) {
602
+ lines.push(formatShellPathGuidance('--identity-file', report.identityFile || '<path with spaces>'));
603
+ }
604
+
534
605
  lines.push('Checks: gh, gh auth status, git remote, worktree branch, GitFlow guide, SSH identity file');
535
606
 
536
607
  if (dryRun) {
@@ -568,6 +639,12 @@ function formatPrCreateReport({ preflight, plan, result }, options = {}) {
568
639
  lines.push(`Identity file: ${preflight.identityFile}`);
569
640
  }
570
641
 
642
+ if (hasShellSensitivePath(preflight.repoRoot, preflight.identityFile, plan.prBodyPath, ...plan.args)) {
643
+ lines.push('Shell-safe command examples:');
644
+ lines.push(`- macOS/Linux/Git Bash/WSL: ${formatCommandForShell(plan.ghCommand, plan.args, quotePosixArg)}`);
645
+ lines.push(`- Windows PowerShell: ${formatCommandForShell(plan.ghCommand, plan.args, quotePowerShellArg)}`);
646
+ }
647
+
571
648
  if (dryRun) {
572
649
  lines.push('No PR will be created in dry-run mode.');
573
650
  } else if (!create) {
@@ -192,6 +192,8 @@ function savePlanReview(projectRoot, options = {}) {
192
192
  source_kind: options.inputKind || null,
193
193
  source_version: options.inputVersion || null,
194
194
  path: toRelativePosix(projectRoot, reviewPath),
195
+ raw_artifact_path: options.rawArtifactPath || null,
196
+ output_source: options.outputSource || null,
195
197
  reviewed_at: now,
196
198
  };
197
199
  fs.writeFileSync(planReviewMetaPath(projectRoot), `${JSON.stringify(meta, null, 2)}\n`);