godpowers 2.0.0 → 2.1.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.
Files changed (53) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +141 -0
  3. package/README.md +45 -5
  4. package/RELEASE.md +30 -48
  5. package/SKILL.md +9 -1
  6. package/agents/god-design-reviewer.md +6 -6
  7. package/agents/god-designer.md +1 -1
  8. package/agents/god-executor.md +23 -0
  9. package/agents/god-quality-reviewer.md +12 -1
  10. package/agents/god-spec-reviewer.md +10 -0
  11. package/bin/install.js +119 -655
  12. package/extensions/launch-pack/README.md +1 -1
  13. package/lib/README.md +16 -0
  14. package/lib/agent-browser-driver.js +13 -13
  15. package/lib/agent-cache.js +8 -1
  16. package/lib/agent-refs.js +161 -0
  17. package/lib/budget.js +25 -11
  18. package/lib/context-writer.js +17 -6
  19. package/lib/events.js +11 -4
  20. package/lib/extension-authoring.js +27 -0
  21. package/lib/feature-awareness.js +18 -0
  22. package/lib/fs-async.js +28 -0
  23. package/lib/installer-args.js +99 -0
  24. package/lib/installer-core.js +345 -0
  25. package/lib/installer-files.js +80 -0
  26. package/lib/installer-runtimes.js +112 -0
  27. package/lib/intent.js +111 -16
  28. package/lib/release-surface-sync.js +8 -1
  29. package/lib/repo-surface-sync.js +9 -2
  30. package/lib/review-required.js +2 -1
  31. package/lib/router.js +23 -3
  32. package/lib/skill-surface.js +42 -0
  33. package/lib/state-lock.js +10 -0
  34. package/lib/state.js +101 -8
  35. package/lib/workflow-runner.js +42 -5
  36. package/package.json +4 -3
  37. package/references/HAVE-NOTS.md +4 -3
  38. package/references/orchestration/GOD-MODE-RUNBOOK.md +273 -0
  39. package/routing/god-arch.yaml +1 -1
  40. package/routing/god-build.yaml +1 -1
  41. package/skills/god-add-backlog.md +1 -1
  42. package/skills/god-agent-audit.md +2 -2
  43. package/skills/god-build.md +5 -3
  44. package/skills/god-context-scan.md +2 -3
  45. package/skills/god-design.md +2 -2
  46. package/skills/god-doctor.md +2 -2
  47. package/skills/god-help.md +4 -3
  48. package/skills/god-mode.md +10 -266
  49. package/skills/god-org-context.md +1 -1
  50. package/skills/god-repair.md +3 -3
  51. package/skills/god-review.md +9 -0
  52. package/skills/god-stories.md +1 -1
  53. 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
- // Strip inline comments (but not # inside quotes)
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.indexOf(':');
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.indexOf(':');
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) || /^'.*'$/.test(str)) return str.slice(1, -1);
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.split(',').map(s => parseValue(s.trim()));
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 = JSON.stringify(pkg.scripts || {});
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(
@@ -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 = JSON.stringify(pkg.scripts || {});
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 = JSON.stringify(pkg.scripts || {});
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',
@@ -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
- return fs.existsSync(path.join(projectRoot, filePath));
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) => (acc ? acc[k] : undefined), s.tiers || s);
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
- // unknown check: assume satisfied to not block legitimate work
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. Returns null if not initialized.
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
- return JSON.parse(fs.readFileSync(file, 'utf8'));
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
- * Write state.json to a project. Validates basic structure.
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 write(projectRoot, state) {
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
- * Initialize a new state.json for a project.
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 init(projectRoot, projectName, opts = {}) {
92
- const state = {
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
- return write(projectRoot, state);
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,
@@ -19,9 +19,9 @@
19
19
  *
20
20
  * Public API:
21
21
  * loadByName(workflowName, opts?) -> workflow object
22
- * plan(workflow, ctx?) -> { steps: [...], waves: [...], summary }
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: extractAgent(job.uses),
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
- const m = String(uses).match(/^([a-z][a-z0-9-]*)/);
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.0.0",
3
+ "version": "2.1.1",
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/validate-skills.js && node scripts/test-doc-surface-counts.js && node scripts/test-quick-proof.js && bash scripts/smoke.sh && node scripts/test-runtime.js && node scripts/test-router.js && node scripts/test-recipes.js && node scripts/test-context-writer.js && node scripts/test-pillars.js && node scripts/test-artifact-linter.js && node scripts/test-artifact-diff.js && node scripts/test-design-foundation.js && node scripts/test-linkage.js && node scripts/test-impact.js && node scripts/test-reverse-sync.js && node scripts/test-planning-systems.js && node scripts/test-feature-awareness.js && node scripts/test-repo-doc-sync.js && node scripts/test-repo-surface-sync.js && node scripts/test-automation-surface-sync.js && node scripts/test-host-capabilities.js && node scripts/test-extension-authoring.js && node scripts/test-dogfood-runner.js && node scripts/test-integration.js && node scripts/test-cross-artifact.js && node scripts/test-awesome-design.js && node scripts/test-skillui-bridge.js && node scripts/test-runtime-verification.js && node scripts/test-agent-browser.js && node scripts/test-mode-d.js && node scripts/test-runtime-heuristics.js && node scripts/test-agent-validator.js && node scripts/test-story-validator.js && node scripts/test-state.js && node scripts/test-dashboard.js && node scripts/test-automation-providers.js && node scripts/test-intent.js && node scripts/test-events.js && node scripts/test-golden-artifacts.js && node scripts/test-install-smoke.js && node scripts/test-checkpoint.js && node scripts/test-extensions.js && node scripts/test-event-reader.js && node scripts/test-state-lock.js && node scripts/test-cost-saver.js && node scripts/test-budget-onoff.js && node scripts/test-workflow-runner.js && npm run test:e2e && node scripts/test-otel-exporter.js && node scripts/test-extensions-publish.js",
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",