godpowers 0.15.1 → 0.15.2

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/CHANGELOG.md CHANGED
@@ -5,6 +5,38 @@ All notable changes to Godpowers will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.15.2] - 2026-05-11
9
+
10
+ Runtime hardening release. Fixes packaging and workflow edge cases found by
11
+ a deep audit, then locks them behind regression tests and pack publish gates.
12
+
13
+ ### Fixed
14
+ - Installed `/god`, `/god-next`, `/god-help`, `/god-standards`, and
15
+ `/god-version` now point agents at the installed `godpowers-runtime`
16
+ bundle when the user is not inside the repository checkout.
17
+ - Installer now copies `package.json` into `godpowers-runtime`, so installed
18
+ OTel exports report the real Godpowers version instead of `0.0.0`.
19
+ - Linkage scans now replace stale scanner-owned links instead of only adding
20
+ new links. Manual links are preserved, and legacy maps without source
21
+ metadata are migrated safely.
22
+ - `checkpoint.recordFact()` now preserves existing actions instead of wiping
23
+ the checkpoint action history.
24
+ - Context writer now reads root-level `mode` and `scale`, matching the
25
+ canonical `state.json` shape produced by `lib/state.js`.
26
+ - Event hash chains now stay valid when an event line is larger than 4KB.
27
+ - Extension reinstall now clears the old installed pack directory first, so
28
+ deleted files do not remain active after reinstall.
29
+ - Installer uninstall now removes all installed data/runtime directories,
30
+ including workflows, schema, routing, and `godpowers-runtime`.
31
+
32
+ ### Tests
33
+ - Added regression coverage for stale linkage cleanup, checkpoint action
34
+ preservation, root-level mode/scale context rendering, large event hash
35
+ chains, installed OTel version metadata, runtime bundle guidance in installed
36
+ skills, extension reinstall cleanup, and uninstall cleanup.
37
+ - Extension pack publish gate now verifies package peer dependency ranges
38
+ match the manifest and include the current Godpowers version.
39
+
8
40
  ## [0.15.1] - 2026-05-11
9
41
 
10
42
  Metadata + documentation polish. No code changes.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/aihxp/godpowers/actions/workflows/ci.yml/badge.svg)](https://github.com/aihxp/godpowers/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
- [![Version](https://img.shields.io/badge/version-0.15.0-blue)](CHANGELOG.md)
5
+ [![Version](https://img.shields.io/badge/version-0.15.2-blue)](CHANGELOG.md)
6
6
  [![npm](https://img.shields.io/npm/v/godpowers.svg)](https://www.npmjs.com/package/godpowers)
7
7
 
8
8
  **Ship fast. Ship right. Ship everything. Ship accountably.**
package/bin/install.js CHANGED
@@ -157,6 +157,20 @@ function copyRecursive(src, dest) {
157
157
  }
158
158
  }
159
159
 
160
+ function copyRuntimeBundle(srcDir, destDir) {
161
+ ensureDir(destDir);
162
+ for (const dir of ['lib', 'routing', 'workflows', 'schema', 'templates', 'references']) {
163
+ const src = path.join(srcDir, dir);
164
+ if (fs.existsSync(src)) {
165
+ copyRecursive(src, path.join(destDir, dir));
166
+ }
167
+ }
168
+ const packageJson = path.join(srcDir, 'package.json');
169
+ if (fs.existsSync(packageJson)) {
170
+ fs.copyFileSync(packageJson, path.join(destDir, 'package.json'));
171
+ }
172
+ }
173
+
160
174
  // ---------------------------------------------------------------------------
161
175
  // Parse args
162
176
  // ---------------------------------------------------------------------------
@@ -299,6 +313,11 @@ function installForRuntime(runtimeKey, srcDir) {
299
313
  success('Installed routing/');
300
314
  }
301
315
 
316
+ // 4f. Install the executable runtime bundle with lib/ next to its data dirs.
317
+ const runtimeBundleDest = path.join(runtime.configDir, 'godpowers-runtime');
318
+ copyRuntimeBundle(srcDir, runtimeBundleDest);
319
+ success('Installed runtime bundle/');
320
+
302
321
  // 5. Install hooks (Claude Code only for now)
303
322
  if (runtimeKey === 'claude') {
304
323
  const hooksSrc = path.join(srcDir, 'hooks');
@@ -339,6 +358,10 @@ function uninstallForRuntime(runtimeKey) {
339
358
  const agentsDir = path.join(runtime.configDir, 'agents');
340
359
  const templatesDir = path.join(runtime.configDir, 'godpowers-templates');
341
360
  const referencesDir = path.join(runtime.configDir, 'godpowers-references');
361
+ const workflowsDir = path.join(runtime.configDir, 'godpowers-workflows');
362
+ const schemaDir = path.join(runtime.configDir, 'godpowers-schema');
363
+ const routingDir = path.join(runtime.configDir, 'godpowers-routing');
364
+ const runtimeBundleDir = path.join(runtime.configDir, 'godpowers-runtime');
342
365
  const versionFile = path.join(runtime.configDir, 'GODPOWERS_VERSION');
343
366
 
344
367
  let removed = 0;
@@ -366,14 +389,19 @@ function uninstallForRuntime(runtimeKey) {
366
389
  success(`Removed ${agentsRemoved} god-* agent(s)`);
367
390
  }
368
391
 
369
- // Remove templates and references directories
370
- if (fs.existsSync(templatesDir)) {
371
- fs.rmSync(templatesDir, { recursive: true, force: true });
372
- success('Removed godpowers-templates/');
373
- }
374
- if (fs.existsSync(referencesDir)) {
375
- fs.rmSync(referencesDir, { recursive: true, force: true });
376
- success('Removed godpowers-references/');
392
+ // Remove installed data and runtime directories
393
+ for (const dir of [
394
+ templatesDir,
395
+ referencesDir,
396
+ workflowsDir,
397
+ schemaDir,
398
+ routingDir,
399
+ runtimeBundleDir
400
+ ]) {
401
+ if (fs.existsSync(dir)) {
402
+ fs.rmSync(dir, { recursive: true, force: true });
403
+ success(`Removed ${path.basename(dir)}/`);
404
+ }
377
405
  }
378
406
 
379
407
  // Remove hooks (Claude Code only)
package/lib/checkpoint.js CHANGED
@@ -228,6 +228,7 @@ function recordFact(projectRoot, fact) {
228
228
  facts.unshift(f);
229
229
 
230
230
  const state = existing ? frontmatterToState(existing.frontmatter, facts) : { facts };
231
+ state.actions = existing ? existing.actions.slice() : [];
231
232
  state.facts = facts;
232
233
  return write(projectRoot, state);
233
234
  }
@@ -230,12 +230,9 @@ function scan(projectRoot, opts = {}) {
230
230
  */
231
231
  function applyScan(projectRoot, scanResult, opts = {}) {
232
232
  const { links } = scanResult;
233
- let added = 0;
234
- for (const l of links) {
235
- const r = linkage.addLink(projectRoot, l.artifactId, l.file, { source: l.source });
236
- if (r.added) added++;
237
- }
238
- return { totalLinks: links.length, added };
233
+ const source = opts.source || 'code-scanner';
234
+ const result = linkage.bulkReplaceFromSource(projectRoot, source, links);
235
+ return { totalLinks: links.length, added: result.added, removed: result.removed };
239
236
  }
240
237
 
241
238
  // ============================================================================
@@ -79,8 +79,8 @@ function buildCanonicalContent(state, opts = {}) {
79
79
  lines.push('');
80
80
 
81
81
  const projectName = (state && state.project && state.project.name) || opts.projectName || '(unnamed)';
82
- const mode = (state && state.project && state.project.mode) || opts.mode || 'unknown';
83
- const scale = (state && state.project && state.project.scale) || opts.scale || 'unknown';
82
+ const mode = (state && (state.mode || (state.project && state.project.mode))) || opts.mode || 'unknown';
83
+ const scale = (state && (state.scale || (state.project && state.project.scale))) || opts.scale || 'unknown';
84
84
  lines.push(`- Project: ${projectName}`);
85
85
  lines.push(`- Mode: ${mode} Scale: ${scale}`);
86
86
  lines.push('- State: `.godpowers/state.json` (machine) and `.godpowers/PROGRESS.md` (human)');
package/lib/events.js CHANGED
@@ -38,6 +38,13 @@ function eventsPath(projectRoot, runId) {
38
38
  return path.join(projectRoot, '.godpowers', 'runs', runId, 'events.jsonl');
39
39
  }
40
40
 
41
+ function readLastNonEmptyLine(file) {
42
+ const raw = fs.readFileSync(file, 'utf8').trimEnd();
43
+ if (!raw) return null;
44
+ const i = raw.lastIndexOf('\n');
45
+ return i >= 0 ? raw.slice(i + 1) : raw;
46
+ }
47
+
41
48
  /**
42
49
  * Start a new run. Returns a run handle with trace_id and write functions.
43
50
  */
@@ -105,17 +112,10 @@ function emit(file, event) {
105
112
  if (fs.existsSync(file)) {
106
113
  const stat = fs.statSync(file);
107
114
  if (stat.size > 0) {
108
- // Read just the last 4KB to find the last full line.
109
- const fd = fs.openSync(file, 'r');
110
- const chunkSize = Math.min(4096, stat.size);
111
- const buf = Buffer.alloc(chunkSize);
112
- fs.readSync(fd, buf, 0, chunkSize, stat.size - chunkSize);
113
- fs.closeSync(fd);
114
- const tail = buf.toString('utf8');
115
- const lines = tail.split('\n').filter(l => l.trim());
116
- if (lines.length > 0) {
115
+ const line = readLastNonEmptyLine(file);
116
+ if (line) {
117
117
  prev = 'sha256:' + crypto.createHash('sha256')
118
- .update(lines[lines.length - 1])
118
+ .update(line)
119
119
  .digest('hex');
120
120
  }
121
121
  }
package/lib/extensions.js CHANGED
@@ -212,6 +212,7 @@ function install(runtimeConfigDir, sourceDir, godpowersVersion) {
212
212
  }
213
213
 
214
214
  const destDir = path.join(extensionsDir(runtimeConfigDir), manifest.metadata.name);
215
+ fs.rmSync(destDir, { recursive: true, force: true });
215
216
  fs.mkdirSync(destDir, { recursive: true });
216
217
 
217
218
  // Copy manifest + standard pack subdirs if they exist
package/lib/intent.js CHANGED
@@ -38,7 +38,8 @@ function parseSimpleYaml(content) {
38
38
  const result = {};
39
39
  const stack = [{ obj: result, indent: -1, key: null, isArray: false, parent: null }];
40
40
 
41
- for (let line of lines) {
41
+ for (let i = 0; i < lines.length; i++) {
42
+ let line = lines[i];
42
43
  if (!line.trim() || line.trim().startsWith('#')) continue;
43
44
  // Strip inline comments (but not # inside quotes)
44
45
  const hashIdx = line.indexOf(' #');
@@ -60,8 +61,6 @@ function parseSimpleYaml(content) {
60
61
  stack.pop();
61
62
  }
62
63
  const parent = stack[stack.length - 1].obj;
63
- const parentMeta = stack[stack.length - 1];
64
-
65
64
  // List item: "- key: value" or "- value"
66
65
  if (trimmed.startsWith('- ')) {
67
66
  const rest = trimmed.slice(2);
@@ -108,7 +107,9 @@ function parseSimpleYaml(content) {
108
107
  parent[key] = child;
109
108
  stack.push({ obj: child, indent, key, parent });
110
109
  } else if (valueStr === '|' || valueStr === '>') {
111
- parent[key] = '';
110
+ const block = readBlockScalar(lines, i + 1, indent, valueStr === '>');
111
+ parent[key] = block.value;
112
+ i = block.nextIndex - 1;
112
113
  } else {
113
114
  parent[key] = parseValue(valueStr);
114
115
  }
@@ -117,6 +118,37 @@ function parseSimpleYaml(content) {
117
118
  return cleanArrays(result);
118
119
  }
119
120
 
121
+ function readBlockScalar(lines, startIndex, parentIndent, folded) {
122
+ const blockLines = [];
123
+ let i = startIndex;
124
+
125
+ for (; i < lines.length; i++) {
126
+ const raw = lines[i];
127
+ if (!raw.trim()) {
128
+ blockLines.push('');
129
+ continue;
130
+ }
131
+ const indent = raw.length - raw.trimStart().length;
132
+ if (indent <= parentIndent) break;
133
+ blockLines.push(raw);
134
+ }
135
+
136
+ const nonBlankIndents = blockLines
137
+ .filter(line => line.trim())
138
+ .map(line => line.length - line.trimStart().length);
139
+ const trimIndent = nonBlankIndents.length
140
+ ? Math.min(...nonBlankIndents)
141
+ : parentIndent + 2;
142
+ const normalized = blockLines.map(line => (
143
+ line.trim() ? line.slice(Math.min(trimIndent, line.length)) : ''
144
+ ));
145
+
146
+ return {
147
+ value: folded ? normalized.join(' ').replace(/\s+/g, ' ').trim() : normalized.join('\n').trimEnd(),
148
+ nextIndex: i
149
+ };
150
+ }
151
+
120
152
  function parseValue(str) {
121
153
  if (str === 'true') return true;
122
154
  if (str === 'false') return false;
package/lib/linkage.js CHANGED
@@ -8,6 +8,7 @@
8
8
  * Storage:
9
9
  * .godpowers/links/artifact-to-code.json - forward map
10
10
  * .godpowers/links/code-to-artifact.json - reverse map
11
+ * .godpowers/links/link-sources.json - source ownership map
11
12
  * .godpowers/links/LINKAGE-LOG.md - append-only history
12
13
  *
13
14
  * Stable ID format:
@@ -53,6 +54,10 @@ function reversePath(projectRoot) {
53
54
  return path.join(linksDir(projectRoot), 'code-to-artifact.json');
54
55
  }
55
56
 
57
+ function sourcePath(projectRoot) {
58
+ return path.join(linksDir(projectRoot), 'link-sources.json');
59
+ }
60
+
56
61
  function logPath(projectRoot) {
57
62
  return path.join(linksDir(projectRoot), 'LINKAGE-LOG.md');
58
63
  }
@@ -75,6 +80,19 @@ function writeMap(filePath, data) {
75
80
  fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
76
81
  }
77
82
 
83
+ function linkKey(artifactId, filePath) {
84
+ return `${artifactId}\u0000${filePath}`;
85
+ }
86
+
87
+ function parseLinkKey(key) {
88
+ const i = key.indexOf('\u0000');
89
+ return { artifactId: key.slice(0, i), file: key.slice(i + 1) };
90
+ }
91
+
92
+ function readSources(projectRoot) {
93
+ return readMap(sourcePath(projectRoot));
94
+ }
95
+
78
96
  /**
79
97
  * Read forward map: artifact ID -> array of file paths.
80
98
  */
@@ -111,8 +129,16 @@ function addLink(projectRoot, artifactId, filePath, opts = {}) {
111
129
  fwd[artifactId].sort();
112
130
  rev[normFile].sort();
113
131
 
132
+ const sources = readSources(projectRoot);
133
+ const source = opts.source || 'manual';
134
+ const key = linkKey(artifactId, normFile);
135
+ if (!sources[key]) sources[key] = [];
136
+ if (!sources[key].includes(source)) sources[key].push(source);
137
+ sources[key].sort();
138
+
114
139
  writeMap(forwardPath(projectRoot), fwd);
115
140
  writeMap(reversePath(projectRoot), rev);
141
+ writeMap(sourcePath(projectRoot), sources);
116
142
 
117
143
  if (!fwdHad || !revHad) {
118
144
  appendLog(projectRoot, `+ link: ${artifactId} <-> ${normFile}` + (opts.source ? ` (via ${opts.source})` : ''));
@@ -143,8 +169,12 @@ function removeLink(projectRoot, artifactId, filePath) {
143
169
  if (before !== (rev[normFile] ? rev[normFile].length : 0)) removed = true;
144
170
  }
145
171
 
172
+ const sources = readSources(projectRoot);
173
+ delete sources[linkKey(artifactId, normFile)];
174
+
146
175
  writeMap(forwardPath(projectRoot), fwd);
147
176
  writeMap(reversePath(projectRoot), rev);
177
+ writeMap(sourcePath(projectRoot), sources);
148
178
 
149
179
  if (removed) {
150
180
  appendLog(projectRoot, `- link: ${artifactId} <-> ${normFile}`);
@@ -204,12 +234,84 @@ function appendLog(projectRoot, message) {
204
234
  */
205
235
  function bulkReplaceFromSource(projectRoot, source, links) {
206
236
  ensureDir(projectRoot);
207
- const log = [];
237
+ const fwd = readForward(projectRoot);
238
+ const rev = readReverse(projectRoot);
239
+ const sources = readSources(projectRoot);
240
+ const desired = new Set();
241
+ let removed = 0;
242
+ let added = 0;
243
+
208
244
  for (const { artifactId, file } of links) {
209
- const r = addLink(projectRoot, artifactId, file, { source });
210
- if (r.added) log.push(`from ${source}: ${artifactId} -> ${file}`);
245
+ const normFile = path.relative(projectRoot, path.resolve(projectRoot, file));
246
+ desired.add(linkKey(artifactId, normFile));
247
+ }
248
+
249
+ const knownSourceKeys = Object.keys(sources)
250
+ .filter(key => Array.isArray(sources[key]) && sources[key].includes(source));
251
+ const hasSourceMetadata = Object.keys(sources).length > 0;
252
+ const replaceKeys = knownSourceKeys.length > 0
253
+ ? knownSourceKeys
254
+ : hasSourceMetadata
255
+ ? []
256
+ : Object.entries(fwd).flatMap(([artifactId, files]) =>
257
+ files.map(file => linkKey(artifactId, file)));
258
+
259
+ for (const key of replaceKeys) {
260
+ if (desired.has(key)) continue;
261
+ const { artifactId, file } = parseLinkKey(key);
262
+ sources[key] = (sources[key] || []).filter(s => s !== source);
263
+ if (sources[key] && sources[key].length > 0) continue;
264
+
265
+ delete sources[key];
266
+ if (fwd[artifactId]) {
267
+ fwd[artifactId] = fwd[artifactId].filter(f => f !== file);
268
+ if (fwd[artifactId].length === 0) delete fwd[artifactId];
269
+ }
270
+ if (rev[file]) {
271
+ rev[file] = rev[file].filter(id => id !== artifactId);
272
+ if (rev[file].length === 0) delete rev[file];
273
+ }
274
+ appendLog(projectRoot, `- link: ${artifactId} <-> ${file} (via ${source})`);
275
+ removed++;
276
+ }
277
+
278
+ for (const { artifactId, file } of links) {
279
+ const normFile = path.relative(projectRoot, path.resolve(projectRoot, file));
280
+ const key = linkKey(artifactId, normFile);
281
+ if (!fwd[artifactId]) fwd[artifactId] = [];
282
+ if (!rev[normFile]) rev[normFile] = [];
283
+ const hadLink = fwd[artifactId].includes(normFile) && rev[normFile].includes(artifactId);
284
+ if (!fwd[artifactId].includes(normFile)) fwd[artifactId].push(normFile);
285
+ if (!rev[normFile].includes(artifactId)) rev[normFile].push(artifactId);
286
+ if (!sources[key]) sources[key] = [];
287
+ if (!sources[key].includes(source)) sources[key].push(source);
288
+ sources[key].sort();
289
+ if (!hadLink) {
290
+ appendLog(projectRoot, `+ link: ${artifactId} <-> ${normFile} (via ${source})`);
291
+ added++;
292
+ }
293
+ }
294
+
295
+ for (const files of Object.values(fwd)) files.sort();
296
+ for (const ids of Object.values(rev)) ids.sort();
297
+
298
+ writeMap(forwardPath(projectRoot), fwd);
299
+ writeMap(reversePath(projectRoot), rev);
300
+ writeMap(sourcePath(projectRoot), sources);
301
+
302
+ return {
303
+ count: links.length,
304
+ added,
305
+ removed
306
+ };
307
+ }
308
+
309
+ function clearSourceMetadata(projectRoot) {
310
+ ensureDir(projectRoot);
311
+ const file = sourcePath(projectRoot);
312
+ if (fs.existsSync(file)) {
313
+ fs.rmSync(file, { force: true });
211
314
  }
212
- return { count: links.length, added: log.length };
213
315
  }
214
316
 
215
317
  module.exports = {
@@ -225,8 +327,10 @@ module.exports = {
225
327
  coverage,
226
328
  appendLog,
227
329
  bulkReplaceFromSource,
330
+ clearSourceMetadata,
228
331
  forwardPath,
229
332
  reversePath,
333
+ sourcePath,
230
334
  logPath,
231
335
  linksDir
232
336
  };
package/lib/router.js CHANGED
@@ -83,6 +83,12 @@ function checkPrerequisites(command, projectRoot) {
83
83
  * Predicates: file:path, state:dotted.path == value, env:VAR
84
84
  */
85
85
  function evaluateCheck(check, projectRoot) {
86
+ // OR conditions must be handled before prefix-specific checks so both
87
+ // branches can be evaluated as independent predicates.
88
+ if (check.includes(' OR ')) {
89
+ return check.split(' OR ').some(part => evaluateCheck(part.trim(), projectRoot));
90
+ }
91
+
86
92
  // file:path
87
93
  if (check.startsWith('file:')) {
88
94
  const filePath = check.slice(5).trim();
@@ -101,11 +107,6 @@ function evaluateCheck(check, projectRoot) {
101
107
  return actual === expected || actual === parseValue(expected);
102
108
  }
103
109
 
104
- // OR conditions
105
- if (check.includes(' OR ')) {
106
- return check.split(' OR ').some(part => evaluateCheck(part.trim(), projectRoot));
107
- }
108
-
109
110
  // mode-A-greenfield: pass-through hint, treat as satisfiable
110
111
  if (check.includes('greenfield') || check.includes('mode-A')) {
111
112
  return !fs.existsSync(path.join(projectRoot, '.godpowers'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "godpowers",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "AI-powered development system: 104 slash commands and 38 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"
@@ -51,7 +51,9 @@ Show only one category (lifecycle, planning, building, shipping, etc.).
51
51
  Built-in, no spawned agent. Reads:
52
52
  - `<runtime>/skills/*.md` frontmatter
53
53
  - `.godpowers/state.json` (for current state line)
54
- - `lib/recipes.js` (for suggested next)
54
+ - `<runtimeRoot>/lib/recipes.js` (for suggested next)
55
+
56
+ Resolve `<runtimeRoot>` as `<projectRoot>` when `<projectRoot>/lib/recipes.js` exists. Otherwise use the installed bundle at `<tool-config-dir>/godpowers-runtime`, where `<tool-config-dir>` is the directory that contains this installed skill.
55
57
 
56
58
  ## When to use
57
59
 
@@ -3,7 +3,7 @@ name: god-next
3
3
  description: |
4
4
  Decision engine. For any command intent, checks prerequisites, proposes
5
5
  auto-completion of missing prerequisites, runs standards checks at gates,
6
- and suggests next commands after success. Backed by routing/<command>.yaml
6
+ and suggests next commands after success. Backed by runtime routing YAML
7
7
  configurations.
8
8
 
9
9
  Triggers on: "god next", "/god-next", "what's next", "what should I do next",
@@ -15,16 +15,24 @@ description: |
15
15
  The unified decision engine. Routes between commands based on disk state,
16
16
  routing definitions, and user intent.
17
17
 
18
+ ## Runtime module resolution
19
+
20
+ Before reading routing data or calling runtime modules, resolve the Godpowers runtime root:
21
+
22
+ 1. If `<projectRoot>/lib/router.js` exists, use the repository checkout runtime at `<projectRoot>`.
23
+ 2. Otherwise use the installed bundle at `<tool-config-dir>/godpowers-runtime`, where `<tool-config-dir>` is the directory that contains this installed skill, such as `~/.claude`, `~/.codex`, `~/.cursor`, `~/.windsurf`, or `~/.gemini`.
24
+ 3. Read routing definitions from `<runtimeRoot>/routing/*.yaml` and recipes from `<runtimeRoot>/routing/recipes/*.yaml`.
25
+
18
26
  ## Three modes of invocation
19
27
 
20
28
  ### Mode 1: After a command (post-completion routing)
21
29
  The completing skill calls /god-next to determine what's next.
22
- Reads `routing/<just-completed>.yaml`, gets `success-path.next-recommended`,
30
+ Reads `<runtimeRoot>/routing/<just-completed>.yaml`, gets `success-path.next-recommended`,
23
31
  suggests it.
24
32
 
25
33
  ### Mode 2: Before a command (pre-flight)
26
34
  User wants to run /god-X. /god-next checks prerequisites first.
27
- Reads `routing/<X>.yaml`, evaluates prerequisites, proposes auto-completion
35
+ Reads `<runtimeRoot>/routing/<X>.yaml`, evaluates prerequisites, proposes auto-completion
28
36
  if any are missing.
29
37
 
30
38
  ### Mode 3: Standalone (state-driven)
@@ -40,7 +48,7 @@ A skill completes (e.g., /god-prd just finished)
40
48
  Skill calls: /god-next --after=/god-prd
41
49
  |
42
50
  v
43
- Read routing/god-prd.yaml
51
+ Read <runtimeRoot>/routing/god-prd.yaml
44
52
  |
45
53
  v
46
54
  Get success-path.next-recommended (e.g., "/god-arch")
@@ -76,7 +84,7 @@ User types: /god-arch
76
84
  The /god-arch skill calls: /god-next --before=/god-arch
77
85
  |
78
86
  v
79
- Read routing/god-arch.yaml
87
+ Read <runtimeRoot>/routing/god-arch.yaml
80
88
  |
81
89
  v
82
90
  For each prerequisite in prerequisites.required:
@@ -109,7 +117,7 @@ User types: /god-next
109
117
  Read .godpowers/state.json (or PROGRESS.md as fallback)
110
118
  |
111
119
  v
112
- Use lib/router.suggestNext(projectRoot) <- structural next
120
+ Use <runtimeRoot>/lib/router.js suggestNext(projectRoot) <- structural next
113
121
  |
114
122
  v
115
123
  For each tier in order:
@@ -118,7 +126,7 @@ For each tier in order:
118
126
  |
119
127
  v
120
128
  If all tiers done:
121
- - Use lib/recipes.suggestForState(projectRoot) <- scenario-aware
129
+ - Use <runtimeRoot>/lib/recipes.js suggestForState(projectRoot) <- scenario-aware
122
130
  - Returns recipes matching current lifecycle phase
123
131
  |
124
132
  v
@@ -132,7 +140,7 @@ Display: "Suggested next: /god-arch
132
140
  User says: "I need to add a new feature mid-development"
133
141
  |
134
142
  v
135
- /god-next consults lib/recipes.matchIntent(text, projectRoot)
143
+ /god-next consults <runtimeRoot>/lib/recipes.js matchIntent(text, projectRoot)
136
144
  |
137
145
  v
138
146
  Returns ranked recipes matching:
@@ -150,20 +158,20 @@ Display top match with the recipe's sequence:
150
158
  Run this sequence? Or see other matches?"
151
159
  ```
152
160
 
153
- This is the recipe-driven decision support: agents consult `routing/recipes/`
161
+ This is the recipe-driven decision support: agents consult `<runtimeRoot>/routing/recipes/`
154
162
  when user intent is fuzzy or doesn't map to a single command.
155
163
 
156
164
  ## Routing data
157
165
 
158
- Routing definitions live in `routing/*.yaml`. Each command has a file:
159
- - `routing/god-prd.yaml`
160
- - `routing/god-arch.yaml`
166
+ Routing definitions live in `<runtimeRoot>/routing/*.yaml`. Each command has a file:
167
+ - `<runtimeRoot>/routing/god-prd.yaml`
168
+ - `<runtimeRoot>/routing/god-arch.yaml`
161
169
  - ...
162
170
 
163
171
  These define prerequisites, execution, success-path, failure-path, and
164
172
  endoff for each command.
165
173
 
166
- The `lib/router.js` JS module provides programmatic queries:
174
+ The `<runtimeRoot>/lib/router.js` JS module provides programmatic queries:
167
175
  - `getRouting(command)` - load a command's routing
168
176
  - `checkPrerequisites(command, projectRoot)` - prereq check
169
177
  - `getNextCommand(command)` - get success-path next
@@ -23,8 +23,9 @@ Run quality gate check on an artifact.
23
23
  ## Process
24
24
 
25
25
  1. Identify the artifact to check (user provides, or auto-detect from PROGRESS.md)
26
- 2. Look up the routing for the relevant tier (lib/router.js getStandards)
27
- 3. Spawn god-standards-check in fresh context with:
26
+ 2. Resolve the Godpowers runtime root: use `<projectRoot>` when `<projectRoot>/lib/router.js` exists, otherwise use the installed bundle at `<tool-config-dir>/godpowers-runtime`
27
+ 3. Look up the routing for the relevant tier (`<runtimeRoot>/lib/router.js` getStandards)
28
+ 4. Spawn god-standards-check in fresh context with:
28
29
  - artifact-path
29
30
  - tier
30
31
  - have-nots-list (from routing)
@@ -34,4 +34,4 @@ soft if offline).
34
34
 
35
35
  Built-in, no spawned agent. Reads:
36
36
  - `<runtime>/GODPOWERS_VERSION`
37
- - File counts in `<runtime>/skills/`, `<runtime>/agents/`, `workflows/`, `routing/recipes/`
37
+ - File counts in `<runtime>/skills/`, `<runtime>/agents/`, `<runtime>/godpowers-runtime/workflows/`, and `<runtime>/godpowers-runtime/routing/recipes/`
package/skills/god.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: god
3
3
  description: |
4
4
  Front door. Take free-text intent from the user, match it to a recipe via
5
- lib/recipes.matchIntent, and propose the matching command sequence. If no
5
+ the Godpowers runtime recipes module, and propose the matching command sequence. If no
6
6
  text given, fall back to state-driven suggestion (same as /god-next Mode 3).
7
7
 
8
8
  Triggers on: "/god", "god", "/god help", "I want to ...", "how do I ..."
@@ -13,7 +13,16 @@ description: |
13
13
 
14
14
  The natural-language entry point. Users describe what they want; this skill
15
15
  matches the intent to a recipe and suggests the right command sequence. No
16
- agent is spawned here. This is a thin router on top of `lib/recipes.js`.
16
+ agent is spawned here. This is a thin router on top of the Godpowers runtime
17
+ recipes module.
18
+
19
+ ## Runtime module resolution
20
+
21
+ Before calling runtime modules, resolve the Godpowers runtime root:
22
+
23
+ 1. If `<projectRoot>/lib/recipes.js` exists, use the repository checkout runtime at `<projectRoot>`.
24
+ 2. Otherwise use the installed bundle at `<tool-config-dir>/godpowers-runtime`, where `<tool-config-dir>` is the directory that contains this installed skill, such as `~/.claude`, `~/.codex`, `~/.cursor`, `~/.windsurf`, or `~/.gemini`.
25
+ 3. Load recipes from `<runtimeRoot>/lib/recipes.js`, routing from `<runtimeRoot>/lib/router.js`, and recipe YAML from `<runtimeRoot>/routing/recipes/`.
17
26
 
18
27
  ## Why this exists
19
28
 
@@ -50,11 +59,11 @@ Treat everything after `/god` as free text. If empty, treat as state-driven.
50
59
 
51
60
  ```
52
61
  text empty?
53
- yes -> state-driven: call lib/recipes.suggestForState(projectRoot)
62
+ yes -> state-driven: call <runtimeRoot>/lib/recipes.js suggestForState(projectRoot)
54
63
  display top 3 recipes ranked by current lifecycle phase
55
- also call lib/router.suggestNext(projectRoot) for structural next
64
+ also call <runtimeRoot>/lib/router.js suggestNext(projectRoot) for structural next
56
65
 
57
- no -> intent-driven: call lib/recipes.matchIntent(text, projectRoot)
66
+ no -> intent-driven: call <runtimeRoot>/lib/recipes.js matchIntent(text, projectRoot)
58
67
  take top 1-3 matches by score
59
68
  if highest score >= 10 (exact phrase match): propose directly
60
69
  if highest score 5-9 (all-words match): propose with confirmation
@@ -116,8 +125,8 @@ If user picks the structural next:
116
125
  ## Interaction model
117
126
 
118
127
  This skill is a router, not an orchestrator. It:
119
- - Reads recipes (via `lib/recipes.js`)
120
- - Reads state (via `lib/state.js`)
128
+ - Reads recipes (via `<runtimeRoot>/lib/recipes.js`)
129
+ - Reads state (via `<runtimeRoot>/lib/state.js`)
121
130
  - Proposes commands
122
131
 
123
132
  It does NOT:
@@ -194,7 +203,7 @@ Suggested: /god-next show all valid next-step options
194
203
  ## Why a skill, not an agent
195
204
 
196
205
  The matching and dispatch logic is mechanical (lookups against
197
- `routing/recipes/*.yaml`) and has no need for fresh-context isolation. Running
206
+ `<runtimeRoot>/routing/recipes/*.yaml`) and has no need for fresh-context isolation. Running
198
207
  it as a skill keeps it fast, lets the user see the proposed commands, and
199
208
  avoids stacking another orchestrator layer above `god-orchestrator`. See
200
209
  `docs/concepts.md` (the Quarterback section) for why we don't add a second