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
@@ -2,11 +2,25 @@ 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
+ collectActiveSliceState,
9
+ filterSlicesForExecution,
10
+ groupSlicesBySpec: groupResolvedSlicesBySpec,
11
+ isBlockedStatus: isCanonicalBlockedStatus,
12
+ isCompletedStatus: isCanonicalCompletedStatus,
13
+ normalizeStatus,
14
+ progressForSlice: resolveProgressForSlice,
15
+ resolveProjectState,
16
+ summarizeGraph: summarizeResolvedGraph,
17
+ summarizeSliceProgress,
18
+ } = require('../project-state-resolver');
19
+ const { detectFileConflicts } = require('../slice-graph');
20
+ const { readPlanReview } = require('./plan-review');
7
21
  const { listAiRuns, nextCommandForPhase } = require('./run-state');
8
22
 
9
- const EXPORT_SCHEMA_VERSION = 1;
23
+ const EXPORT_SCHEMA_VERSION = 2;
10
24
 
11
25
  function toPosix(relativePath) {
12
26
  return String(relativePath || '').split(path.sep).join('/');
@@ -38,43 +52,19 @@ function readPackageSummary(projectRoot) {
38
52
  }
39
53
 
40
54
  function isCompletedStatus(status) {
41
- return ['closed', 'completed', 'done'].includes(String(status || '').toLowerCase());
55
+ return isCanonicalCompletedStatus('slice', status);
42
56
  }
43
57
 
44
58
  function isBlockedStatus(slice) {
45
- return String(slice?.status || '').toLowerCase() === 'blocked' || Boolean(slice?.json?.blocked_reason);
59
+ return isCanonicalBlockedStatus('slice', slice?.canonical_status || slice?.status, slice);
46
60
  }
47
61
 
48
62
  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;
63
+ return resolveProgressForSlice(slice);
62
64
  }
63
65
 
64
66
  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
- };
67
+ return summarizeSliceProgress(items);
78
68
  }
79
69
 
80
70
  function statusForSpec(specSlices) {
@@ -94,59 +84,11 @@ function statusForSpec(specSlices) {
94
84
  }
95
85
 
96
86
  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));
87
+ return groupResolvedSlicesBySpec(slices);
113
88
  }
114
89
 
115
90
  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
- }
91
+ return summarizeResolvedGraph(slices);
150
92
  }
151
93
 
152
94
  function filterGraphSummary(graph, selectedRefs) {
@@ -187,6 +129,7 @@ function normalizeSlice(projectRoot, slice, dependencyMap) {
187
129
  id: slice.sliceId,
188
130
  title: slice.title,
189
131
  status: slice.status,
132
+ canonical_status: slice.canonical_status || normalizeStatus('slice', slice.status, 'planned'),
190
133
  progress: progressForSlice(slice),
191
134
  spec_slug: slice.specSlug,
192
135
  spec_family: slice.specFamily,
@@ -209,6 +152,7 @@ function normalizeRuns(projectRoot) {
209
152
  return listAiRuns(projectRoot).map((run) => ({
210
153
  run_id: run.run_id,
211
154
  status: run.status,
155
+ canonical_status: normalizeStatus('run', run.status, 'draft'),
212
156
  phase: run.phase,
213
157
  spec_slug: run.spec_slug || null,
214
158
  requirement_path: run.requirement?.path || null,
@@ -219,9 +163,73 @@ function normalizeRuns(projectRoot) {
219
163
  }));
220
164
  }
221
165
 
166
+ function safeReadApproval(projectRoot, phase) {
167
+ try {
168
+ return readPhaseApproval(projectRoot, phase);
169
+ } catch (error) {
170
+ return {
171
+ phase,
172
+ status: 'invalid',
173
+ draft: null,
174
+ approved: null,
175
+ meta: null,
176
+ error: error.message,
177
+ };
178
+ }
179
+ }
180
+
181
+ function safeReadPlanReview(projectRoot) {
182
+ try {
183
+ return readPlanReview(projectRoot);
184
+ } catch (error) {
185
+ return {
186
+ status: 'invalid',
187
+ review: null,
188
+ meta: null,
189
+ error: error.message,
190
+ };
191
+ }
192
+ }
193
+
194
+ function normalizeApproval(projectRoot, phase, approval) {
195
+ const drafts = Array.isArray(approval?.meta?.drafts) ? approval.meta.drafts : [];
196
+ return {
197
+ phase,
198
+ status: approval?.status || 'missing',
199
+ canonical_status: normalizeStatus('approval', approval?.status || 'missing', 'pending'),
200
+ draft_path: approval?.draft?.path || null,
201
+ approved_path: approval?.approved?.path || null,
202
+ latest_draft_version: Number(approval?.meta?.draft?.version || 0) || null,
203
+ approved_version: Number(approval?.meta?.approved?.version || 0) || null,
204
+ draft_count: drafts.length,
205
+ source_file: approval?.meta?.approved?.source_file || approval?.meta?.draft?.source_file || null,
206
+ error: approval?.error || null,
207
+ };
208
+ }
209
+
210
+ function normalizeApprovals(projectRoot) {
211
+ const plannerApprovals = PLANNER_APPROVAL_PHASES.map((phase) => normalizeApproval(projectRoot, phase, safeReadApproval(projectRoot, phase)));
212
+ const planReview = safeReadPlanReview(projectRoot);
213
+
214
+ return plannerApprovals.concat({
215
+ phase: 'plan-review',
216
+ status: planReview.status || 'missing',
217
+ canonical_status: normalizeStatus('approval', planReview.status || 'missing', 'pending'),
218
+ draft_path: null,
219
+ approved_path: planReview.review?.path || null,
220
+ latest_draft_version: Number(planReview.meta?.source_version || 0) || null,
221
+ approved_version: null,
222
+ draft_count: 0,
223
+ source_file: planReview.meta?.source_file || null,
224
+ error: planReview.error || null,
225
+ });
226
+ }
227
+
222
228
  function normalizeAgents(projectRoot) {
223
229
  return listAgentProfiles(projectRoot).map((item) => ({
224
230
  role: item.role,
231
+ status: 'idle',
232
+ canonical_status: normalizeStatus('agent', 'idle', 'idle'),
225
233
  configured: item.configured,
226
234
  provider: item.profile?.provider || null,
227
235
  model: item.profile?.model || null,
@@ -231,10 +239,141 @@ function normalizeAgents(projectRoot) {
231
239
  }));
232
240
  }
233
241
 
242
+ function collectEvidenceEntries(slices) {
243
+ return (Array.isArray(slices) ? slices : [])
244
+ .flatMap((slice) => {
245
+ const evidence = Array.isArray(slice.json?.evidence) ? slice.json.evidence : [];
246
+ return evidence.map((item, index) => ({
247
+ slice_ref: slice.ref,
248
+ index,
249
+ value: item,
250
+ }));
251
+ })
252
+ .sort((left, right) => left.slice_ref.localeCompare(right.slice_ref) || left.index - right.index);
253
+ }
254
+
255
+ function countByStatus(items, statusKey = 'canonical_status') {
256
+ return (Array.isArray(items) ? items : []).reduce((acc, item) => {
257
+ const key = item?.[statusKey] || item?.status || 'unknown';
258
+ acc[key] = (acc[key] || 0) + 1;
259
+ return acc;
260
+ }, {});
261
+ }
262
+
263
+ function collectWarnings({ graph, layout, specs, slices }) {
264
+ const warnings = [];
265
+
266
+ if (!graph.ok && graph.error?.message) {
267
+ warnings.push({
268
+ code: graph.error.code || 'GRAPH_ERROR',
269
+ message: graph.error.message,
270
+ });
271
+ }
272
+
273
+ if (layout.layout === 'legacy' || layout.layout === 'hybrid' || layout.layout === 'incomplete') {
274
+ warnings.push({
275
+ code: 'LAYOUT_REQUIRES_ATTENTION',
276
+ message: layout.recommendations.join(' '),
277
+ });
278
+ }
279
+
280
+ if (specs.length === 0) {
281
+ warnings.push({
282
+ code: 'NO_SPECS_FOUND',
283
+ message: 'No specs were found for the selected export filters.',
284
+ });
285
+ }
286
+
287
+ if (slices.length === 0) {
288
+ warnings.push({
289
+ code: 'NO_SLICES_FOUND',
290
+ message: 'No slices were found for the selected export filters.',
291
+ });
292
+ }
293
+
294
+ return warnings;
295
+ }
296
+
297
+ function collectNextSteps(data) {
298
+ const activeRun = [...data.runs].reverse().find((run) => run.status !== 'closed');
299
+ const commands = [];
300
+ const firstSpec = data.specs[0] || null;
301
+ const firstSlice = data.slices[0] || null;
302
+ const activeRunWantsSpecCreate = Boolean(activeRun?.next_command && activeRun.next_command.includes('spec create'));
303
+
304
+ if (activeRun && activeRunWantsSpecCreate && firstSpec) {
305
+ commands.push({
306
+ id: 'validate-existing-spec',
307
+ command: `npx create-quiver spec validate ${firstSpec.path}`,
308
+ reason: `A spec already exists while run ${activeRun.run_id} points to spec creation.`,
309
+ });
310
+ commands.push({
311
+ id: 'find-ready-slice',
312
+ command: 'npx create-quiver next --all-ready',
313
+ reason: 'Find ready slices before creating another spec.',
314
+ });
315
+ if (firstSlice?.slice_json) {
316
+ commands.push({
317
+ id: 'prompt-existing-slice',
318
+ command: `npx create-quiver ai prompt-slice --slice ${firstSlice.slice_json}`,
319
+ reason: 'Prepare a minimal executor prompt for the existing spec.',
320
+ });
321
+ }
322
+ } else {
323
+ commands.push({
324
+ id: activeRun ? 'continue-active-run' : 'create-ai-run',
325
+ command: activeRun ? activeRun.next_command : 'npx create-quiver ai run create --input <requirements.md>',
326
+ reason: activeRun ? `Continue AI run ${activeRun.run_id}.` : 'Start a new AI lifecycle run.',
327
+ });
328
+ }
329
+
330
+ if (data.summary.slices > 0) {
331
+ if (data.active_slice?.reconciliation?.decision && data.active_slice.reconciliation.decision !== 'preserve') {
332
+ commands.push({
333
+ id: 'reconcile-active-slice',
334
+ command: 'npx create-quiver ai active-slice reconcile --dry-run',
335
+ reason: 'Review active-slice state before assigning more execution work.',
336
+ });
337
+ }
338
+ commands.push({
339
+ id: 'inspect-slices',
340
+ command: 'npx create-quiver ai slices list',
341
+ reason: 'Inspect current slice state.',
342
+ });
343
+ commands.push({
344
+ id: 'export-json',
345
+ command: 'npx create-quiver ai export --format json',
346
+ reason: 'Export machine-readable lifecycle state.',
347
+ });
348
+ } else {
349
+ commands.push({
350
+ id: 'draft-acceptance',
351
+ command: 'npx create-quiver ai plan --phase acceptance --input <requirements.md> --dry-run',
352
+ reason: 'Preview acceptance criteria generation.',
353
+ });
354
+ }
355
+
356
+ if (data.migration.layout === 'legacy' || data.migration.layout === 'hybrid' || data.migration.layout === 'incomplete') {
357
+ commands.push({
358
+ id: 'preview-migration',
359
+ command: 'npx create-quiver migrate --dry-run',
360
+ reason: 'Preview migration to the current layout.',
361
+ });
362
+ }
363
+
364
+ return commands;
365
+ }
366
+
234
367
  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);
368
+ const state = resolveProjectState(projectRoot, {
369
+ allowGraphErrors: true,
370
+ specSlug: options.specSlug,
371
+ });
372
+ const allSlices = state.graph.nodes;
373
+ const slices = filterSlicesForExecution(allSlices, {
374
+ includeCompleted: options.includeCompleted === true,
375
+ });
376
+ const fullGraph = buildGraphSummary(state.graph);
238
377
  const selectedRefs = new Set(slices.map((slice) => slice.ref));
239
378
  const graph = filterGraphSummary(fullGraph, selectedRefs);
240
379
  if (graph.ok) {
@@ -255,7 +394,8 @@ function collectLifecycleExport(projectRoot, options = {}) {
255
394
  spec_path: fs.existsSync(path.join(projectRoot, specPath, 'SPEC.md')) ? toPosix(path.join(specPath, 'SPEC.md')) : null,
256
395
  status_path: fs.existsSync(path.join(projectRoot, specPath, 'STATUS.md')) ? toPosix(path.join(specPath, 'STATUS.md')) : null,
257
396
  pr_path: fs.existsSync(path.join(projectRoot, specPath, 'pr.md')) ? toPosix(path.join(specPath, 'pr.md')) : null,
258
- status: statusForSpec(spec.slices),
397
+ status: spec.status || statusForSpec(spec.slices),
398
+ canonical_status: spec.canonical_status || normalizeStatus('spec', spec.status || statusForSpec(spec.slices), 'planned'),
259
399
  progress,
260
400
  slices: spec.slices.map((slice) => slice.ref),
261
401
  blockers: spec.slices.filter((slice) => isBlockedStatus(slice)).map((slice) => ({
@@ -267,7 +407,10 @@ function collectLifecycleExport(projectRoot, options = {}) {
267
407
  const layout = collectLayoutReport(projectRoot);
268
408
  const runs = normalizeRuns(projectRoot);
269
409
  const agents = normalizeAgents(projectRoot);
410
+ const approvals = normalizeApprovals(projectRoot);
270
411
  const progress = summarizeProgress(slices);
412
+ const evidence = collectEvidenceEntries(slices);
413
+ const activeSlice = collectActiveSliceState(projectRoot, { slices: allSlices });
271
414
  const blockers = normalizedSlices
272
415
  .filter((slice) => slice.blocked_reason || String(slice.status).toLowerCase() === 'blocked')
273
416
  .map((slice) => ({ ref: slice.ref, reason: slice.blocked_reason || 'blocked' }));
@@ -278,10 +421,22 @@ function collectLifecycleExport(projectRoot, options = {}) {
278
421
  if (layout.layout === 'legacy' || layout.layout === 'hybrid' || layout.layout === 'incomplete') {
279
422
  blockers.push({ ref: 'migration', reason: layout.recommendations.join(' ') });
280
423
  }
424
+ if (activeSlice.reconciliation.decision === 'blocked') {
425
+ blockers.push({ ref: 'active-slice', reason: activeSlice.reconciliation.reason });
426
+ }
281
427
 
282
- return {
428
+ const exportData = {
283
429
  schema_version: EXPORT_SCHEMA_VERSION,
284
430
  generated_at: new Date().toISOString(),
431
+ source_metadata: {
432
+ generator: 'create-quiver',
433
+ command: 'ai export',
434
+ resolver: 'project-state-resolver',
435
+ project_root_name: path.basename(projectRoot),
436
+ include_completed: options.includeCompleted === true,
437
+ spec_filter: options.specSlug || null,
438
+ families: Array.from(new Set(allSlices.map((slice) => slice.specFamily))).sort((left, right) => left.localeCompare(right)),
439
+ },
285
440
  project: readPackageSummary(projectRoot),
286
441
  summary: {
287
442
  specs: specs.length,
@@ -292,8 +447,12 @@ function collectLifecycleExport(projectRoot, options = {}) {
292
447
  progress_percent: progress.percent,
293
448
  runs: runs.length,
294
449
  configured_agents: agents.filter((agent) => agent.configured).length,
450
+ approvals: approvals.length,
451
+ active_slice_sources: activeSlice.sources.length,
452
+ warnings: 0,
295
453
  },
296
454
  agents,
455
+ approvals,
297
456
  runs,
298
457
  specs,
299
458
  slices: normalizedSlices,
@@ -313,6 +472,27 @@ function collectLifecycleExport(projectRoot, options = {}) {
313
472
  recommendations: layout.recommendations,
314
473
  dry_run_command: 'npx create-quiver migrate --dry-run',
315
474
  },
475
+ evidence,
476
+ active_slice: activeSlice,
477
+ warnings: [],
478
+ blockers,
479
+ next_steps: [],
480
+ lifecycle: {
481
+ phase: runs.length > 0 ? runs[runs.length - 1].phase : 'no-active-run',
482
+ active_run_id: (runs.length > 0 ? [...runs].reverse().find((run) => run.status !== 'closed') : null)?.run_id || null,
483
+ include_completed: options.includeCompleted === true,
484
+ spec_filter: options.specSlug || null,
485
+ levels: graph.levels,
486
+ },
487
+ aggregates: {
488
+ specs_by_status: countByStatus(specs),
489
+ slices_by_status: countByStatus(normalizedSlices),
490
+ runs_by_status: countByStatus(runs),
491
+ approvals_by_status: countByStatus(approvals),
492
+ blockers: blockers.length,
493
+ evidence: evidence.length,
494
+ progress_percent: progress.percent,
495
+ },
316
496
  dashboard: {
317
497
  progress,
318
498
  blockers,
@@ -337,8 +517,21 @@ function collectLifecycleExport(projectRoot, options = {}) {
337
517
  blocker: slice.blocked_reason,
338
518
  })),
339
519
  dependencies: graph.edges,
520
+ active_slice: activeSlice,
340
521
  },
341
522
  };
523
+
524
+ exportData.warnings = collectWarnings({
525
+ graph,
526
+ layout,
527
+ specs,
528
+ slices: normalizedSlices,
529
+ });
530
+ exportData.summary.warnings = exportData.warnings.length;
531
+ exportData.next_steps = collectNextSteps(exportData);
532
+ exportData.lifecycle.next_commands = exportData.next_steps.map((step) => step.command);
533
+
534
+ return exportData;
342
535
  }
343
536
 
344
537
  function formatLifecycleInspect(data) {
@@ -354,18 +547,17 @@ function formatLifecycleInspect(data) {
354
547
  'Next safe commands',
355
548
  ];
356
549
 
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');
550
+ for (const step of data.next_steps || collectNextSteps(data)) {
551
+ lines.push(`- ${step.command}`);
365
552
  }
366
553
 
367
- if (data.migration.layout === 'legacy' || data.migration.layout === 'hybrid' || data.migration.layout === 'incomplete') {
368
- lines.push('- npx create-quiver migrate --dry-run');
554
+ if (data.active_slice) {
555
+ lines.push(
556
+ '',
557
+ 'Active slice state',
558
+ `- Sources: ${data.active_slice.sources.length}`,
559
+ `- Reconciliation: ${data.active_slice.reconciliation.decision} (${data.active_slice.reconciliation.reason})`,
560
+ );
369
561
  }
370
562
 
371
563
  if (data.dashboard.blockers.length > 0) {
@@ -59,10 +59,75 @@ function formatGhInstallGuidance() {
59
59
  'GitHub CLI is not installed.',
60
60
  'macOS: brew install gh',
61
61
  'Linux: follow https://github.com/cli/cli/blob/trunk/docs/install_linux.md or use your distro package manager',
62
- 'Windows: winget install GitHub.cli',
62
+ 'Windows PowerShell: winget install GitHub.cli',
63
+ 'Git Bash/WSL: install gh inside the environment where the command will run, then authenticate there',
63
64
  ].join('\n');
64
65
  }
65
66
 
67
+ function quotePosixArg(arg) {
68
+ const value = String(arg);
69
+ return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`;
70
+ }
71
+
72
+ function quotePowerShellArg(arg) {
73
+ const value = String(arg);
74
+ return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replace(/'/g, "''")}'`;
75
+ }
76
+
77
+ function hasShellSensitivePath(...values) {
78
+ return values.some((value) => /\s/.test(String(value || '')));
79
+ }
80
+
81
+ function formatShellPathGuidance(optionName, examplePath) {
82
+ const fallbackPath = examplePath || '~/ssh/github work';
83
+ const windowsFallback = examplePath || '$HOME\\ssh\\github work';
84
+ return [
85
+ 'Path guidance:',
86
+ `- macOS/Linux: ${optionName} ${quotePosixArg(fallbackPath)}`,
87
+ `- Windows PowerShell: ${optionName} ${quotePowerShellArg(windowsFallback)}`,
88
+ `- Git Bash/WSL: ${optionName} ${quotePosixArg(fallbackPath)}`,
89
+ '- Quote paths with spaces; do not remove spaces from real file names.',
90
+ ].join('\n');
91
+ }
92
+
93
+ function formatSshAliasGuidance(alias = '<alias>') {
94
+ const resolvedAlias = alias || '<alias>';
95
+ return [
96
+ 'SSH alias setup:',
97
+ `- macOS/Linux/Git Bash/WSL: edit ~/.ssh/config and add a Host entry such as \`Host ${resolvedAlias}\`, \`HostName github.com\`, \`User git\`, and \`IdentityFile ~/.ssh/<key>\`.`,
98
+ `- Windows PowerShell: edit $HOME\\.ssh\\config and add the same Host entry for ${resolvedAlias}.`,
99
+ `- Verify the alias with: ssh -T ${resolvedAlias}`,
100
+ ].join('\n');
101
+ }
102
+
103
+ function formatCommandForShell(command, args, quoter) {
104
+ return `${command} ${args.map(quoter).join(' ')}`;
105
+ }
106
+
107
+ function classifyGhAuthFailure(output) {
108
+ const text = String(output || '').toLowerCase();
109
+ const issues = [];
110
+
111
+ if (/not logged|not authenticated|authentication required|no account/.test(text)) {
112
+ issues.push('no GitHub account is authenticated for this host');
113
+ }
114
+ if (/scope|permission|forbidden|403|oauth/.test(text)) {
115
+ issues.push('the active token may be missing repo/org scopes');
116
+ }
117
+ if (/account|user|login|host/.test(text) && !issues.some((issue) => issue.includes('account'))) {
118
+ issues.push('the active GitHub account or host may not match this repository');
119
+ }
120
+ if (/ssh|identity|alias|public key|permission denied/.test(text)) {
121
+ issues.push('the SSH alias or identity may not match the authenticated GitHub account');
122
+ }
123
+
124
+ if (issues.length === 0) {
125
+ issues.push('GitHub CLI authentication is not usable for this repository yet');
126
+ }
127
+
128
+ return `Likely issue: ${issues.join('; ')}.`;
129
+ }
130
+
66
131
  function createError(code, message, details = {}) {
67
132
  return new GitHubPreflightError(code, message, details);
68
133
  }
@@ -150,7 +215,15 @@ function ensureGhAuthenticated(options = {}) {
150
215
  const details = [stderr, stdout].filter(Boolean).join('\n');
151
216
  throw createError(
152
217
  'GH_NOT_AUTHENTICATED',
153
- `${formatError('gh auth status failed. Run gh auth login and then re-run the preflight.')}${details ? `\n${details}` : ''}`,
218
+ `${formatActionableError({
219
+ failure: 'gh auth status failed. GitHub CLI is not authenticated or the active account/scopes are not usable.',
220
+ impact: 'Quiver cannot verify the GitHub account, repository permissions, or PR readiness.',
221
+ fix: [
222
+ classifyGhAuthFailure(details),
223
+ '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.',
224
+ ].join(' '),
225
+ nextCommand: 'gh auth status',
226
+ })}${details ? `\nDetails:\n${details}` : ''}`,
154
227
  {
155
228
  command,
156
229
  authArgs,
@@ -240,7 +313,12 @@ function ensureIdentityFile(repoRoot, identityFile) {
240
313
  if (!fs.existsSync(resolved)) {
241
314
  throw createError(
242
315
  'MISSING_IDENTITY_FILE',
243
- formatError(`missing SSH identity file at ${resolved}. Check the path you passed as identityFile.`),
316
+ formatActionableError({
317
+ failure: `missing SSH identity file at ${resolved}.`,
318
+ impact: 'Quiver cannot verify the SSH identity that should be used for GitHub PR commands.',
319
+ fix: `Check the path passed with --identity-file and quote it for your shell when it contains spaces.\n${formatShellPathGuidance('--identity-file', normalized)}`,
320
+ nextCommand: 'npx create-quiver ai doctor --dry-run --ssh-host-alias <alias> --identity-file <path>',
321
+ }),
244
322
  {
245
323
  identityFile: normalized,
246
324
  resolvedIdentityFile: resolved,
@@ -259,7 +337,7 @@ function ensureSshHostAlias(sshHostAlias) {
259
337
  formatActionableError({
260
338
  failure: 'missing SSH host alias. Pass --ssh-host-alias <alias> before opening the PR.',
261
339
  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.',
340
+ fix: formatSshAliasGuidance('github-work'),
263
341
  nextCommand: 'ssh -T <alias>',
264
342
  }),
265
343
  );
@@ -531,6 +609,10 @@ function formatPreflightReport(report, options = {}) {
531
609
  lines.push(`Identity file: ${report.identityFile}`);
532
610
  }
533
611
 
612
+ if (hasShellSensitivePath(report.repoRoot, report.guidePath, report.identityFile)) {
613
+ lines.push(formatShellPathGuidance('--identity-file', report.identityFile || '<path with spaces>'));
614
+ }
615
+
534
616
  lines.push('Checks: gh, gh auth status, git remote, worktree branch, GitFlow guide, SSH identity file');
535
617
 
536
618
  if (dryRun) {
@@ -568,6 +650,12 @@ function formatPrCreateReport({ preflight, plan, result }, options = {}) {
568
650
  lines.push(`Identity file: ${preflight.identityFile}`);
569
651
  }
570
652
 
653
+ if (hasShellSensitivePath(preflight.repoRoot, preflight.identityFile, plan.prBodyPath, ...plan.args)) {
654
+ lines.push('Shell-safe command examples:');
655
+ lines.push(`- macOS/Linux/Git Bash/WSL: ${formatCommandForShell(plan.ghCommand, plan.args, quotePosixArg)}`);
656
+ lines.push(`- Windows PowerShell: ${formatCommandForShell(plan.ghCommand, plan.args, quotePowerShellArg)}`);
657
+ }
658
+
571
659
  if (dryRun) {
572
660
  lines.push('No PR will be created in dry-run mode.');
573
661
  } else if (!create) {
@@ -598,6 +686,7 @@ module.exports = {
598
686
  ensureWorktreeReady,
599
687
  findPrBodyCandidates,
600
688
  formatGhInstallGuidance,
689
+ formatSshAliasGuidance,
601
690
  formatPreflightReport,
602
691
  formatPrCreateReport,
603
692
  preflightGitHubPr,