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.
- package/CHANGELOG.md +8 -0
- package/README.md +16 -8
- package/README_FOR_AI.md +11 -6
- package/ROADMAP.md +9 -2
- package/docs/COMMANDS.md.template +9 -2
- package/package.json +2 -1
- package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +2 -2
- package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +5 -5
- package/specs/quiver-v27-reliability-ai-workflow-hardening/AUDIT_V24_V25_V26.md +67 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/COMMAND_CONTRACTS.md +125 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/COVERAGE_MATRIX.md +74 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/EVIDENCE_REPORT.md +179 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/EXECUTION_PLAN.md +71 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/SPEC.md +176 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/STATUS.md +37 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/pr.md +132 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-00-docs-audit-coverage-and-contracts/slice.json +75 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-01-core-state-resolver-and-canonical-statuses/slice.json +79 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-02-json-export-contract-and-machine-output/slice.json +75 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-03-approved-plan-to-spec-create/slice.json +78 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-04-ai-artifact-storage-redaction-and-token-compaction/slice.json +77 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-05-worktree-lifecycle-locks-and-recovery/slice.json +84 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-06-validation-gates-and-scope-safety/slice.json +99 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-07-context-analysis-and-doctor-flow/slice.json +88 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/CLOSURE_BRIEF.md +31 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-08-cross-platform-help-auth-and-dx/slice.json +85 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v27-reliability-ai-workflow-hardening/slices/slice-09-fixtures-smoke-docs-and-release-readiness/slice.json +91 -0
- package/src/create-quiver/commands/ai.js +84 -9
- package/src/create-quiver/commands/flow.js +52 -4
- package/src/create-quiver/commands/graph.js +7 -7
- package/src/create-quiver/commands/plan.js +6 -15
- package/src/create-quiver/commands/spec.js +282 -0
- package/src/create-quiver/index.js +83 -21
- package/src/create-quiver/lib/agent-profiles.js +15 -3
- package/src/create-quiver/lib/ai/artifacts.js +318 -0
- package/src/create-quiver/lib/ai/execution-plan.js +9 -0
- package/src/create-quiver/lib/ai/executor.js +3 -2
- package/src/create-quiver/lib/ai/export-state.js +242 -97
- package/src/create-quiver/lib/ai/github.js +80 -3
- package/src/create-quiver/lib/ai/plan-review.js +2 -0
- package/src/create-quiver/lib/ai/spec-generator.js +72 -13
- package/src/create-quiver/lib/ai/spec-templates.js +72 -12
- package/src/create-quiver/lib/analyze.js +2 -2
- package/src/create-quiver/lib/approvals.js +14 -2
- package/src/create-quiver/lib/doctor.js +79 -0
- package/src/create-quiver/lib/git.js +40 -1
- package/src/create-quiver/lib/handoff.js +43 -1
- package/src/create-quiver/lib/init-docs.js +11 -7
- package/src/create-quiver/lib/init-layout.js +1 -0
- package/src/create-quiver/lib/lifecycle.js +52 -3
- package/src/create-quiver/lib/locks.js +134 -0
- package/src/create-quiver/lib/package-safety.js +7 -0
- package/src/create-quiver/lib/paths.js +74 -0
- package/src/create-quiver/lib/project-scan.js +74 -0
- package/src/create-quiver/lib/project-state-resolver.js +236 -0
- package/src/create-quiver/lib/readiness.js +48 -7
- package/src/create-quiver/lib/scope.js +2 -1
- package/src/create-quiver/lib/slice.js +8 -4
- package/src/create-quiver/lib/spec-worktrees.js +121 -38
- 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 {
|
|
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 =
|
|
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
|
|
54
|
+
return isCanonicalCompletedStatus('slice', status);
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
function isBlockedStatus(slice) {
|
|
45
|
-
return
|
|
58
|
+
return isCanonicalBlockedStatus('slice', slice?.canonical_status || slice?.status, slice);
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
function progressForSlice(slice) {
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
|
358
|
-
|
|
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
|
-
`${
|
|
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
|
-
|
|
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
|
|
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`);
|