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.
Files changed (86) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -0
  3. package/bin/commands/_helpers.js +53 -0
  4. package/bin/commands/_usage.js +67 -0
  5. package/bin/commands/capture.js +46 -0
  6. package/bin/commands/codebase-status.js +41 -0
  7. package/bin/commands/complete-milestone.js +57 -0
  8. package/bin/commands/config.js +70 -0
  9. package/bin/commands/doctor.js +139 -0
  10. package/bin/commands/gsd-import.js +90 -0
  11. package/bin/commands/inbox.js +81 -0
  12. package/bin/commands/index.js +33 -0
  13. package/bin/commands/init.js +87 -0
  14. package/bin/commands/install.js +43 -0
  15. package/bin/commands/scaffold-codebase.js +53 -0
  16. package/bin/commands/scaffold-milestone.js +58 -0
  17. package/bin/commands/scaffold-phase.js +65 -0
  18. package/bin/commands/status.js +42 -0
  19. package/bin/commands/statusline.js +108 -0
  20. package/bin/commands/tick.js +49 -0
  21. package/bin/commands/version.js +9 -0
  22. package/bin/commands/worktree.js +218 -0
  23. package/bin/commands/write-summary.js +54 -0
  24. package/bin/cp.cmd +2 -0
  25. package/bin/cp.js +54 -0
  26. package/commands/cp/capture.md +107 -0
  27. package/commands/cp/complete-milestone.md +166 -0
  28. package/commands/cp/execute-phase.md +220 -0
  29. package/commands/cp/map-codebase.md +211 -0
  30. package/commands/cp/new-milestone.md +136 -0
  31. package/commands/cp/new-project.md +132 -0
  32. package/commands/cp/plan-phase.md +195 -0
  33. package/commands/cp/progress.md +147 -0
  34. package/commands/cp/quick.md +104 -0
  35. package/commands/cp/resume.md +125 -0
  36. package/commands/cp/write-summary.md +33 -0
  37. package/docs/MIGRATION-v0.5.md +140 -0
  38. package/docs/architecture.md +189 -0
  39. package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-01-design-md-infrastructure.md +1064 -0
  40. package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-02-review-log-infrastructure.md +418 -0
  41. package/docs/superpowers/plans/2026-05-20-v0-7-plan-16-03-key-decisions-hard-block.md +295 -0
  42. package/docs/superpowers/specs/2026-05-20-generic-provider-harness-detection-design.md +380 -0
  43. package/docs/superpowers/specs/2026-05-20-v0-7-design-capture-design.md +400 -0
  44. package/docs/writing-providers.md +76 -0
  45. package/install/aider.js +204 -0
  46. package/install/claude.js +116 -0
  47. package/install/common.js +65 -0
  48. package/install/copilot.js +86 -0
  49. package/install/cursor.js +120 -0
  50. package/install/echo-provider.js +50 -0
  51. package/lib/codebase-mapper.js +169 -0
  52. package/lib/detect.js +280 -0
  53. package/lib/frontmatter.js +72 -0
  54. package/lib/gsd-compat.js +165 -0
  55. package/lib/import.js +543 -0
  56. package/lib/inbox.js +226 -0
  57. package/lib/lifecycle.js +929 -0
  58. package/lib/merge.js +157 -0
  59. package/lib/milestone.js +595 -0
  60. package/lib/paths.js +191 -0
  61. package/lib/provider.js +168 -0
  62. package/lib/roadmap.js +134 -0
  63. package/lib/state.js +99 -0
  64. package/lib/worktree.js +253 -0
  65. package/package.json +45 -0
  66. package/templates/DESIGN.md +78 -0
  67. package/templates/INBOX.md +13 -0
  68. package/templates/MILESTONE-CONTEXT.md +40 -0
  69. package/templates/MILESTONES.md +29 -0
  70. package/templates/PLAN.md +84 -0
  71. package/templates/PROJECT.md +43 -0
  72. package/templates/REVIEW-LOG.md +38 -0
  73. package/templates/ROADMAP.md +34 -0
  74. package/templates/STATE.md +78 -0
  75. package/templates/SUMMARY.md +75 -0
  76. package/templates/codebase/ARCHITECTURE.md +30 -0
  77. package/templates/codebase/CONCERNS.md +30 -0
  78. package/templates/codebase/CONVENTIONS.md +30 -0
  79. package/templates/codebase/INTEGRATIONS.md +30 -0
  80. package/templates/codebase/STACK.md +26 -0
  81. package/templates/codebase/STRUCTURE.md +32 -0
  82. package/templates/codebase/TESTING.md +39 -0
  83. package/templates/config.json +173 -0
  84. package/templates/phase-PLAN.md +32 -0
  85. package/templates/quick-PLAN.md +24 -0
  86. package/templates/quick-SUMMARY.md +25 -0
package/lib/detect.js ADDED
@@ -0,0 +1,280 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Harness × provider detection engine (v0.5).
5
+ *
6
+ * Scans harness plugin_roots for provider plugin_shape matches, with
7
+ * fallback to legacy detect.any_of literal sentinels. All filesystem
8
+ * reads happen at call time — no caching.
9
+ *
10
+ * Trailing-* glob: `~/.copilot/installed-plugins/⁕/` expands to the
11
+ * immediate child directories of the parent. No full minimatch — the
12
+ * "zero deps in lib/" constraint from PROJECT.md stays honoured.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const os = require('os');
18
+ const { repoRoot } = require('./paths');
19
+
20
+ // ───────────────────────── tilde + trailing-* expansion ─────────────────
21
+
22
+ /**
23
+ * Expand a root spec with tilde and trailing-* support.
24
+ *
25
+ * Rules:
26
+ * - Leading `~/` replaced with os.homedir().
27
+ * - If no `*` in the string → literal: return [abs] if exists, else [].
28
+ * - First path segment containing `*` is expanded via readdirSync of
29
+ * its parent, keeping only directories. Remaining segments are
30
+ * appended verbatim.
31
+ *
32
+ * Returns string[] (may be empty).
33
+ */
34
+ function expandRoot(rootSpec) {
35
+ let spec = rootSpec;
36
+
37
+ // Tilde expansion
38
+ if (spec.startsWith('~/') || spec.startsWith('~\\')) {
39
+ spec = path.join(os.homedir(), spec.slice(2));
40
+ }
41
+ spec = path.normalize(spec);
42
+
43
+ // Find first segment with a wildcard
44
+ const segments = spec.split(path.sep);
45
+ const wildIdx = segments.findIndex((s) => s.includes('*'));
46
+
47
+ if (wildIdx < 0) {
48
+ // Literal path — no glob
49
+ return fs.existsSync(spec) ? [spec] : [];
50
+ }
51
+
52
+ // Build parent from segments before the wildcard
53
+ const parentSegments = segments.slice(0, wildIdx);
54
+ const parent = parentSegments.join(path.sep) || path.sep;
55
+
56
+ if (!fs.existsSync(parent)) return [];
57
+
58
+ let entries;
59
+ try {
60
+ entries = fs.readdirSync(parent, { withFileTypes: true });
61
+ } catch {
62
+ return [];
63
+ }
64
+
65
+ const pattern = segments[wildIdx]; // e.g. '*' or 'sp-*'
66
+ const dirs = entries
67
+ .filter((e) => e.isDirectory())
68
+ .map((e) => e.name)
69
+ .filter((name) => matchSegment(name, pattern));
70
+
71
+ // Remaining segments after the wildcard
72
+ const rest = segments.slice(wildIdx + 1);
73
+
74
+ const results = [];
75
+ for (const d of dirs) {
76
+ const candidate = path.join(parent, d, ...rest);
77
+ // Only include if the full expanded path exists
78
+ if (fs.existsSync(candidate)) {
79
+ results.push(candidate);
80
+ }
81
+ }
82
+ return results;
83
+ }
84
+
85
+ /**
86
+ * Match a directory name against a simple wildcard pattern.
87
+ * Only supports `*` as "match anything" (not `?`, `[...]`, etc.).
88
+ * Examples: `*` matches all, `sp-*` matches `sp-foo`, `*-mkt` matches `abc-mkt`.
89
+ */
90
+ function matchSegment(name, pattern) {
91
+ if (pattern === '*') return true;
92
+ // Convert to a regex: escape everything except *, replace * with .*
93
+ const re = new RegExp(
94
+ '^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$'
95
+ );
96
+ return re.test(name);
97
+ }
98
+
99
+ // ───────────────────────── legacy literal sentinel search ───────────────
100
+
101
+ /**
102
+ * Search the 5 standard base directories for a literal sentinel path.
103
+ * Preserved for back-compat with detect.any_of entries.
104
+ */
105
+ function existsAnywhere(candidate) {
106
+ const home = os.homedir();
107
+ const root = repoRoot();
108
+ const tries = [
109
+ path.join(root, candidate),
110
+ path.join(home, candidate),
111
+ path.join(home, '.claude', candidate),
112
+ path.join(home, '.github', candidate),
113
+ path.join(home, '.copilot', candidate),
114
+ ];
115
+ return tries.find((t) => fs.existsSync(t)) || null;
116
+ }
117
+
118
+ // ───────────────────────── per-provider detection ──────────────────────
119
+
120
+ /**
121
+ * Detect whether a single provider is installed across any harness.
122
+ *
123
+ * Lookup order (first hit wins):
124
+ * 1. plugin_shape match: for each harness, expand plugin_roots, look for
125
+ * a child dir matching plugin_shape.dir_name, verify required_subdirs.
126
+ * 2. Legacy detect.any_of literal sentinels via existsAnywhere().
127
+ * 3. detect.always === true (manual provider).
128
+ *
129
+ * Returns { name, installed, via?, source?, evidence?, reason? }
130
+ */
131
+ function detectProviderAtAnyHarness(cfg, providerName) {
132
+ const providers = (cfg.cp && cfg.cp.providers) || {};
133
+ const provider = providers[providerName];
134
+ if (!provider) return { name: providerName, installed: false, reason: 'unknown provider' };
135
+
136
+ const det = provider.detect || {};
137
+
138
+ // always:true shortcut (manual provider)
139
+ if (det.always === true) {
140
+ return { name: providerName, installed: true, via: '_builtin', source: 'always' };
141
+ }
142
+
143
+ // 1. plugin_shape detection via harnesses
144
+ const shape = provider.plugin_shape;
145
+ if (shape && shape.dir_name) {
146
+ const harnesses = (cfg.cp && cfg.cp.harnesses) || {};
147
+ for (const [harnessName, harness] of Object.entries(harnesses)) {
148
+ const roots = harness.plugin_roots || [];
149
+ for (const rootSpec of roots) {
150
+ const expanded = expandRoot(rootSpec);
151
+ for (const expandedRoot of expanded) {
152
+ const pluginDir = path.join(expandedRoot, shape.dir_name);
153
+ if (fs.existsSync(pluginDir)) {
154
+ // Verify required_subdirs
155
+ const subdirs = Array.isArray(shape.required_subdirs) ? shape.required_subdirs : [];
156
+ const allPresent = subdirs.every((sub) =>
157
+ fs.existsSync(path.join(pluginDir, sub))
158
+ );
159
+ if (allPresent) {
160
+ return {
161
+ name: providerName,
162
+ installed: true,
163
+ via: harnessName,
164
+ source: 'plugin_shape',
165
+ evidence: pluginDir,
166
+ };
167
+ }
168
+ }
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ // 2. Legacy literal sentinel fallback
175
+ const candidates = det.any_of || [];
176
+ for (const c of candidates) {
177
+ const hit = existsAnywhere(c);
178
+ if (hit) {
179
+ return {
180
+ name: providerName,
181
+ installed: true,
182
+ via: '_anywhere',
183
+ source: 'literal',
184
+ evidence: hit,
185
+ };
186
+ }
187
+ }
188
+
189
+ return { name: providerName, installed: false, reason: 'no sentinel matched' };
190
+ }
191
+
192
+ // ───────────────────────── full scan ───────────────────────────────────
193
+
194
+ /**
195
+ * Scan all harnesses × all providers and return a full detection report.
196
+ *
197
+ * Returns {
198
+ * harnesses: [{ name, scannedRoots: [{ root, expanded }], pluginCount }],
199
+ * providers: [{ name, installed, hits: [{ via, source, evidence }] }]
200
+ * }
201
+ */
202
+ function detectAllInstalled(cfg) {
203
+ const harnesses = (cfg.cp && cfg.cp.harnesses) || {};
204
+ const providers = (cfg.cp && cfg.cp.providers) || {};
205
+
206
+ // Harness scan
207
+ const harnessReports = [];
208
+ for (const [name, harness] of Object.entries(harnesses)) {
209
+ const roots = harness.plugin_roots || [];
210
+ const scannedRoots = [];
211
+ let pluginCount = 0;
212
+ for (const rootSpec of roots) {
213
+ const expanded = expandRoot(rootSpec);
214
+ scannedRoots.push({ root: rootSpec, expanded });
215
+ pluginCount += expanded.length;
216
+ }
217
+ harnessReports.push({ name, scannedRoots, pluginCount });
218
+ }
219
+
220
+ // Provider scan — collect ALL hits (not just first)
221
+ const providerReports = [];
222
+ for (const providerName of Object.keys(providers)) {
223
+ const provider = providers[providerName];
224
+ const det = provider.detect || {};
225
+ const hits = [];
226
+
227
+ // always:true
228
+ if (det.always === true) {
229
+ hits.push({ via: '_builtin', source: 'always', evidence: null });
230
+ } else {
231
+ // plugin_shape across harnesses
232
+ const shape = provider.plugin_shape;
233
+ if (shape && shape.dir_name) {
234
+ for (const [harnessName, harness] of Object.entries(harnesses)) {
235
+ const roots = harness.plugin_roots || [];
236
+ for (const rootSpec of roots) {
237
+ const expanded = expandRoot(rootSpec);
238
+ for (const expandedRoot of expanded) {
239
+ const pluginDir = path.join(expandedRoot, shape.dir_name);
240
+ if (fs.existsSync(pluginDir)) {
241
+ const subdirs = Array.isArray(shape.required_subdirs) ? shape.required_subdirs : [];
242
+ const allPresent = subdirs.every((sub) =>
243
+ fs.existsSync(path.join(pluginDir, sub))
244
+ );
245
+ if (allPresent) {
246
+ hits.push({ via: harnessName, source: 'plugin_shape', evidence: pluginDir });
247
+ }
248
+ }
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ // Legacy literal sentinels
255
+ const candidates = det.any_of || [];
256
+ for (const c of candidates) {
257
+ const hit = existsAnywhere(c);
258
+ if (hit) {
259
+ hits.push({ via: '_anywhere', source: 'literal', evidence: hit });
260
+ }
261
+ }
262
+ }
263
+
264
+ providerReports.push({
265
+ name: providerName,
266
+ installed: hits.length > 0,
267
+ hits,
268
+ });
269
+ }
270
+
271
+ return { harnesses: harnessReports, providers: providerReports };
272
+ }
273
+
274
+ module.exports = {
275
+ expandRoot,
276
+ matchSegment,
277
+ existsAnywhere,
278
+ detectProviderAtAnyHarness,
279
+ detectAllInstalled,
280
+ };
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * YAML frontmatter reader/writer. Backed by the `yaml` package so it handles
5
+ * the full GSD frontmatter shape (nested maps, lists, list-of-maps, inline
6
+ * flow `[]` / `{}`, quoted strings, multiline literals, etc.).
7
+ *
8
+ * Returns `{ frontmatter, body }` for compatibility with earlier callers.
9
+ */
10
+
11
+ const yaml = require('yaml');
12
+
13
+ const FENCE = '---';
14
+
15
+ function split(content) {
16
+ if (!content.startsWith(FENCE)) {
17
+ return { fmText: '', body: content };
18
+ }
19
+ // First fence line, then locate the closing fence on its own line.
20
+ const after = content.slice(FENCE.length);
21
+ // Allow optional leading newline / whitespace after opening fence.
22
+ const m = after.match(/^[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*\r?\n?/);
23
+ if (!m) return { fmText: '', body: content };
24
+ const fmText = m[1];
25
+ const body = after.slice(m[0].length);
26
+ return { fmText, body };
27
+ }
28
+
29
+ function parse(content) {
30
+ const { fmText, body } = split(content);
31
+ let frontmatter = {};
32
+ let parseError = null;
33
+ if (fmText.trim()) {
34
+ try {
35
+ const parsed = yaml.parse(fmText);
36
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
37
+ frontmatter = parsed;
38
+ }
39
+ } catch (e) {
40
+ // Preserve body for lenient callers but surface the error so strict
41
+ // callers (e.g. `cp gsd-import`'s auditor) can report it.
42
+ frontmatter = {};
43
+ parseError = e.message || String(e);
44
+ }
45
+ }
46
+ return { frontmatter, body, parseError };
47
+ }
48
+
49
+ function stringify(frontmatter, body) {
50
+ const dumped = yaml
51
+ .stringify(frontmatter || {}, {
52
+ lineWidth: 0, // never wrap
53
+ defaultStringType: 'PLAIN',
54
+ defaultKeyType: 'PLAIN',
55
+ })
56
+ .replace(/\n$/, ''); // yaml.stringify always ends in a newline
57
+ return `${FENCE}\n${dumped}\n${FENCE}\n${body || ''}`;
58
+ }
59
+
60
+ /** Get a single top-level frontmatter key. */
61
+ function get(content, key) {
62
+ return parse(content).frontmatter[key];
63
+ }
64
+
65
+ /** Set a single top-level frontmatter key, returning the updated content. */
66
+ function set(content, key, value) {
67
+ const { frontmatter, body } = parse(content);
68
+ frontmatter[key] = value;
69
+ return stringify(frontmatter, body);
70
+ }
71
+
72
+ module.exports = { parse, stringify, get, set };
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * GSD compatibility helpers.
5
+ *
6
+ * cp's state-document format is a SUPERSET of GSD's: same filenames, same
7
+ * frontmatter keys, plus cp-specific behavior driven by a `cp` block inside
8
+ * `.planning/config.json` (which GSD ignores as an unknown top-level key).
9
+ *
10
+ * This module:
11
+ * - Detects whether a project was created/managed by GSD.
12
+ * - Detects whether a project's .planning/ is already cp-aware.
13
+ * - Reports interop diagnostics for `cp doctor`.
14
+ *
15
+ * No mutating helpers — repairs are user-initiated via `cp config set` or
16
+ * dedicated commands (eventually `cp gsd-import`).
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const { planningDir, repoRoot } = require('./paths');
22
+
23
+ /** Sentinel files/dirs that strongly indicate "this is a GSD project".
24
+ * These are things GSD creates but cp does NOT. Items removed over time:
25
+ * - MILESTONES.md — cp also writes it (via `cp init` + `cp complete-milestone`).
26
+ * - .planning/codebase — cp also writes it (via `cp scaffold-codebase` /
27
+ * /cp-map-codebase, v0.3.2+).
28
+ * - Short-form PLAN.md / SUMMARY.md — cp emits these by design (v0.3+). */
29
+ const GSD_SENTINELS = [
30
+ '.planning/research',
31
+ '.planning/todos',
32
+ '.planning/seeds',
33
+ '.planning/REQUIREMENTS.md',
34
+ '.planning/DEBUG.md',
35
+ '.planning/SECURITY.md',
36
+ ];
37
+
38
+ const SHARED_FILES = [
39
+ '.planning/PROJECT.md',
40
+ '.planning/ROADMAP.md',
41
+ '.planning/STATE.md',
42
+ '.planning/config.json',
43
+ ];
44
+
45
+ function fileExists(rel, root) {
46
+ return fs.existsSync(path.join(root, rel));
47
+ }
48
+
49
+ /** True if .planning/ exists at all. */
50
+ function hasPlanning(root = repoRoot()) {
51
+ return fs.existsSync(planningDir(root));
52
+ }
53
+
54
+ /** True if any of the GSD-only sentinels exist. */
55
+ function isGsdProject(root = repoRoot()) {
56
+ return GSD_SENTINELS.some((s) => fileExists(s, root));
57
+ }
58
+
59
+ /** True if the merged config has a `cp` block (i.e., cp-initialised). */
60
+ function isCpAware(root = repoRoot()) {
61
+ const cfg = path.join(planningDir(root), 'config.json');
62
+ if (!fs.existsSync(cfg)) return false;
63
+ try {
64
+ const c = JSON.parse(fs.readFileSync(cfg, 'utf8'));
65
+ return !!(c && typeof c === 'object' && c.cp && typeof c.cp === 'object');
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
71
+ /** Find shared files that are present. */
72
+ function presentSharedFiles(root = repoRoot()) {
73
+ return SHARED_FILES.filter((s) => fileExists(s, root));
74
+ }
75
+
76
+ /** Find phase dirs scanned from .planning/phases/. */
77
+ function scanPhases(root = repoRoot()) {
78
+ const phasesDir = path.join(planningDir(root), 'phases');
79
+ if (!fs.existsSync(phasesDir)) return [];
80
+ return fs
81
+ .readdirSync(phasesDir, { withFileTypes: true })
82
+ .filter((e) => e.isDirectory())
83
+ .map((e) => {
84
+ const dir = path.join(phasesDir, e.name);
85
+ const files = fs.readdirSync(dir);
86
+ // Count GSD-style {phase}-{plan}-PLAN.md vs short PLAN.md
87
+ const planFiles = files.filter((f) => /-PLAN\.md$/.test(f));
88
+ const summaryFiles = files.filter((f) => /-SUMMARY\.md$/.test(f));
89
+ const hasShortPlan = files.includes('PLAN.md');
90
+ const hasShortSummary = files.includes('SUMMARY.md');
91
+ return {
92
+ name: e.name,
93
+ path: dir,
94
+ planFiles,
95
+ summaryFiles,
96
+ hasShortPlan,
97
+ hasShortSummary,
98
+ };
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Compute a status report suitable for `cp doctor`.
104
+ */
105
+ function report(root = repoRoot()) {
106
+ const r = {
107
+ planning: hasPlanning(root),
108
+ cpAware: isCpAware(root),
109
+ gsdProject: isGsdProject(root),
110
+ sharedFiles: presentSharedFiles(root),
111
+ phases: scanPhases(root),
112
+ warnings: [],
113
+ };
114
+
115
+ if (r.planning && !r.cpAware) {
116
+ r.warnings.push(
117
+ '.planning/ exists but has no `cp` block in config.json. Run `cp init` ' +
118
+ 'to add cp settings without touching existing files.'
119
+ );
120
+ }
121
+ if (r.gsdProject && r.cpAware) {
122
+ r.warnings.push(
123
+ 'GSD sentinels detected (research/ / todos/ / seeds/ / REQUIREMENTS.md). ' +
124
+ 'cp will read GSD-shaped state and write GSD-compatible filenames. ' +
125
+ 'Switch back to GSD any time — `cp` only ADDS to .planning/, never ' +
126
+ 'rewrites GSD files.'
127
+ );
128
+ }
129
+ for (const p of r.phases) {
130
+ // Short-form `PLAN.md` / `SUMMARY.md` are cp-canonical (cp emits them by
131
+ // design — see `lib/lifecycle.js scaffoldPhase`). They round-trip through
132
+ // `cp gsd-import` cleanly. Only warn on the genuinely-broken case where
133
+ // BOTH a short-form AND long-form `{NN-MM}-PLAN.md` coexist in the same
134
+ // phase dir (the parser would have to pick one, and that's ambiguous).
135
+ //
136
+ // v0.3.3 — closes CONCERNS High: "cp scaffolds short-form PLAN.md, then
137
+ // cp doctor warns the same shape is GSD-incompatible."
138
+ if (p.hasShortPlan && p.planFiles.length > 0) {
139
+ r.warnings.push(
140
+ `Phase ${p.name} has BOTH short-form PLAN.md AND long-form ` +
141
+ `${p.planFiles.join(', ')}. Pick one — cp's canonical shape is ` +
142
+ `short-form PLAN.md; the long-form files exist for GSD round-trip only.`
143
+ );
144
+ }
145
+ if (p.hasShortSummary && p.summaryFiles.length > 0) {
146
+ r.warnings.push(
147
+ `Phase ${p.name} has BOTH short-form SUMMARY.md AND long-form ` +
148
+ `${p.summaryFiles.join(', ')}. Pick one — cp writes per-plan ` +
149
+ `{NN-MM}-SUMMARY.md files via \`cp write-summary\`.`
150
+ );
151
+ }
152
+ }
153
+ return r;
154
+ }
155
+
156
+ module.exports = {
157
+ GSD_SENTINELS,
158
+ SHARED_FILES,
159
+ hasPlanning,
160
+ isGsdProject,
161
+ isCpAware,
162
+ presentSharedFiles,
163
+ scanPhases,
164
+ report,
165
+ };