godpowers 1.6.20 → 1.6.22

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 (34) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +28 -9
  3. package/RELEASE.md +38 -21
  4. package/bin/install.js +81 -1
  5. package/fixtures/dogfood/extension-authoring/manifest.json +13 -0
  6. package/fixtures/dogfood/half-migrated-gsd/.planning/PROJECT.md +5 -0
  7. package/fixtures/dogfood/half-migrated-gsd/.planning/REQUIREMENTS.md +5 -0
  8. package/fixtures/dogfood/half-migrated-gsd/.planning/ROADMAP.md +5 -0
  9. package/fixtures/dogfood/half-migrated-gsd/manifest.json +16 -0
  10. package/fixtures/dogfood/host-degraded/manifest.json +5 -0
  11. package/fixtures/dogfood/host-full/home/.codex/agents/god-orchestrator.toml +2 -0
  12. package/fixtures/dogfood/host-full/manifest.json +5 -0
  13. package/fixtures/dogfood/suite-release-dry-run/.godpowers/suite-config.yaml +9 -0
  14. package/fixtures/dogfood/suite-release-dry-run/manifest.json +7 -0
  15. package/fixtures/dogfood/suite-release-dry-run/repo-a/package.json +4 -0
  16. package/fixtures/dogfood/suite-release-dry-run/repo-b/package.json +7 -0
  17. package/lib/README.md +3 -0
  18. package/lib/dashboard.js +73 -3
  19. package/lib/dogfood-runner.js +193 -0
  20. package/lib/extension-authoring.js +154 -0
  21. package/lib/feature-awareness.js +18 -0
  22. package/lib/host-capabilities.js +125 -0
  23. package/lib/release-surface-sync.js +30 -0
  24. package/lib/repo-surface-sync.js +128 -0
  25. package/lib/route-quality-sync.js +31 -4
  26. package/lib/suite-state.js +90 -1
  27. package/package.json +4 -3
  28. package/routing/god-dogfood.yaml +35 -0
  29. package/routing/god-init.yaml +1 -1
  30. package/routing/god-roadmap-update.yaml +1 -1
  31. package/routing/god-sync.yaml +1 -1
  32. package/skills/god-doctor.md +1 -1
  33. package/skills/god-dogfood.md +63 -0
  34. package/skills/god-version.md +1 -1
package/lib/dashboard.js CHANGED
@@ -14,6 +14,7 @@ const router = require('./router');
14
14
  const automationProviders = require('./automation-providers');
15
15
  const repoDocSync = require('./repo-doc-sync');
16
16
  const repoSurfaceSync = require('./repo-surface-sync');
17
+ const hostCapabilities = require('./host-capabilities');
17
18
 
18
19
  const GOD_DIR = '.godpowers';
19
20
  const PRD_PATH = '.godpowers/prd/PRD.md';
@@ -168,6 +169,7 @@ function proactiveChecks(projectRoot, changedFiles = []) {
168
169
  const repoSurfaceStatus = repoSurface.status === 'fresh'
169
170
  ? 'fresh'
170
171
  : `${repoSurface.stale.length} stale, suggest /god-doctor`;
172
+ const host = hostCapabilities.detect(projectRoot);
171
173
 
172
174
  return {
173
175
  checkpoint,
@@ -175,6 +177,7 @@ function proactiveChecks(projectRoot, changedFiles = []) {
175
177
  sync,
176
178
  docs: repoDocsStatus,
177
179
  repoSurface: repoSurfaceStatus,
180
+ host: hostCapabilities.summary(host),
178
181
  runtime: 'not-applicable',
179
182
  automation: automationSummary(projectRoot),
180
183
  security: sensitiveChanged ? 'sensitive files changed, suggest /god-harden' : 'clear',
@@ -217,7 +220,7 @@ function compute(projectRoot, opts = {}) {
217
220
 
218
221
  if (!s) {
219
222
  const next = { command: '/god-init', reason: 'No Godpowers project initialized' };
220
- return {
223
+ const result = {
221
224
  source: 'runtime dashboard (lib/dashboard.js)',
222
225
  state: 'not initialized',
223
226
  mode: null,
@@ -234,9 +237,12 @@ function compute(projectRoot, opts = {}) {
234
237
  completionBasis: 'missing .godpowers/state.json'
235
238
  },
236
239
  proactive: proactiveChecks(projectRoot, git.entries.map(statusPath)),
240
+ host: hostCapabilities.detect(projectRoot, opts.host || {}),
237
241
  next,
238
242
  openItems: ['No .godpowers/state.json found']
239
243
  };
244
+ result.actionBrief = actionBrief(result);
245
+ return result;
240
246
  }
241
247
 
242
248
  const progress = state.progressSummary(s);
@@ -250,7 +256,7 @@ function compute(projectRoot, opts = {}) {
250
256
  if (reviewCount(projectRoot) > 0) openItems.push('pending review items');
251
257
  if (openItems.length === 0) openItems.push('none');
252
258
 
253
- return {
259
+ const result = {
254
260
  source: 'runtime dashboard (lib/dashboard.js)',
255
261
  state: progress.remaining === 0 ? 'complete' : 'in progress',
256
262
  mode: s.mode || s['mode-announced-as'] || null,
@@ -261,12 +267,46 @@ function compute(projectRoot, opts = {}) {
261
267
  index: git.index,
262
268
  planning: planningVisibility(projectRoot, progress),
263
269
  proactive: proactiveChecks(projectRoot, git.entries.map(statusPath)),
270
+ host: hostCapabilities.detect(projectRoot, opts.host || {}),
264
271
  next,
265
272
  openItems
266
273
  };
274
+ result.actionBrief = actionBrief(result);
275
+ return result;
276
+ }
277
+
278
+ function actionBrief(dashboard) {
279
+ const proactive = dashboard.proactive || {};
280
+ const next = dashboard.next || {};
281
+ const blockers = [];
282
+ for (const [label, value] of [
283
+ ['Repo surface', proactive.repoSurface],
284
+ ['Docs', proactive.docs],
285
+ ['Host', proactive.host],
286
+ ['Reviews', proactive.reviews],
287
+ ['Sync', proactive.sync],
288
+ ['Security', proactive.security],
289
+ ['Dependencies', proactive.dependencies],
290
+ ['Hygiene', proactive.hygiene]
291
+ ]) {
292
+ if (!value) continue;
293
+ if (value === 'fresh' || value === 'none' || value === 'clear' || value === 'not-applicable') continue;
294
+ if (/^full on /.test(value)) continue;
295
+ if (/^available via /.test(value)) continue;
296
+ blockers.push(`${label}: ${value}`);
297
+ }
298
+
299
+ const recommended = next.command || 'describe the next intent';
300
+ return {
301
+ recommended,
302
+ reason: next.reason || 'No route was computed.',
303
+ confidence: blockers.length === 0 ? 'ready' : 'needs attention',
304
+ blockers: blockers.slice(0, 3),
305
+ overflow: Math.max(0, blockers.length - 3)
306
+ };
267
307
  }
268
308
 
269
- function render(dashboard) {
309
+ function render(dashboard, opts = {}) {
270
310
  const current = dashboard.current || {};
271
311
  const planning = dashboard.planning || {};
272
312
  const proactive = dashboard.proactive || {};
@@ -277,6 +317,27 @@ function render(dashboard) {
277
317
  const openItems = dashboard.openItems && dashboard.openItems.length > 0
278
318
  ? dashboard.openItems
279
319
  : ['none'];
320
+ const brief = dashboard.actionBrief || actionBrief(dashboard);
321
+ if (opts.mode === 'brief' || opts.brief === true) {
322
+ return [
323
+ 'Godpowers Dashboard',
324
+ '',
325
+ 'Action brief:',
326
+ ` Next: ${brief.recommended}`,
327
+ ` Why: ${brief.reason}`,
328
+ ` Readiness: ${brief.confidence}`,
329
+ ` Attention: ${brief.blockers && brief.blockers.length > 0 ? brief.blockers.join('; ') : 'none'}${brief.overflow ? `; ${brief.overflow} more` : ''}`,
330
+ ` Host guarantees: ${dashboard.host ? hostCapabilities.summary(dashboard.host) : proactive.host || 'unknown'}`,
331
+ '',
332
+ 'Current status:',
333
+ ` State: ${dashboard.state}`,
334
+ ` Progress: ${progress.percent || 0}% workflow progress (${progress.completed || 0} of ${progress.total || 0} tracked steps complete)`,
335
+ '',
336
+ 'Next:',
337
+ ` Recommended: ${next.command || 'describe the next intent'}`,
338
+ ` Why: ${next.reason || 'No route was computed.'}`
339
+ ].join('\n');
340
+ }
280
341
 
281
342
  return [
282
343
  'Godpowers Dashboard',
@@ -291,6 +352,13 @@ function render(dashboard) {
291
352
  ` Worktree: ${dashboard.worktree}`,
292
353
  ` Index: ${dashboard.index}`,
293
354
  '',
355
+ 'Action brief:',
356
+ ` Next: ${brief.recommended}`,
357
+ ` Why: ${brief.reason}`,
358
+ ` Readiness: ${brief.confidence}`,
359
+ ` Attention: ${brief.blockers && brief.blockers.length > 0 ? brief.blockers.join('; ') : 'none'}${brief.overflow ? `; ${brief.overflow} more` : ''}`,
360
+ ` Host guarantees: ${dashboard.host ? hostCapabilities.summary(dashboard.host) : proactive.host || 'unknown'}`,
361
+ '',
294
362
  'Planning visibility:',
295
363
  ` PRD: ${prd.status || 'missing'}${prd.path ? ` ${prd.path}` : ''}`,
296
364
  ` Roadmap: ${roadmap.status || 'missing'}${roadmap.path ? ` ${roadmap.path}` : ''}`,
@@ -303,6 +371,7 @@ function render(dashboard) {
303
371
  ` Sync: ${proactive.sync || 'unknown'}`,
304
372
  ` Docs: ${proactive.docs || 'unknown'}`,
305
373
  ` Repo surface: ${proactive.repoSurface || 'unknown'}`,
374
+ ` Host: ${proactive.host || 'unknown'}`,
306
375
  ` Runtime: ${proactive.runtime || 'unknown'}`,
307
376
  ` Automation: ${proactive.automation || 'unknown'}`,
308
377
  ` Security: ${proactive.security || 'unknown'}`,
@@ -325,5 +394,6 @@ module.exports = {
325
394
  parseGitStatus,
326
395
  proactiveChecks,
327
396
  automationSummary,
397
+ actionBrief,
328
398
  planningVisibility
329
399
  };
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Dogfood runner.
3
+ *
4
+ * Executes deterministic messy-repo scenarios against the Godpowers runtime.
5
+ * The runner uses fixtures so release gates can verify migrations, sync-back,
6
+ * host guarantees, package surfaces, extension authoring, and suite planning
7
+ * without touching real user projects.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const os = require('os');
13
+
14
+ const planningSystems = require('./planning-systems');
15
+ const sourceSync = require('./source-sync');
16
+ const hostCapabilities = require('./host-capabilities');
17
+ const extensionAuthoring = require('./extension-authoring');
18
+ const suiteState = require('./suite-state');
19
+
20
+ const FIXTURE_ROOT = path.join(__dirname, '..', 'fixtures', 'dogfood');
21
+
22
+ function readJson(file) {
23
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
24
+ }
25
+
26
+ function copyDir(src, dest) {
27
+ fs.mkdirSync(dest, { recursive: true });
28
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
29
+ if (entry.name === 'manifest.json') continue;
30
+ const source = path.join(src, entry.name);
31
+ const target = path.join(dest, entry.name);
32
+ if (entry.isDirectory()) copyDir(source, target);
33
+ else fs.copyFileSync(source, target);
34
+ }
35
+ }
36
+
37
+ function listScenarios(root = FIXTURE_ROOT) {
38
+ if (!fs.existsSync(root)) return [];
39
+ return fs.readdirSync(root, { withFileTypes: true })
40
+ .filter((entry) => entry.isDirectory())
41
+ .map((entry) => {
42
+ const dir = path.join(root, entry.name);
43
+ const manifest = readJson(path.join(dir, 'manifest.json'));
44
+ return { id: entry.name, dir, manifest };
45
+ })
46
+ .sort((a, b) => a.id.localeCompare(b.id));
47
+ }
48
+
49
+ function prepareScenario(scenario) {
50
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `godpowers-dogfood-${scenario.id}-`));
51
+ copyDir(scenario.dir, tmp);
52
+ return tmp;
53
+ }
54
+
55
+ function check(condition, id, message, details = {}) {
56
+ return {
57
+ id,
58
+ status: condition ? 'pass' : 'fail',
59
+ message,
60
+ details
61
+ };
62
+ }
63
+
64
+ function runMigrationScenario(projectRoot, manifest) {
65
+ const detection = planningSystems.detect(projectRoot);
66
+ const report = { detection, importResult: null, syncResult: null };
67
+ if (manifest.actions && manifest.actions.includes('import-planning-context')) {
68
+ report.importResult = planningSystems.importPlanningContext(projectRoot, { detection });
69
+ }
70
+ if (manifest.actions && manifest.actions.includes('sync-back')) {
71
+ report.syncResult = sourceSync.run(projectRoot);
72
+ }
73
+ return report;
74
+ }
75
+
76
+ function runExtensionScenario(projectRoot, manifest) {
77
+ const packName = manifest.extensionName || '@godpowers/dogfood-pack';
78
+ const target = path.join(projectRoot, 'extension-out');
79
+ return extensionAuthoring.scaffold(target, {
80
+ name: packName,
81
+ version: '0.1.0',
82
+ skill: 'god-dogfood-extension',
83
+ agent: 'god-dogfood-agent',
84
+ workflow: 'dogfood-workflow',
85
+ godpowersRange: '>=1.6.0'
86
+ });
87
+ }
88
+
89
+ function runSuiteScenario(projectRoot, manifest) {
90
+ const plan = suiteState.planRelease(projectRoot, manifest.repo || 'repo-a', manifest.version || '1.2.4');
91
+ return plan;
92
+ }
93
+
94
+ function runScenario(scenario, opts = {}) {
95
+ const projectRoot = opts.projectRoot || prepareScenario(scenario);
96
+ const manifest = scenario.manifest;
97
+ const checks = [];
98
+ const context = {};
99
+
100
+ if (manifest.kind === 'planning-migration') {
101
+ context.migration = runMigrationScenario(projectRoot, manifest);
102
+ const detected = context.migration.detection.systems.map((system) => system.id);
103
+ for (const id of manifest.expectedSystems || []) {
104
+ checks.push(check(detected.includes(id), `detect-${id}`, `Detected ${id}.`, { detected }));
105
+ }
106
+ for (const relPath of manifest.expectedFiles || []) {
107
+ checks.push(check(fs.existsSync(path.join(projectRoot, relPath)), `file-${relPath}`, `${relPath} exists.`));
108
+ }
109
+ }
110
+
111
+ if (manifest.kind === 'host-capabilities') {
112
+ context.host = hostCapabilities.detect(projectRoot, {
113
+ homeDir: path.join(projectRoot, 'home'),
114
+ env: { SHELL: '/bin/zsh', CODEX_HOME: path.join(projectRoot, 'home', '.codex') }
115
+ });
116
+ checks.push(check(context.host.level === manifest.expectedLevel,
117
+ 'host-level',
118
+ `Host capability level is ${manifest.expectedLevel}.`,
119
+ { level: context.host.level }));
120
+ }
121
+
122
+ if (manifest.kind === 'extension-authoring') {
123
+ context.extension = runExtensionScenario(projectRoot, manifest);
124
+ for (const relPath of manifest.expectedFiles || []) {
125
+ checks.push(check(fs.existsSync(path.join(context.extension.path, relPath)),
126
+ `extension-file-${relPath}`,
127
+ `Extension file ${relPath} exists.`));
128
+ }
129
+ checks.push(check(context.extension.validation.length === 0,
130
+ 'extension-validation',
131
+ 'Scaffolded extension validates.',
132
+ { validation: context.extension.validation }));
133
+ }
134
+
135
+ if (manifest.kind === 'suite-release') {
136
+ context.suite = runSuiteScenario(projectRoot, manifest);
137
+ checks.push(check(context.suite.mode === 'dry-run',
138
+ 'suite-dry-run',
139
+ 'Suite release plan is dry-run mode.'));
140
+ checks.push(check(context.suite.impacted.length === (manifest.expectedImpacted || 0),
141
+ 'suite-impact-count',
142
+ `Suite release impact count is ${manifest.expectedImpacted || 0}.`,
143
+ { impacted: context.suite.impacted }));
144
+ }
145
+
146
+ const failed = checks.filter((item) => item.status !== 'pass');
147
+ return {
148
+ id: scenario.id,
149
+ name: manifest.name || scenario.id,
150
+ projectRoot,
151
+ status: failed.length === 0 ? 'pass' : 'fail',
152
+ checks,
153
+ context
154
+ };
155
+ }
156
+
157
+ function runAll(opts = {}) {
158
+ const scenarios = listScenarios(opts.fixtureRoot);
159
+ const results = scenarios.map((scenario) => runScenario(scenario, opts));
160
+ const failed = results.filter((result) => result.status !== 'pass');
161
+ return {
162
+ status: failed.length === 0 ? 'pass' : 'fail',
163
+ total: results.length,
164
+ passed: results.length - failed.length,
165
+ failed: failed.length,
166
+ results
167
+ };
168
+ }
169
+
170
+ function render(report) {
171
+ const lines = [];
172
+ lines.push('Godpowers Dogfood Report');
173
+ lines.push('');
174
+ lines.push(`Status: ${report.status}`);
175
+ lines.push(`Scenarios: ${report.passed} passed, ${report.failed} failed, ${report.total} total`);
176
+ for (const result of report.results) {
177
+ lines.push('');
178
+ lines.push(`## ${result.name}`);
179
+ lines.push(`Status: ${result.status}`);
180
+ for (const item of result.checks) {
181
+ lines.push(`- [${item.status.toUpperCase()}] ${item.message}`);
182
+ }
183
+ }
184
+ return lines.join('\n');
185
+ }
186
+
187
+ module.exports = {
188
+ FIXTURE_ROOT,
189
+ listScenarios,
190
+ runScenario,
191
+ runAll,
192
+ render
193
+ };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Extension authoring helpers.
3
+ *
4
+ * Creates a publishable Godpowers extension skeleton that follows the same
5
+ * manifest, package, README, skill, agent, and workflow contracts as the
6
+ * first-party packs.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const extensions = require('./extensions');
13
+
14
+ function ensureDir(filePath) {
15
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
16
+ }
17
+
18
+ function write(filePath, content, overwrite) {
19
+ if (fs.existsSync(filePath) && overwrite !== true) return false;
20
+ ensureDir(filePath);
21
+ fs.writeFileSync(filePath, content);
22
+ return true;
23
+ }
24
+
25
+ function packageFolderName(name) {
26
+ return name.replace(/^@/, '').replace(/\//g, '-');
27
+ }
28
+
29
+ function scaffold(outputRoot, opts = {}) {
30
+ const name = opts.name || '@godpowers/custom-pack';
31
+ const version = opts.version || '0.1.0';
32
+ const skill = opts.skill || 'god-custom-pack';
33
+ const agent = opts.agent || null;
34
+ const workflow = opts.workflow || null;
35
+ const range = opts.godpowersRange || '>=1.6.0';
36
+ const folder = opts.folder || packageFolderName(name);
37
+ const root = path.join(outputRoot, folder);
38
+ const written = [];
39
+
40
+ const manifest = [
41
+ 'apiVersion: godpowers/v1',
42
+ 'kind: Extension',
43
+ 'metadata:',
44
+ ` name: ${name}`,
45
+ ` version: ${version}`,
46
+ ' description: Custom Godpowers extension pack.',
47
+ 'engines:',
48
+ ` godpowers: ${range}`,
49
+ 'provides:',
50
+ ' skills:',
51
+ ` - ${skill}`,
52
+ ...(agent ? [' agents:', ` - ${agent}`] : []),
53
+ ...(workflow ? [' workflows:', ` - ${workflow}`] : []),
54
+ ''
55
+ ].join('\n');
56
+
57
+ const pkg = {
58
+ name,
59
+ version,
60
+ description: 'Custom Godpowers extension pack.',
61
+ license: 'MIT',
62
+ files: [
63
+ 'manifest.yaml',
64
+ 'README.md',
65
+ 'skills/',
66
+ 'agents/',
67
+ 'workflows/',
68
+ 'references/'
69
+ ],
70
+ peerDependencies: {
71
+ godpowers: range
72
+ },
73
+ publishConfig: {
74
+ access: 'public'
75
+ }
76
+ };
77
+
78
+ const readme = [
79
+ `# ${name}`,
80
+ '',
81
+ 'Custom Godpowers extension pack.',
82
+ '',
83
+ '## Contents',
84
+ '',
85
+ `- Skill: ${skill}`,
86
+ ...(agent ? [`- Agent: ${agent}`] : []),
87
+ ...(workflow ? [`- Workflow: ${workflow}`] : []),
88
+ ''
89
+ ].join('\n');
90
+
91
+ const skillMd = [
92
+ '---',
93
+ `name: ${skill}`,
94
+ 'description: |',
95
+ ' Custom Godpowers extension command.',
96
+ '',
97
+ ` Triggers on: "${skill.replace(/^god-/, 'god ')}", "/${skill}"`,
98
+ '---',
99
+ '',
100
+ `# /${skill}`,
101
+ '',
102
+ '- [DECISION] This extension command is scaffolded for project-specific behavior.',
103
+ '- [OPEN QUESTION] Replace this placeholder with the command workflow. Owner: extension author. Due: before publish.',
104
+ ''
105
+ ].join('\n');
106
+
107
+ if (write(path.join(root, 'manifest.yaml'), manifest, opts.overwrite)) written.push('manifest.yaml');
108
+ if (write(path.join(root, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`, opts.overwrite)) written.push('package.json');
109
+ if (write(path.join(root, 'README.md'), readme, opts.overwrite)) written.push('README.md');
110
+ if (write(path.join(root, 'skills', `${skill}.md`), skillMd, opts.overwrite)) written.push(`skills/${skill}.md`);
111
+
112
+ if (agent) {
113
+ const agentMd = [
114
+ '---',
115
+ `name: ${agent}`,
116
+ 'description: Custom Godpowers extension specialist.',
117
+ '---',
118
+ '',
119
+ `# ${agent}`,
120
+ '',
121
+ '- [DECISION] Follow the extension command handoff and return only user-facing progress.',
122
+ ''
123
+ ].join('\n');
124
+ if (write(path.join(root, 'agents', `${agent}.md`), agentMd, opts.overwrite)) written.push(`agents/${agent}.md`);
125
+ }
126
+
127
+ if (workflow) {
128
+ const workflowYaml = [
129
+ 'apiVersion: godpowers/v1',
130
+ 'kind: Workflow',
131
+ `name: ${workflow}`,
132
+ 'steps:',
133
+ ` - command: /${skill}`,
134
+ ''
135
+ ].join('\n');
136
+ if (write(path.join(root, 'workflows', `${workflow}.yaml`), workflowYaml, opts.overwrite)) written.push(`workflows/${workflow}.yaml`);
137
+ }
138
+
139
+ const parsed = extensions.parseManifest(fs.readFileSync(path.join(root, 'manifest.yaml'), 'utf8')).manifest;
140
+ const validation = extensions.validateManifest(parsed, opts.runtimeVersion || '1.6.0');
141
+
142
+ return {
143
+ path: root,
144
+ name,
145
+ version,
146
+ written,
147
+ validation
148
+ };
149
+ }
150
+
151
+ module.exports = {
152
+ scaffold,
153
+ packageFolderName
154
+ };
@@ -64,6 +64,24 @@ const FEATURES = [
64
64
  since: '1.6.19',
65
65
  commands: ['/god-sync', '/god-docs', '/god-doctor', '/god-status', '/god-mode'],
66
66
  description: 'Detect release-facing drift across badges, release notes, changelog, package checks, and release checklist policy.'
67
+ },
68
+ {
69
+ id: 'dogfood-runner',
70
+ since: '1.6.22',
71
+ commands: ['/god-dogfood'],
72
+ description: 'Run messy-repo fixtures for migration, host guarantee, extension authoring, and suite release readiness.'
73
+ },
74
+ {
75
+ id: 'host-capabilities',
76
+ since: '1.6.22',
77
+ commands: ['/god-status', '/god-next'],
78
+ description: 'Report full, degraded, or unknown host guarantees in dashboard output.'
79
+ },
80
+ {
81
+ id: 'extension-authoring',
82
+ since: '1.6.22',
83
+ commands: ['/god-extension-add', '/god-test-extension'],
84
+ description: 'Scaffold and validate publishable Godpowers extension packs.'
67
85
  }
68
86
  ];
69
87
 
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Host capability detection.
3
+ *
4
+ * Reports what the current AI coding host can actually guarantee at runtime.
5
+ * This keeps Godpowers honest when true fresh-context spawning or release
6
+ * tools depend on the host environment.
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+ const cp = require('child_process');
13
+
14
+ function exists(filePath) {
15
+ return fs.existsSync(filePath);
16
+ }
17
+
18
+ function commandVersion(command, args, opts = {}) {
19
+ try {
20
+ const out = cp.execFileSync(command, args, {
21
+ cwd: opts.cwd || process.cwd(),
22
+ encoding: 'utf8',
23
+ stdio: ['ignore', 'pipe', 'ignore'],
24
+ timeout: opts.timeout || 1500
25
+ }).trim();
26
+ return out.split(/\r?\n/)[0] || 'installed';
27
+ } catch (err) {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function hostName(env) {
33
+ if (env.CODEX_HOME || env.CODEX_SANDBOX || env.CODEX_ENV_PWD) return 'codex';
34
+ if (env.CLAUDECODE || env.CLAUDE_CODE || env.CLAUDE_CONFIG_DIR) return 'claude';
35
+ if (env.CURSOR_TRACE_ID || env.CURSOR_AGENT) return 'cursor';
36
+ if (env.WINDSURF) return 'windsurf';
37
+ return 'unknown';
38
+ }
39
+
40
+ function installedAgentSurfaces(homeDir) {
41
+ const codexAgents = path.join(homeDir, '.codex', 'agents');
42
+ const claudeAgents = path.join(homeDir, '.claude', 'agents');
43
+ return {
44
+ codex: exists(path.join(codexAgents, 'god-orchestrator.toml'))
45
+ || exists(path.join(codexAgents, 'god-orchestrator.md')),
46
+ claude: exists(path.join(claudeAgents, 'god-orchestrator.md'))
47
+ };
48
+ }
49
+
50
+ function detect(projectRoot, opts = {}) {
51
+ const env = opts.env || process.env;
52
+ const homeDir = opts.homeDir || os.homedir();
53
+ const root = projectRoot || process.cwd();
54
+ const installedAgents = opts.installedAgents || installedAgentSurfaces(homeDir);
55
+ const git = commandVersion('git', ['--version'], { cwd: root });
56
+ const npm = commandVersion('npm', ['--version'], { cwd: root });
57
+ const gh = commandVersion('gh', ['--version'], { cwd: root });
58
+ const shell = Boolean(env.SHELL || env.ComSpec);
59
+ const agentSpawn = Boolean(installedAgents.codex || installedAgents.claude || opts.agentSpawn);
60
+ const extensionAuthoring = exists(path.join(root, 'lib', 'extension-authoring.js'))
61
+ && exists(path.join(root, 'schema', 'extension-manifest.v1.json'));
62
+ const suiteReleaseDryRun = exists(path.join(root, 'lib', 'suite-state.js'));
63
+
64
+ const gaps = [];
65
+ if (!shell) gaps.push('shell unavailable');
66
+ if (!git) gaps.push('git unavailable');
67
+ if (!npm) gaps.push('npm unavailable');
68
+ if (!agentSpawn) gaps.push('fresh-context agent spawn not detected');
69
+ if (!extensionAuthoring) gaps.push('extension authoring scaffold unavailable');
70
+ if (!suiteReleaseDryRun) gaps.push('suite release dry-run unavailable');
71
+
72
+ let level = 'unknown';
73
+ if (shell && git && npm && agentSpawn) level = 'full';
74
+ else if (shell && git && npm) level = 'degraded';
75
+
76
+ return {
77
+ host: opts.host || hostName(env),
78
+ level,
79
+ guarantees: {
80
+ shell,
81
+ fileEdit: true,
82
+ node: process.version,
83
+ git,
84
+ npm,
85
+ gh,
86
+ agentSpawn,
87
+ extensionAuthoring,
88
+ suiteReleaseDryRun
89
+ },
90
+ installedAgents,
91
+ gaps
92
+ };
93
+ }
94
+
95
+ function summary(report) {
96
+ if (!report) return 'unknown';
97
+ if (report.level === 'full') return `full on ${report.host}`;
98
+ const gap = report.gaps && report.gaps.length > 0 ? `, ${report.gaps[0]}` : '';
99
+ return `${report.level} on ${report.host}${gap}`;
100
+ }
101
+
102
+ function render(report) {
103
+ const lines = [];
104
+ lines.push('Host capabilities:');
105
+ lines.push(` Host: ${report.host}`);
106
+ lines.push(` Guarantee level: ${report.level}`);
107
+ lines.push(` Agent spawn: ${report.guarantees.agentSpawn ? 'detected' : 'not detected'}`);
108
+ lines.push(` Shell: ${report.guarantees.shell ? 'detected' : 'not detected'}`);
109
+ lines.push(` Git: ${report.guarantees.git || 'not detected'}`);
110
+ lines.push(` npm: ${report.guarantees.npm || 'not detected'}`);
111
+ lines.push(` GitHub CLI: ${report.guarantees.gh || 'not detected'}`);
112
+ lines.push(` Gaps: ${report.gaps.length > 0 ? report.gaps.join('; ') : 'none'}`);
113
+ return lines.join('\n');
114
+ }
115
+
116
+ module.exports = {
117
+ detect,
118
+ summary,
119
+ render,
120
+ _private: {
121
+ commandVersion,
122
+ hostName,
123
+ installedAgentSurfaces
124
+ }
125
+ };