godpowers 2.2.0 → 2.3.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 (59) hide show
  1. package/CHANGELOG.md +44 -7
  2. package/INSPIRATION.md +4 -2
  3. package/README.md +25 -12
  4. package/RELEASE.md +23 -27
  5. package/SKILL.md +2 -2
  6. package/agents/god-auditor.md +1 -1
  7. package/agents/god-deps-auditor.md +11 -0
  8. package/agents/god-executor.md +22 -5
  9. package/agents/god-greenfieldifier.md +1 -1
  10. package/agents/god-orchestrator.md +2 -2
  11. package/agents/god-planner.md +14 -0
  12. package/agents/god-pm.md +1 -1
  13. package/agents/god-reconciler.md +1 -1
  14. package/agents/god-roadmapper.md +1 -1
  15. package/agents/god-spec-reviewer.md +10 -0
  16. package/agents/god-stack-selector.md +4 -0
  17. package/agents/god-updater.md +1 -1
  18. package/bin/install.js +9 -3
  19. package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/manifest.json +2 -2
  20. package/lib/README.md +7 -1
  21. package/lib/atomic-write.js +85 -0
  22. package/lib/checkpoint.js +2 -1
  23. package/lib/context-writer.js +1 -1
  24. package/lib/executor-repair.js +65 -0
  25. package/lib/feature-awareness.js +1 -1
  26. package/lib/fs-async.js +2 -1
  27. package/lib/install-profiles.js +154 -0
  28. package/lib/installer-args.js +12 -0
  29. package/lib/installer-core.js +43 -8
  30. package/lib/linkage.js +2 -1
  31. package/lib/package-identity.js +38 -0
  32. package/lib/package-legitimacy.js +158 -0
  33. package/lib/planning-systems.js +9 -9
  34. package/lib/repo-surface-sync.js +1 -1
  35. package/lib/requirements.js +38 -5
  36. package/lib/reverse-sync.js +12 -2
  37. package/lib/source-grounding.js +177 -0
  38. package/lib/source-sync.js +6 -5
  39. package/lib/state.js +2 -1
  40. package/package.json +1 -1
  41. package/references/HAVE-NOTS.md +1 -1
  42. package/references/orchestration/MODE-DETECTION.md +2 -2
  43. package/references/shared/ORCHESTRATORS.md +3 -3
  44. package/routing/god-design.yaml +1 -1
  45. package/routing/god-migrate.yaml +3 -3
  46. package/schema/state.v1.json +1 -1
  47. package/skills/god-build.md +17 -3
  48. package/skills/god-doctor.md +2 -2
  49. package/skills/god-dogfood.md +2 -2
  50. package/skills/god-init.md +6 -6
  51. package/skills/god-migrate.md +10 -10
  52. package/skills/god-sync.md +2 -2
  53. package/skills/god-update-deps.md +4 -2
  54. package/skills/god-version.md +1 -1
  55. package/templates/IMPORTED-CONTEXT.md +1 -1
  56. package/templates/INITIAL-FINDINGS.md +2 -2
  57. /package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/.planning/PROJECT.md +0 -0
  58. /package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/.planning/REQUIREMENTS.md +0 -0
  59. /package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/.planning/ROADMAP.md +0 -0
@@ -0,0 +1,158 @@
1
+ const { execFileSync } = require('child_process');
2
+
3
+ const STALE_MONTHS = 18;
4
+
5
+ function normalizeName(name) {
6
+ return String(name || '')
7
+ .toLowerCase()
8
+ .replace(/^@/, '')
9
+ .replace(/[^a-z0-9]+/g, '');
10
+ }
11
+
12
+ function levenshtein(a, b) {
13
+ a = normalizeName(a);
14
+ b = normalizeName(b);
15
+ const dp = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
16
+ for (let i = 0; i <= a.length; i++) dp[i][0] = i;
17
+ for (let j = 0; j <= b.length; j++) dp[0][j] = j;
18
+ for (let i = 1; i <= a.length; i++) {
19
+ for (let j = 1; j <= b.length; j++) {
20
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
21
+ dp[i][j] = Math.min(
22
+ dp[i - 1][j] + 1,
23
+ dp[i][j - 1] + 1,
24
+ dp[i - 1][j - 1] + cost
25
+ );
26
+ }
27
+ }
28
+ return dp[a.length][b.length];
29
+ }
30
+
31
+ function monthsBetween(a, b) {
32
+ return Math.abs((b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth()));
33
+ }
34
+
35
+ function latestPublishedAt(metadata) {
36
+ if (!metadata || !metadata.time) return null;
37
+ const latest = metadata['dist-tags'] && metadata['dist-tags'].latest;
38
+ const raw = latest && metadata.time[latest] ? metadata.time[latest] : metadata.time.modified;
39
+ return raw ? new Date(raw) : null;
40
+ }
41
+
42
+ function repoUrl(metadata) {
43
+ if (!metadata || !metadata.repository) return null;
44
+ if (typeof metadata.repository === 'string') return metadata.repository;
45
+ return metadata.repository.url || null;
46
+ }
47
+
48
+ function typoRisk(name, knownNames = []) {
49
+ const risks = [];
50
+ for (const known of knownNames) {
51
+ if (normalizeName(name) === normalizeName(known)) continue;
52
+ const distance = levenshtein(name, known);
53
+ if (distance > 0 && distance <= 2) {
54
+ risks.push({ known, distance });
55
+ }
56
+ }
57
+ return risks.sort((a, b) => a.distance - b.distance);
58
+ }
59
+
60
+ function assessNpmMetadata(name, metadata, opts = {}) {
61
+ const now = opts.now || new Date();
62
+ const findings = [];
63
+ if (!metadata || metadata.missing) {
64
+ findings.push({
65
+ severity: 'fail',
66
+ code: 'package-missing',
67
+ message: `${name} was not found in the npm registry data.`
68
+ });
69
+ return { status: 'fail', findings, signals: { exists: false } };
70
+ }
71
+
72
+ const risks = typoRisk(name, opts.knownNames || []);
73
+ for (const risk of risks) {
74
+ findings.push({
75
+ severity: 'fail',
76
+ code: 'typo-risk',
77
+ message: `${name} is within edit distance ${risk.distance} of ${risk.known}.`
78
+ });
79
+ }
80
+
81
+ const published = latestPublishedAt(metadata);
82
+ if (!published) {
83
+ findings.push({
84
+ severity: 'warn',
85
+ code: 'unknown-recency',
86
+ message: `${name} has no publish timestamp in registry metadata.`
87
+ });
88
+ } else if (monthsBetween(published, now) > STALE_MONTHS) {
89
+ findings.push({
90
+ severity: 'warn',
91
+ code: 'stale-package',
92
+ message: `${name} latest release is older than ${STALE_MONTHS} months.`
93
+ });
94
+ }
95
+
96
+ if (!repoUrl(metadata)) {
97
+ findings.push({
98
+ severity: 'warn',
99
+ code: 'missing-repository',
100
+ message: `${name} does not expose a repository URL.`
101
+ });
102
+ }
103
+
104
+ const status = findings.some(f => f.severity === 'fail')
105
+ ? 'fail'
106
+ : findings.some(f => f.severity === 'warn')
107
+ ? 'warn'
108
+ : 'pass';
109
+
110
+ return {
111
+ status,
112
+ findings,
113
+ signals: {
114
+ exists: true,
115
+ latest: metadata['dist-tags'] && metadata['dist-tags'].latest,
116
+ publishedAt: published ? published.toISOString() : null,
117
+ repository: repoUrl(metadata)
118
+ }
119
+ };
120
+ }
121
+
122
+ function npmView(name) {
123
+ try {
124
+ const raw = execFileSync('npm', ['view', name, '--json'], {
125
+ encoding: 'utf8',
126
+ stdio: ['ignore', 'pipe', 'pipe']
127
+ });
128
+ return JSON.parse(raw);
129
+ } catch (error) {
130
+ return { missing: true, error: error.message };
131
+ }
132
+ }
133
+
134
+ function checkNpmPackage(name, opts = {}) {
135
+ const metadata = opts.metadata || npmView(name);
136
+ if (metadata && metadata.error && opts.failOnUnknown === false) {
137
+ return {
138
+ status: 'unknown',
139
+ findings: [{
140
+ severity: 'warn',
141
+ code: 'registry-unavailable',
142
+ message: `${name} could not be verified from npm metadata.`
143
+ }],
144
+ signals: { exists: null }
145
+ };
146
+ }
147
+ return assessNpmMetadata(name, metadata, opts);
148
+ }
149
+
150
+ module.exports = {
151
+ STALE_MONTHS,
152
+ normalizeName,
153
+ levenshtein,
154
+ typoRisk,
155
+ assessNpmMetadata,
156
+ checkNpmPackage,
157
+ latestPublishedAt
158
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Planning System Migration
3
3
  *
4
- * Detects GSD, BMAD, and Superpowers project artifacts, converts their useful
4
+ * Detects legacy planning, BMAD, and Superpowers project artifacts, converts their useful
5
5
  * signals into Godpowers preparation artifacts, and records source-system state
6
6
  * so /god-sync can later write progress back through lib/source-sync.js.
7
7
  */
@@ -71,11 +71,11 @@ const GODPOWERS_ARTIFACTS = [
71
71
  ];
72
72
 
73
73
  const SYSTEMS = {
74
- gsd: {
75
- displayName: 'GSD',
76
- markerPaths: ['.planning', '.gsd', 'GSD.md'],
77
- fileRoots: ['.planning', '.gsd'],
78
- standalonePatterns: [/^gsd.*\.md$/i]
74
+ 'legacy-planning': {
75
+ displayName: 'legacy planning',
76
+ markerPaths: ['.planning', '.legacy-planning', 'LEGACY-PLANNING.md'],
77
+ fileRoots: ['.planning', '.legacy-planning'],
78
+ standalonePatterns: [/^legacy-planning.*\.md$/i]
79
79
  },
80
80
  bmad: {
81
81
  displayName: 'BMAD',
@@ -246,7 +246,7 @@ function detectSystem(projectRoot, id, system) {
246
246
  };
247
247
  });
248
248
 
249
- const hasPrimaryRoot = markerHits.some((marker) => ['.planning', '.gsd', '_bmad-output', 'docs/superpowers'].includes(marker));
249
+ const hasPrimaryRoot = markerHits.some((marker) => ['.planning', '.legacy-planning', '_bmad-output', 'docs/superpowers'].includes(marker));
250
250
  const score = markerHits.length * 3 + fileRecords.length;
251
251
  const confidence = score >= 10 || (hasPrimaryRoot && fileRecords.length >= 3)
252
252
  ? 'high'
@@ -294,7 +294,7 @@ function buildImportedContext(detection) {
294
294
  lines.push('');
295
295
 
296
296
  if (detection.systems.length === 0) {
297
- lines.push('- [DECISION] No GSD, BMAD, or Superpowers planning context was detected.');
297
+ lines.push('- [DECISION] No legacy planning, BMAD, or Superpowers planning context was detected.');
298
298
  }
299
299
 
300
300
  for (const system of detection.systems) {
@@ -331,7 +331,7 @@ function buildImportedContext(detection) {
331
331
  lines.push('## Sync-Back Policy');
332
332
  lines.push('');
333
333
  lines.push('- [DECISION] Godpowers may write managed sync-back summaries only inside Godpowers-owned fences or companion files.');
334
- lines.push('- [DECISION] Godpowers must not rewrite GSD, BMAD, or Superpowers source documents outside managed sync-back sections.');
334
+ lines.push('- [DECISION] Godpowers must not rewrite legacy planning, BMAD, or Superpowers source documents outside managed sync-back sections.');
335
335
  lines.push('- [DECISION] Sync-back exists so a project can return to the prior planning system with current Godpowers progress visible.');
336
336
 
337
337
  lines.push('');
@@ -442,7 +442,7 @@ function dogfoodChecks(projectRoot) {
442
442
  const pkg = readJson(projectRoot, 'package.json') || {};
443
443
  const scriptsText = releaseGateText(projectRoot, pkg);
444
444
  const scenarios = [
445
- 'fixtures/dogfood/half-migrated-gsd/manifest.json',
445
+ 'fixtures/dogfood/half-migrated-planning/manifest.json',
446
446
  'fixtures/dogfood/host-degraded/manifest.json',
447
447
  'fixtures/dogfood/host-full/manifest.json',
448
448
  'fixtures/dogfood/extension-authoring/manifest.json',
@@ -31,6 +31,7 @@ const path = require('path');
31
31
 
32
32
  const linkage = require('./linkage');
33
33
  const state = require('./state');
34
+ const atomic = require('./atomic-write');
34
35
 
35
36
  const PRD_PATH = '.godpowers/prd/PRD.md';
36
37
  const ROADMAP_PATH = '.godpowers/roadmap/ROADMAP.md';
@@ -363,6 +364,27 @@ function progressBar(done, total, width = 20) {
363
364
 
364
365
  const MARK = { done: '[x]', 'in-progress': '[~]', untouched: '[ ]' };
365
366
  const INC_MARK = { done: '[x]', building: '[~]', pending: '[ ]' };
367
+ const UPDATED_LINE_RE = /^Updated: .+$/m;
368
+
369
+ function finishLedger(lines) {
370
+ while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
371
+ return lines.join('\n');
372
+ }
373
+
374
+ function normalizeLedgerTimestamp(content) {
375
+ return String(content).replace(UPDATED_LINE_RE, 'Updated: <timestamp>');
376
+ }
377
+
378
+ function withoutUpdated(value) {
379
+ if (!value || typeof value !== 'object') return value;
380
+ const copy = JSON.parse(JSON.stringify(value));
381
+ delete copy.updated;
382
+ return copy;
383
+ }
384
+
385
+ function sameIgnoringUpdated(a, b) {
386
+ return JSON.stringify(withoutUpdated(a)) === JSON.stringify(withoutUpdated(b));
387
+ }
366
388
 
367
389
  /**
368
390
  * Compact lines for the dashboard "Deliverable progress" section.
@@ -411,7 +433,7 @@ function renderLedger(derived) {
411
433
  out.push('No requirements are declared yet. Once the PRD lists MUST/SHOULD/COULD');
412
434
  out.push('requirements with stable ids (P-MUST-01, ...), they appear here.');
413
435
  out.push('');
414
- return out.join('\n');
436
+ return finishLedger(out);
415
437
  }
416
438
 
417
439
  out.push(`Progress: ${progressBar(s.done, s.total)} done (${s.percent}%) | ${s.inProgress} in progress | ${s.untouched} not started`);
@@ -469,23 +491,30 @@ function renderLedger(derived) {
469
491
  out.push('');
470
492
  }
471
493
 
472
- return out.join('\n');
494
+ return finishLedger(out);
473
495
  }
474
496
 
475
497
  function writeLedger(projectRoot, derived) {
476
498
  const data = derived || derive(projectRoot);
477
499
  const file = path.join(projectRoot, LEDGER_PATH);
478
500
  fs.mkdirSync(path.dirname(file), { recursive: true });
479
- fs.writeFileSync(file, renderLedger(data) + '\n');
501
+ const rendered = renderLedger(data) + '\n';
502
+ if (fs.existsSync(file)) {
503
+ const current = fs.readFileSync(file, 'utf8');
504
+ if (normalizeLedgerTimestamp(current) === normalizeLedgerTimestamp(rendered)) {
505
+ return LEDGER_PATH;
506
+ }
507
+ }
508
+ atomic.writeFileAtomic(file, rendered);
480
509
  return LEDGER_PATH;
481
510
  }
482
511
 
483
512
  /**
484
513
  * Small cacheable summary for state.json (state.deliverables).
485
514
  */
486
- function summarizeForState(derived) {
515
+ function summarizeForState(derived, currentSummary = null) {
487
516
  const s = derived.summary;
488
- return {
517
+ const next = {
489
518
  updated: derived.updated,
490
519
  source: 'PRD + ROADMAP + linkage + build state',
491
520
  requirements: {
@@ -505,6 +534,10 @@ function summarizeForState(derived) {
505
534
  })),
506
535
  gaps: derived.gaps.length
507
536
  };
537
+ if (currentSummary && currentSummary.updated && sameIgnoringUpdated(currentSummary, next)) {
538
+ next.updated = currentSummary.updated;
539
+ }
540
+ return next;
508
541
  }
509
542
 
510
543
  module.exports = {
@@ -27,6 +27,8 @@ const reviewRequired = require('./review-required');
27
27
  const impeccable = require('./impeccable-bridge');
28
28
  const sourceSync = require('./source-sync');
29
29
  const requirements = require('./requirements');
30
+ const state = require('./state');
31
+ const atomic = require('./atomic-write');
30
32
 
31
33
  const FENCE_BEGIN = '<!-- godpowers:linkage:begin -->';
32
34
  const FENCE_END = '<!-- godpowers:linkage:end -->';
@@ -67,7 +69,7 @@ function writeFenced(filePath, sectionContent) {
67
69
  } else {
68
70
  next = `${parsed.before}${fencedBlock}${parsed.after}`;
69
71
  }
70
- fs.writeFileSync(filePath, next);
72
+ atomic.writeFileAtomic(filePath, next);
71
73
  return { written: true, hadFenceBefore: parsed.fenced !== '' };
72
74
  }
73
75
 
@@ -321,7 +323,15 @@ function run(projectRoot, opts = {}) {
321
323
  const derived = requirements.derive(projectRoot);
322
324
  if (derived.hasRequirements) {
323
325
  requirements.writeLedger(projectRoot, derived);
324
- requirementsSummary = requirements.summarizeForState(derived);
326
+ const currentState = state.read(projectRoot);
327
+ requirementsSummary = requirements.summarizeForState(
328
+ derived,
329
+ currentState && currentState.deliverables
330
+ );
331
+ if (currentState) {
332
+ currentState.deliverables = requirementsSummary;
333
+ state.write(projectRoot, currentState);
334
+ }
325
335
  }
326
336
  } catch (e) {
327
337
  requirementsSummary = null;
@@ -0,0 +1,177 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const DEFAULT_IGNORE_DIRS = new Set([
5
+ '.git',
6
+ '.godpowers',
7
+ 'node_modules',
8
+ 'dist',
9
+ 'build',
10
+ 'coverage',
11
+ '.next',
12
+ '.turbo'
13
+ ]);
14
+
15
+ function normalizeRel(filePath) {
16
+ return String(filePath || '').split(path.sep).join('/');
17
+ }
18
+
19
+ function listFiles(root, opts = {}) {
20
+ const ignoreDirs = opts.ignoreDirs || DEFAULT_IGNORE_DIRS;
21
+ const out = [];
22
+ function walk(dir) {
23
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
24
+ if (entry.isDirectory()) {
25
+ if (ignoreDirs.has(entry.name)) continue;
26
+ walk(path.join(dir, entry.name));
27
+ } else if (entry.isFile()) {
28
+ const rel = normalizeRel(path.relative(root, path.join(dir, entry.name)));
29
+ out.push(rel);
30
+ }
31
+ }
32
+ }
33
+ if (fs.existsSync(root)) walk(root);
34
+ return out.sort();
35
+ }
36
+
37
+ function parseSectionLists(planText) {
38
+ const result = {
39
+ existingFiles: [],
40
+ existingSymbols: [],
41
+ newArtifacts: [],
42
+ unchecked: []
43
+ };
44
+ let section = null;
45
+ const lines = String(planText || '').split(/\r?\n/);
46
+ for (const raw of lines) {
47
+ const heading = raw.match(/^#{2,4}\s+(.+)$/);
48
+ if (heading) {
49
+ const title = heading[1].toLowerCase();
50
+ if (/existing (files|references)/.test(title)) section = 'existingFiles';
51
+ else if (/existing symbols/.test(title)) section = 'existingSymbols';
52
+ else if (/new artifacts|files to create/.test(title)) section = 'newArtifacts';
53
+ else if (/unchecked references|unknown references/.test(title)) section = 'unchecked';
54
+ else section = null;
55
+ continue;
56
+ }
57
+ if (!section) continue;
58
+ const item = raw.match(/^\s*[-*]\s+(.*)$/);
59
+ if (!item) continue;
60
+ const value = item[1]
61
+ .replace(/^(file|symbol|new|unchecked)\s*:\s*/i, '')
62
+ .replace(/^`|`$/g, '')
63
+ .trim();
64
+ if (value) result[section].push(value);
65
+ }
66
+ return result;
67
+ }
68
+
69
+ function unique(values) {
70
+ return [...new Set(values.filter(Boolean))];
71
+ }
72
+
73
+ function findSymbolInFiles(root, symbol, files) {
74
+ const needle = String(symbol || '').trim();
75
+ if (!needle) return [];
76
+ const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
77
+ const re = new RegExp(`\\b${escaped}\\b`);
78
+ const hits = [];
79
+ for (const rel of files) {
80
+ const full = path.join(root, rel);
81
+ let text;
82
+ try {
83
+ text = fs.readFileSync(full, 'utf8');
84
+ } catch (_) {
85
+ continue;
86
+ }
87
+ if (re.test(text)) hits.push(rel);
88
+ }
89
+ return hits;
90
+ }
91
+
92
+ function checkPlan(projectRoot, planText, opts = {}) {
93
+ const parsed = parseSectionLists(planText);
94
+ const existingFiles = unique([...(opts.existingFiles || []), ...parsed.existingFiles]);
95
+ const existingSymbols = unique([...(opts.existingSymbols || []), ...parsed.existingSymbols]);
96
+ const newArtifacts = unique([...(opts.newArtifacts || []), ...parsed.newArtifacts]);
97
+ const unchecked = unique([...(opts.unchecked || []), ...parsed.unchecked]);
98
+ const files = listFiles(projectRoot, opts);
99
+ const fileSet = new Set(files);
100
+
101
+ const fileChecks = existingFiles.map(file => ({
102
+ type: 'file',
103
+ value: normalizeRel(file),
104
+ status: fileSet.has(normalizeRel(file)) ? 'pass' : 'fail'
105
+ }));
106
+
107
+ const symbolChecks = existingSymbols.map(symbol => {
108
+ const hits = findSymbolInFiles(projectRoot, symbol, files);
109
+ return {
110
+ type: 'symbol',
111
+ value: symbol,
112
+ status: hits.length > 0 ? 'pass' : 'fail',
113
+ files: hits
114
+ };
115
+ });
116
+
117
+ const checks = [...fileChecks, ...symbolChecks];
118
+ const failures = checks.filter(check => check.status === 'fail');
119
+ const warnings = unchecked.map(value => ({
120
+ type: 'unchecked',
121
+ value,
122
+ status: 'warn'
123
+ }));
124
+ const newArtifactSet = new Set(newArtifacts.map(normalizeRel));
125
+ const declaredNew = newArtifacts.map(file => ({
126
+ type: 'new-artifact',
127
+ value: normalizeRel(file),
128
+ status: fileSet.has(normalizeRel(file)) ? 'exists' : 'declared-new'
129
+ }));
130
+
131
+ return {
132
+ ok: failures.length === 0,
133
+ checks,
134
+ failures,
135
+ warnings,
136
+ declaredNew,
137
+ newArtifacts: [...newArtifactSet],
138
+ summary: {
139
+ pass: checks.filter(check => check.status === 'pass').length,
140
+ fail: failures.length,
141
+ warn: warnings.length,
142
+ declaredNew: declaredNew.length
143
+ }
144
+ };
145
+ }
146
+
147
+ function renderReport(result) {
148
+ const lines = [];
149
+ lines.push(`Source grounding: ${result.ok ? 'PASS' : 'FAIL'}`);
150
+ lines.push(` passed: ${result.summary.pass}`);
151
+ lines.push(` failed: ${result.summary.fail}`);
152
+ lines.push(` unchecked: ${result.summary.warn}`);
153
+ if (result.failures.length > 0) {
154
+ lines.push('');
155
+ lines.push('Failures:');
156
+ for (const failure of result.failures) {
157
+ lines.push(` - ${failure.type}: ${failure.value}`);
158
+ }
159
+ }
160
+ if (result.warnings.length > 0) {
161
+ lines.push('');
162
+ lines.push('Unchecked references:');
163
+ for (const warning of result.warnings) {
164
+ lines.push(` - ${warning.value}`);
165
+ }
166
+ }
167
+ return lines.join('\n');
168
+ }
169
+
170
+ module.exports = {
171
+ DEFAULT_IGNORE_DIRS,
172
+ listFiles,
173
+ parseSectionLists,
174
+ checkPlan,
175
+ renderReport,
176
+ findSymbolInFiles
177
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Source System Sync-Back
3
3
  *
4
- * Writes Godpowers progress back to detected GSD, BMAD, and Superpowers
4
+ * Writes Godpowers progress back to detected legacy planning, BMAD, and Superpowers
5
5
  * projects through managed companion files and optional managed fences.
6
6
  */
7
7
 
@@ -10,13 +10,14 @@ const path = require('path');
10
10
  const crypto = require('crypto');
11
11
 
12
12
  const state = require('./state');
13
+ const atomic = require('./atomic-write');
13
14
 
14
15
  const FENCE_BEGIN = '<!-- godpowers:source-sync:begin -->';
15
16
  const FENCE_END = '<!-- godpowers:source-sync:end -->';
16
17
 
17
18
  const SYSTEM_TARGETS = {
18
- gsd: {
19
- companionCandidates: ['.planning/GODPOWERS-SYNC.md', '.gsd/GODPOWERS-SYNC.md'],
19
+ 'legacy-planning': {
20
+ companionCandidates: ['.planning/GODPOWERS-SYNC.md', '.legacy-planning/GODPOWERS-SYNC.md'],
20
21
  pointerCandidates: ['.planning/STATE.md']
21
22
  },
22
23
  bmad: {
@@ -68,7 +69,7 @@ function writeFenced(filePath, sectionContent) {
68
69
  next = `${parsed.before}${block}${parsed.after}`;
69
70
  }
70
71
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
71
- fs.writeFileSync(filePath, next);
72
+ atomic.writeFileAtomic(filePath, next);
72
73
  return {
73
74
  written: true,
74
75
  hadFenceBefore: parsed.exists && parsed.fenced !== ''
@@ -134,7 +135,7 @@ function progressLines(projectRoot) {
134
135
  lines.push('');
135
136
  lines.push('## Return Path');
136
137
  lines.push('');
137
- lines.push('- [DECISION] If the project returns to GSD, BMAD, or Superpowers, use this file as a migration note rather than treating it as a native source-system artifact.');
138
+ lines.push('- [DECISION] If the project returns to legacy planning, BMAD, or Superpowers, use this file as a migration note rather than treating it as a native source-system artifact.');
138
139
  lines.push('- [OPEN QUESTION] Confirm which Godpowers decisions should be copied into native source-system documents before switching systems. Owner: user. Due: before switching systems.');
139
140
  lines.push('');
140
141
  return lines.join('\n');
package/lib/state.js CHANGED
@@ -9,6 +9,7 @@ const fs = require('fs');
9
9
  const path = require('path');
10
10
  const crypto = require('crypto');
11
11
  const asyncFs = require('./fs-async');
12
+ const atomic = require('./atomic-write');
12
13
 
13
14
  const STATE_VERSION = '1.0.0';
14
15
  const COMPLETE_STATUSES = new Set(['done', 'imported', 'skipped', 'not-required']);
@@ -138,7 +139,7 @@ function write(projectRoot, state) {
138
139
 
139
140
  const file = statePath(projectRoot);
140
141
  fs.mkdirSync(path.dirname(file), { recursive: true });
141
- fs.writeFileSync(file, JSON.stringify(state, null, 2) + '\n');
142
+ atomic.writeJsonAtomic(file, state);
142
143
  return state;
143
144
  }
144
145
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "godpowers",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "AI-powered development system: 111 slash commands and 40 specialist agents that take a project from raw idea to hardened production. Runs inside Claude Code, Codex, Cursor, Windsurf, Gemini, and 10+ other AI coding tools.",
5
5
  "bin": {
6
6
  "godpowers": "./bin/install.js"
@@ -87,7 +87,7 @@ A command completed without a disk-derived dashboard, action brief, or
87
87
  recommended next command. Fail.
88
88
 
89
89
  ### O-09 Sync-back ambiguity
90
- Imported GSD, BMAD, or Superpowers context exists but the project does not say
90
+ Imported legacy planning, BMAD, or Superpowers context exists but the project does not say
91
91
  whether managed sync-back is enabled, disabled, or not applicable. Fail.
92
92
 
93
93
  ### O-10 Host guarantees hidden
@@ -18,7 +18,7 @@
18
18
  - Some `.godpowers/<tier>/<artifact>` files already exist
19
19
  - OR existing codebase signals: package.json, Dockerfile, .github/workflows
20
20
  - User describes an existing project they want to add Godpowers to
21
- - GSD, BMAD, or Superpowers planning context is detected and should be imported
21
+ - legacy planning, BMAD, or Superpowers planning context is detected and should be imported
22
22
  into Godpowers preparation artifacts
23
23
 
24
24
  **Behavior**:
@@ -33,7 +33,7 @@ Codebase signals (for inferring partial completion):
33
33
  - `.github/workflows/` or `.gitlab-ci.yml` exists -> CI present
34
34
  - `tests/` or `*.test.*` files exist -> Build tier in progress
35
35
  - `Dockerfile` + deploy config -> Deploy tier may be done
36
- - `.planning/`, `.gsd/`, `_bmad-output/`, `.bmad/`, or Superpowers specs ->
36
+ - `.planning/`, `.legacy-planning/`, `_bmad-output/`, `.bmad/`, or Superpowers specs ->
37
37
  source-system import and managed sync-back may be needed
38
38
 
39
39
  ## Mode C: Audit
@@ -45,7 +45,7 @@ If you arrive at Godpowers carrying artifacts from another system,
45
45
  `/god-init` Mode B (gap-fill) and `/god-migrate` read what exists and map it
46
46
  forward:
47
47
 
48
- - GSD `.planning/` or `.gsd/` context -> `.godpowers/prep/IMPORTED-CONTEXT.md`
48
+ - legacy planning `.planning/` or `.legacy-planning/` context -> `.godpowers/prep/IMPORTED-CONTEXT.md`
49
49
  and optional native seed artifacts
50
50
  - BMAD `_bmad-output/` or `.bmad/` context -> imported preparation context and
51
51
  open questions for PRD, architecture, roadmap, and stack
@@ -74,7 +74,7 @@ authoritative. `/god-sync` writes managed companion files such as:
74
74
  The sync-back file is a bridge, not a second source of truth. It should contain
75
75
  the current Godpowers status, imported-context mapping, open conflicts, and the
76
76
  next safe route back into either system. It must be fenced or companion-owned
77
- so Godpowers does not overwrite arbitrary GSD, BMAD, or Superpowers artifacts.
77
+ so Godpowers does not overwrite arbitrary legacy planning, BMAD, or Superpowers artifacts.
78
78
 
79
79
  ## Existing Godpowers projects after upgrades
80
80
 
@@ -94,7 +94,7 @@ frontmatter. There's no proprietary binary state. To leave:
94
94
  system doesn't understand them (they're recoverable from code
95
95
  annotations).
96
96
  3. Use the most recent managed sync-back file as the return-path summary if the
97
- target system is GSD, BMAD, or Superpowers.
97
+ target system is legacy planning, BMAD, or Superpowers.
98
98
  4. Delete `.godpowers/`.
99
99
 
100
100
  ## What Godpowers does not try to be
@@ -17,7 +17,7 @@ prerequisites:
17
17
  - check: file:.godpowers/prep/INITIAL-FINDINGS.md
18
18
  reason: "Initial findings can show UI and product-experience signals"
19
19
  - check: file:.godpowers/prep/IMPORTED-CONTEXT.md
20
- reason: "Imported GSD, Superpowers, or BMAD context can inform design hypotheses"
20
+ reason: "Imported legacy planning, Superpowers, or BMAD context can inform design hypotheses"
21
21
  - check: file:.godpowers/arch/ARCH.md
22
22
  reason: "ARCH names the UI surface that DESIGN applies to"
23
23
  - check: file:.godpowers/stack/DECISION.md