godpowers 1.6.24 → 2.1.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/AGENTS.md +1 -1
- package/CHANGELOG.md +166 -0
- package/README.md +103 -8
- package/RELEASE.md +48 -50
- package/SKILL.md +9 -1
- package/agents/god-design-reviewer.md +6 -6
- package/agents/god-designer.md +1 -1
- package/agents/god-executor.md +23 -0
- package/agents/god-quality-reviewer.md +12 -1
- package/agents/god-spec-reviewer.md +10 -0
- package/bin/install.js +137 -655
- package/extensions/data-pack/manifest.yaml +1 -1
- package/extensions/data-pack/package.json +1 -1
- package/extensions/launch-pack/README.md +1 -1
- package/extensions/launch-pack/manifest.yaml +1 -1
- package/extensions/launch-pack/package.json +1 -1
- package/extensions/security-pack/manifest.yaml +1 -1
- package/extensions/security-pack/package.json +1 -1
- package/fixtures/quick-proof/manifest.json +19 -0
- package/fixtures/quick-proof/project/.godpowers/prep/INITIAL-FINDINGS.md +5 -0
- package/fixtures/quick-proof/project/.godpowers/state.json +69 -0
- package/fixtures/quick-proof/project/README.md +5 -0
- package/fixtures/quick-proof/project/package.json +6 -0
- package/lib/agent-browser-driver.js +13 -13
- package/lib/agent-cache.js +8 -1
- package/lib/agent-refs.js +161 -0
- package/lib/budget.js +25 -11
- package/lib/events.js +11 -4
- package/lib/extension-authoring.js +27 -0
- package/lib/feature-awareness.js +24 -0
- package/lib/fs-async.js +28 -0
- package/lib/installer-args.js +99 -0
- package/lib/installer-core.js +345 -0
- package/lib/installer-files.js +80 -0
- package/lib/installer-runtimes.js +112 -0
- package/lib/intent.js +111 -16
- package/lib/quick-proof.js +153 -0
- package/lib/release-surface-sync.js +8 -1
- package/lib/repo-surface-sync.js +9 -2
- package/lib/review-required.js +2 -1
- package/lib/router.js +23 -3
- package/lib/skill-surface.js +42 -0
- package/lib/state-lock.js +10 -0
- package/lib/state.js +101 -8
- package/lib/workflow-runner.js +42 -5
- package/package.json +7 -3
- package/references/HAVE-NOTS.md +4 -3
- package/references/orchestration/GOD-MODE-RUNBOOK.md +273 -0
- package/routing/god-arch.yaml +1 -1
- package/routing/god-build.yaml +1 -1
- package/skills/god-add-backlog.md +1 -1
- package/skills/god-agent-audit.md +2 -2
- package/skills/god-build.md +5 -3
- package/skills/god-context-scan.md +2 -3
- package/skills/god-design.md +2 -2
- package/skills/god-doctor.md +2 -2
- package/skills/god-extension-info.md +1 -1
- package/skills/god-help.md +4 -3
- package/skills/god-mode.md +10 -266
- package/skills/god-org-context.md +1 -1
- package/skills/god-repair.md +3 -3
- package/skills/god-review.md +9 -0
- package/skills/god-stories.md +1 -1
- package/skills/god-test-extension.md +1 -1
- package/skills/god-version.md +2 -2
package/lib/intent.js
CHANGED
|
@@ -10,6 +10,16 @@
|
|
|
10
10
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
|
+
const asyncFs = require('./fs-async');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} IntentDocument
|
|
17
|
+
* @property {string} apiVersion Expected to be `godpowers/v1`.
|
|
18
|
+
* @property {string} kind Expected to be `Project`.
|
|
19
|
+
* @property {{ name: string, description?: string }} metadata Project metadata.
|
|
20
|
+
* @property {string} mode Project mode.
|
|
21
|
+
* @property {string} scale Project scale.
|
|
22
|
+
*/
|
|
13
23
|
|
|
14
24
|
function intentPath(projectRoot) {
|
|
15
25
|
return path.join(projectRoot, '.godpowers', 'intent.yaml');
|
|
@@ -21,6 +31,9 @@ function intentPath(projectRoot) {
|
|
|
21
31
|
* Minimal YAML parser: handles the subset our schema uses
|
|
22
32
|
* (key: value, nested objects, arrays of strings, booleans).
|
|
23
33
|
* For full YAML, agents should use a real parser.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} projectRoot
|
|
36
|
+
* @returns {IntentDocument|null}
|
|
24
37
|
*/
|
|
25
38
|
function read(projectRoot) {
|
|
26
39
|
const file = intentPath(projectRoot);
|
|
@@ -29,6 +42,26 @@ function read(projectRoot) {
|
|
|
29
42
|
return parseSimpleYaml(content);
|
|
30
43
|
}
|
|
31
44
|
|
|
45
|
+
/**
|
|
46
|
+
* @param {string} projectRoot
|
|
47
|
+
* @returns {Promise<IntentDocument|null>}
|
|
48
|
+
*/
|
|
49
|
+
async function readAsync(projectRoot) {
|
|
50
|
+
const file = intentPath(projectRoot);
|
|
51
|
+
if (!(await asyncFs.exists(file))) return null;
|
|
52
|
+
const content = await asyncFs.fs.readFile(file, 'utf8');
|
|
53
|
+
return parseSimpleYaml(content);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reject keys that would mutate the Object prototype chain. intent.yaml and
|
|
58
|
+
* extension manifests are untrusted input, so a key like `__proto__` must
|
|
59
|
+
* never be assigned during parsing.
|
|
60
|
+
*/
|
|
61
|
+
function isUnsafeKey(key) {
|
|
62
|
+
return key === '__proto__' || key === 'constructor' || key === 'prototype';
|
|
63
|
+
}
|
|
64
|
+
|
|
32
65
|
/**
|
|
33
66
|
* Parse a simple YAML subset. Just enough for intent.yaml structure.
|
|
34
67
|
* Real-world: replace with `yaml` npm package when we add deps.
|
|
@@ -41,17 +74,7 @@ function parseSimpleYaml(content) {
|
|
|
41
74
|
for (let i = 0; i < lines.length; i++) {
|
|
42
75
|
let line = lines[i];
|
|
43
76
|
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
44
|
-
|
|
45
|
-
const hashIdx = line.indexOf(' #');
|
|
46
|
-
if (hashIdx !== -1) {
|
|
47
|
-
// Make sure the # isn't inside a quoted string
|
|
48
|
-
const before = line.slice(0, hashIdx);
|
|
49
|
-
const dquoteCount = (before.match(/"/g) || []).length;
|
|
50
|
-
const squoteCount = (before.match(/'/g) || []).length;
|
|
51
|
-
if (dquoteCount % 2 === 0 && squoteCount % 2 === 0) {
|
|
52
|
-
line = before;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
77
|
+
line = stripInlineComment(line);
|
|
55
78
|
|
|
56
79
|
const indent = line.length - line.trimStart().length;
|
|
57
80
|
const trimmed = line.trim();
|
|
@@ -66,7 +89,7 @@ function parseSimpleYaml(content) {
|
|
|
66
89
|
const rest = trimmed.slice(2);
|
|
67
90
|
// If the rest is a quoted string, treat as simple value (don't split on colon)
|
|
68
91
|
const isQuotedSimple = /^"[^"]*"$|^'[^']*'$/.test(rest.trim());
|
|
69
|
-
const restColonIdx = isQuotedSimple ? -1 : rest
|
|
92
|
+
const restColonIdx = isQuotedSimple ? -1 : findUnquotedColon(rest);
|
|
70
93
|
|
|
71
94
|
// Ensure parent is array
|
|
72
95
|
if (!Array.isArray(parent.__items__)) {
|
|
@@ -79,6 +102,7 @@ function parseSimpleYaml(content) {
|
|
|
79
102
|
} else {
|
|
80
103
|
// List of objects: "- key: value"
|
|
81
104
|
const itemKey = rest.slice(0, restColonIdx).trim();
|
|
105
|
+
if (isUnsafeKey(itemKey)) continue;
|
|
82
106
|
const itemVal = rest.slice(restColonIdx + 1).trim();
|
|
83
107
|
const newObj = {};
|
|
84
108
|
if (itemVal) {
|
|
@@ -97,9 +121,10 @@ function parseSimpleYaml(content) {
|
|
|
97
121
|
continue;
|
|
98
122
|
}
|
|
99
123
|
|
|
100
|
-
const colonIdx = trimmed
|
|
124
|
+
const colonIdx = findUnquotedColon(trimmed);
|
|
101
125
|
if (colonIdx === -1) continue;
|
|
102
126
|
const key = trimmed.slice(0, colonIdx).trim();
|
|
127
|
+
if (isUnsafeKey(key)) continue;
|
|
103
128
|
const valueStr = trimmed.slice(colonIdx + 1).trim();
|
|
104
129
|
|
|
105
130
|
if (!valueStr) {
|
|
@@ -150,21 +175,91 @@ function readBlockScalar(lines, startIndex, parentIndent, folded) {
|
|
|
150
175
|
}
|
|
151
176
|
|
|
152
177
|
function parseValue(str) {
|
|
178
|
+
str = str.trim();
|
|
153
179
|
if (str === 'true') return true;
|
|
154
180
|
if (str === 'false') return false;
|
|
155
181
|
if (str === 'null' || str === '~') return null;
|
|
156
182
|
if (/^-?\d+$/.test(str)) return parseInt(str, 10);
|
|
157
183
|
if (/^-?\d+\.\d+$/.test(str)) return parseFloat(str);
|
|
158
|
-
if (/^".*"$/.test(str)
|
|
184
|
+
if (/^".*"$/.test(str)) {
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(str);
|
|
187
|
+
} catch (e) {
|
|
188
|
+
return str.slice(1, -1);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (/^'.*'$/.test(str)) return str.slice(1, -1).replace(/''/g, "'");
|
|
159
192
|
// Inline array: [/god-mode, /god-foo]
|
|
160
193
|
if (/^\[.*\]$/.test(str)) {
|
|
161
194
|
const inner = str.slice(1, -1).trim();
|
|
162
195
|
if (!inner) return [];
|
|
163
|
-
return inner
|
|
196
|
+
return splitInlineArray(inner).map(s => parseValue(s.trim()));
|
|
164
197
|
}
|
|
165
198
|
return str;
|
|
166
199
|
}
|
|
167
200
|
|
|
201
|
+
function stripInlineComment(line) {
|
|
202
|
+
let quote = null;
|
|
203
|
+
for (let i = 0; i < line.length; i++) {
|
|
204
|
+
const ch = line[i];
|
|
205
|
+
if (quote) {
|
|
206
|
+
if (ch === quote && line[i - 1] !== '\\') quote = null;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (ch === '"' || ch === "'") {
|
|
210
|
+
quote = ch;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (ch === '#' && (i === 0 || /\s/.test(line[i - 1]))) {
|
|
214
|
+
return line.slice(0, i).trimEnd();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return line;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function findUnquotedColon(text) {
|
|
221
|
+
let quote = null;
|
|
222
|
+
for (let i = 0; i < text.length; i++) {
|
|
223
|
+
const ch = text[i];
|
|
224
|
+
if (quote) {
|
|
225
|
+
if (ch === quote && text[i - 1] !== '\\') quote = null;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (ch === '"' || ch === "'") {
|
|
229
|
+
quote = ch;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (ch === ':') return i;
|
|
233
|
+
}
|
|
234
|
+
return -1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function splitInlineArray(text) {
|
|
238
|
+
const parts = [];
|
|
239
|
+
let quote = null;
|
|
240
|
+
let depth = 0;
|
|
241
|
+
let start = 0;
|
|
242
|
+
for (let i = 0; i < text.length; i++) {
|
|
243
|
+
const ch = text[i];
|
|
244
|
+
if (quote) {
|
|
245
|
+
if (ch === quote && text[i - 1] !== '\\') quote = null;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (ch === '"' || ch === "'") {
|
|
249
|
+
quote = ch;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (ch === '[') depth++;
|
|
253
|
+
if (ch === ']') depth--;
|
|
254
|
+
if (ch === ',' && depth === 0) {
|
|
255
|
+
parts.push(text.slice(start, i));
|
|
256
|
+
start = i + 1;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
parts.push(text.slice(start));
|
|
260
|
+
return parts;
|
|
261
|
+
}
|
|
262
|
+
|
|
168
263
|
function cleanArrays(obj) {
|
|
169
264
|
if (Array.isArray(obj)) return obj.map(cleanArrays);
|
|
170
265
|
if (obj && typeof obj === 'object') {
|
|
@@ -206,4 +301,4 @@ function validate(intent) {
|
|
|
206
301
|
return errors;
|
|
207
302
|
}
|
|
208
303
|
|
|
209
|
-
module.exports = { read, get, validate, intentPath, parseSimpleYaml };
|
|
304
|
+
module.exports = { read, readAsync, get, validate, intentPath, parseSimpleYaml };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick proof runner.
|
|
3
|
+
*
|
|
4
|
+
* Renders a deterministic proof from a shipped fixture while detecting host
|
|
5
|
+
* guarantees from the caller's actual project and environment.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
const dashboard = require('./dashboard');
|
|
12
|
+
const hostCapabilities = require('./host-capabilities');
|
|
13
|
+
|
|
14
|
+
const FIXTURE_ROOT = path.join(__dirname, '..', 'fixtures', 'quick-proof', 'project');
|
|
15
|
+
const MANIFEST_PATH = path.join(__dirname, '..', 'fixtures', 'quick-proof', 'manifest.json');
|
|
16
|
+
|
|
17
|
+
function readJson(filePath) {
|
|
18
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function relFixturePath(absPath) {
|
|
22
|
+
return path.relative(path.join(__dirname, '..'), absPath).split(path.sep).join('/');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function compute(projectRoot = process.cwd(), opts = {}) {
|
|
26
|
+
const fixtureRoot = opts.fixtureRoot || FIXTURE_ROOT;
|
|
27
|
+
const manifestPath = opts.manifestPath || MANIFEST_PATH;
|
|
28
|
+
const manifest = readJson(manifestPath);
|
|
29
|
+
const fixtureDashboard = dashboard.compute(fixtureRoot, { git: false });
|
|
30
|
+
const host = opts.hostReport || hostCapabilities.detect(projectRoot, opts.hostOptions || {});
|
|
31
|
+
const focusedActionBrief = {
|
|
32
|
+
recommended: fixtureDashboard.next && fixtureDashboard.next.command
|
|
33
|
+
? fixtureDashboard.next.command
|
|
34
|
+
: 'describe the next intent',
|
|
35
|
+
reason: fixtureDashboard.next && fixtureDashboard.next.reason
|
|
36
|
+
? fixtureDashboard.next.reason
|
|
37
|
+
: 'No route was computed.',
|
|
38
|
+
confidence: host.level === 'unknown' ? 'needs attention' : 'ready',
|
|
39
|
+
blockers: host.gaps && host.gaps.length > 0 ? [`Host: ${host.gaps[0]}`] : [],
|
|
40
|
+
overflow: host.gaps && host.gaps.length > 1 ? host.gaps.length - 1 : 0
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const proof = {
|
|
44
|
+
source: 'quick-proof fixture',
|
|
45
|
+
manifest,
|
|
46
|
+
projectRoot: path.resolve(projectRoot),
|
|
47
|
+
fixtureRoot,
|
|
48
|
+
fixturePath: relFixturePath(fixtureRoot),
|
|
49
|
+
statePath: relFixturePath(path.join(fixtureRoot, '.godpowers', 'state.json')),
|
|
50
|
+
dashboard: {
|
|
51
|
+
state: fixtureDashboard.state,
|
|
52
|
+
progress: fixtureDashboard.progress,
|
|
53
|
+
planning: fixtureDashboard.planning,
|
|
54
|
+
next: fixtureDashboard.next,
|
|
55
|
+
actionBrief: focusedActionBrief
|
|
56
|
+
},
|
|
57
|
+
host,
|
|
58
|
+
commands: [
|
|
59
|
+
`npx godpowers quick-proof --project=${projectRoot}`,
|
|
60
|
+
`npx godpowers status --project=${fixtureRoot} --brief`,
|
|
61
|
+
`npx godpowers next --project=${fixtureRoot} --brief`,
|
|
62
|
+
`npx godpowers status --project=${projectRoot} --brief`
|
|
63
|
+
],
|
|
64
|
+
evidence: [
|
|
65
|
+
{
|
|
66
|
+
label: 'State on disk',
|
|
67
|
+
value: relFixturePath(path.join(fixtureRoot, '.godpowers', 'state.json'))
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: 'Next action',
|
|
71
|
+
value: fixtureDashboard.next && fixtureDashboard.next.command
|
|
72
|
+
? fixtureDashboard.next.command
|
|
73
|
+
: 'describe the next intent'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
label: 'Missing artifact',
|
|
77
|
+
value: fixtureDashboard.planning.prd.status === 'missing'
|
|
78
|
+
? '.godpowers/prd/PRD.md'
|
|
79
|
+
: 'none'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
label: 'Host guarantees',
|
|
83
|
+
value: hostCapabilities.summary(host)
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
return proof;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function render(proof, opts = {}) {
|
|
92
|
+
const brief = proof.dashboard.actionBrief || {};
|
|
93
|
+
const next = proof.dashboard.next || {};
|
|
94
|
+
const progress = proof.dashboard.progress || {};
|
|
95
|
+
const planning = proof.dashboard.planning || {};
|
|
96
|
+
|
|
97
|
+
if (opts.brief) {
|
|
98
|
+
return [
|
|
99
|
+
'Godpowers Quick Proof',
|
|
100
|
+
'',
|
|
101
|
+
'Action brief:',
|
|
102
|
+
` Next: ${brief.recommended || next.command || 'describe the next intent'}`,
|
|
103
|
+
` Why: ${brief.reason || next.reason || 'No route was computed.'}`,
|
|
104
|
+
` Readiness: ${brief.confidence || 'unknown'}`,
|
|
105
|
+
` Host guarantees: ${hostCapabilities.summary(proof.host)}`,
|
|
106
|
+
'',
|
|
107
|
+
'Evidence:',
|
|
108
|
+
` State on disk: ${proof.statePath}`,
|
|
109
|
+
` Fixture: ${proof.fixturePath}`,
|
|
110
|
+
` PRD: ${planning.prd ? planning.prd.status : 'unknown'}`,
|
|
111
|
+
` Roadmap: ${planning.roadmap ? planning.roadmap.status : 'unknown'}`
|
|
112
|
+
].join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [
|
|
116
|
+
'Godpowers Quick Proof',
|
|
117
|
+
'',
|
|
118
|
+
`Source: shipped fixture (${proof.fixturePath})`,
|
|
119
|
+
'',
|
|
120
|
+
'What this proves:',
|
|
121
|
+
' 1. Godpowers can read project state from disk.',
|
|
122
|
+
' 2. Godpowers can name missing artifacts instead of inventing completion.',
|
|
123
|
+
' 3. Godpowers can recommend the next command from state.',
|
|
124
|
+
' 4. Godpowers can report host guarantees separately from fixture state.',
|
|
125
|
+
'',
|
|
126
|
+
'Dashboard proof:',
|
|
127
|
+
` State: ${proof.dashboard.state}`,
|
|
128
|
+
` Progress: ${progress.percent || 0}% (${progress.completed || 0} of ${progress.total || 0} tracked steps complete)`,
|
|
129
|
+
` PRD: ${planning.prd ? planning.prd.status : 'unknown'}`,
|
|
130
|
+
` Roadmap: ${planning.roadmap ? planning.roadmap.status : 'unknown'}`,
|
|
131
|
+
` Next: ${next.command || 'describe the next intent'}`,
|
|
132
|
+
` Why: ${next.reason || 'No route was computed.'}`,
|
|
133
|
+
` Host guarantees: ${hostCapabilities.summary(proof.host)}`,
|
|
134
|
+
'',
|
|
135
|
+
'Evidence:',
|
|
136
|
+
...proof.evidence.map((item, index) => ` ${index + 1}. ${item.label}: ${item.value}`),
|
|
137
|
+
'',
|
|
138
|
+
'Try it on the fixture:',
|
|
139
|
+
` npx godpowers status --project=${proof.fixtureRoot} --brief`,
|
|
140
|
+
` npx godpowers next --project=${proof.fixtureRoot} --brief`,
|
|
141
|
+
'',
|
|
142
|
+
'Try it on your project:',
|
|
143
|
+
` npx godpowers status --project=${proof.projectRoot} --brief`,
|
|
144
|
+
` npx godpowers next --project=${proof.projectRoot} --brief`
|
|
145
|
+
].join('\n');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
compute,
|
|
150
|
+
render,
|
|
151
|
+
FIXTURE_ROOT,
|
|
152
|
+
MANIFEST_PATH
|
|
153
|
+
};
|
|
@@ -51,6 +51,13 @@ function readJson(projectRoot, relPath) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function releaseGateText(projectRoot, pkg) {
|
|
55
|
+
return [
|
|
56
|
+
JSON.stringify((pkg && pkg.scripts) || {}),
|
|
57
|
+
read(projectRoot, 'scripts/run-tests.js')
|
|
58
|
+
].join('\n');
|
|
59
|
+
}
|
|
60
|
+
|
|
54
61
|
function addCheck(checks, id, status, relPath, message, opts = {}) {
|
|
55
62
|
checks.push({
|
|
56
63
|
area: 'release-surface',
|
|
@@ -114,7 +121,7 @@ function detect(projectRoot) {
|
|
|
114
121
|
);
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
const scriptsText =
|
|
124
|
+
const scriptsText = releaseGateText(projectRoot, pkg);
|
|
118
125
|
for (const required of REQUIRED_RELEASE_TESTS) {
|
|
119
126
|
const ok = scriptsText.includes(required);
|
|
120
127
|
addCheck(
|
package/lib/repo-surface-sync.js
CHANGED
|
@@ -88,6 +88,13 @@ function readJson(projectRoot, relPath) {
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
function releaseGateText(projectRoot, pkg) {
|
|
92
|
+
return [
|
|
93
|
+
JSON.stringify((pkg && pkg.scripts) || {}),
|
|
94
|
+
read(projectRoot, 'scripts/run-tests.js')
|
|
95
|
+
].join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
function routeForSkill(skillPath) {
|
|
92
99
|
const base = path.basename(skillPath, '.md');
|
|
93
100
|
return `routing/${base}.yaml`;
|
|
@@ -364,7 +371,7 @@ function extensionChecks(projectRoot) {
|
|
|
364
371
|
function suiteChecks(projectRoot) {
|
|
365
372
|
const checks = [];
|
|
366
373
|
const pkg = readJson(projectRoot, 'package.json') || {};
|
|
367
|
-
const scriptsText =
|
|
374
|
+
const scriptsText = releaseGateText(projectRoot, pkg);
|
|
368
375
|
const roadmap = read(projectRoot, 'docs/ROADMAP.md');
|
|
369
376
|
const suiteCommands = [
|
|
370
377
|
'god-suite-init',
|
|
@@ -433,7 +440,7 @@ function suiteChecks(projectRoot) {
|
|
|
433
440
|
function dogfoodChecks(projectRoot) {
|
|
434
441
|
const checks = [];
|
|
435
442
|
const pkg = readJson(projectRoot, 'package.json') || {};
|
|
436
|
-
const scriptsText =
|
|
443
|
+
const scriptsText = releaseGateText(projectRoot, pkg);
|
|
437
444
|
const scenarios = [
|
|
438
445
|
'fixtures/dogfood/half-migrated-gsd/manifest.json',
|
|
439
446
|
'fixtures/dogfood/host-degraded/manifest.json',
|
package/lib/review-required.js
CHANGED
|
@@ -23,7 +23,7 @@ const fs = require('fs');
|
|
|
23
23
|
const path = require('path');
|
|
24
24
|
|
|
25
25
|
function filePath(projectRoot) {
|
|
26
|
-
return path.join(projectRoot, 'REVIEW-REQUIRED.md');
|
|
26
|
+
return path.join(projectRoot, '.godpowers', 'REVIEW-REQUIRED.md');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function rejectedPath(projectRoot) {
|
|
@@ -41,6 +41,7 @@ function rejectedPath(projectRoot) {
|
|
|
41
41
|
*/
|
|
42
42
|
function appendBatch(projectRoot, batch) {
|
|
43
43
|
const file = filePath(projectRoot);
|
|
44
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
44
45
|
const ts = new Date().toISOString();
|
|
45
46
|
const id = `${ts.replace(/[:.]/g, '-')}-${batch.source}`;
|
|
46
47
|
|
package/lib/router.js
CHANGED
|
@@ -98,7 +98,9 @@ function evaluateCheck(check, projectRoot) {
|
|
|
98
98
|
// file:path
|
|
99
99
|
if (check.startsWith('file:')) {
|
|
100
100
|
const filePath = check.slice(5).trim();
|
|
101
|
-
|
|
101
|
+
const resolved = resolveProjectRelative(projectRoot, filePath);
|
|
102
|
+
if (!resolved) return false;
|
|
103
|
+
return fs.existsSync(resolved);
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
// state:dotted.path == value
|
|
@@ -109,7 +111,10 @@ function evaluateCheck(check, projectRoot) {
|
|
|
109
111
|
const [, dottedPath, expected] = match;
|
|
110
112
|
const s = state.read(projectRoot);
|
|
111
113
|
if (!s) return false;
|
|
112
|
-
const actual = dottedPath.split('.').reduce((acc, k) =>
|
|
114
|
+
const actual = dottedPath.split('.').reduce((acc, k) => {
|
|
115
|
+
if (!acc || k === '__proto__' || k === 'constructor' || k === 'prototype') return undefined;
|
|
116
|
+
return acc[k];
|
|
117
|
+
}, s.tiers || s);
|
|
113
118
|
return actual === expected || actual === parseValue(expected);
|
|
114
119
|
}
|
|
115
120
|
|
|
@@ -126,7 +131,11 @@ function evaluateCheck(check, projectRoot) {
|
|
|
126
131
|
return !fs.existsSync(path.join(projectRoot, '.godpowers'));
|
|
127
132
|
}
|
|
128
133
|
|
|
129
|
-
//
|
|
134
|
+
// Unknown check predicate: log a warning but assume satisfied to avoid
|
|
135
|
+
// blocking legitimate work from an outdated or unrecognized routing file.
|
|
136
|
+
if (typeof process !== 'undefined' && process.env.GODPOWERS_DEBUG) {
|
|
137
|
+
console.warn(`[router] unknown check predicate, assuming satisfied: ${check}`);
|
|
138
|
+
}
|
|
130
139
|
return true;
|
|
131
140
|
}
|
|
132
141
|
|
|
@@ -139,6 +148,16 @@ function parseValue(s) {
|
|
|
139
148
|
return s;
|
|
140
149
|
}
|
|
141
150
|
|
|
151
|
+
function resolveProjectRelative(projectRoot, relPath) {
|
|
152
|
+
if (!projectRoot || !relPath) return null;
|
|
153
|
+
if (path.isAbsolute(relPath) || relPath.includes('\0')) return null;
|
|
154
|
+
|
|
155
|
+
const root = path.resolve(projectRoot);
|
|
156
|
+
const resolved = path.resolve(root, relPath);
|
|
157
|
+
if (resolved === root || resolved.startsWith(root + path.sep)) return resolved;
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
142
161
|
/**
|
|
143
162
|
* Get the recommended next command after a successful run.
|
|
144
163
|
* If conditional-next is declared, evaluates each branch's condition
|
|
@@ -354,6 +373,7 @@ module.exports = {
|
|
|
354
373
|
getSpawnedAgents,
|
|
355
374
|
suggestNext,
|
|
356
375
|
evaluateCheck,
|
|
376
|
+
resolveProjectRelative,
|
|
357
377
|
detectSafeSyncBlocker,
|
|
358
378
|
hasNoCriticalFindings,
|
|
359
379
|
clearCache
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
function parseFrontmatter(text) {
|
|
5
|
+
if (!text.startsWith('---\n')) return {};
|
|
6
|
+
const end = text.indexOf('\n---', 4);
|
|
7
|
+
if (end === -1) return {};
|
|
8
|
+
const out = {};
|
|
9
|
+
for (const line of text.slice(4, end).split('\n')) {
|
|
10
|
+
const match = line.match(/^([a-zA-Z0-9_-]+):\s*(.*)$/);
|
|
11
|
+
if (match) out[match[1]] = match[2].replace(/^["']|["']$/g, '');
|
|
12
|
+
}
|
|
13
|
+
return out;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function listSkills(rootDir = path.join(__dirname, '..', 'skills')) {
|
|
17
|
+
return fs.readdirSync(rootDir)
|
|
18
|
+
.filter((file) => /^god.*\.md$/.test(file))
|
|
19
|
+
.sort()
|
|
20
|
+
.map((file) => {
|
|
21
|
+
const full = path.join(rootDir, file);
|
|
22
|
+
const text = fs.readFileSync(full, 'utf8');
|
|
23
|
+
const frontmatter = parseFrontmatter(text);
|
|
24
|
+
return {
|
|
25
|
+
file,
|
|
26
|
+
command: `/${path.basename(file, '.md')}`,
|
|
27
|
+
name: frontmatter.name || path.basename(file, '.md'),
|
|
28
|
+
description: frontmatter.description || '',
|
|
29
|
+
path: full
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function commandNames(rootDir) {
|
|
35
|
+
return listSkills(rootDir).map((skill) => skill.command);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
parseFrontmatter,
|
|
40
|
+
listSkills,
|
|
41
|
+
commandNames
|
|
42
|
+
};
|
package/lib/state-lock.js
CHANGED
|
@@ -27,6 +27,16 @@
|
|
|
27
27
|
* isStale(lock, nowMs?) -> bool
|
|
28
28
|
* reclaim(projectRoot, holder) -> { reclaimed: bool, previousHolder? }
|
|
29
29
|
* scopesConflict(a, b) -> bool
|
|
30
|
+
*
|
|
31
|
+
* Limitations:
|
|
32
|
+
* This lock is COOPERATIVE and ADVISORY. acquire() performs a non-atomic
|
|
33
|
+
* read-then-write of state.json, so two separate OS processes that race can
|
|
34
|
+
* both observe "no lock" and both proceed. It reliably serializes
|
|
35
|
+
* well-behaved in-process callers (the common single-developer case) but is
|
|
36
|
+
* NOT a cross-process mutex, and withLock() therefore guarantees mutual
|
|
37
|
+
* exclusion only within one process. If true multi-process exclusion is ever
|
|
38
|
+
* required, replace the acquire step with an atomic primitive such as an
|
|
39
|
+
* fs.mkdir lock directory or an O_EXCL (wx) lockfile.
|
|
30
40
|
*/
|
|
31
41
|
|
|
32
42
|
const fs = require('fs');
|