godpowers 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +65 -7
- package/INSPIRATION.md +4 -2
- package/README.md +35 -22
- package/RELEASE.md +44 -23
- package/SKILL.md +2 -2
- package/agents/god-auditor.md +1 -1
- package/agents/god-debugger.md +44 -8
- package/agents/god-deps-auditor.md +11 -0
- package/agents/god-executor.md +50 -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-quality-reviewer.md +23 -1
- package/agents/god-reconciler.md +1 -1
- package/agents/god-roadmapper.md +1 -1
- package/agents/god-spec-reviewer.md +16 -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 +9 -2
- package/lib/agent-cache.js +25 -3
- package/lib/atomic-write.js +85 -0
- package/lib/checkpoint.js +2 -1
- package/lib/code-intelligence.js +161 -0
- package/lib/context-writer.js +1 -1
- package/lib/dashboard.js +54 -20
- package/lib/executor-repair.js +65 -0
- package/lib/extensions.js +6 -13
- package/lib/feature-awareness.js +2 -2
- package/lib/fs-async.js +2 -1
- package/lib/host-capabilities.js +4 -0
- package/lib/install-profiles.js +155 -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 +16 -13
- package/lib/quick-proof.js +5 -2
- package/lib/repo-surface-sync.js +1 -1
- package/lib/requirements.js +2 -1
- package/lib/reverse-sync.js +2 -1
- package/lib/route-quality-sync.js +2 -0
- package/lib/source-grounding.js +177 -0
- package/lib/source-sync.js +6 -5
- package/lib/state.js +2 -1
- package/package.json +2 -2
- 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-extension-scaffold.yaml +25 -0
- package/routing/god-migrate.yaml +3 -3
- package/routing/god-stack.yaml +1 -1
- package/routing/recipes/add-feature-mid-arc-pause.yaml +6 -0
- package/routing/recipes/brownfield-onboarding.yaml +5 -2
- package/routing/recipes/extension-authoring.yaml +32 -0
- package/routing/recipes/greenfield-fast.yaml +3 -0
- package/routing/recipes/production-broken.yaml +4 -0
- package/routing/recipes/release-maintenance.yaml +3 -0
- package/routing/recipes/returning-after-break.yaml +3 -0
- package/routing/recipes/weekly-health-check.yaml +2 -0
- package/schema/state.v1.json +1 -1
- package/skills/god-build.md +17 -3
- package/skills/god-discuss.md +10 -5
- package/skills/god-doctor.md +9 -3
- package/skills/god-dogfood.md +2 -2
- package/skills/god-extension-scaffold.md +66 -0
- 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 +2 -2
- package/skills/god.md +8 -11
- 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',
|
|
@@ -112,7 +112,7 @@ function hashFiles(projectRoot, files) {
|
|
|
112
112
|
for (const file of files.map((f) => f.path).sort()) {
|
|
113
113
|
const full = path.join(projectRoot, file);
|
|
114
114
|
h.update(file);
|
|
115
|
-
if (fs.existsSync(full) && fs.
|
|
115
|
+
if (fs.existsSync(full) && fs.lstatSync(full).isFile()) {
|
|
116
116
|
h.update(fs.readFileSync(full));
|
|
117
117
|
}
|
|
118
118
|
}
|
|
@@ -125,8 +125,10 @@ function isTextFile(filePath) {
|
|
|
125
125
|
|
|
126
126
|
function readText(projectRoot, relPath) {
|
|
127
127
|
const full = path.join(projectRoot, relPath);
|
|
128
|
-
if (!fs.existsSync(full)
|
|
129
|
-
const
|
|
128
|
+
if (!fs.existsSync(full)) return '';
|
|
129
|
+
const stat = fs.lstatSync(full);
|
|
130
|
+
if (!stat.isFile()) return '';
|
|
131
|
+
const size = stat.size;
|
|
130
132
|
const buffer = fs.readFileSync(full);
|
|
131
133
|
const raw = buffer.slice(0, Math.min(size, MAX_FILE_BYTES)).toString('utf8');
|
|
132
134
|
return raw.replace(/\r\n/g, '\n');
|
|
@@ -134,7 +136,8 @@ function readText(projectRoot, relPath) {
|
|
|
134
136
|
|
|
135
137
|
function walkFiles(rootDir, projectRoot, out = []) {
|
|
136
138
|
if (!fs.existsSync(rootDir) || out.length >= MAX_SYSTEM_FILES) return out;
|
|
137
|
-
const stat = fs.
|
|
139
|
+
const stat = fs.lstatSync(rootDir);
|
|
140
|
+
if (stat.isSymbolicLink()) return out;
|
|
138
141
|
if (stat.isFile()) {
|
|
139
142
|
if (isTextFile(rootDir)) out.push(rel(projectRoot, rootDir));
|
|
140
143
|
return out;
|
|
@@ -246,7 +249,7 @@ function detectSystem(projectRoot, id, system) {
|
|
|
246
249
|
};
|
|
247
250
|
});
|
|
248
251
|
|
|
249
|
-
const hasPrimaryRoot = markerHits.some((marker) => ['.planning', '.
|
|
252
|
+
const hasPrimaryRoot = markerHits.some((marker) => ['.planning', '.legacy-planning', '_bmad-output', 'docs/superpowers'].includes(marker));
|
|
250
253
|
const score = markerHits.length * 3 + fileRecords.length;
|
|
251
254
|
const confidence = score >= 10 || (hasPrimaryRoot && fileRecords.length >= 3)
|
|
252
255
|
? 'high'
|
|
@@ -294,7 +297,7 @@ function buildImportedContext(detection) {
|
|
|
294
297
|
lines.push('');
|
|
295
298
|
|
|
296
299
|
if (detection.systems.length === 0) {
|
|
297
|
-
lines.push('- [DECISION] No
|
|
300
|
+
lines.push('- [DECISION] No legacy planning, BMAD, or Superpowers planning context was detected.');
|
|
298
301
|
}
|
|
299
302
|
|
|
300
303
|
for (const system of detection.systems) {
|
|
@@ -331,7 +334,7 @@ function buildImportedContext(detection) {
|
|
|
331
334
|
lines.push('## Sync-Back Policy');
|
|
332
335
|
lines.push('');
|
|
333
336
|
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
|
|
337
|
+
lines.push('- [DECISION] Godpowers must not rewrite legacy planning, BMAD, or Superpowers source documents outside managed sync-back sections.');
|
|
335
338
|
lines.push('- [DECISION] Sync-back exists so a project can return to the prior planning system with current Godpowers progress visible.');
|
|
336
339
|
|
|
337
340
|
lines.push('');
|
package/lib/quick-proof.js
CHANGED
|
@@ -93,6 +93,9 @@ function render(proof, opts = {}) {
|
|
|
93
93
|
const next = proof.dashboard.next || {};
|
|
94
94
|
const progress = proof.dashboard.progress || {};
|
|
95
95
|
const planning = proof.dashboard.planning || {};
|
|
96
|
+
const projectCommandRoot = path.resolve(proof.projectRoot) === path.resolve(proof.fixtureRoot)
|
|
97
|
+
? '.'
|
|
98
|
+
: proof.projectRoot;
|
|
96
99
|
|
|
97
100
|
if (opts.brief) {
|
|
98
101
|
return [
|
|
@@ -140,8 +143,8 @@ function render(proof, opts = {}) {
|
|
|
140
143
|
` npx godpowers next --project=${proof.fixtureRoot} --brief`,
|
|
141
144
|
'',
|
|
142
145
|
'Try it on your project:',
|
|
143
|
-
` npx godpowers status --project=${
|
|
144
|
-
` npx godpowers next --project=${
|
|
146
|
+
` npx godpowers status --project=${projectCommandRoot} --brief`,
|
|
147
|
+
` npx godpowers next --project=${projectCommandRoot} --brief`
|
|
145
148
|
].join('\n');
|
|
146
149
|
}
|
|
147
150
|
|
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';
|
|
@@ -504,7 +505,7 @@ function writeLedger(projectRoot, derived) {
|
|
|
504
505
|
return LEDGER_PATH;
|
|
505
506
|
}
|
|
506
507
|
}
|
|
507
|
-
|
|
508
|
+
atomic.writeFileAtomic(file, rendered);
|
|
508
509
|
return LEDGER_PATH;
|
|
509
510
|
}
|
|
510
511
|
|
package/lib/reverse-sync.js
CHANGED
|
@@ -28,6 +28,7 @@ const impeccable = require('./impeccable-bridge');
|
|
|
28
28
|
const sourceSync = require('./source-sync');
|
|
29
29
|
const requirements = require('./requirements');
|
|
30
30
|
const state = require('./state');
|
|
31
|
+
const atomic = require('./atomic-write');
|
|
31
32
|
|
|
32
33
|
const FENCE_BEGIN = '<!-- godpowers:linkage:begin -->';
|
|
33
34
|
const FENCE_END = '<!-- godpowers:linkage:end -->';
|
|
@@ -68,7 +69,7 @@ function writeFenced(filePath, sectionContent) {
|
|
|
68
69
|
} else {
|
|
69
70
|
next = `${parsed.before}${fencedBlock}${parsed.after}`;
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
atomic.writeFileAtomic(filePath, next);
|
|
72
73
|
return { written: true, hadFenceBefore: parsed.fenced !== '' };
|
|
73
74
|
}
|
|
74
75
|
|
|
@@ -27,6 +27,7 @@ const CONTEXTUAL_NEXT_ALLOWED = new Set([
|
|
|
27
27
|
'/god-extension-info',
|
|
28
28
|
'/god-extension-list',
|
|
29
29
|
'/god-extension-remove',
|
|
30
|
+
'/god-extension-scaffold',
|
|
30
31
|
'/god-graph',
|
|
31
32
|
'/god-help',
|
|
32
33
|
'/god-lifecycle',
|
|
@@ -61,6 +62,7 @@ const STANDARDS_EXEMPT_COMMANDS = new Set([
|
|
|
61
62
|
'/god-progress',
|
|
62
63
|
'/god-reconstruct',
|
|
63
64
|
'/god-roadmap-check',
|
|
65
|
+
'/god-extension-scaffold',
|
|
64
66
|
'/god-smite',
|
|
65
67
|
'/god-tech-debt'
|
|
66
68
|
]);
|
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "godpowers",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "AI-powered development system:
|
|
3
|
+
"version": "2.3.1",
|
|
4
|
+
"description": "AI-powered development system: 112 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"
|
|
7
7
|
},
|
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
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
apiVersion: godpowers/v1
|
|
2
|
+
kind: CommandRouting
|
|
3
|
+
metadata:
|
|
4
|
+
command: /god-extension-scaffold
|
|
5
|
+
description: Create a publishable extension pack skeleton
|
|
6
|
+
tier: 0
|
|
7
|
+
|
|
8
|
+
prerequisites:
|
|
9
|
+
required: []
|
|
10
|
+
|
|
11
|
+
execution:
|
|
12
|
+
spawns: [built-in]
|
|
13
|
+
context: fresh
|
|
14
|
+
writes:
|
|
15
|
+
- extension pack skeleton
|
|
16
|
+
|
|
17
|
+
success-path:
|
|
18
|
+
next-recommended: /god-test-extension
|
|
19
|
+
|
|
20
|
+
failure-path:
|
|
21
|
+
on-error: /god-doctor
|
|
22
|
+
|
|
23
|
+
endoff:
|
|
24
|
+
state-update: tier-0 updated for /god-extension-scaffold
|
|
25
|
+
events: [agent.start, artifact.created, agent.end]
|