godpowers 1.6.17 → 1.6.20
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/AGENTS.md +29 -6
- package/CHANGELOG.md +66 -0
- package/README.md +11 -9
- package/RELEASE.md +51 -37
- package/SKILL.md +17 -1
- package/agents/god-orchestrator.md +6 -1
- package/lib/README.md +4 -0
- package/lib/dashboard.js +17 -4
- package/lib/feature-awareness.js +24 -0
- package/lib/recipe-coverage-sync.js +149 -0
- package/lib/release-surface-sync.js +153 -0
- package/lib/repo-surface-sync.js +524 -0
- package/lib/route-quality-sync.js +286 -0
- package/lib/router.js +4 -1
- package/package.json +2 -2
- package/routing/god-party.yaml +4 -2
- package/routing/god-story-build.yaml +11 -2
- package/routing/recipes/automation-setup.yaml +25 -0
- package/routing/recipes/context-refresh.yaml +26 -0
- package/routing/recipes/release-maintenance.yaml +27 -0
- package/routing/recipes/story-work.yaml +29 -0
- package/skills/god-docs.md +6 -0
- package/skills/god-doctor.md +21 -0
- package/skills/god-mode.md +7 -0
- package/skills/god-next.md +7 -2
- package/skills/god-status.md +11 -3
- package/skills/god-sync.md +15 -4
- package/skills/god-version.md +3 -3
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release surface sync.
|
|
3
|
+
*
|
|
4
|
+
* Detects whether release-facing repo surfaces agree before a package or tag
|
|
5
|
+
* is treated as ready: package metadata, lockfile, release notes, changelog,
|
|
6
|
+
* README badge, release checklist, and package payload guardrails.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const LOG_PATH = '.godpowers/surface/RELEASE-SURFACE-SYNC.md';
|
|
13
|
+
|
|
14
|
+
const REQUIRED_PACKAGE_GUARDS = [
|
|
15
|
+
'lib/route-quality-sync.js',
|
|
16
|
+
'lib/recipe-coverage-sync.js',
|
|
17
|
+
'lib/release-surface-sync.js'
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function read(projectRoot, relPath) {
|
|
21
|
+
const file = path.join(projectRoot, relPath);
|
|
22
|
+
if (!fs.existsSync(file)) return '';
|
|
23
|
+
return fs.readFileSync(file, 'utf8');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function write(projectRoot, relPath, content) {
|
|
27
|
+
const file = path.join(projectRoot, relPath);
|
|
28
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
29
|
+
fs.writeFileSync(file, content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readJson(projectRoot, relPath) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(read(projectRoot, relPath));
|
|
35
|
+
} catch (err) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function addCheck(checks, id, status, relPath, message, opts = {}) {
|
|
41
|
+
checks.push({
|
|
42
|
+
area: 'release-surface',
|
|
43
|
+
id,
|
|
44
|
+
status,
|
|
45
|
+
path: relPath,
|
|
46
|
+
message,
|
|
47
|
+
severity: opts.severity || (status === 'fresh' ? 'info' : 'warning'),
|
|
48
|
+
spawn: opts.spawn || null
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detect(projectRoot) {
|
|
53
|
+
const checks = [];
|
|
54
|
+
const pkg = readJson(projectRoot, 'package.json') || {};
|
|
55
|
+
const lock = readJson(projectRoot, 'package-lock.json') || {};
|
|
56
|
+
const version = pkg.version || '0.0.0';
|
|
57
|
+
|
|
58
|
+
addCheck(
|
|
59
|
+
checks,
|
|
60
|
+
'package-lock-version',
|
|
61
|
+
lock.version === version ? 'fresh' : 'stale',
|
|
62
|
+
'package-lock.json',
|
|
63
|
+
lock.version === version
|
|
64
|
+
? 'package-lock.json version matches package.json.'
|
|
65
|
+
: `package-lock.json version ${lock.version || 'missing'} does not match package.json ${version}.`
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const surfaces = [
|
|
69
|
+
['readme-version-badge', 'README.md', `version-${version}-blue`],
|
|
70
|
+
['changelog-version', 'CHANGELOG.md', `## [${version}]`],
|
|
71
|
+
['release-version', 'RELEASE.md', `Godpowers ${version}`],
|
|
72
|
+
['release-checklist-route-quality', 'docs/RELEASE-CHECKLIST.md', 'route-quality-sync'],
|
|
73
|
+
['release-checklist-recipe-coverage', 'docs/RELEASE-CHECKLIST.md', 'recipe-coverage-sync'],
|
|
74
|
+
['release-checklist-release-surface', 'docs/RELEASE-CHECKLIST.md', 'release-surface-sync']
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
for (const [id, relPath, expected] of surfaces) {
|
|
78
|
+
const ok = read(projectRoot, relPath).includes(expected);
|
|
79
|
+
addCheck(
|
|
80
|
+
checks,
|
|
81
|
+
id,
|
|
82
|
+
ok ? 'fresh' : 'stale',
|
|
83
|
+
relPath,
|
|
84
|
+
ok ? `${relPath} includes ${expected}.` : `${relPath} is missing ${expected}.`,
|
|
85
|
+
{ spawn: ok ? null : 'god-docs-writer' }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const packageCheckText = read(projectRoot, 'scripts/check-package-contents.js');
|
|
90
|
+
for (const required of REQUIRED_PACKAGE_GUARDS) {
|
|
91
|
+
const ok = packageCheckText.includes(required);
|
|
92
|
+
addCheck(
|
|
93
|
+
checks,
|
|
94
|
+
`package-guard-${required.replace(/[^a-z0-9]+/gi, '-')}`,
|
|
95
|
+
ok ? 'fresh' : 'stale',
|
|
96
|
+
'scripts/check-package-contents.js',
|
|
97
|
+
ok
|
|
98
|
+
? `Package contents check requires ${required}.`
|
|
99
|
+
: `Package contents check does not require ${required}.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stale = checks.filter((check) => check.status !== 'fresh');
|
|
104
|
+
return {
|
|
105
|
+
status: stale.length === 0 ? 'fresh' : 'stale',
|
|
106
|
+
checks,
|
|
107
|
+
stale
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function appendLog(projectRoot, before, after) {
|
|
112
|
+
const now = new Date().toISOString();
|
|
113
|
+
const lines = [];
|
|
114
|
+
if (fs.existsSync(path.join(projectRoot, LOG_PATH))) {
|
|
115
|
+
lines.push(read(projectRoot, LOG_PATH).replace(/\s*$/, ''));
|
|
116
|
+
lines.push('');
|
|
117
|
+
} else {
|
|
118
|
+
lines.push('# Release Surface Sync Log');
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('- [DECISION] This file records release-surface sync checks run by Godpowers.');
|
|
121
|
+
lines.push('');
|
|
122
|
+
}
|
|
123
|
+
lines.push(`## ${now}`);
|
|
124
|
+
lines.push('');
|
|
125
|
+
lines.push(`- [DECISION] Release surface status before apply was ${before.status}.`);
|
|
126
|
+
lines.push(`- [DECISION] Release surface status after apply is ${after.status}.`);
|
|
127
|
+
lines.push('');
|
|
128
|
+
write(projectRoot, LOG_PATH, lines.join('\n'));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function run(projectRoot, opts = {}) {
|
|
132
|
+
const before = detect(projectRoot);
|
|
133
|
+
const after = detect(projectRoot);
|
|
134
|
+
if (opts.log !== false) appendLog(projectRoot, before, after);
|
|
135
|
+
return {
|
|
136
|
+
before,
|
|
137
|
+
after,
|
|
138
|
+
applied: [],
|
|
139
|
+
logPath: opts.log === false ? null : LOG_PATH
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function summary(report) {
|
|
144
|
+
return report.status === 'fresh' ? 'fresh' : `${report.stale.length} stale`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
LOG_PATH,
|
|
149
|
+
REQUIRED_PACKAGE_GUARDS,
|
|
150
|
+
detect,
|
|
151
|
+
run,
|
|
152
|
+
summary
|
|
153
|
+
};
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repository surface sync.
|
|
3
|
+
*
|
|
4
|
+
* Detects structural drift between commands, routing, package metadata,
|
|
5
|
+
* agent contracts, workflows, recipes, extensions, and release policy docs.
|
|
6
|
+
* The helper is read-only by default. The apply path writes only safe local
|
|
7
|
+
* logs and optional missing routing stubs when explicitly requested.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const { parseSimpleYaml } = require('./intent');
|
|
14
|
+
const extensions = require('./extensions');
|
|
15
|
+
const repoDocSync = require('./repo-doc-sync');
|
|
16
|
+
const routeQualitySync = require('./route-quality-sync');
|
|
17
|
+
const recipeCoverageSync = require('./recipe-coverage-sync');
|
|
18
|
+
const releaseSurfaceSync = require('./release-surface-sync');
|
|
19
|
+
|
|
20
|
+
const LOG_PATH = '.godpowers/surface/REPO-SURFACE-SYNC.md';
|
|
21
|
+
|
|
22
|
+
const REQUIRED_PACKAGE_FILE_ENTRIES = [
|
|
23
|
+
'bin/',
|
|
24
|
+
'skills/',
|
|
25
|
+
'agents/god-*.md',
|
|
26
|
+
'templates/',
|
|
27
|
+
'references/',
|
|
28
|
+
'routing/',
|
|
29
|
+
'workflows/',
|
|
30
|
+
'schema/',
|
|
31
|
+
'lib/',
|
|
32
|
+
'extensions/',
|
|
33
|
+
'RELEASE.md',
|
|
34
|
+
'SKILL.md',
|
|
35
|
+
'AGENTS.md',
|
|
36
|
+
'CHANGELOG.md',
|
|
37
|
+
'LICENSE'
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const REQUIRED_PACKAGE_CHECKS = [
|
|
41
|
+
'lib/feature-awareness.js',
|
|
42
|
+
'lib/repo-doc-sync.js',
|
|
43
|
+
'lib/repo-surface-sync.js',
|
|
44
|
+
'lib/route-quality-sync.js',
|
|
45
|
+
'lib/recipe-coverage-sync.js',
|
|
46
|
+
'lib/release-surface-sync.js',
|
|
47
|
+
'routing/god-export-otel.yaml'
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
function rel(projectRoot, absPath) {
|
|
51
|
+
return path.relative(projectRoot, absPath).split(path.sep).join('/');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function exists(projectRoot, relPath) {
|
|
55
|
+
return fs.existsSync(path.join(projectRoot, relPath));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function read(projectRoot, relPath) {
|
|
59
|
+
const file = path.join(projectRoot, relPath);
|
|
60
|
+
if (!fs.existsSync(file)) return '';
|
|
61
|
+
return fs.readFileSync(file, 'utf8');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function write(projectRoot, relPath, content) {
|
|
65
|
+
const file = path.join(projectRoot, relPath);
|
|
66
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
67
|
+
fs.writeFileSync(file, content);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function listFiles(projectRoot, relDir, pattern) {
|
|
71
|
+
const dir = path.join(projectRoot, relDir);
|
|
72
|
+
if (!fs.existsSync(dir)) return [];
|
|
73
|
+
return fs.readdirSync(dir)
|
|
74
|
+
.filter((name) => pattern.test(name))
|
|
75
|
+
.sort()
|
|
76
|
+
.map((name) => `${relDir}/${name}`.replace(/\\/g, '/'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readJson(projectRoot, relPath) {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(read(projectRoot, relPath));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function routeForSkill(skillPath) {
|
|
88
|
+
const base = path.basename(skillPath, '.md');
|
|
89
|
+
return `routing/${base}.yaml`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function commandForSkill(skillPath) {
|
|
93
|
+
return `/${path.basename(skillPath, '.md')}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function addCheck(checks, area, id, status, relPath, message, opts = {}) {
|
|
97
|
+
checks.push({
|
|
98
|
+
area,
|
|
99
|
+
id,
|
|
100
|
+
status,
|
|
101
|
+
path: relPath,
|
|
102
|
+
message,
|
|
103
|
+
severity: opts.severity || (status === 'fresh' ? 'info' : 'warning'),
|
|
104
|
+
safeFix: opts.safeFix === true,
|
|
105
|
+
spawn: opts.spawn || null
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function routingChecks(projectRoot) {
|
|
110
|
+
const checks = [];
|
|
111
|
+
const skills = listFiles(projectRoot, 'skills', /^god.*\.md$/);
|
|
112
|
+
const routes = listFiles(projectRoot, 'routing', /^god.*\.yaml$/);
|
|
113
|
+
const skillRoutes = new Set(skills.map(routeForSkill));
|
|
114
|
+
const routeSet = new Set(routes);
|
|
115
|
+
|
|
116
|
+
for (const skill of skills) {
|
|
117
|
+
const route = routeForSkill(skill);
|
|
118
|
+
addCheck(
|
|
119
|
+
checks,
|
|
120
|
+
'routing',
|
|
121
|
+
`route-for-${path.basename(skill, '.md')}`,
|
|
122
|
+
routeSet.has(route) ? 'fresh' : 'stale',
|
|
123
|
+
route,
|
|
124
|
+
routeSet.has(route)
|
|
125
|
+
? `${commandForSkill(skill)} has routing metadata.`
|
|
126
|
+
: `${commandForSkill(skill)} is missing routing metadata.`,
|
|
127
|
+
{ safeFix: !routeSet.has(route) }
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const route of routes) {
|
|
132
|
+
if (!skillRoutes.has(route)) {
|
|
133
|
+
addCheck(
|
|
134
|
+
checks,
|
|
135
|
+
'routing',
|
|
136
|
+
`skill-for-${path.basename(route, '.yaml')}`,
|
|
137
|
+
'stale',
|
|
138
|
+
route,
|
|
139
|
+
`${route} has no matching skill file.`,
|
|
140
|
+
{ severity: 'warning' }
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return checks;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function packageChecks(projectRoot) {
|
|
148
|
+
const checks = [];
|
|
149
|
+
const pkg = readJson(projectRoot, 'package.json') || {};
|
|
150
|
+
const lock = readJson(projectRoot, 'package-lock.json');
|
|
151
|
+
const fileEntries = Array.isArray(pkg.files) ? pkg.files : [];
|
|
152
|
+
|
|
153
|
+
for (const entry of REQUIRED_PACKAGE_FILE_ENTRIES) {
|
|
154
|
+
addCheck(
|
|
155
|
+
checks,
|
|
156
|
+
'package',
|
|
157
|
+
`package-files-${entry.replace(/[^a-z0-9]+/gi, '-')}`,
|
|
158
|
+
fileEntries.includes(entry) ? 'fresh' : 'stale',
|
|
159
|
+
'package.json',
|
|
160
|
+
fileEntries.includes(entry)
|
|
161
|
+
? `package.json includes ${entry}.`
|
|
162
|
+
: `package.json files is missing ${entry}.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const packageCheckText = read(projectRoot, 'scripts/check-package-contents.js');
|
|
167
|
+
for (const required of REQUIRED_PACKAGE_CHECKS) {
|
|
168
|
+
addCheck(
|
|
169
|
+
checks,
|
|
170
|
+
'package',
|
|
171
|
+
`package-check-${required.replace(/[^a-z0-9]+/gi, '-')}`,
|
|
172
|
+
packageCheckText.includes(required) ? 'fresh' : 'stale',
|
|
173
|
+
'scripts/check-package-contents.js',
|
|
174
|
+
packageCheckText.includes(required)
|
|
175
|
+
? `package contents check requires ${required}.`
|
|
176
|
+
: `package contents check does not require ${required}.`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (lock && lock.version) {
|
|
181
|
+
addCheck(
|
|
182
|
+
checks,
|
|
183
|
+
'package',
|
|
184
|
+
'package-lock-version',
|
|
185
|
+
lock.version === pkg.version ? 'fresh' : 'stale',
|
|
186
|
+
'package-lock.json',
|
|
187
|
+
lock.version === pkg.version
|
|
188
|
+
? 'package-lock.json version matches package.json.'
|
|
189
|
+
: `package-lock.json version ${lock.version} does not match package.json ${pkg.version}.`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return checks;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function parseRoute(projectRoot, routePath) {
|
|
197
|
+
try {
|
|
198
|
+
return parseSimpleYaml(read(projectRoot, routePath));
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function agentChecks(projectRoot) {
|
|
205
|
+
const checks = [];
|
|
206
|
+
const agents = new Set(listFiles(projectRoot, 'agents', /^god.*\.md$/)
|
|
207
|
+
.map((file) => path.basename(file, '.md')));
|
|
208
|
+
const routes = listFiles(projectRoot, 'routing', /^god.*\.yaml$/);
|
|
209
|
+
const missing = new Set();
|
|
210
|
+
|
|
211
|
+
for (const route of routes) {
|
|
212
|
+
const parsed = parseRoute(projectRoot, route);
|
|
213
|
+
const execution = parsed && parsed.execution ? parsed.execution : {};
|
|
214
|
+
const spawns = [
|
|
215
|
+
...(Array.isArray(execution.spawns) ? execution.spawns : []),
|
|
216
|
+
...(Array.isArray(execution['secondary-spawns']) ? execution['secondary-spawns'] : []),
|
|
217
|
+
...(Array.isArray(execution['parallel-spawns']) ? execution['parallel-spawns'] : [])
|
|
218
|
+
].map((spawn) => (spawn && typeof spawn === 'object' && spawn.agent) ? spawn.agent : spawn);
|
|
219
|
+
for (const spawn of spawns) {
|
|
220
|
+
if (!String(spawn).startsWith('god-')) continue;
|
|
221
|
+
if (!/^god-[a-z0-9-]+$/.test(String(spawn))) continue;
|
|
222
|
+
if (!agents.has(spawn)) {
|
|
223
|
+
missing.add(`${route}:${spawn}`);
|
|
224
|
+
addCheck(
|
|
225
|
+
checks,
|
|
226
|
+
'agents',
|
|
227
|
+
`missing-agent-${spawn}`,
|
|
228
|
+
'stale',
|
|
229
|
+
route,
|
|
230
|
+
`${route} references missing agent ${spawn}.`,
|
|
231
|
+
{ spawn: 'god-auditor' }
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (missing.size === 0) {
|
|
238
|
+
addCheck(checks, 'agents', 'route-spawn-targets', 'fresh',
|
|
239
|
+
'routing/', 'All routed specialist spawns resolve to agent files.');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return checks;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function workflowRecipeChecks(projectRoot) {
|
|
246
|
+
const checks = [];
|
|
247
|
+
const workflows = listFiles(projectRoot, 'workflows', /\.yaml$/);
|
|
248
|
+
const recipes = listFiles(projectRoot, path.join('routing', 'recipes'), /\.yaml$/);
|
|
249
|
+
const commandFlows = read(projectRoot, 'docs/command-flows.md');
|
|
250
|
+
|
|
251
|
+
for (const workflow of workflows) {
|
|
252
|
+
const text = read(projectRoot, workflow);
|
|
253
|
+
const parsed = parseSimpleYaml(text);
|
|
254
|
+
const hasMetadata = Boolean(parsed.apiVersion || parsed.name || parsed.metadata);
|
|
255
|
+
addCheck(
|
|
256
|
+
checks,
|
|
257
|
+
'workflow',
|
|
258
|
+
`workflow-metadata-${path.basename(workflow, '.yaml')}`,
|
|
259
|
+
hasMetadata ? 'fresh' : 'stale',
|
|
260
|
+
workflow,
|
|
261
|
+
hasMetadata ? `${workflow} has metadata.` : `${workflow} is missing parseable metadata.`,
|
|
262
|
+
{ spawn: hasMetadata ? null : 'god-roadmap-reconciler' }
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
for (const recipe of recipes) {
|
|
267
|
+
const text = read(projectRoot, recipe);
|
|
268
|
+
const hasCommand = /\/god-[a-z0-9-]+/.test(text);
|
|
269
|
+
const recipeName = path.basename(recipe, '.yaml');
|
|
270
|
+
addCheck(
|
|
271
|
+
checks,
|
|
272
|
+
'workflow',
|
|
273
|
+
`recipe-command-${recipeName}`,
|
|
274
|
+
hasCommand ? 'fresh' : 'stale',
|
|
275
|
+
recipe,
|
|
276
|
+
hasCommand ? `${recipe} includes a slash-command route.` : `${recipe} has no slash-command route.`,
|
|
277
|
+
{ spawn: hasCommand ? null : 'god-roadmap-reconciler' }
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (workflows.length > 0) {
|
|
282
|
+
addCheck(
|
|
283
|
+
checks,
|
|
284
|
+
'workflow',
|
|
285
|
+
'command-flows-workflows',
|
|
286
|
+
commandFlows.includes('/god-docs') && commandFlows.includes('/god-sync') ? 'fresh' : 'stale',
|
|
287
|
+
'docs/command-flows.md',
|
|
288
|
+
'docs/command-flows.md includes core docs and sync flows.',
|
|
289
|
+
{ spawn: 'god-roadmap-reconciler' }
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
return checks;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function extensionChecks(projectRoot) {
|
|
296
|
+
const checks = [];
|
|
297
|
+
const extRoot = path.join(projectRoot, 'extensions');
|
|
298
|
+
const pkg = readJson(projectRoot, 'package.json') || {};
|
|
299
|
+
if (!fs.existsSync(extRoot)) return checks;
|
|
300
|
+
for (const entry of fs.readdirSync(extRoot, { withFileTypes: true })) {
|
|
301
|
+
if (!entry.isDirectory()) continue;
|
|
302
|
+
const packRel = `extensions/${entry.name}`;
|
|
303
|
+
const manifestRel = `${packRel}/manifest.yaml`;
|
|
304
|
+
const packageRel = `${packRel}/package.json`;
|
|
305
|
+
const manifestText = read(projectRoot, manifestRel);
|
|
306
|
+
const packPkg = readJson(projectRoot, packageRel);
|
|
307
|
+
const parsed = manifestText ? extensions.parseManifest(manifestText).manifest : null;
|
|
308
|
+
const validation = parsed ? extensions.validateManifest(parsed, pkg.version || '0.0.0') : ['missing manifest'];
|
|
309
|
+
|
|
310
|
+
addCheck(
|
|
311
|
+
checks,
|
|
312
|
+
'extensions',
|
|
313
|
+
`extension-manifest-${entry.name}`,
|
|
314
|
+
validation.length === 0 ? 'fresh' : 'stale',
|
|
315
|
+
manifestRel,
|
|
316
|
+
validation.length === 0
|
|
317
|
+
? `${entry.name} manifest validates against current Godpowers.`
|
|
318
|
+
: `${entry.name} manifest validation failed: ${validation.join('; ')}.`,
|
|
319
|
+
{ spawn: validation.length === 0 ? null : 'god-coordinator' }
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
if (parsed && packPkg) {
|
|
323
|
+
const sameName = parsed.metadata && parsed.metadata.name === packPkg.name;
|
|
324
|
+
const sameVersion = parsed.metadata && parsed.metadata.version === packPkg.version;
|
|
325
|
+
const sameEngine = packPkg.peerDependencies
|
|
326
|
+
&& packPkg.peerDependencies.godpowers === parsed.engines.godpowers;
|
|
327
|
+
addCheck(checks, 'extensions', `extension-name-${entry.name}`,
|
|
328
|
+
sameName ? 'fresh' : 'stale', packageRel,
|
|
329
|
+
sameName ? `${entry.name} package name matches manifest.` : `${entry.name} package name does not match manifest.`,
|
|
330
|
+
{ spawn: sameName ? null : 'god-coordinator' });
|
|
331
|
+
addCheck(checks, 'extensions', `extension-version-${entry.name}`,
|
|
332
|
+
sameVersion ? 'fresh' : 'stale', packageRel,
|
|
333
|
+
sameVersion ? `${entry.name} package version matches manifest.` : `${entry.name} package version does not match manifest.`,
|
|
334
|
+
{ spawn: sameVersion ? null : 'god-coordinator' });
|
|
335
|
+
addCheck(checks, 'extensions', `extension-engine-${entry.name}`,
|
|
336
|
+
sameEngine ? 'fresh' : 'stale', packageRel,
|
|
337
|
+
sameEngine ? `${entry.name} peer dependency matches manifest engine.` : `${entry.name} peer dependency does not match manifest engine.`,
|
|
338
|
+
{ spawn: sameEngine ? null : 'god-coordinator' });
|
|
339
|
+
for (const kind of ['skills', 'agents', 'workflows']) {
|
|
340
|
+
const provided = parsed.provides && Array.isArray(parsed.provides[kind])
|
|
341
|
+
? parsed.provides[kind]
|
|
342
|
+
: [];
|
|
343
|
+
for (const item of provided) {
|
|
344
|
+
const ext = kind === 'workflows' ? 'yaml' : 'md';
|
|
345
|
+
const providedRel = `${packRel}/${kind}/${item}.${ext}`;
|
|
346
|
+
addCheck(checks, 'extensions', `extension-${kind}-${entry.name}-${item}`,
|
|
347
|
+
exists(projectRoot, providedRel) ? 'fresh' : 'stale',
|
|
348
|
+
providedRel,
|
|
349
|
+
exists(projectRoot, providedRel)
|
|
350
|
+
? `${providedRel} exists.`
|
|
351
|
+
: `${providedRel} is missing.`,
|
|
352
|
+
{ spawn: exists(projectRoot, providedRel) ? null : 'god-coordinator' });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return checks;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function releasePolicyChecks(projectRoot) {
|
|
361
|
+
const checks = [];
|
|
362
|
+
const docs = repoDocSync.detect(projectRoot);
|
|
363
|
+
addCheck(
|
|
364
|
+
checks,
|
|
365
|
+
'release',
|
|
366
|
+
'repo-doc-sync-fresh',
|
|
367
|
+
docs.status === 'fresh' ? 'fresh' : 'stale',
|
|
368
|
+
'docs/repo-doc-sync.md',
|
|
369
|
+
docs.status === 'fresh'
|
|
370
|
+
? 'Repository documentation sync reports fresh.'
|
|
371
|
+
: `Repository documentation sync reports ${docs.stale.length} stale checks.`,
|
|
372
|
+
{ spawn: docs.status === 'fresh' ? null : 'god-docs-writer' }
|
|
373
|
+
);
|
|
374
|
+
addCheck(
|
|
375
|
+
checks,
|
|
376
|
+
'release',
|
|
377
|
+
'release-checklist-surface-sync',
|
|
378
|
+
read(projectRoot, 'docs/RELEASE-CHECKLIST.md').includes('repo-surface-sync'),
|
|
379
|
+
'docs/RELEASE-CHECKLIST.md',
|
|
380
|
+
'Release checklist references repo-surface-sync readiness.',
|
|
381
|
+
{ spawn: 'god-docs-writer' }
|
|
382
|
+
);
|
|
383
|
+
return checks.map((check) => ({
|
|
384
|
+
...check,
|
|
385
|
+
status: check.status === true ? 'fresh' : (check.status === false ? 'stale' : check.status)
|
|
386
|
+
}));
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function detect(projectRoot) {
|
|
390
|
+
const checks = [
|
|
391
|
+
...routingChecks(projectRoot),
|
|
392
|
+
...packageChecks(projectRoot),
|
|
393
|
+
...agentChecks(projectRoot),
|
|
394
|
+
...workflowRecipeChecks(projectRoot),
|
|
395
|
+
...extensionChecks(projectRoot),
|
|
396
|
+
...releasePolicyChecks(projectRoot),
|
|
397
|
+
...routeQualitySync.detect(projectRoot).checks,
|
|
398
|
+
...recipeCoverageSync.detect(projectRoot).checks,
|
|
399
|
+
...releaseSurfaceSync.detect(projectRoot).checks
|
|
400
|
+
];
|
|
401
|
+
const stale = checks.filter((check) => check.status !== 'fresh');
|
|
402
|
+
const byArea = {};
|
|
403
|
+
for (const check of checks) {
|
|
404
|
+
if (!byArea[check.area]) byArea[check.area] = { total: 0, stale: 0 };
|
|
405
|
+
byArea[check.area].total++;
|
|
406
|
+
if (check.status !== 'fresh') byArea[check.area].stale++;
|
|
407
|
+
}
|
|
408
|
+
const spawnRecommendations = [...new Set(stale.map((check) => check.spawn).filter(Boolean))]
|
|
409
|
+
.map((agent) => ({
|
|
410
|
+
agent,
|
|
411
|
+
reason: `Repo surface drift requires ${agent} judgment.`,
|
|
412
|
+
paths: [...new Set(stale.filter((check) => check.spawn === agent).map((check) => check.path))].sort()
|
|
413
|
+
}));
|
|
414
|
+
return {
|
|
415
|
+
status: stale.length === 0 ? 'fresh' : 'stale',
|
|
416
|
+
checks,
|
|
417
|
+
stale,
|
|
418
|
+
byArea,
|
|
419
|
+
spawnRecommendations
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function routeTemplate(command) {
|
|
424
|
+
const name = command.replace(/^\//, '');
|
|
425
|
+
return [
|
|
426
|
+
'apiVersion: godpowers/v1',
|
|
427
|
+
'kind: CommandRouting',
|
|
428
|
+
'metadata:',
|
|
429
|
+
` command: ${command}`,
|
|
430
|
+
' description: ',
|
|
431
|
+
' tier: 0',
|
|
432
|
+
'',
|
|
433
|
+
'prerequisites:',
|
|
434
|
+
' required: []',
|
|
435
|
+
'',
|
|
436
|
+
'execution:',
|
|
437
|
+
' spawns: [built-in]',
|
|
438
|
+
' context: fresh',
|
|
439
|
+
' writes: []',
|
|
440
|
+
'',
|
|
441
|
+
'success-path:',
|
|
442
|
+
' next-recommended: /god-status',
|
|
443
|
+
'',
|
|
444
|
+
'failure-path:',
|
|
445
|
+
' on-error: /god-doctor',
|
|
446
|
+
'',
|
|
447
|
+
'endoff:',
|
|
448
|
+
' state-update: none',
|
|
449
|
+
' events: [agent.start, agent.end]',
|
|
450
|
+
''
|
|
451
|
+
].join('\n').replace('description: ', `description: Route metadata for /${name}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function applySafeFixes(projectRoot, report, opts = {}) {
|
|
455
|
+
const applied = [];
|
|
456
|
+
if (!opts.fixRouting) return applied;
|
|
457
|
+
for (const check of report.stale) {
|
|
458
|
+
if (check.area !== 'routing' || !check.safeFix || !check.path.startsWith('routing/')) continue;
|
|
459
|
+
if (exists(projectRoot, check.path)) continue;
|
|
460
|
+
const command = `/${path.basename(check.path, '.yaml')}`;
|
|
461
|
+
write(projectRoot, check.path, routeTemplate(command));
|
|
462
|
+
applied.push({ path: check.path, check: check.id });
|
|
463
|
+
}
|
|
464
|
+
return applied;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function appendLog(projectRoot, before, after, applied) {
|
|
468
|
+
const now = new Date().toISOString();
|
|
469
|
+
const lines = [];
|
|
470
|
+
if (exists(projectRoot, LOG_PATH)) {
|
|
471
|
+
lines.push(read(projectRoot, LOG_PATH).replace(/\s*$/, ''));
|
|
472
|
+
lines.push('');
|
|
473
|
+
} else {
|
|
474
|
+
lines.push('# Repo Surface Sync Log');
|
|
475
|
+
lines.push('');
|
|
476
|
+
lines.push('- [DECISION] This file records structural repository surface sync checks run by Godpowers.');
|
|
477
|
+
lines.push('- [DECISION] Detection is read-only by default and safe fixes require explicit fix options.');
|
|
478
|
+
lines.push('');
|
|
479
|
+
}
|
|
480
|
+
lines.push(`## ${now}`);
|
|
481
|
+
lines.push('');
|
|
482
|
+
lines.push(`- [DECISION] Repo surface sync status before apply was ${before.status}.`);
|
|
483
|
+
lines.push(`- [DECISION] Repo surface sync status after apply is ${after.status}.`);
|
|
484
|
+
if (applied.length === 0) {
|
|
485
|
+
lines.push('- [DECISION] No structural repository surface files were changed.');
|
|
486
|
+
} else {
|
|
487
|
+
for (const item of applied) {
|
|
488
|
+
lines.push(`- [DECISION] Created or refreshed ${item.path} for ${item.check}.`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
for (const rec of after.spawnRecommendations) {
|
|
492
|
+
lines.push(`- [HYPOTHESIS] ${rec.agent} should review ${rec.paths.join(', ')}.`);
|
|
493
|
+
}
|
|
494
|
+
lines.push('');
|
|
495
|
+
write(projectRoot, LOG_PATH, lines.join('\n'));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function run(projectRoot, opts = {}) {
|
|
499
|
+
const before = detect(projectRoot);
|
|
500
|
+
const applied = applySafeFixes(projectRoot, before, opts);
|
|
501
|
+
const after = detect(projectRoot);
|
|
502
|
+
if (opts.log !== false) appendLog(projectRoot, before, after, applied);
|
|
503
|
+
return {
|
|
504
|
+
before,
|
|
505
|
+
after,
|
|
506
|
+
applied,
|
|
507
|
+
logPath: opts.log === false ? null : LOG_PATH
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function summary(report) {
|
|
512
|
+
if (report.status === 'fresh') return 'fresh';
|
|
513
|
+
return `${report.stale.length} stale across ${Object.keys(report.byArea).length} areas`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
module.exports = {
|
|
517
|
+
LOG_PATH,
|
|
518
|
+
REQUIRED_PACKAGE_CHECKS,
|
|
519
|
+
REQUIRED_PACKAGE_FILE_ENTRIES,
|
|
520
|
+
detect,
|
|
521
|
+
run,
|
|
522
|
+
summary,
|
|
523
|
+
routeTemplate
|
|
524
|
+
};
|