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.
Files changed (65) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +166 -0
  3. package/README.md +103 -8
  4. package/RELEASE.md +48 -50
  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 +137 -655
  12. package/extensions/data-pack/manifest.yaml +1 -1
  13. package/extensions/data-pack/package.json +1 -1
  14. package/extensions/launch-pack/README.md +1 -1
  15. package/extensions/launch-pack/manifest.yaml +1 -1
  16. package/extensions/launch-pack/package.json +1 -1
  17. package/extensions/security-pack/manifest.yaml +1 -1
  18. package/extensions/security-pack/package.json +1 -1
  19. package/fixtures/quick-proof/manifest.json +19 -0
  20. package/fixtures/quick-proof/project/.godpowers/prep/INITIAL-FINDINGS.md +5 -0
  21. package/fixtures/quick-proof/project/.godpowers/state.json +69 -0
  22. package/fixtures/quick-proof/project/README.md +5 -0
  23. package/fixtures/quick-proof/project/package.json +6 -0
  24. package/lib/agent-browser-driver.js +13 -13
  25. package/lib/agent-cache.js +8 -1
  26. package/lib/agent-refs.js +161 -0
  27. package/lib/budget.js +25 -11
  28. package/lib/events.js +11 -4
  29. package/lib/extension-authoring.js +27 -0
  30. package/lib/feature-awareness.js +24 -0
  31. package/lib/fs-async.js +28 -0
  32. package/lib/installer-args.js +99 -0
  33. package/lib/installer-core.js +345 -0
  34. package/lib/installer-files.js +80 -0
  35. package/lib/installer-runtimes.js +112 -0
  36. package/lib/intent.js +111 -16
  37. package/lib/quick-proof.js +153 -0
  38. package/lib/release-surface-sync.js +8 -1
  39. package/lib/repo-surface-sync.js +9 -2
  40. package/lib/review-required.js +2 -1
  41. package/lib/router.js +23 -3
  42. package/lib/skill-surface.js +42 -0
  43. package/lib/state-lock.js +10 -0
  44. package/lib/state.js +101 -8
  45. package/lib/workflow-runner.js +42 -5
  46. package/package.json +7 -3
  47. package/references/HAVE-NOTS.md +4 -3
  48. package/references/orchestration/GOD-MODE-RUNBOOK.md +273 -0
  49. package/routing/god-arch.yaml +1 -1
  50. package/routing/god-build.yaml +1 -1
  51. package/skills/god-add-backlog.md +1 -1
  52. package/skills/god-agent-audit.md +2 -2
  53. package/skills/god-build.md +5 -3
  54. package/skills/god-context-scan.md +2 -3
  55. package/skills/god-design.md +2 -2
  56. package/skills/god-doctor.md +2 -2
  57. package/skills/god-extension-info.md +1 -1
  58. package/skills/god-help.md +4 -3
  59. package/skills/god-mode.md +10 -266
  60. package/skills/god-org-context.md +1 -1
  61. package/skills/god-repair.md +3 -3
  62. package/skills/god-review.md +9 -0
  63. package/skills/god-stories.md +1 -1
  64. package/skills/god-test-extension.md +1 -1
  65. 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 };
@@ -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 = 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');