godpowers 2.0.0 → 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 +126 -0
- package/README.md +45 -5
- package/RELEASE.md +45 -39
- 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 +119 -655
- package/extensions/launch-pack/README.md +1 -1
- 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 +18 -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/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 +4 -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-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-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 };
|
|
@@ -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');
|
package/lib/state.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const fs = require('fs');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const crypto = require('crypto');
|
|
11
|
+
const asyncFs = require('./fs-async');
|
|
11
12
|
|
|
12
13
|
const STATE_VERSION = '1.0.0';
|
|
13
14
|
const COMPLETE_STATUSES = new Set(['done', 'imported', 'skipped', 'not-required']);
|
|
@@ -34,6 +35,22 @@ const SUBSTEP_LABELS = {
|
|
|
34
35
|
harden: 'Harden'
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {Object} GodpowersState
|
|
40
|
+
* @property {string} $schema State schema URL.
|
|
41
|
+
* @property {string} version State schema version.
|
|
42
|
+
* @property {{ name: string, started?: string }} project Project identity.
|
|
43
|
+
* @property {Record<string, Record<string, Object>>} tiers Tier and sub-step state.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} ProgressStep
|
|
48
|
+
* @property {string} tierKey Tier key such as `tier-1`.
|
|
49
|
+
* @property {string} subStepKey Sub-step key such as `prd`.
|
|
50
|
+
* @property {string} status Sub-step status.
|
|
51
|
+
* @property {number} ordinal One-based step position.
|
|
52
|
+
*/
|
|
53
|
+
|
|
37
54
|
function statePath(projectRoot) {
|
|
38
55
|
return path.join(projectRoot, '.godpowers', 'state.json');
|
|
39
56
|
}
|
|
@@ -57,18 +74,46 @@ function tierComparator(a, b) {
|
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
/**
|
|
60
|
-
* Read state.json from a project.
|
|
77
|
+
* Read state.json from a project.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} projectRoot
|
|
80
|
+
* @returns {GodpowersState|null}
|
|
61
81
|
*/
|
|
62
82
|
function read(projectRoot) {
|
|
63
83
|
const file = statePath(projectRoot);
|
|
64
84
|
if (!fs.existsSync(file)) return null;
|
|
65
|
-
|
|
85
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(raw);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Corrupt state file at ${file}: ${e.message}. ` +
|
|
91
|
+
`Fix the JSON or remove the file to let Godpowers reinitialize it.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
66
94
|
}
|
|
67
95
|
|
|
68
96
|
/**
|
|
69
|
-
*
|
|
97
|
+
* Async state.json reader for callers that should not block the event loop.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} projectRoot
|
|
100
|
+
* @returns {Promise<GodpowersState|null>}
|
|
70
101
|
*/
|
|
71
|
-
function
|
|
102
|
+
async function readAsync(projectRoot) {
|
|
103
|
+
const file = statePath(projectRoot);
|
|
104
|
+
if (!(await asyncFs.exists(file))) return null;
|
|
105
|
+
const raw = await asyncFs.fs.readFile(file, 'utf8');
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(raw);
|
|
108
|
+
} catch (e) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Corrupt state file at ${file}: ${e.message}. ` +
|
|
111
|
+
`Fix the JSON or remove the file to let Godpowers reinitialize it.`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function normalizeForWrite(state) {
|
|
72
117
|
if (!state || typeof state !== 'object') {
|
|
73
118
|
throw new Error('state must be an object');
|
|
74
119
|
}
|
|
@@ -78,6 +123,18 @@ function write(projectRoot, state) {
|
|
|
78
123
|
throw new Error('state.project.name is required');
|
|
79
124
|
}
|
|
80
125
|
if (!state.tiers) state.tiers = {};
|
|
126
|
+
return state;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Write state.json to a project. Validates basic structure.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} projectRoot
|
|
133
|
+
* @param {GodpowersState} state
|
|
134
|
+
* @returns {GodpowersState}
|
|
135
|
+
*/
|
|
136
|
+
function write(projectRoot, state) {
|
|
137
|
+
normalizeForWrite(state);
|
|
81
138
|
|
|
82
139
|
const file = statePath(projectRoot);
|
|
83
140
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -86,10 +143,19 @@ function write(projectRoot, state) {
|
|
|
86
143
|
}
|
|
87
144
|
|
|
88
145
|
/**
|
|
89
|
-
*
|
|
146
|
+
* Async state.json writer with the same validation contract as write().
|
|
147
|
+
*
|
|
148
|
+
* @param {string} projectRoot
|
|
149
|
+
* @param {GodpowersState} state
|
|
150
|
+
* @returns {Promise<GodpowersState>}
|
|
90
151
|
*/
|
|
91
|
-
function
|
|
92
|
-
|
|
152
|
+
async function writeAsync(projectRoot, state) {
|
|
153
|
+
normalizeForWrite(state);
|
|
154
|
+
return asyncFs.writeJson(statePath(projectRoot), state);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function createInitialState(projectName, opts = {}) {
|
|
158
|
+
return {
|
|
93
159
|
$schema: 'https://godpowers.dev/schema/state.v1.json',
|
|
94
160
|
version: STATE_VERSION,
|
|
95
161
|
project: {
|
|
@@ -128,7 +194,17 @@ function init(projectRoot, projectName, opts = {}) {
|
|
|
128
194
|
'yolo-decisions': [],
|
|
129
195
|
...opts
|
|
130
196
|
};
|
|
131
|
-
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Initialize a new state.json for a project.
|
|
201
|
+
*/
|
|
202
|
+
function init(projectRoot, projectName, opts = {}) {
|
|
203
|
+
return write(projectRoot, createInitialState(projectName, opts));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function initAsync(projectRoot, projectName, opts = {}) {
|
|
207
|
+
return writeAsync(projectRoot, createInitialState(projectName, opts));
|
|
132
208
|
}
|
|
133
209
|
|
|
134
210
|
/**
|
|
@@ -147,6 +223,19 @@ function updateSubStep(projectRoot, tierKey, subStepKey, updates) {
|
|
|
147
223
|
return state.tiers[tierKey][subStepKey];
|
|
148
224
|
}
|
|
149
225
|
|
|
226
|
+
async function updateSubStepAsync(projectRoot, tierKey, subStepKey, updates) {
|
|
227
|
+
const state = await readAsync(projectRoot);
|
|
228
|
+
if (!state) throw new Error('state.json not found');
|
|
229
|
+
if (!state.tiers[tierKey]) throw new Error(`Tier not found: ${tierKey}`);
|
|
230
|
+
state.tiers[tierKey][subStepKey] = {
|
|
231
|
+
...(state.tiers[tierKey][subStepKey] || {}),
|
|
232
|
+
...updates,
|
|
233
|
+
updated: new Date().toISOString()
|
|
234
|
+
};
|
|
235
|
+
await writeAsync(projectRoot, state);
|
|
236
|
+
return state.tiers[tierKey][subStepKey];
|
|
237
|
+
}
|
|
238
|
+
|
|
150
239
|
/**
|
|
151
240
|
* Hash a file. Used for artifact-hash tracking.
|
|
152
241
|
*/
|
|
@@ -271,9 +360,13 @@ function renderProgressLine(summary) {
|
|
|
271
360
|
|
|
272
361
|
module.exports = {
|
|
273
362
|
read,
|
|
363
|
+
readAsync,
|
|
274
364
|
write,
|
|
365
|
+
writeAsync,
|
|
275
366
|
init,
|
|
367
|
+
initAsync,
|
|
276
368
|
updateSubStep,
|
|
369
|
+
updateSubStepAsync,
|
|
277
370
|
hashFile,
|
|
278
371
|
detectDrift,
|
|
279
372
|
statePath,
|
package/lib/workflow-runner.js
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
*
|
|
20
20
|
* Public API:
|
|
21
21
|
* loadByName(workflowName, opts?) -> workflow object
|
|
22
|
-
* plan(workflow, ctx?) ->
|
|
22
|
+
* plan(workflow, ctx?) -> WorkflowPlan
|
|
23
23
|
* writePlan(projectRoot, runId, plan) -> path
|
|
24
|
-
* readPlan(projectRoot, runId) -> plan | null
|
|
24
|
+
* readPlan(projectRoot, runId) -> serialized plan | null
|
|
25
25
|
* listWorkflows(opts?) -> [{ name, version, description, file }]
|
|
26
26
|
*/
|
|
27
27
|
|
|
@@ -29,6 +29,26 @@ const fs = require('fs');
|
|
|
29
29
|
const path = require('path');
|
|
30
30
|
|
|
31
31
|
const parser = require('./workflow-parser');
|
|
32
|
+
const asyncFs = require('./fs-async');
|
|
33
|
+
const agentRefs = require('./agent-refs');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @typedef {Object} WorkflowPlanStep
|
|
37
|
+
* @property {string} jobKey Workflow job key.
|
|
38
|
+
* @property {string} agent Validated agent name.
|
|
39
|
+
* @property {string} agentRange SemVer range declared by the workflow.
|
|
40
|
+
* @property {string} agentContractVersion Runtime agent contract version.
|
|
41
|
+
* @property {string|null} tier Tier label such as `tier-2`.
|
|
42
|
+
* @property {string[]} needs Dependency job keys.
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} WorkflowPlan
|
|
47
|
+
* @property {{ name: string, version: string, description?: string }} workflow
|
|
48
|
+
* @property {WorkflowPlanStep[]} steps
|
|
49
|
+
* @property {string[][]} waves Parallel execution waves.
|
|
50
|
+
* @property {string} summary Human-readable plan summary.
|
|
51
|
+
*/
|
|
32
52
|
|
|
33
53
|
function workflowsDir(opts) {
|
|
34
54
|
if (opts && opts.dir) return opts.dir;
|
|
@@ -102,9 +122,12 @@ function plan(workflow, ctx = {}) {
|
|
|
102
122
|
for (const wave of waves) {
|
|
103
123
|
for (const jobKey of wave) {
|
|
104
124
|
const job = workflow.jobs[jobKey] || {};
|
|
125
|
+
const ref = agentRefs.assertAgentRef(job.uses);
|
|
105
126
|
steps.push({
|
|
106
127
|
jobKey,
|
|
107
|
-
agent:
|
|
128
|
+
agent: ref.agent,
|
|
129
|
+
agentRange: ref.range,
|
|
130
|
+
agentContractVersion: ref.contractVersion,
|
|
108
131
|
tier: job.tier != null ? `tier-${job.tier}` : null,
|
|
109
132
|
needs: parser.normalizeNeeds(job.needs),
|
|
110
133
|
uses: job.uses,
|
|
@@ -133,8 +156,7 @@ function plan(workflow, ctx = {}) {
|
|
|
133
156
|
|
|
134
157
|
function extractAgent(uses) {
|
|
135
158
|
if (!uses) return null;
|
|
136
|
-
|
|
137
|
-
return m ? m[1] : uses;
|
|
159
|
+
return agentRefs.parseAgentRef(uses).agent;
|
|
138
160
|
}
|
|
139
161
|
|
|
140
162
|
function formatSummary(workflow, waves, steps) {
|
|
@@ -169,6 +191,13 @@ function writePlan(projectRoot, runId, planObj) {
|
|
|
169
191
|
return file;
|
|
170
192
|
}
|
|
171
193
|
|
|
194
|
+
async function writePlanAsync(projectRoot, runId, planObj) {
|
|
195
|
+
const file = path.join(projectRoot, '.godpowers', 'runs', runId, 'plan.yaml');
|
|
196
|
+
await asyncFs.fs.mkdir(path.dirname(file), { recursive: true });
|
|
197
|
+
await asyncFs.fs.writeFile(file, serializePlan(planObj));
|
|
198
|
+
return file;
|
|
199
|
+
}
|
|
200
|
+
|
|
172
201
|
function serializePlan(p) {
|
|
173
202
|
// YAML-ish hand-roll, paired with our minimal parser
|
|
174
203
|
const lines = [];
|
|
@@ -214,12 +243,20 @@ function readPlan(projectRoot, runId) {
|
|
|
214
243
|
return fs.readFileSync(file, 'utf8');
|
|
215
244
|
}
|
|
216
245
|
|
|
246
|
+
async function readPlanAsync(projectRoot, runId) {
|
|
247
|
+
const file = path.join(projectRoot, '.godpowers', 'runs', runId, 'plan.yaml');
|
|
248
|
+
if (!(await asyncFs.exists(file))) return null;
|
|
249
|
+
return asyncFs.fs.readFile(file, 'utf8');
|
|
250
|
+
}
|
|
251
|
+
|
|
217
252
|
module.exports = {
|
|
218
253
|
workflowsDir,
|
|
219
254
|
listWorkflows,
|
|
220
255
|
loadByName,
|
|
221
256
|
plan,
|
|
222
257
|
writePlan,
|
|
258
|
+
writePlanAsync,
|
|
223
259
|
readPlan,
|
|
260
|
+
readPlanAsync,
|
|
224
261
|
serializePlan
|
|
225
262
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "godpowers",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "AI-powered development system: 110 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
|
},
|
|
8
8
|
"scripts": {
|
|
9
|
-
"test": "node scripts/
|
|
9
|
+
"test": "node scripts/run-tests.js",
|
|
10
10
|
"prepublishOnly": "npm run release:check",
|
|
11
11
|
"validate-skills": "node scripts/validate-skills.js",
|
|
12
12
|
"test:surface": "node scripts/test-doc-surface-counts.js",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"test:e2e": "node tests/integration/full-arc.test.js",
|
|
25
25
|
"test:audit": "npm audit --omit=dev && git diff --check && npm run test:surface",
|
|
26
26
|
"pack:check": "node scripts/check-package-contents.js",
|
|
27
|
-
"release:check": "npm test && npm run test:audit && npm run pack:check"
|
|
27
|
+
"release:check": "npm test && npm run test:audit && npm run pack:check",
|
|
28
|
+
"lint": "node scripts/static-check.js"
|
|
28
29
|
},
|
|
29
30
|
"keywords": [
|
|
30
31
|
"ai",
|