create-quiver 0.12.0 → 0.12.1

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 (109) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +49 -17
  3. package/README_FOR_AI.md +31 -29
  4. package/ROADMAP.md +15 -3
  5. package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
  6. package/docs/COMMANDS.md.template +44 -18
  7. package/docs/STATUS.md.template +5 -1
  8. package/docs/WORKFLOW.md.template +13 -11
  9. package/package.json +9 -3
  10. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EVIDENCE_REPORT.md +293 -0
  11. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EXECUTION_PLAN.md +58 -0
  12. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/SPEC.md +242 -0
  13. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/STATUS.md +35 -0
  14. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/pr.md +77 -0
  15. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +34 -0
  16. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
  17. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/slice.json +52 -0
  18. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/CLOSURE_BRIEF.md +36 -0
  19. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/EXECUTION_BRIEF.md +52 -0
  20. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/slice.json +56 -0
  21. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/CLOSURE_BRIEF.md +43 -0
  22. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/EXECUTION_BRIEF.md +54 -0
  23. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/slice.json +52 -0
  24. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/CLOSURE_BRIEF.md +35 -0
  25. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/EXECUTION_BRIEF.md +53 -0
  26. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/slice.json +54 -0
  27. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/CLOSURE_BRIEF.md +34 -0
  28. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/EXECUTION_BRIEF.md +54 -0
  29. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/slice.json +52 -0
  30. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/CLOSURE_BRIEF.md +34 -0
  31. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/EXECUTION_BRIEF.md +54 -0
  32. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/slice.json +53 -0
  33. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/CLOSURE_BRIEF.md +33 -0
  34. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/EXECUTION_BRIEF.md +56 -0
  35. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/slice.json +55 -0
  36. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/CLOSURE_BRIEF.md +33 -0
  37. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/EXECUTION_BRIEF.md +54 -0
  38. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/slice.json +52 -0
  39. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/CLOSURE_BRIEF.md +39 -0
  40. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/EXECUTION_BRIEF.md +56 -0
  41. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/slice.json +53 -0
  42. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/CLOSURE_BRIEF.md +38 -0
  43. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/EXECUTION_BRIEF.md +57 -0
  44. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/slice.json +52 -0
  45. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/CLOSURE_BRIEF.md +39 -0
  46. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/EXECUTION_BRIEF.md +55 -0
  47. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/slice.json +56 -0
  48. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/CLOSURE_BRIEF.md +36 -0
  49. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/EXECUTION_BRIEF.md +54 -0
  50. package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/slice.json +53 -0
  51. package/specs/quiver-v26-0121-smoke-hardening/EVIDENCE_REPORT.md +208 -0
  52. package/specs/quiver-v26-0121-smoke-hardening/EXECUTION_PLAN.md +57 -0
  53. package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +137 -0
  54. package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +32 -0
  55. package/specs/quiver-v26-0121-smoke-hardening/pr.md +96 -0
  56. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/CLOSURE_BRIEF.md +35 -0
  57. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/EXECUTION_BRIEF.md +55 -0
  58. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/slice.json +73 -0
  59. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/CLOSURE_BRIEF.md +38 -0
  60. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/EXECUTION_BRIEF.md +51 -0
  61. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/slice.json +76 -0
  62. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/CLOSURE_BRIEF.md +37 -0
  63. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/EXECUTION_BRIEF.md +52 -0
  64. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/slice.json +75 -0
  65. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/CLOSURE_BRIEF.md +37 -0
  66. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/EXECUTION_BRIEF.md +53 -0
  67. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/slice.json +77 -0
  68. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/CLOSURE_BRIEF.md +35 -0
  69. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/EXECUTION_BRIEF.md +52 -0
  70. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/slice.json +77 -0
  71. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/CLOSURE_BRIEF.md +34 -0
  72. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/EXECUTION_BRIEF.md +54 -0
  73. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/slice.json +84 -0
  74. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/CLOSURE_BRIEF.md +35 -0
  75. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/EXECUTION_BRIEF.md +53 -0
  76. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/slice.json +82 -0
  77. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/CLOSURE_BRIEF.md +35 -0
  78. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/EXECUTION_BRIEF.md +55 -0
  79. package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/slice.json +92 -0
  80. package/src/create-quiver/commands/ai.js +577 -27
  81. package/src/create-quiver/commands/flow.js +6 -5
  82. package/src/create-quiver/commands/graph.js +6 -4
  83. package/src/create-quiver/commands/plan.js +3 -3
  84. package/src/create-quiver/index.js +328 -12
  85. package/src/create-quiver/lib/actionable-error.js +27 -0
  86. package/src/create-quiver/lib/agent-profiles.js +1 -1
  87. package/src/create-quiver/lib/ai/context-packs.js +4 -0
  88. package/src/create-quiver/lib/ai/execution-plan.js +7 -1
  89. package/src/create-quiver/lib/ai/executor.js +270 -20
  90. package/src/create-quiver/lib/ai/export-state.js +534 -0
  91. package/src/create-quiver/lib/ai/github.js +83 -0
  92. package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
  93. package/src/create-quiver/lib/ai/plan-review.js +5 -2
  94. package/src/create-quiver/lib/ai/providers.js +4 -3
  95. package/src/create-quiver/lib/ai/run-state.js +414 -0
  96. package/src/create-quiver/lib/ai/spec-generator.js +12 -0
  97. package/src/create-quiver/lib/ai/spec-templates.js +78 -9
  98. package/src/create-quiver/lib/approvals.js +22 -3
  99. package/src/create-quiver/lib/demo.js +189 -14
  100. package/src/create-quiver/lib/doctor.js +75 -0
  101. package/src/create-quiver/lib/handoff.js +81 -12
  102. package/src/create-quiver/lib/init-docs.js +24 -6
  103. package/src/create-quiver/lib/init-layout.js +8 -0
  104. package/src/create-quiver/lib/json.js +53 -3
  105. package/src/create-quiver/lib/readiness.js +18 -3
  106. package/src/create-quiver/lib/scope.js +50 -7
  107. package/src/create-quiver/lib/slice-graph.js +138 -38
  108. package/src/create-quiver/lib/slice.js +6 -1
  109. package/src/create-quiver/lib/spec-worktrees.js +16 -2
@@ -0,0 +1,534 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ const { listAgentProfiles } = require('../agent-profiles');
5
+ const { collectLayoutReport } = require('../doctor');
6
+ const { buildGraph, computeLevels, detectFileConflicts, readAllSlices, SliceGraphError } = require('../slice-graph');
7
+ const { listAiRuns, nextCommandForPhase } = require('./run-state');
8
+
9
+ const EXPORT_SCHEMA_VERSION = 1;
10
+
11
+ function toPosix(relativePath) {
12
+ return String(relativePath || '').split(path.sep).join('/');
13
+ }
14
+
15
+ function relativePath(projectRoot, filePath) {
16
+ return toPosix(path.relative(projectRoot, filePath));
17
+ }
18
+
19
+ function readJsonIfExists(filePath) {
20
+ if (!fs.existsSync(filePath)) {
21
+ return null;
22
+ }
23
+
24
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
25
+ }
26
+
27
+ function readPackageSummary(projectRoot) {
28
+ const packageJson = readJsonIfExists(path.join(projectRoot, 'package.json'));
29
+
30
+ return {
31
+ name: packageJson?.name || path.basename(projectRoot) || 'project',
32
+ package_manager: fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml'))
33
+ ? 'pnpm'
34
+ : fs.existsSync(path.join(projectRoot, 'yarn.lock'))
35
+ ? 'yarn'
36
+ : 'npm',
37
+ };
38
+ }
39
+
40
+ function isCompletedStatus(status) {
41
+ return ['closed', 'completed', 'done'].includes(String(status || '').toLowerCase());
42
+ }
43
+
44
+ function isBlockedStatus(slice) {
45
+ return String(slice?.status || '').toLowerCase() === 'blocked' || Boolean(slice?.json?.blocked_reason);
46
+ }
47
+
48
+ 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
+ }
63
+
64
+ 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
+ };
78
+ }
79
+
80
+ function statusForSpec(specSlices) {
81
+ if (specSlices.length === 0) {
82
+ return 'empty';
83
+ }
84
+ if (specSlices.some((slice) => isBlockedStatus(slice))) {
85
+ return 'blocked';
86
+ }
87
+ if (specSlices.every((slice) => isCompletedStatus(slice.status))) {
88
+ return 'done';
89
+ }
90
+ if (specSlices.some((slice) => progressForSlice(slice) > 0)) {
91
+ return 'in-progress';
92
+ }
93
+ return 'planned';
94
+ }
95
+
96
+ 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));
113
+ }
114
+
115
+ 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
+ }
150
+ }
151
+
152
+ function filterGraphSummary(graph, selectedRefs) {
153
+ if (!graph.ok) {
154
+ return graph;
155
+ }
156
+
157
+ const levels = graph.levels
158
+ .map((level) => ({
159
+ original_level: level.level,
160
+ slices: level.slices.filter((ref) => selectedRefs.has(ref)),
161
+ }))
162
+ .filter((level) => level.slices.length > 0)
163
+ .map((level, index) => ({
164
+ level: index,
165
+ original_level: level.original_level,
166
+ slices: level.slices,
167
+ }));
168
+
169
+ return {
170
+ ...graph,
171
+ edges: graph.edges.filter((edge) => selectedRefs.has(edge.to)),
172
+ levels,
173
+ conflicts: graph.conflicts
174
+ .map((conflict) => ({
175
+ files: conflict.files,
176
+ slices: conflict.slices.filter((ref) => selectedRefs.has(ref)),
177
+ }))
178
+ .filter((conflict) => conflict.slices.length > 1),
179
+ };
180
+ }
181
+
182
+ function normalizeSlice(projectRoot, slice, dependencyMap) {
183
+ const dependencies = dependencyMap.get(slice.ref) || slice.depends_on || slice.dependencies || [];
184
+
185
+ return {
186
+ ref: slice.ref,
187
+ id: slice.sliceId,
188
+ title: slice.title,
189
+ status: slice.status,
190
+ progress: progressForSlice(slice),
191
+ spec_slug: slice.specSlug,
192
+ spec_family: slice.specFamily,
193
+ path: relativePath(projectRoot, slice.sliceDir),
194
+ slice_json: toPosix(path.join(slice.specFamily, slice.specSlug, 'slices', path.basename(slice.sliceDir), 'slice.json')),
195
+ dependencies,
196
+ parallel_safe: slice.parallel_safe,
197
+ parallel_safe_reason: slice.parallel_safe_reason,
198
+ allowed_write_paths: slice.allowed_write_paths,
199
+ expected_read_paths: slice.expected_read_paths,
200
+ validation_hints: slice.validation_hints,
201
+ files: slice.files,
202
+ evidence: Array.isArray(slice.json?.evidence) ? slice.json.evidence : [],
203
+ tests: Array.isArray(slice.json?.tests) ? slice.json.tests : [],
204
+ blocked_reason: slice.json?.blocked_reason || null,
205
+ };
206
+ }
207
+
208
+ function normalizeRuns(projectRoot) {
209
+ return listAiRuns(projectRoot).map((run) => ({
210
+ run_id: run.run_id,
211
+ status: run.status,
212
+ phase: run.phase,
213
+ spec_slug: run.spec_slug || null,
214
+ requirement_path: run.requirement?.path || null,
215
+ approvals_path: run.approvals_path || null,
216
+ state_path: toPosix(path.join('.quiver', 'runs', run.run_id, 'state.json')),
217
+ next_command: nextCommandForPhase(run.phase),
218
+ updated_at: run.updated_at || run.created_at || null,
219
+ }));
220
+ }
221
+
222
+ function normalizeAgents(projectRoot) {
223
+ return listAgentProfiles(projectRoot).map((item) => ({
224
+ role: item.role,
225
+ configured: item.configured,
226
+ provider: item.profile?.provider || null,
227
+ model: item.profile?.model || null,
228
+ label: item.profile?.label || null,
229
+ context: item.profile?.context || null,
230
+ updated_at: item.profile?.updated_at || null,
231
+ }));
232
+ }
233
+
234
+ 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);
238
+ const selectedRefs = new Set(slices.map((slice) => slice.ref));
239
+ const graph = filterGraphSummary(fullGraph, selectedRefs);
240
+ if (graph.ok) {
241
+ graph.conflicts = detectFileConflicts(slices).map((conflict) => ({
242
+ files: conflict.files,
243
+ slices: conflict.slices,
244
+ }));
245
+ }
246
+ const dependencyMap = new Map(fullGraph.nodes.map((node) => [node.ref, node.depends_on || node.dependencies || []]));
247
+ const normalizedSlices = slices.map((slice) => normalizeSlice(projectRoot, slice, dependencyMap));
248
+ const specs = groupSlicesBySpec(slices).map((spec) => {
249
+ const progress = summarizeProgress(spec.slices);
250
+ const specPath = path.join(spec.specFamily, spec.specSlug);
251
+ return {
252
+ slug: spec.specSlug,
253
+ family: spec.specFamily,
254
+ path: specPath,
255
+ spec_path: fs.existsSync(path.join(projectRoot, specPath, 'SPEC.md')) ? toPosix(path.join(specPath, 'SPEC.md')) : null,
256
+ status_path: fs.existsSync(path.join(projectRoot, specPath, 'STATUS.md')) ? toPosix(path.join(specPath, 'STATUS.md')) : null,
257
+ pr_path: fs.existsSync(path.join(projectRoot, specPath, 'pr.md')) ? toPosix(path.join(specPath, 'pr.md')) : null,
258
+ status: statusForSpec(spec.slices),
259
+ progress,
260
+ slices: spec.slices.map((slice) => slice.ref),
261
+ blockers: spec.slices.filter((slice) => isBlockedStatus(slice)).map((slice) => ({
262
+ ref: slice.ref,
263
+ reason: slice.json?.blocked_reason || 'blocked',
264
+ })),
265
+ };
266
+ });
267
+ const layout = collectLayoutReport(projectRoot);
268
+ const runs = normalizeRuns(projectRoot);
269
+ const agents = normalizeAgents(projectRoot);
270
+ const progress = summarizeProgress(slices);
271
+ const blockers = normalizedSlices
272
+ .filter((slice) => slice.blocked_reason || String(slice.status).toLowerCase() === 'blocked')
273
+ .map((slice) => ({ ref: slice.ref, reason: slice.blocked_reason || 'blocked' }));
274
+
275
+ if (!graph.ok) {
276
+ blockers.push({ ref: 'slice-graph', reason: graph.error.message });
277
+ }
278
+ if (layout.layout === 'legacy' || layout.layout === 'hybrid' || layout.layout === 'incomplete') {
279
+ blockers.push({ ref: 'migration', reason: layout.recommendations.join(' ') });
280
+ }
281
+
282
+ return {
283
+ schema_version: EXPORT_SCHEMA_VERSION,
284
+ generated_at: new Date().toISOString(),
285
+ project: readPackageSummary(projectRoot),
286
+ summary: {
287
+ specs: specs.length,
288
+ slices: progress.total,
289
+ completed_slices: progress.completed,
290
+ open_slices: progress.open,
291
+ blocked_slices: progress.blocked,
292
+ progress_percent: progress.percent,
293
+ runs: runs.length,
294
+ configured_agents: agents.filter((agent) => agent.configured).length,
295
+ },
296
+ agents,
297
+ runs,
298
+ specs,
299
+ slices: normalizedSlices,
300
+ graph: {
301
+ ok: graph.ok,
302
+ edges: graph.edges,
303
+ levels: graph.levels,
304
+ conflicts: graph.conflicts,
305
+ error: graph.error,
306
+ },
307
+ migration: {
308
+ layout: layout.layout,
309
+ has_new_layout: layout.hasNewLayout,
310
+ has_legacy_layout: layout.hasLegacyLayout,
311
+ legacy_signals: layout.legacySignals,
312
+ missing_new_layout_files: layout.missingNewLayoutFiles,
313
+ recommendations: layout.recommendations,
314
+ dry_run_command: 'npx create-quiver migrate --dry-run',
315
+ },
316
+ dashboard: {
317
+ progress,
318
+ blockers,
319
+ agents: agents.map((agent) => ({
320
+ id: agent.role,
321
+ role: agent.role,
322
+ configured: agent.configured,
323
+ provider: agent.provider,
324
+ })),
325
+ specs: specs.map((spec) => ({
326
+ id: spec.slug,
327
+ status: spec.status,
328
+ progress: spec.progress.percent,
329
+ slice_count: spec.progress.total,
330
+ blockers: spec.blockers,
331
+ })),
332
+ slices: normalizedSlices.map((slice) => ({
333
+ id: slice.ref,
334
+ status: slice.status,
335
+ progress: slice.progress,
336
+ dependencies: slice.dependencies,
337
+ blocker: slice.blocked_reason,
338
+ })),
339
+ dependencies: graph.edges,
340
+ },
341
+ };
342
+ }
343
+
344
+ function formatLifecycleInspect(data) {
345
+ const lines = [
346
+ 'Quiver lifecycle inspect',
347
+ `Project: ${data.project.name}`,
348
+ `Specs: ${data.summary.specs}`,
349
+ `Slices: ${data.summary.slices} total, ${data.summary.open_slices} open, ${data.summary.blocked_slices} blocked, ${data.summary.progress_percent}% done`,
350
+ `Runs: ${data.summary.runs}`,
351
+ `Agents configured: ${data.summary.configured_agents}/${data.agents.length}`,
352
+ `Layout: ${data.migration.layout}`,
353
+ '',
354
+ 'Next safe commands',
355
+ ];
356
+
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');
369
+ }
370
+
371
+ if (data.dashboard.blockers.length > 0) {
372
+ lines.push('', 'Blockers');
373
+ for (const blocker of data.dashboard.blockers) {
374
+ lines.push(`- ${blocker.ref}: ${blocker.reason}`);
375
+ }
376
+ }
377
+
378
+ lines.push('');
379
+ return `${lines.join('\n')}\n`;
380
+ }
381
+
382
+ function formatSpecsList(data) {
383
+ const lines = ['Quiver specs list'];
384
+
385
+ if (data.specs.length === 0) {
386
+ lines.push('- No specs found. Next: npx create-quiver spec create --dry-run');
387
+ lines.push('');
388
+ return `${lines.join('\n')}\n`;
389
+ }
390
+
391
+ for (const spec of data.specs) {
392
+ lines.push(`- ${spec.slug}: ${spec.status}, ${spec.progress.percent}% done, ${spec.progress.total} slices (${spec.path})`);
393
+ }
394
+
395
+ lines.push('');
396
+ return `${lines.join('\n')}\n`;
397
+ }
398
+
399
+ function formatSlicesList(data) {
400
+ const lines = ['Quiver slices list'];
401
+
402
+ if (data.slices.length === 0) {
403
+ lines.push('- No slices found. Next: npx create-quiver spec create --dry-run');
404
+ lines.push('');
405
+ return `${lines.join('\n')}\n`;
406
+ }
407
+
408
+ for (const slice of data.slices) {
409
+ const deps = slice.dependencies.length > 0 ? ` deps=${slice.dependencies.join(',')}` : '';
410
+ const blocked = slice.blocked_reason ? ` blocked=${slice.blocked_reason}` : '';
411
+ lines.push(`- ${slice.ref}: ${slice.status}, ${slice.progress}% done${deps}${blocked}`);
412
+ }
413
+
414
+ lines.push('');
415
+ return `${lines.join('\n')}\n`;
416
+ }
417
+
418
+ function formatTraceReport(data) {
419
+ const lines = [
420
+ 'Quiver trace report',
421
+ `Project: ${data.project.name}`,
422
+ `Schema: ${data.schema_version}`,
423
+ '',
424
+ 'Runs',
425
+ ];
426
+
427
+ if (data.runs.length === 0) {
428
+ lines.push('- none');
429
+ } else {
430
+ for (const run of data.runs) {
431
+ lines.push(`- ${run.run_id}: ${run.phase} (${run.status}) -> ${run.next_command}`);
432
+ }
433
+ }
434
+
435
+ lines.push('', 'Execution waves');
436
+ if (!data.graph.ok) {
437
+ lines.push(`- graph error: ${data.graph.error.message}`);
438
+ } else if (data.graph.levels.length === 0) {
439
+ lines.push('- none');
440
+ } else {
441
+ for (const level of data.graph.levels) {
442
+ lines.push(`- wave ${level.level}: ${level.slices.join(', ')}`);
443
+ }
444
+ }
445
+
446
+ lines.push('', 'Migration');
447
+ lines.push(`- layout: ${data.migration.layout}`);
448
+ for (const recommendation of data.migration.recommendations) {
449
+ lines.push(`- ${recommendation}`);
450
+ }
451
+
452
+ lines.push('');
453
+ return `${lines.join('\n')}\n`;
454
+ }
455
+
456
+ function formatLifecycleExportMarkdown(data) {
457
+ const lines = [
458
+ '# Quiver Lifecycle Export',
459
+ '',
460
+ `- Project: ${data.project.name}`,
461
+ `- Generated: ${data.generated_at}`,
462
+ `- Schema version: ${data.schema_version}`,
463
+ `- Specs: ${data.summary.specs}`,
464
+ `- Slices: ${data.summary.slices} total, ${data.summary.completed_slices} completed, ${data.summary.open_slices} open`,
465
+ `- Progress: ${data.summary.progress_percent}%`,
466
+ `- Layout: ${data.migration.layout}`,
467
+ '',
468
+ '## Specs',
469
+ '',
470
+ ];
471
+
472
+ if (data.specs.length === 0) {
473
+ lines.push('No specs found.');
474
+ } else {
475
+ lines.push('| Spec | Status | Progress | Slices | Path |');
476
+ lines.push('|---|---|---:|---:|---|');
477
+ for (const spec of data.specs) {
478
+ lines.push(`| ${spec.slug} | ${spec.status} | ${spec.progress.percent}% | ${spec.progress.total} | ${spec.path} |`);
479
+ }
480
+ }
481
+
482
+ lines.push('', '## Slices', '');
483
+ if (data.slices.length === 0) {
484
+ lines.push('No slices found.');
485
+ } else {
486
+ lines.push('| Slice | Status | Progress | Dependencies | Write Scope |');
487
+ lines.push('|---|---|---:|---|---|');
488
+ for (const slice of data.slices) {
489
+ lines.push(`| ${slice.ref} | ${slice.status} | ${slice.progress}% | ${slice.dependencies.join(', ') || '-'} | ${slice.allowed_write_paths.join(', ') || slice.files.join(', ') || '-'} |`);
490
+ }
491
+ }
492
+
493
+ lines.push('', '## Agents', '');
494
+ if (data.agents.length === 0) {
495
+ lines.push('No agent roles available.');
496
+ } else {
497
+ lines.push('| Role | Configured | Provider | Model |');
498
+ lines.push('|---|---|---|---|');
499
+ for (const agent of data.agents) {
500
+ lines.push(`| ${agent.role} | ${agent.configured ? 'yes' : 'no'} | ${agent.provider || '-'} | ${agent.model || '-'} |`);
501
+ }
502
+ }
503
+
504
+ lines.push('', '## Runs', '');
505
+ if (data.runs.length === 0) {
506
+ lines.push('No AI runs found.');
507
+ } else {
508
+ lines.push('| Run | Phase | Status | Next Command |');
509
+ lines.push('|---|---|---|---|');
510
+ for (const run of data.runs) {
511
+ lines.push(`| ${run.run_id} | ${run.phase} | ${run.status} | \`${run.next_command}\` |`);
512
+ }
513
+ }
514
+
515
+ lines.push('', '## Migration', '');
516
+ lines.push(`- Layout: ${data.migration.layout}`);
517
+ for (const recommendation of data.migration.recommendations) {
518
+ lines.push(`- ${recommendation}`);
519
+ }
520
+ lines.push(`- Dry-run: \`${data.migration.dry_run_command}\``);
521
+ lines.push('');
522
+
523
+ return `${lines.join('\n')}\n`;
524
+ }
525
+
526
+ module.exports = {
527
+ EXPORT_SCHEMA_VERSION,
528
+ collectLifecycleExport,
529
+ formatLifecycleExportMarkdown,
530
+ formatLifecycleInspect,
531
+ formatSlicesList,
532
+ formatSpecsList,
533
+ formatTraceReport,
534
+ };
@@ -4,6 +4,8 @@ const path = require('node:path');
4
4
  const { spawnSync } = require('node:child_process');
5
5
 
6
6
  const { currentBranch, hasRemote, isCleanWorktree } = require('../git');
7
+ const { parseJsonWithComments } = require('../json');
8
+ const { formatActionableError } = require('../actionable-error');
7
9
 
8
10
  const DEFAULT_GH_COMMAND = 'gh';
9
11
  const DEFAULT_REMOTE = 'origin';
@@ -249,6 +251,82 @@ function ensureIdentityFile(repoRoot, identityFile) {
249
251
  return resolved;
250
252
  }
251
253
 
254
+ function ensureSshHostAlias(sshHostAlias) {
255
+ const value = String(sshHostAlias || '').trim();
256
+ if (!value) {
257
+ throw createError(
258
+ 'MISSING_SSH_HOST_ALIAS',
259
+ formatActionableError({
260
+ failure: 'missing SSH host alias. Pass --ssh-host-alias <alias> before opening the PR.',
261
+ 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.',
263
+ nextCommand: 'ssh -T <alias>',
264
+ }),
265
+ );
266
+ }
267
+ return value;
268
+ }
269
+
270
+ function prBodySpecDir(repoRoot, prBodyPath) {
271
+ const relative = path.relative(repoRoot, prBodyPath).split(path.sep).join('/');
272
+ const parts = relative.split('/');
273
+ if (parts[0] !== 'specs' || parts.length !== 3 || parts[2] !== 'pr.md') {
274
+ return '';
275
+ }
276
+ return path.join(repoRoot, parts[0], parts[1]);
277
+ }
278
+
279
+ function listOpenSlicesForSpec(specDir) {
280
+ const slicesDir = path.join(specDir, 'slices');
281
+ if (!fs.existsSync(slicesDir)) {
282
+ return [];
283
+ }
284
+
285
+ return fs.readdirSync(slicesDir, { withFileTypes: true })
286
+ .filter((entry) => entry.isDirectory())
287
+ .map((entry) => {
288
+ const slicePath = path.join(slicesDir, entry.name, 'slice.json');
289
+ if (!fs.existsSync(slicePath)) {
290
+ return null;
291
+ }
292
+ const json = parseJsonWithComments(fs.readFileSync(slicePath, 'utf8'));
293
+ const status = String(json.status || 'draft').trim() || 'draft';
294
+ return {
295
+ id: json.slice_id || entry.name,
296
+ status,
297
+ };
298
+ })
299
+ .filter(Boolean)
300
+ .filter((slice) => slice.status !== 'completed')
301
+ .sort((left, right) => left.id.localeCompare(right.id));
302
+ }
303
+
304
+ function ensureNoOpenSlicesForPrBody(repoRoot, prBodyPath) {
305
+ const specDir = prBodySpecDir(repoRoot, prBodyPath);
306
+ if (!specDir) {
307
+ return [];
308
+ }
309
+
310
+ const openSlices = listOpenSlicesForSpec(specDir);
311
+ if (openSlices.length > 0) {
312
+ throw createError(
313
+ 'OPEN_SLICES',
314
+ formatActionableError({
315
+ failure: `cannot create PR while spec slices are still open: ${openSlices.map((slice) => `${slice.id} (${slice.status})`).join(', ')}.`,
316
+ impact: 'The PR would not represent a closed spec and could miss required slice commits or evidence.',
317
+ fix: 'Finish, validate, and close every slice in the spec before creating the PR.',
318
+ nextCommand: 'npx create-quiver ai execute-plan --dry-run --commit',
319
+ }),
320
+ {
321
+ openSlices,
322
+ specDir,
323
+ },
324
+ );
325
+ }
326
+
327
+ return openSlices;
328
+ }
329
+
252
330
  function findPrBodyCandidates(repoRoot) {
253
331
  const candidates = [];
254
332
  const rootPr = path.join(repoRoot, 'pr.md');
@@ -352,6 +430,7 @@ function buildPrCreateArgs(plan) {
352
430
 
353
431
  function buildPrCreatePlan(repoRoot, preflightReport, options = {}) {
354
432
  const prBody = readPrBody(repoRoot, options.prBodyPath || options.input);
433
+ ensureNoOpenSlicesForPrBody(repoRoot, prBody.path);
355
434
  const baseBranch = String(options.baseBranch || 'main').trim() || 'main';
356
435
  const title = String(options.title || '').trim() || extractPrTitle(prBody.body, preflightReport.branchName);
357
436
  const plan = {
@@ -420,6 +499,7 @@ function preflightGitHubPr(repoRoot, options = {}) {
420
499
  const guidePath = ensureGitFlowGuide(repoRoot, options.gitFlowGuidePath);
421
500
  const remote = ensureRemote(repoRoot, options.remote || DEFAULT_REMOTE);
422
501
  const branchName = ensureWorktreeReady(repoRoot, options);
502
+ const sshHostAlias = ensureSshHostAlias(options.sshHostAlias);
423
503
  const identityFile = ensureIdentityFile(repoRoot, options.identityFile);
424
504
 
425
505
  return buildPreflightReport(repoRoot, options, {
@@ -428,6 +508,7 @@ function preflightGitHubPr(repoRoot, options = {}) {
428
508
  guidePath,
429
509
  remote,
430
510
  branchName,
511
+ sshHostAlias,
431
512
  identityFile,
432
513
  });
433
514
  }
@@ -511,7 +592,9 @@ module.exports = {
511
592
  ensureGhInstalled,
512
593
  ensureGitFlowGuide,
513
594
  ensureIdentityFile,
595
+ ensureNoOpenSlicesForPrBody,
514
596
  ensureRemote,
597
+ ensureSshHostAlias,
515
598
  ensureWorktreeReady,
516
599
  findPrBodyCandidates,
517
600
  formatGhInstallGuidance,