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.
- package/CHANGELOG.md +44 -0
- package/README.md +49 -17
- package/README_FOR_AI.md +31 -29
- package/ROADMAP.md +15 -3
- package/docs/AI_ONBOARDING_PROMPT.md.template +7 -1
- package/docs/COMMANDS.md.template +44 -18
- package/docs/STATUS.md.template +5 -1
- package/docs/WORKFLOW.md.template +13 -11
- package/package.json +9 -3
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EVIDENCE_REPORT.md +293 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/EXECUTION_PLAN.md +58 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/SPEC.md +242 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/STATUS.md +35 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/pr.md +77 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-00-spec-foundation/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-01-cli-contract-compatibility/slice.json +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/CLOSURE_BRIEF.md +43 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-02-run-state-phase-locks/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-03-safe-ai-onboarding-docs/slice.json +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-04-agent-profiles-adapters/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-05-approval-gates/slice.json +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-06-spec-slice-generator/slice.json +55 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/CLOSURE_BRIEF.md +33 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-07-slice-execution-planner/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/CLOSURE_BRIEF.md +39 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/EXECUTION_BRIEF.md +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-08-controlled-slice-execution/slice.json +53 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/EXECUTION_BRIEF.md +57 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-09-git-worktree-pr-lifecycle/slice.json +52 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/CLOSURE_BRIEF.md +39 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-10-validation-errors-fixtures/slice.json +56 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/CLOSURE_BRIEF.md +36 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v25-ai-first-lifecycle-orchestrator/slices/slice-11-export-dashboard-migration/slice.json +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/EVIDENCE_REPORT.md +208 -0
- package/specs/quiver-v26-0121-smoke-hardening/EXECUTION_PLAN.md +57 -0
- package/specs/quiver-v26-0121-smoke-hardening/SPEC.md +137 -0
- package/specs/quiver-v26-0121-smoke-hardening/STATUS.md +32 -0
- package/specs/quiver-v26-0121-smoke-hardening/pr.md +96 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-00-docs-foundation/slice.json +73 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/CLOSURE_BRIEF.md +38 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/EXECUTION_BRIEF.md +51 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-01-cli-help-version-contract/slice.json +76 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-02-init-doc-links-and-flow-guidance/slice.json +75 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/CLOSURE_BRIEF.md +37 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-03-ai-approval-review-consistency/slice.json +77 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/EXECUTION_BRIEF.md +52 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-04-local-validation-brief-contracts/slice.json +77 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/CLOSURE_BRIEF.md +34 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/EXECUTION_BRIEF.md +54 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-05-demo-scaffold-readiness/slice.json +84 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/EXECUTION_BRIEF.md +53 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-06-plan-graph-scope-performance/slice.json +82 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/CLOSURE_BRIEF.md +35 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/EXECUTION_BRIEF.md +55 -0
- package/specs/quiver-v26-0121-smoke-hardening/slices/slice-07-smoke-release-readiness/slice.json +92 -0
- package/src/create-quiver/commands/ai.js +577 -27
- package/src/create-quiver/commands/flow.js +6 -5
- package/src/create-quiver/commands/graph.js +6 -4
- package/src/create-quiver/commands/plan.js +3 -3
- package/src/create-quiver/index.js +328 -12
- package/src/create-quiver/lib/actionable-error.js +27 -0
- package/src/create-quiver/lib/agent-profiles.js +1 -1
- package/src/create-quiver/lib/ai/context-packs.js +4 -0
- package/src/create-quiver/lib/ai/execution-plan.js +7 -1
- package/src/create-quiver/lib/ai/executor.js +270 -20
- package/src/create-quiver/lib/ai/export-state.js +534 -0
- package/src/create-quiver/lib/ai/github.js +83 -0
- package/src/create-quiver/lib/ai/onboarding-template.js +215 -2
- package/src/create-quiver/lib/ai/plan-review.js +5 -2
- package/src/create-quiver/lib/ai/providers.js +4 -3
- package/src/create-quiver/lib/ai/run-state.js +414 -0
- package/src/create-quiver/lib/ai/spec-generator.js +12 -0
- package/src/create-quiver/lib/ai/spec-templates.js +78 -9
- package/src/create-quiver/lib/approvals.js +22 -3
- package/src/create-quiver/lib/demo.js +189 -14
- package/src/create-quiver/lib/doctor.js +75 -0
- package/src/create-quiver/lib/handoff.js +81 -12
- package/src/create-quiver/lib/init-docs.js +24 -6
- package/src/create-quiver/lib/init-layout.js +8 -0
- package/src/create-quiver/lib/json.js +53 -3
- package/src/create-quiver/lib/readiness.js +18 -3
- package/src/create-quiver/lib/scope.js +50 -7
- package/src/create-quiver/lib/slice-graph.js +138 -38
- package/src/create-quiver/lib/slice.js +6 -1
- 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,
|