context-planning 0.7.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/LICENSE +21 -0
- package/README.md +454 -0
- package/bin/commands/_helpers.js +53 -0
- package/bin/commands/_usage.js +67 -0
- package/bin/commands/capture.js +46 -0
- package/bin/commands/codebase-status.js +41 -0
- package/bin/commands/complete-milestone.js +57 -0
- package/bin/commands/config.js +70 -0
- package/bin/commands/doctor.js +139 -0
- package/bin/commands/gsd-import.js +90 -0
- package/bin/commands/inbox.js +81 -0
- package/bin/commands/index.js +33 -0
- package/bin/commands/init.js +87 -0
- package/bin/commands/install.js +43 -0
- package/bin/commands/scaffold-codebase.js +53 -0
- package/bin/commands/scaffold-milestone.js +58 -0
- package/bin/commands/scaffold-phase.js +65 -0
- package/bin/commands/status.js +42 -0
- package/bin/commands/statusline.js +108 -0
- package/bin/commands/tick.js +49 -0
- package/bin/commands/version.js +9 -0
- package/bin/commands/worktree.js +218 -0
- package/bin/commands/write-summary.js +54 -0
- package/bin/cp.cmd +2 -0
- package/bin/cp.js +54 -0
- package/commands/cp/capture.md +107 -0
- package/commands/cp/complete-milestone.md +166 -0
- package/commands/cp/execute-phase.md +220 -0
- package/commands/cp/map-codebase.md +211 -0
- package/commands/cp/new-milestone.md +136 -0
- package/commands/cp/new-project.md +132 -0
- package/commands/cp/plan-phase.md +195 -0
- package/commands/cp/progress.md +147 -0
- package/commands/cp/quick.md +104 -0
- package/commands/cp/resume.md +125 -0
- package/commands/cp/write-summary.md +33 -0
- package/docs/MIGRATION-v0.5.md +140 -0
- package/docs/architecture.md +189 -0
- package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-01-design-md-infrastructure.md +1064 -0
- package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-02-review-log-infrastructure.md +418 -0
- package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-03-key-decisions-hard-block.md +295 -0
- package/docs/superpowers/specs/2026-05-20-generic-provider-harness-detection-design.md +380 -0
- package/docs/superpowers/specs/2026-05-20-v0-7-design-capture-design.md +400 -0
- package/docs/writing-providers.md +76 -0
- package/install/aider.js +204 -0
- package/install/claude.js +116 -0
- package/install/common.js +65 -0
- package/install/copilot.js +86 -0
- package/install/cursor.js +120 -0
- package/install/echo-provider.js +50 -0
- package/lib/codebase-mapper.js +169 -0
- package/lib/detect.js +280 -0
- package/lib/frontmatter.js +72 -0
- package/lib/gsd-compat.js +165 -0
- package/lib/import.js +543 -0
- package/lib/inbox.js +226 -0
- package/lib/lifecycle.js +929 -0
- package/lib/merge.js +157 -0
- package/lib/milestone.js +595 -0
- package/lib/paths.js +191 -0
- package/lib/provider.js +168 -0
- package/lib/roadmap.js +134 -0
- package/lib/state.js +99 -0
- package/lib/worktree.js +253 -0
- package/package.json +45 -0
- package/templates/DESIGN.md +78 -0
- package/templates/INBOX.md +13 -0
- package/templates/MILESTONE-CONTEXT.md +40 -0
- package/templates/MILESTONES.md +29 -0
- package/templates/PLAN.md +84 -0
- package/templates/PROJECT.md +43 -0
- package/templates/REVIEW-LOG.md +38 -0
- package/templates/ROADMAP.md +34 -0
- package/templates/STATE.md +78 -0
- package/templates/SUMMARY.md +75 -0
- package/templates/codebase/ARCHITECTURE.md +30 -0
- package/templates/codebase/CONCERNS.md +30 -0
- package/templates/codebase/CONVENTIONS.md +30 -0
- package/templates/codebase/INTEGRATIONS.md +30 -0
- package/templates/codebase/STACK.md +26 -0
- package/templates/codebase/STRUCTURE.md +32 -0
- package/templates/codebase/TESTING.md +39 -0
- package/templates/config.json +173 -0
- package/templates/phase-PLAN.md +32 -0
- package/templates/quick-PLAN.md +24 -0
- package/templates/quick-SUMMARY.md +25 -0
package/lib/import.js
ADDED
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cp gsd-import — read-only audit of any planning directory.
|
|
5
|
+
*
|
|
6
|
+
* Inspects a project root and reports:
|
|
7
|
+
* - Whether `.planning/` is GSD-shape, cp-aware, both, or neither
|
|
8
|
+
* - Sentinel presence (research/, todos/, REQUIREMENTS.md, etc.)
|
|
9
|
+
* - Phase / plan / summary inventory cross-referenced against ROADMAP.md
|
|
10
|
+
* - Frontmatter parse health of every PLAN.md and SUMMARY.md
|
|
11
|
+
* - What `cp init` would do if --apply were passed (additive only)
|
|
12
|
+
*
|
|
13
|
+
* Everything here is pure read-only — never writes a byte. The CLI wrapper
|
|
14
|
+
* decides whether to fall through to `cmdInit` based on --apply.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
|
|
20
|
+
const compat = require('./gsd-compat');
|
|
21
|
+
const provider = require('./provider');
|
|
22
|
+
const roadmap = require('./roadmap');
|
|
23
|
+
const paths = require('./paths');
|
|
24
|
+
const fm = require('./frontmatter');
|
|
25
|
+
|
|
26
|
+
const SHARED_REQUIRED = [
|
|
27
|
+
'.planning/PROJECT.md',
|
|
28
|
+
'.planning/ROADMAP.md',
|
|
29
|
+
'.planning/STATE.md',
|
|
30
|
+
];
|
|
31
|
+
const SHARED_OPTIONAL = [
|
|
32
|
+
'.planning/MILESTONES.md',
|
|
33
|
+
'.planning/MILESTONE-CONTEXT.md',
|
|
34
|
+
'.planning/config.json',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Run the audit. Returns a structured report object — never throws on bad
|
|
39
|
+
* project state (corrupt YAML, missing files), but records each problem in
|
|
40
|
+
* `issues`.
|
|
41
|
+
*/
|
|
42
|
+
function audit(root) {
|
|
43
|
+
const report = {
|
|
44
|
+
root,
|
|
45
|
+
planning: { present: false, path: paths.planningDir(root) },
|
|
46
|
+
classification: 'unknown',
|
|
47
|
+
cpAware: false,
|
|
48
|
+
gsdProject: false,
|
|
49
|
+
sentinels: { gsd: {}, shared: {} },
|
|
50
|
+
config: { present: false, parseable: false, hasCpBlock: false, gsdKeys: [] },
|
|
51
|
+
phases: [],
|
|
52
|
+
quickTasks: [],
|
|
53
|
+
activeMilestone: null,
|
|
54
|
+
provider: { configured: null, installed: null, fallback: null },
|
|
55
|
+
issues: [],
|
|
56
|
+
recommendation: null,
|
|
57
|
+
plan: { wouldCreate: [], wouldModify: [], wouldDelete: [] },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ---------- planning dir presence ----------
|
|
61
|
+
report.planning.present = compat.hasPlanning(root);
|
|
62
|
+
if (!report.planning.present) {
|
|
63
|
+
report.classification = 'no-planning';
|
|
64
|
+
report.recommendation =
|
|
65
|
+
`No .planning/ directory at ${root}. ` +
|
|
66
|
+
`Run \`cp init\` to scaffold one (creates PROJECT.md / ROADMAP.md / STATE.md / MILESTONES.md / config.json).`;
|
|
67
|
+
report.plan.wouldCreate = [
|
|
68
|
+
'.planning/',
|
|
69
|
+
'.planning/phases/',
|
|
70
|
+
'.planning/quick/',
|
|
71
|
+
'.planning/PROJECT.md',
|
|
72
|
+
'.planning/ROADMAP.md',
|
|
73
|
+
'.planning/STATE.md',
|
|
74
|
+
'.planning/MILESTONES.md',
|
|
75
|
+
'.planning/config.json',
|
|
76
|
+
];
|
|
77
|
+
return report;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------- sentinels ----------
|
|
81
|
+
for (const s of compat.GSD_SENTINELS) {
|
|
82
|
+
report.sentinels.gsd[s] = fs.existsSync(path.join(root, s));
|
|
83
|
+
}
|
|
84
|
+
for (const s of [...SHARED_REQUIRED, ...SHARED_OPTIONAL]) {
|
|
85
|
+
report.sentinels.shared[s] = fs.existsSync(path.join(root, s));
|
|
86
|
+
}
|
|
87
|
+
report.gsdProject = compat.isGsdProject(root);
|
|
88
|
+
|
|
89
|
+
// ---------- config.json ----------
|
|
90
|
+
const cfgPath = path.join(root, '.planning', 'config.json');
|
|
91
|
+
if (fs.existsSync(cfgPath)) {
|
|
92
|
+
report.config.present = true;
|
|
93
|
+
try {
|
|
94
|
+
const raw = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
95
|
+
report.config.parseable = true;
|
|
96
|
+
report.config.hasCpBlock = !!(raw && typeof raw === 'object' && raw.cp);
|
|
97
|
+
report.config.gsdKeys = Object.keys(raw || {}).filter((k) => k !== 'cp').sort();
|
|
98
|
+
report.cpAware = report.config.hasCpBlock;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
report.config.parseable = false;
|
|
101
|
+
report.issues.push({
|
|
102
|
+
severity: 'error',
|
|
103
|
+
kind: 'config-parse',
|
|
104
|
+
message: `config.json is not valid JSON: ${e.message}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ---------- required shared files ----------
|
|
110
|
+
for (const rel of SHARED_REQUIRED) {
|
|
111
|
+
if (!report.sentinels.shared[rel]) {
|
|
112
|
+
report.issues.push({
|
|
113
|
+
severity: 'warn',
|
|
114
|
+
kind: 'missing-shared',
|
|
115
|
+
message: `Required state file missing: ${rel}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------- active milestone ----------
|
|
121
|
+
if (report.sentinels.shared['.planning/MILESTONE-CONTEXT.md']) {
|
|
122
|
+
const mcPath = path.join(root, '.planning', 'MILESTONE-CONTEXT.md');
|
|
123
|
+
const text = fs.readFileSync(mcPath, 'utf8');
|
|
124
|
+
const titleMatch = text.match(/^#\s+Milestone Context:\s*(.+)$/m) || text.match(/^#\s+(.+)$/m);
|
|
125
|
+
const statusMatch = text.match(/\*\*Status\*\*:\s*(.+)$/m);
|
|
126
|
+
report.activeMilestone = {
|
|
127
|
+
file: '.planning/MILESTONE-CONTEXT.md',
|
|
128
|
+
name: titleMatch ? titleMatch[1].trim() : '(unknown)',
|
|
129
|
+
status: statusMatch ? statusMatch[1].trim() : '(unknown)',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------- phases on disk ----------
|
|
134
|
+
const diskPhases = compat.scanPhases(root);
|
|
135
|
+
const planByPhase = {}; // numKey -> [{file, frontmatter, parseError, expectedId}]
|
|
136
|
+
const summaryByPhase = {};
|
|
137
|
+
|
|
138
|
+
for (const p of diskPhases) {
|
|
139
|
+
const prefixMatch = p.name.match(/^([\d.]+)-/);
|
|
140
|
+
const phaseNum = prefixMatch ? prefixMatch[1].replace(/^0+(?=\d)/, '') || prefixMatch[1] : null;
|
|
141
|
+
const normNum = phaseNum ? String(parseFloat(phaseNum) === parseInt(phaseNum, 10) && !phaseNum.includes('.') ? parseInt(phaseNum, 10) : phaseNum) : null;
|
|
142
|
+
planByPhase[normNum] = [];
|
|
143
|
+
summaryByPhase[normNum] = [];
|
|
144
|
+
|
|
145
|
+
if (p.hasShortPlan || p.hasShortSummary) {
|
|
146
|
+
report.issues.push({
|
|
147
|
+
severity: 'warn',
|
|
148
|
+
kind: 'short-form-name',
|
|
149
|
+
phase: p.name,
|
|
150
|
+
message: `Phase ${p.name} has short-form PLAN.md/SUMMARY.md (GSD expects {phase}-{plan}-PLAN.md). cp will read both, but GSD won't see the short form.`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const f of p.planFiles) {
|
|
155
|
+
const idMatch = f.match(/^([\d.]+-\d+)-PLAN\.md$/);
|
|
156
|
+
const id = idMatch ? idMatch[1] : null;
|
|
157
|
+
const full = path.join(p.path, f);
|
|
158
|
+
let frontmatter = null;
|
|
159
|
+
let parseError = null;
|
|
160
|
+
try {
|
|
161
|
+
const parsed = fm.parse(fs.readFileSync(full, 'utf8'));
|
|
162
|
+
frontmatter = parsed.frontmatter || null;
|
|
163
|
+
parseError = parsed.parseError || null;
|
|
164
|
+
} catch (e) {
|
|
165
|
+
parseError = e.message;
|
|
166
|
+
}
|
|
167
|
+
if (parseError) {
|
|
168
|
+
report.issues.push({
|
|
169
|
+
severity: 'error',
|
|
170
|
+
kind: 'frontmatter-parse',
|
|
171
|
+
file: path.relative(root, full).replace(/\\/g, '/'),
|
|
172
|
+
message: `Frontmatter parse failed: ${parseError}`,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
planByPhase[normNum].push({ file: f, id, frontmatter, parseError });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const f of p.summaryFiles) {
|
|
179
|
+
const idMatch = f.match(/^([\d.]+-\d+)-SUMMARY\.md$/);
|
|
180
|
+
const id = idMatch ? idMatch[1] : null;
|
|
181
|
+
const full = path.join(p.path, f);
|
|
182
|
+
let frontmatter = null;
|
|
183
|
+
let parseError = null;
|
|
184
|
+
try {
|
|
185
|
+
const parsed = fm.parse(fs.readFileSync(full, 'utf8'));
|
|
186
|
+
frontmatter = parsed.frontmatter || null;
|
|
187
|
+
parseError = parsed.parseError || null;
|
|
188
|
+
} catch (e) {
|
|
189
|
+
parseError = e.message;
|
|
190
|
+
}
|
|
191
|
+
if (parseError) {
|
|
192
|
+
report.issues.push({
|
|
193
|
+
severity: 'error',
|
|
194
|
+
kind: 'frontmatter-parse',
|
|
195
|
+
file: path.relative(root, full).replace(/\\/g, '/'),
|
|
196
|
+
message: `Frontmatter parse failed: ${parseError}`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
summaryByPhase[normNum].push({ file: f, id, frontmatter, parseError });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
report.phases.push({
|
|
203
|
+
dir: p.name,
|
|
204
|
+
num: normNum,
|
|
205
|
+
planCount: p.planFiles.length,
|
|
206
|
+
summaryCount: p.summaryFiles.length,
|
|
207
|
+
shortFormPresent: p.hasShortPlan || p.hasShortSummary,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---------- cross-check against ROADMAP.md ----------
|
|
212
|
+
const roadmapPath = path.join(root, '.planning', 'ROADMAP.md');
|
|
213
|
+
let roadmapPhases = [];
|
|
214
|
+
if (fs.existsSync(roadmapPath)) {
|
|
215
|
+
try {
|
|
216
|
+
const content = roadmap.read(roadmapPath);
|
|
217
|
+
roadmapPhases = roadmap.listPhases(content);
|
|
218
|
+
} catch (e) {
|
|
219
|
+
report.issues.push({
|
|
220
|
+
severity: 'error',
|
|
221
|
+
kind: 'roadmap-parse',
|
|
222
|
+
message: `ROADMAP.md unreadable: ${e.message}`,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ROADMAP phases not present as a dir on disk:
|
|
228
|
+
for (const rp of roadmapPhases) {
|
|
229
|
+
const onDisk = report.phases.find((dp) => dp.num === rp.num);
|
|
230
|
+
if (!onDisk) {
|
|
231
|
+
report.issues.push({
|
|
232
|
+
severity: 'info',
|
|
233
|
+
kind: 'roadmap-only-phase',
|
|
234
|
+
phase: rp.num,
|
|
235
|
+
message: `Phase ${rp.num} (${rp.name}) is in ROADMAP.md but has no .planning/phases/ dir yet (run \`cp plan-phase ${rp.num}\` to create one).`,
|
|
236
|
+
});
|
|
237
|
+
} else {
|
|
238
|
+
// Per-plan cross-check.
|
|
239
|
+
for (const pl of rp.plans) {
|
|
240
|
+
const planOnDisk = planByPhase[rp.num] && planByPhase[rp.num].find((x) => x.id === pl.id);
|
|
241
|
+
const summaryOnDisk = summaryByPhase[rp.num] && summaryByPhase[rp.num].find((x) => x.id === pl.id);
|
|
242
|
+
if (!planOnDisk) {
|
|
243
|
+
report.issues.push({
|
|
244
|
+
severity: 'warn',
|
|
245
|
+
kind: 'roadmap-plan-no-file',
|
|
246
|
+
phase: rp.num,
|
|
247
|
+
plan: pl.id,
|
|
248
|
+
message: `ROADMAP plan ${pl.id} has no ${pl.id}-PLAN.md on disk.`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
if (pl.done && !summaryOnDisk) {
|
|
252
|
+
report.issues.push({
|
|
253
|
+
severity: 'warn',
|
|
254
|
+
kind: 'done-plan-no-summary',
|
|
255
|
+
phase: rp.num,
|
|
256
|
+
plan: pl.id,
|
|
257
|
+
message: `ROADMAP plan ${pl.id} is checked done but has no ${pl.id}-SUMMARY.md.`,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Disk phases not in ROADMAP:
|
|
265
|
+
for (const dp of report.phases) {
|
|
266
|
+
if (!roadmapPhases.find((rp) => rp.num === dp.num)) {
|
|
267
|
+
report.issues.push({
|
|
268
|
+
severity: 'info',
|
|
269
|
+
kind: 'orphan-phase-dir',
|
|
270
|
+
phase: dp.num,
|
|
271
|
+
message: `Phase dir ${dp.dir} exists but ROADMAP.md has no \`### Phase ${dp.num}:\` entry.`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Plan files whose id doesn't match the phase dir prefix:
|
|
277
|
+
for (const dp of report.phases) {
|
|
278
|
+
const plans = planByPhase[dp.num] || [];
|
|
279
|
+
for (const pl of plans) {
|
|
280
|
+
if (pl.id && !pl.id.startsWith(dp.num + '-') && !pl.id.startsWith(paths.padPhaseNum(dp.num) + '-')) {
|
|
281
|
+
report.issues.push({
|
|
282
|
+
severity: 'warn',
|
|
283
|
+
kind: 'plan-id-prefix-mismatch',
|
|
284
|
+
phase: dp.num,
|
|
285
|
+
plan: pl.id,
|
|
286
|
+
file: `${dp.dir}/${pl.file}`,
|
|
287
|
+
message: `Plan file ${pl.file} sits in phase ${dp.num} but its id (${pl.id}) doesn't share the phase prefix.`,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------- quick tasks ----------
|
|
294
|
+
const quickDir = path.join(root, '.planning', 'quick');
|
|
295
|
+
if (fs.existsSync(quickDir)) {
|
|
296
|
+
for (const entry of fs.readdirSync(quickDir, { withFileTypes: true })) {
|
|
297
|
+
if (!entry.isDirectory()) continue;
|
|
298
|
+
const dir = path.join(quickDir, entry.name);
|
|
299
|
+
const files = fs.readdirSync(dir);
|
|
300
|
+
report.quickTasks.push({
|
|
301
|
+
slug: entry.name,
|
|
302
|
+
hasPlan: files.includes('PLAN.md'),
|
|
303
|
+
hasSummary: files.includes('SUMMARY.md'),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---------- provider resolution ----------
|
|
309
|
+
try {
|
|
310
|
+
const cfg = report.config.parseable
|
|
311
|
+
? JSON.parse(fs.readFileSync(cfgPath, 'utf8'))
|
|
312
|
+
: provider.loadDefaults();
|
|
313
|
+
if (!cfg.cp) cfg.cp = provider.loadDefaults().cp;
|
|
314
|
+
const configured = provider.cpGet(cfg, 'workflow_provider', 'superpowers');
|
|
315
|
+
const det = provider.detectProvider(cfg, configured);
|
|
316
|
+
report.provider.configured = configured;
|
|
317
|
+
report.provider.installed = det.installed;
|
|
318
|
+
if (!det.installed) {
|
|
319
|
+
const manual = provider.detectProvider(cfg, 'manual');
|
|
320
|
+
report.provider.fallback = manual.installed ? 'manual' : null;
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
report.issues.push({
|
|
324
|
+
severity: 'info',
|
|
325
|
+
kind: 'provider-resolve',
|
|
326
|
+
message: `Provider resolution skipped: ${e.message}`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ---------- classification ----------
|
|
331
|
+
if (report.cpAware && report.gsdProject) {
|
|
332
|
+
report.classification = 'cp-aware-gsd-superset';
|
|
333
|
+
} else if (report.cpAware) {
|
|
334
|
+
report.classification = 'cp-aware';
|
|
335
|
+
} else if (report.gsdProject) {
|
|
336
|
+
report.classification = 'pure-gsd';
|
|
337
|
+
} else if (report.sentinels.shared['.planning/config.json'] || SHARED_REQUIRED.some((s) => report.sentinels.shared[s])) {
|
|
338
|
+
report.classification = 'planning-shaped-foreign';
|
|
339
|
+
} else {
|
|
340
|
+
report.classification = 'empty-planning';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---------- plan what `cp init` would do ----------
|
|
344
|
+
for (const rel of [
|
|
345
|
+
'.planning/PROJECT.md',
|
|
346
|
+
'.planning/ROADMAP.md',
|
|
347
|
+
'.planning/STATE.md',
|
|
348
|
+
'.planning/MILESTONES.md',
|
|
349
|
+
]) {
|
|
350
|
+
if (!report.sentinels.shared[rel]) report.plan.wouldCreate.push(rel);
|
|
351
|
+
}
|
|
352
|
+
if (!report.config.present) {
|
|
353
|
+
report.plan.wouldCreate.push('.planning/config.json');
|
|
354
|
+
} else if (!report.config.hasCpBlock && report.config.parseable) {
|
|
355
|
+
report.plan.wouldModify.push('.planning/config.json (add `cp` block; GSD keys preserved)');
|
|
356
|
+
}
|
|
357
|
+
for (const d of ['.planning/phases', '.planning/quick']) {
|
|
358
|
+
if (!fs.existsSync(path.join(root, d))) report.plan.wouldCreate.push(d + '/');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---------- recommendation ----------
|
|
362
|
+
const errors = report.issues.filter((i) => i.severity === 'error').length;
|
|
363
|
+
if (report.classification === 'cp-aware-gsd-superset' || report.classification === 'cp-aware') {
|
|
364
|
+
report.recommendation = errors > 0
|
|
365
|
+
? `Already cp-aware, but ${errors} parse error(s) need attention before cp can drive workflows safely.`
|
|
366
|
+
: `Already cp-aware. Nothing to import. Run \`cp doctor\` for live provider status.`;
|
|
367
|
+
} else if (report.classification === 'pure-gsd' || report.classification === 'planning-shaped-foreign') {
|
|
368
|
+
const changes = report.plan.wouldCreate.length + report.plan.wouldModify.length;
|
|
369
|
+
report.recommendation = changes === 0
|
|
370
|
+
? `GSD-shape project; nothing to import (no missing files, no \`cp\` block needed).`
|
|
371
|
+
: `GSD-shape project. \`cp init\` would make ${changes} additive change(s) — no GSD files would be rewritten. Re-run with --apply to perform the import.`;
|
|
372
|
+
} else if (report.classification === 'empty-planning') {
|
|
373
|
+
report.recommendation = `Empty .planning/ — run \`cp init\` (or pass --apply) to scaffold the standard files.`;
|
|
374
|
+
} else {
|
|
375
|
+
report.recommendation = `No recommendation.`;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return report;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Render a report to a human-readable multi-line string.
|
|
383
|
+
*/
|
|
384
|
+
function render(report) {
|
|
385
|
+
const lines = [];
|
|
386
|
+
const push = (s = '') => lines.push(s);
|
|
387
|
+
|
|
388
|
+
push(`cp gsd-import — audit of ${report.root}`);
|
|
389
|
+
push('='.repeat(Math.min(78, 17 + report.root.length)));
|
|
390
|
+
push('');
|
|
391
|
+
push(`Planning dir: ${report.planning.present ? 'present' : 'MISSING'} (${report.planning.path})`);
|
|
392
|
+
push(`Classification: ${prettyClass(report.classification)}`);
|
|
393
|
+
push(`cp-aware: ${tick(report.cpAware)}`);
|
|
394
|
+
push(`GSD sentinels: ${report.gsdProject ? 'detected' : 'none'}`);
|
|
395
|
+
|
|
396
|
+
if (!report.planning.present) {
|
|
397
|
+
push('');
|
|
398
|
+
push('Plan if you run `cp init`:');
|
|
399
|
+
for (const f of report.plan.wouldCreate) push(` + ${f}`);
|
|
400
|
+
push('');
|
|
401
|
+
push(`Recommendation:`);
|
|
402
|
+
for (const l of wrap(report.recommendation, 76)) push(` ${l}`);
|
|
403
|
+
return lines.join('\n') + '\n';
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Shared / GSD sentinel inventory
|
|
407
|
+
push('');
|
|
408
|
+
push(`Shared files (used by both cp and GSD):`);
|
|
409
|
+
for (const rel of [...SHARED_REQUIRED, ...SHARED_OPTIONAL]) {
|
|
410
|
+
push(` ${tickFile(report.sentinels.shared[rel])} ${rel}`);
|
|
411
|
+
}
|
|
412
|
+
push('');
|
|
413
|
+
push(`GSD-only sentinels:`);
|
|
414
|
+
for (const rel of compat.GSD_SENTINELS) {
|
|
415
|
+
push(` ${tickFile(report.sentinels.gsd[rel])} ${rel}`);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// config.json detail
|
|
419
|
+
push('');
|
|
420
|
+
if (report.config.present) {
|
|
421
|
+
push(`config.json: parseable=${tick(report.config.parseable)} cp-block=${tick(report.config.hasCpBlock)}`);
|
|
422
|
+
if (report.config.parseable && report.config.gsdKeys.length > 0) {
|
|
423
|
+
push(` GSD keys preserved: ${report.config.gsdKeys.join(', ')}`);
|
|
424
|
+
}
|
|
425
|
+
} else {
|
|
426
|
+
push(`config.json: not present`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Phases inventory
|
|
430
|
+
push('');
|
|
431
|
+
push(`Phases on disk (${report.phases.length}):`);
|
|
432
|
+
if (report.phases.length === 0) {
|
|
433
|
+
push(' — none');
|
|
434
|
+
} else {
|
|
435
|
+
const maxDir = Math.max(...report.phases.map((p) => p.dir.length));
|
|
436
|
+
for (const p of report.phases) {
|
|
437
|
+
const flag = p.shortFormPresent ? ' [short-form!]' : '';
|
|
438
|
+
push(` ${p.dir.padEnd(maxDir)} ${p.planCount} plan(s), ${p.summaryCount} summary(ies)${flag}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Quick tasks
|
|
443
|
+
if (report.quickTasks.length > 0) {
|
|
444
|
+
push('');
|
|
445
|
+
push(`Quick tasks (${report.quickTasks.length}):`);
|
|
446
|
+
for (const q of report.quickTasks) {
|
|
447
|
+
const bits = [];
|
|
448
|
+
if (q.hasPlan) bits.push('PLAN');
|
|
449
|
+
if (q.hasSummary) bits.push('SUMMARY');
|
|
450
|
+
push(` ${q.slug} [${bits.join(', ') || 'empty'}]`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Active milestone
|
|
455
|
+
if (report.activeMilestone) {
|
|
456
|
+
push('');
|
|
457
|
+
push(`Active milestone (MILESTONE-CONTEXT.md):`);
|
|
458
|
+
push(` name: ${report.activeMilestone.name}`);
|
|
459
|
+
push(` status: ${report.activeMilestone.status}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Provider
|
|
463
|
+
if (report.provider.configured) {
|
|
464
|
+
push('');
|
|
465
|
+
push(`Workflow provider:`);
|
|
466
|
+
const installed = report.provider.installed ? 'installed' : 'NOT installed';
|
|
467
|
+
push(` configured: ${report.provider.configured} (${installed})`);
|
|
468
|
+
if (!report.provider.installed) {
|
|
469
|
+
push(` fallback: ${report.provider.fallback || '(none — provider commands will fail)'}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Issues
|
|
474
|
+
push('');
|
|
475
|
+
const errors = report.issues.filter((i) => i.severity === 'error');
|
|
476
|
+
const warns = report.issues.filter((i) => i.severity === 'warn');
|
|
477
|
+
const infos = report.issues.filter((i) => i.severity === 'info');
|
|
478
|
+
push(`Issues: ${report.issues.length} (errors: ${errors.length}, warnings: ${warns.length}, info: ${infos.length})`);
|
|
479
|
+
for (const i of [...errors, ...warns, ...infos]) {
|
|
480
|
+
const tag = i.severity === 'error' ? '✗' : i.severity === 'warn' ? '!' : 'i';
|
|
481
|
+
push(` ${tag} [${i.kind}] ${i.message}`);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Plan
|
|
485
|
+
push('');
|
|
486
|
+
push(`Plan if you run \`cp init\` (or this command with --apply):`);
|
|
487
|
+
if (report.plan.wouldCreate.length === 0 && report.plan.wouldModify.length === 0) {
|
|
488
|
+
push(` (no changes; everything already in place)`);
|
|
489
|
+
} else {
|
|
490
|
+
for (const f of report.plan.wouldCreate) push(` + create ${f}`);
|
|
491
|
+
for (const f of report.plan.wouldModify) push(` ~ modify ${f}`);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Recommendation
|
|
495
|
+
push('');
|
|
496
|
+
push(`Recommendation:`);
|
|
497
|
+
for (const l of wrap(report.recommendation, 76)) push(` ${l}`);
|
|
498
|
+
|
|
499
|
+
return lines.join('\n') + '\n';
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function prettyClass(c) {
|
|
503
|
+
return {
|
|
504
|
+
'no-planning': 'no .planning/ directory',
|
|
505
|
+
'empty-planning': 'empty .planning/ (no state files yet)',
|
|
506
|
+
'planning-shaped-foreign': 'planning-shaped (not GSD, not cp-aware)',
|
|
507
|
+
'pure-gsd': 'pure GSD project',
|
|
508
|
+
'cp-aware': 'cp-aware (no GSD sentinels)',
|
|
509
|
+
'cp-aware-gsd-superset': 'cp-aware GSD superset (both)',
|
|
510
|
+
unknown: 'unknown',
|
|
511
|
+
}[c] || c;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function tick(b) {
|
|
515
|
+
return b ? '✓' : '✗';
|
|
516
|
+
}
|
|
517
|
+
function tickFile(b) {
|
|
518
|
+
return b ? '✓' : '·';
|
|
519
|
+
}
|
|
520
|
+
function wrap(text, width) {
|
|
521
|
+
const out = [];
|
|
522
|
+
const words = String(text).split(/\s+/);
|
|
523
|
+
let line = '';
|
|
524
|
+
for (const w of words) {
|
|
525
|
+
if ((line + ' ' + w).trim().length > width) {
|
|
526
|
+
if (line) out.push(line);
|
|
527
|
+
line = w;
|
|
528
|
+
} else {
|
|
529
|
+
line = (line ? line + ' ' : '') + w;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (line) out.push(line);
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** Convenience exit code: 0 = clean / nothing-to-do, 1 = errors, 2 = changes-pending */
|
|
537
|
+
function exitCode(report) {
|
|
538
|
+
if (report.issues.some((i) => i.severity === 'error')) return 1;
|
|
539
|
+
if (report.plan.wouldCreate.length + report.plan.wouldModify.length > 0) return 2;
|
|
540
|
+
return 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
module.exports = { audit, render, exitCode };
|