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.
- package/CHANGELOG.md +44 -7
- package/INSPIRATION.md +4 -2
- package/README.md +25 -12
- package/RELEASE.md +23 -27
- package/SKILL.md +2 -2
- package/agents/god-auditor.md +1 -1
- package/agents/god-deps-auditor.md +11 -0
- package/agents/god-executor.md +22 -5
- package/agents/god-greenfieldifier.md +1 -1
- package/agents/god-orchestrator.md +2 -2
- package/agents/god-planner.md +14 -0
- package/agents/god-pm.md +1 -1
- package/agents/god-reconciler.md +1 -1
- package/agents/god-roadmapper.md +1 -1
- package/agents/god-spec-reviewer.md +10 -0
- package/agents/god-stack-selector.md +4 -0
- package/agents/god-updater.md +1 -1
- package/bin/install.js +9 -3
- package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/manifest.json +2 -2
- package/lib/README.md +7 -1
- package/lib/atomic-write.js +85 -0
- package/lib/checkpoint.js +2 -1
- package/lib/context-writer.js +1 -1
- package/lib/executor-repair.js +65 -0
- package/lib/feature-awareness.js +1 -1
- package/lib/fs-async.js +2 -1
- package/lib/install-profiles.js +154 -0
- package/lib/installer-args.js +12 -0
- package/lib/installer-core.js +43 -8
- package/lib/linkage.js +2 -1
- package/lib/package-identity.js +38 -0
- package/lib/package-legitimacy.js +158 -0
- package/lib/planning-systems.js +9 -9
- package/lib/repo-surface-sync.js +1 -1
- package/lib/requirements.js +38 -5
- package/lib/reverse-sync.js +12 -2
- package/lib/source-grounding.js +177 -0
- package/lib/source-sync.js +6 -5
- package/lib/state.js +2 -1
- package/package.json +1 -1
- package/references/HAVE-NOTS.md +1 -1
- package/references/orchestration/MODE-DETECTION.md +2 -2
- package/references/shared/ORCHESTRATORS.md +3 -3
- package/routing/god-design.yaml +1 -1
- package/routing/god-migrate.yaml +3 -3
- package/schema/state.v1.json +1 -1
- package/skills/god-build.md +17 -3
- package/skills/god-doctor.md +2 -2
- package/skills/god-dogfood.md +2 -2
- package/skills/god-init.md +6 -6
- package/skills/god-migrate.md +10 -10
- package/skills/god-sync.md +2 -2
- package/skills/god-update-deps.md +4 -2
- package/skills/god-version.md +1 -1
- package/templates/IMPORTED-CONTEXT.md +1 -1
- package/templates/INITIAL-FINDINGS.md +2 -2
- /package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/.planning/PROJECT.md +0 -0
- /package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/.planning/REQUIREMENTS.md +0 -0
- /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
|
+
};
|
package/lib/planning-systems.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Planning System Migration
|
|
3
3
|
*
|
|
4
|
-
* Detects
|
|
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
|
-
|
|
75
|
-
displayName: '
|
|
76
|
-
markerPaths: ['.planning', '.
|
|
77
|
-
fileRoots: ['.planning', '.
|
|
78
|
-
standalonePatterns: [/^
|
|
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', '.
|
|
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
|
|
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
|
|
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('');
|
package/lib/repo-surface-sync.js
CHANGED
|
@@ -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-
|
|
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',
|
package/lib/requirements.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|
package/lib/reverse-sync.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/lib/source-sync.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Source System Sync-Back
|
|
3
3
|
*
|
|
4
|
-
* Writes Godpowers progress back to detected
|
|
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
|
-
|
|
19
|
-
companionCandidates: ['.planning/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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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"
|
package/references/HAVE-NOTS.md
CHANGED
|
@@ -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
|
|
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
|
-
-
|
|
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/`, `.
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
package/routing/god-design.yaml
CHANGED
|
@@ -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
|
|
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
|