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.
- package/CHANGELOG.md +27 -0
- package/README.md +24 -9
- package/README_FOR_AI.md +15 -6
- package/ROADMAP.md +15 -2
- package/docs/COMMANDS.md.template +12 -3
- package/docs/TROUBLESHOOTING.md.template +29 -0
- package/docs/WORKFLOW.md.template +13 -12
- 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/specs/quiver-v28-pixel-quiver-feedback-reconciliation/COVERAGE_MATRIX.md +117 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EVIDENCE_REPORT.md +200 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/EXECUTION_PLAN.md +60 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/SPEC.md +132 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/STATUS.md +36 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/pr.md +128 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/CLOSURE_BRIEF.md +44 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-00-reconciliation-and-evidence-freeze/slice.json +71 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-01-ai-run-state-approvals-and-clean-output/slice.json +83 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-02-structured-technical-plan-contract-and-repair-flow/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-03-active-slice-reconciliation-and-ai-inspect/slice.json +82 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/CLOSURE_BRIEF.md +32 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-04-spec-validation-scope-and-worktree-reliability/slice.json +85 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/EXECUTION_BRIEF.md +59 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-05-review-plan-closure-and-agent-dx/slice.json +94 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/CLOSURE_BRIEF.md +40 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v28-pixel-quiver-feedback-reconciliation/slices/slice-06-backward-compatibility-docs-and-release-readiness/slice.json +98 -0
- package/src/create-quiver/commands/ai.js +563 -21
- 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 +292 -0
- package/src/create-quiver/index.js +125 -25
- 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/context-packs.js +2 -2
- 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 +287 -95
- package/src/create-quiver/lib/ai/github.js +93 -4
- package/src/create-quiver/lib/ai/plan-review.js +161 -0
- package/src/create-quiver/lib/ai/run-state.js +17 -2
- package/src/create-quiver/lib/ai/spec-generator.js +87 -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 +430 -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 +169 -38
- 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 {
|
|
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 =
|
|
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
|
|
55
|
+
return isCanonicalCompletedStatus('slice', status);
|
|
42
56
|
}
|
|
43
57
|
|
|
44
58
|
function isBlockedStatus(slice) {
|
|
45
|
-
return
|
|
59
|
+
return isCanonicalBlockedStatus('slice', slice?.canonical_status || slice?.status, slice);
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
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;
|
|
63
|
+
return resolveProgressForSlice(slice);
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
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
|
-
};
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
|
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');
|
|
550
|
+
for (const step of data.next_steps || collectNextSteps(data)) {
|
|
551
|
+
lines.push(`- ${step.command}`);
|
|
365
552
|
}
|
|
366
553
|
|
|
367
|
-
if (data.
|
|
368
|
-
lines.push(
|
|
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
|
-
`${
|
|
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
|
-
|
|
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: '
|
|
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,
|