godpowers 2.2.1 → 2.3.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 (81) hide show
  1. package/CHANGELOG.md +65 -7
  2. package/INSPIRATION.md +4 -2
  3. package/README.md +35 -22
  4. package/RELEASE.md +44 -23
  5. package/SKILL.md +2 -2
  6. package/agents/god-auditor.md +1 -1
  7. package/agents/god-debugger.md +44 -8
  8. package/agents/god-deps-auditor.md +11 -0
  9. package/agents/god-executor.md +50 -5
  10. package/agents/god-greenfieldifier.md +1 -1
  11. package/agents/god-orchestrator.md +2 -2
  12. package/agents/god-planner.md +14 -0
  13. package/agents/god-pm.md +1 -1
  14. package/agents/god-quality-reviewer.md +23 -1
  15. package/agents/god-reconciler.md +1 -1
  16. package/agents/god-roadmapper.md +1 -1
  17. package/agents/god-spec-reviewer.md +16 -0
  18. package/agents/god-stack-selector.md +4 -0
  19. package/agents/god-updater.md +1 -1
  20. package/bin/install.js +9 -3
  21. package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/manifest.json +2 -2
  22. package/lib/README.md +9 -2
  23. package/lib/agent-cache.js +25 -3
  24. package/lib/atomic-write.js +85 -0
  25. package/lib/checkpoint.js +2 -1
  26. package/lib/code-intelligence.js +161 -0
  27. package/lib/context-writer.js +1 -1
  28. package/lib/dashboard.js +54 -20
  29. package/lib/executor-repair.js +65 -0
  30. package/lib/extensions.js +6 -13
  31. package/lib/feature-awareness.js +2 -2
  32. package/lib/fs-async.js +2 -1
  33. package/lib/host-capabilities.js +4 -0
  34. package/lib/install-profiles.js +155 -0
  35. package/lib/installer-args.js +12 -0
  36. package/lib/installer-core.js +43 -8
  37. package/lib/linkage.js +2 -1
  38. package/lib/package-identity.js +38 -0
  39. package/lib/package-legitimacy.js +158 -0
  40. package/lib/planning-systems.js +16 -13
  41. package/lib/quick-proof.js +5 -2
  42. package/lib/repo-surface-sync.js +1 -1
  43. package/lib/requirements.js +2 -1
  44. package/lib/reverse-sync.js +2 -1
  45. package/lib/route-quality-sync.js +2 -0
  46. package/lib/source-grounding.js +177 -0
  47. package/lib/source-sync.js +6 -5
  48. package/lib/state.js +2 -1
  49. package/package.json +2 -2
  50. package/references/HAVE-NOTS.md +1 -1
  51. package/references/orchestration/MODE-DETECTION.md +2 -2
  52. package/references/shared/ORCHESTRATORS.md +3 -3
  53. package/routing/god-design.yaml +1 -1
  54. package/routing/god-extension-scaffold.yaml +25 -0
  55. package/routing/god-migrate.yaml +3 -3
  56. package/routing/god-stack.yaml +1 -1
  57. package/routing/recipes/add-feature-mid-arc-pause.yaml +6 -0
  58. package/routing/recipes/brownfield-onboarding.yaml +5 -2
  59. package/routing/recipes/extension-authoring.yaml +32 -0
  60. package/routing/recipes/greenfield-fast.yaml +3 -0
  61. package/routing/recipes/production-broken.yaml +4 -0
  62. package/routing/recipes/release-maintenance.yaml +3 -0
  63. package/routing/recipes/returning-after-break.yaml +3 -0
  64. package/routing/recipes/weekly-health-check.yaml +2 -0
  65. package/schema/state.v1.json +1 -1
  66. package/skills/god-build.md +17 -3
  67. package/skills/god-discuss.md +10 -5
  68. package/skills/god-doctor.md +9 -3
  69. package/skills/god-dogfood.md +2 -2
  70. package/skills/god-extension-scaffold.md +66 -0
  71. package/skills/god-init.md +6 -6
  72. package/skills/god-migrate.md +10 -10
  73. package/skills/god-sync.md +2 -2
  74. package/skills/god-update-deps.md +4 -2
  75. package/skills/god-version.md +2 -2
  76. package/skills/god.md +8 -11
  77. package/templates/IMPORTED-CONTEXT.md +1 -1
  78. package/templates/INITIAL-FINDINGS.md +2 -2
  79. /package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/.planning/PROJECT.md +0 -0
  80. /package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/.planning/REQUIREMENTS.md +0 -0
  81. /package/fixtures/dogfood/{half-migrated-gsd → half-migrated-planning}/.planning/ROADMAP.md +0 -0
package/lib/dashboard.js CHANGED
@@ -133,20 +133,43 @@ function hasRecentPath(projectRoot, relPath, maxAgeMs) {
133
133
  return Date.now() - modified <= maxAgeMs;
134
134
  }
135
135
 
136
- function proactiveChecks(projectRoot, changedFiles = []) {
136
+ function isGodpowersRuntimeRepo(projectRoot) {
137
+ try {
138
+ const pkgPath = path.join(projectRoot, 'package.json');
139
+ if (!fs.existsSync(pkgPath)) return false;
140
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
141
+ return pkg.name === 'godpowers'
142
+ && exists(projectRoot, 'skills')
143
+ && exists(projectRoot, 'routing')
144
+ && exists(projectRoot, 'agents');
145
+ } catch (e) {
146
+ return false;
147
+ }
148
+ }
149
+
150
+ function proactiveChecks(projectRoot, changedFiles = [], opts = {}) {
137
151
  const oneDay = 24 * 60 * 60 * 1000;
138
152
  const thirtyDays = 30 * oneDay;
153
+ const initialized = opts.initialized !== false;
154
+ const runtimeRepo = opts.runtimeRepo !== undefined
155
+ ? opts.runtimeRepo
156
+ : isGodpowersRuntimeRepo(projectRoot);
139
157
  const reviews = reviewCount(projectRoot);
140
158
 
141
- const checkpoint = exists(projectRoot, CHECKPOINT_PATH)
142
- ? (hasRecentPath(projectRoot, CHECKPOINT_PATH, oneDay) ? 'fresh' : 'stale')
143
- : 'missing';
159
+ const checkpoint = initialized
160
+ ? (exists(projectRoot, CHECKPOINT_PATH)
161
+ ? (hasRecentPath(projectRoot, CHECKPOINT_PATH, oneDay) ? 'fresh' : 'stale')
162
+ : 'missing')
163
+ : 'not-applicable';
144
164
 
145
- const sync = exists(projectRoot, SYNC_LOG_PATH)
146
- ? (hasRecentPath(projectRoot, SYNC_LOG_PATH, oneDay) ? 'fresh' : 'stale, suggest /god-sync')
147
- : 'missing, suggest /god-sync';
165
+ const sync = initialized
166
+ ? (exists(projectRoot, SYNC_LOG_PATH)
167
+ ? (hasRecentPath(projectRoot, SYNC_LOG_PATH, oneDay) ? 'fresh' : 'stale, suggest /god-sync')
168
+ : 'missing, suggest /god-sync')
169
+ : 'not-applicable';
148
170
 
149
- const hygieneFresh = exists(projectRoot, CHECKPOINT_PATH)
171
+ const hygieneFresh = initialized
172
+ && exists(projectRoot, CHECKPOINT_PATH)
150
173
  && hasRecentPath(projectRoot, CHECKPOINT_PATH, thirtyDays);
151
174
 
152
175
  const pkgChanged = changedFiles.some(file => [
@@ -162,14 +185,22 @@ function proactiveChecks(projectRoot, changedFiles = []) {
162
185
  'auth',
163
186
  'security'
164
187
  ]));
165
- const repoDocs = repoDocSync.detect(projectRoot, { changedFiles });
166
- const repoDocsStatus = repoDocs.status === 'fresh'
167
- ? 'fresh'
168
- : `${repoDocs.stale.length} stale, suggest /god-docs`;
169
- const repoSurface = repoSurfaceSync.detect(projectRoot);
170
- const repoSurfaceStatus = repoSurface.status === 'fresh'
171
- ? 'fresh'
172
- : `${repoSurface.stale.length} stale, suggest /god-doctor`;
188
+ const repoDocsStatus = runtimeRepo
189
+ ? (() => {
190
+ const repoDocs = repoDocSync.detect(projectRoot, { changedFiles });
191
+ return repoDocs.status === 'fresh'
192
+ ? 'fresh'
193
+ : `${repoDocs.stale.length} stale, suggest /god-docs`;
194
+ })()
195
+ : 'not-applicable';
196
+ const repoSurfaceStatus = runtimeRepo
197
+ ? (() => {
198
+ const repoSurface = repoSurfaceSync.detect(projectRoot);
199
+ return repoSurface.status === 'fresh'
200
+ ? 'fresh'
201
+ : `${repoSurface.stale.length} stale, suggest /god-doctor`;
202
+ })()
203
+ : 'not-applicable';
173
204
  const host = hostCapabilities.detect(projectRoot);
174
205
 
175
206
  return {
@@ -183,7 +214,7 @@ function proactiveChecks(projectRoot, changedFiles = []) {
183
214
  automation: automationSummary(projectRoot),
184
215
  security: sensitiveChanged ? 'sensitive files changed, suggest /god-harden' : 'clear',
185
216
  dependencies: pkgChanged ? 'dependency files changed, suggest /god-update-deps' : 'clear',
186
- hygiene: hygieneFresh ? 'fresh' : 'stale, suggest /god-hygiene'
217
+ hygiene: initialized ? (hygieneFresh ? 'fresh' : 'stale, suggest /god-hygiene') : 'not-applicable'
187
218
  };
188
219
  }
189
220
 
@@ -237,7 +268,7 @@ function compute(projectRoot, opts = {}) {
237
268
  completion: '0% workflow progress because .godpowers/state.json is missing',
238
269
  completionBasis: 'missing .godpowers/state.json'
239
270
  },
240
- proactive: proactiveChecks(projectRoot, git.entries.map(statusPath)),
271
+ proactive: proactiveChecks(projectRoot, git.entries.map(statusPath), { initialized: false }),
241
272
  host: hostCapabilities.detect(projectRoot, opts.host || {}),
242
273
  next,
243
274
  deliverables: { hasRequirements: false },
@@ -276,7 +307,7 @@ function compute(projectRoot, opts = {}) {
276
307
  worktree: git.worktree,
277
308
  index: git.index,
278
309
  planning: planningVisibility(projectRoot, progress),
279
- proactive: proactiveChecks(projectRoot, git.entries.map(statusPath)),
310
+ proactive: proactiveChecks(projectRoot, git.entries.map(statusPath), { initialized: true }),
280
311
  host: hostCapabilities.detect(projectRoot, opts.host || {}),
281
312
  next,
282
313
  deliverables,
@@ -289,6 +320,7 @@ function compute(projectRoot, opts = {}) {
289
320
  function actionBrief(dashboard) {
290
321
  const proactive = dashboard.proactive || {};
291
322
  const next = dashboard.next || {};
323
+ const recommended = next.command || 'describe the next intent';
292
324
  const blockers = [];
293
325
  for (const [label, value] of [
294
326
  ['Repo surface', proactive.repoSurface],
@@ -304,10 +336,11 @@ function actionBrief(dashboard) {
304
336
  if (value === 'fresh' || value === 'none' || value === 'clear' || value === 'not-applicable') continue;
305
337
  if (/^full on /.test(value)) continue;
306
338
  if (/^available via /.test(value)) continue;
339
+ if (label === 'Sync' && recommended !== '/god-sync') continue;
340
+ if (label === 'Hygiene' && recommended !== '/god-hygiene') continue;
307
341
  blockers.push(`${label}: ${value}`);
308
342
  }
309
343
 
310
- const recommended = next.command || 'describe the next intent';
311
344
  return {
312
345
  recommended,
313
346
  reason: next.reason || 'No route was computed.',
@@ -414,6 +447,7 @@ module.exports = {
414
447
  parseGitStatus,
415
448
  proactiveChecks,
416
449
  automationSummary,
450
+ isGodpowersRuntimeRepo,
417
451
  actionBrief,
418
452
  planningVisibility
419
453
  };
@@ -0,0 +1,65 @@
1
+ const STRATEGIES = Object.freeze({
2
+ RETRY: 'retry',
3
+ DECOMPOSE: 'decompose',
4
+ PRUNE: 'prune',
5
+ ESCALATE: 'escalate'
6
+ });
7
+
8
+ function classifyFailure(input = {}) {
9
+ const attempts = Number(input.attempts || 0);
10
+ const budget = Number(Object.prototype.hasOwnProperty.call(input, 'budget') ? input.budget : 2);
11
+ const error = String(input.error || '').toLowerCase();
12
+ const criteria = String(input.doneCriteria || '').toLowerCase();
13
+
14
+ if (attempts >= budget) {
15
+ return {
16
+ strategy: STRATEGIES.ESCALATE,
17
+ reason: 'repair budget exhausted'
18
+ };
19
+ }
20
+
21
+ if (/architecture|product decision|ambiguous|human/.test(error)) {
22
+ return {
23
+ strategy: STRATEGIES.ESCALATE,
24
+ reason: 'failure requires a human or architecture decision'
25
+ };
26
+ }
27
+
28
+ if (/not found|enoent|missing dependency|cannot find module|wrong path|permission|timeout|network|econn/.test(error)) {
29
+ return {
30
+ strategy: STRATEGIES.RETRY,
31
+ reason: 'failure looks environmental or mechanical'
32
+ };
33
+ }
34
+
35
+ if (/and|multiple|all of|end-to-end|full flow/.test(criteria) || /too broad|partial|only.*part/.test(error)) {
36
+ return {
37
+ strategy: STRATEGIES.DECOMPOSE,
38
+ reason: 'done criteria appears too broad for one verified step'
39
+ };
40
+ }
41
+
42
+ if (/out of scope|blocked by missing prerequisite|unsupported|cannot be implemented/.test(error)) {
43
+ return {
44
+ strategy: STRATEGIES.PRUNE,
45
+ reason: 'task appears infeasible in the current slice'
46
+ };
47
+ }
48
+
49
+ return {
50
+ strategy: STRATEGIES.RETRY,
51
+ reason: 'default to one focused retry before broader action'
52
+ };
53
+ }
54
+
55
+ function renderRepairLog(task, decision) {
56
+ const name = task || 'task';
57
+ const strategy = decision.strategy.toUpperCase();
58
+ return `[Executor Repair - ${strategy}] ${name}: ${decision.reason}`;
59
+ }
60
+
61
+ module.exports = {
62
+ STRATEGIES,
63
+ classifyFailure,
64
+ renderRepairLog
65
+ };
package/lib/extensions.js CHANGED
@@ -27,6 +27,7 @@ const fs = require('fs');
27
27
  const path = require('path');
28
28
 
29
29
  const intentLib = require('./intent'); // reuses parseSimpleYaml
30
+ const { copyRecursive } = require('./installer-files');
30
31
 
31
32
  function extensionsDir(runtimeConfigDir) {
32
33
  return path.join(runtimeConfigDir, 'godpowers-extensions');
@@ -159,11 +160,13 @@ function list(runtimeConfigDir) {
159
160
  const results = [];
160
161
  for (const scope of fs.readdirSync(dir)) {
161
162
  const scopePath = path.join(dir, scope);
162
- if (!fs.statSync(scopePath).isDirectory()) continue;
163
+ const scopeStat = fs.lstatSync(scopePath);
164
+ if (scopeStat.isSymbolicLink() || !scopeStat.isDirectory()) continue;
163
165
  if (!scope.startsWith('@')) continue;
164
166
  for (const name of fs.readdirSync(scopePath)) {
165
167
  const packDir = path.join(scopePath, name);
166
- if (!fs.statSync(packDir).isDirectory()) continue;
168
+ const packStat = fs.lstatSync(packDir);
169
+ if (packStat.isSymbolicLink() || !packStat.isDirectory()) continue;
167
170
  const manifestFile = path.join(packDir, 'manifest.yaml');
168
171
  if (!fs.existsSync(manifestFile)) continue;
169
172
  const { manifest } = parseManifest(fs.readFileSync(manifestFile, 'utf8'));
@@ -220,22 +223,12 @@ function install(runtimeConfigDir, sourceDir, godpowersVersion) {
220
223
  for (const sub of ['agents', 'skills', 'workflows', 'references']) {
221
224
  const src = path.join(sourceDir, sub);
222
225
  if (fs.existsSync(src)) {
223
- copyDirRecursive(src, path.join(destDir, sub));
226
+ copyRecursive(src, path.join(destDir, sub));
224
227
  }
225
228
  }
226
229
  return { installed: true, path: destDir, manifest };
227
230
  }
228
231
 
229
- function copyDirRecursive(src, dest) {
230
- fs.mkdirSync(dest, { recursive: true });
231
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
232
- const s = path.join(src, entry.name);
233
- const d = path.join(dest, entry.name);
234
- if (entry.isDirectory()) copyDirRecursive(s, d);
235
- else fs.copyFileSync(s, d);
236
- }
237
- }
238
-
239
232
  /**
240
233
  * Remove an installed extension by name.
241
234
  */
@@ -21,7 +21,7 @@ const FEATURES = [
21
21
  id: 'planning-system-migration',
22
22
  since: '1.6.15',
23
23
  commands: ['/god-migrate', '/god-init'],
24
- description: 'Detect and import GSD, BMAD, and Superpowers planning artifacts.'
24
+ description: 'Detect and import legacy planning, BMAD, and Superpowers planning artifacts.'
25
25
  },
26
26
  {
27
27
  id: 'source-system-sync-back',
@@ -110,7 +110,7 @@ const FEATURES = [
110
110
  {
111
111
  id: 'extension-authoring',
112
112
  since: '1.6.22',
113
- commands: ['/god-extension-add', '/god-test-extension'],
113
+ commands: ['/god-extension-scaffold', '/god-test-extension', '/god-extension-add'],
114
114
  description: 'Scaffold and validate publishable Godpowers extension packs.'
115
115
  },
116
116
  {
package/lib/fs-async.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs/promises');
2
2
  const path = require('path');
3
+ const atomic = require('./atomic-write');
3
4
 
4
5
  async function exists(filePath) {
5
6
  try {
@@ -16,7 +17,7 @@ async function readJson(filePath) {
16
17
 
17
18
  async function writeJson(filePath, value) {
18
19
  await fs.mkdir(path.dirname(filePath), { recursive: true });
19
- await fs.writeFile(filePath, JSON.stringify(value, null, 2) + '\n');
20
+ await atomic.writeJsonAtomicAsync(filePath, value);
20
21
  return value;
21
22
  }
22
23
 
@@ -10,6 +10,7 @@ const fs = require('fs');
10
10
  const path = require('path');
11
11
  const os = require('os');
12
12
  const cp = require('child_process');
13
+ const codeIntelligence = require('./code-intelligence');
13
14
 
14
15
  function exists(filePath) {
15
16
  return fs.existsSync(filePath);
@@ -55,6 +56,7 @@ function detect(projectRoot, opts = {}) {
55
56
  const git = commandVersion('git', ['--version'], { cwd: root });
56
57
  const npm = commandVersion('npm', ['--version'], { cwd: root });
57
58
  const gh = commandVersion('gh', ['--version'], { cwd: root });
59
+ const codeIntel = opts.codeIntelligence || codeIntelligence.detect(root, opts.codeIntelligenceOpts || {});
58
60
  const shell = Boolean(env.SHELL || env.ComSpec);
59
61
  const agentSpawn = Boolean(installedAgents.codex || installedAgents.claude || opts.agentSpawn);
60
62
  const extensionAuthoring = exists(path.join(root, 'lib', 'extension-authoring.js'))
@@ -84,6 +86,7 @@ function detect(projectRoot, opts = {}) {
84
86
  npm,
85
87
  gh,
86
88
  agentSpawn,
89
+ codeIntelligence: codeIntel,
87
90
  extensionAuthoring,
88
91
  suiteReleaseDryRun
89
92
  },
@@ -109,6 +112,7 @@ function render(report) {
109
112
  lines.push(` Git: ${report.guarantees.git || 'not detected'}`);
110
113
  lines.push(` npm: ${report.guarantees.npm || 'not detected'}`);
111
114
  lines.push(` GitHub CLI: ${report.guarantees.gh || 'not detected'}`);
115
+ lines.push(` Code intelligence: ${codeIntelligence.summary(report.guarantees.codeIntelligence)}`);
112
116
  lines.push(` Gaps: ${report.gaps.length > 0 ? report.gaps.join('; ') : 'none'}`);
113
117
  return lines.join('\n');
114
118
  }
@@ -0,0 +1,155 @@
1
+ const COMMON = [
2
+ 'god',
3
+ 'god-help',
4
+ 'god-version',
5
+ 'god-next',
6
+ 'god-status',
7
+ 'god-progress',
8
+ 'god-doctor',
9
+ 'god-settings'
10
+ ];
11
+
12
+ const PROFILE_SKILLS = {
13
+ core: [
14
+ ...COMMON,
15
+ 'god-init',
16
+ 'god-mode',
17
+ 'god-build',
18
+ 'god-review',
19
+ 'god-sync',
20
+ 'god-quick',
21
+ 'god-fast'
22
+ ],
23
+ builder: [
24
+ ...COMMON,
25
+ 'god-init',
26
+ 'god-mode',
27
+ 'god-discuss',
28
+ 'god-explore',
29
+ 'god-list-assumptions',
30
+ 'god-prd',
31
+ 'god-design',
32
+ 'god-design-impact',
33
+ 'god-arch',
34
+ 'god-roadmap',
35
+ 'god-stack',
36
+ 'god-repo',
37
+ 'god-build',
38
+ 'god-add-tests',
39
+ 'god-feature',
40
+ 'god-story',
41
+ 'god-stories',
42
+ 'god-story-build',
43
+ 'god-story-verify',
44
+ 'god-story-close',
45
+ 'god-review',
46
+ 'god-test-runtime',
47
+ 'god-sync',
48
+ 'god-quick',
49
+ 'god-fast'
50
+ ],
51
+ maintainer: [
52
+ ...COMMON,
53
+ 'god-hygiene',
54
+ 'god-update-deps',
55
+ 'god-docs',
56
+ 'god-repair',
57
+ 'god-lint',
58
+ 'god-standards',
59
+ 'god-preflight',
60
+ 'god-audit',
61
+ 'god-agent-audit',
62
+ 'god-context',
63
+ 'god-context-scan',
64
+ 'god-locate',
65
+ 'god-scan',
66
+ 'god-link',
67
+ 'god-review-changes',
68
+ 'god-reconcile',
69
+ 'god-reconstruct',
70
+ 'god-migrate',
71
+ 'god-automation-status',
72
+ 'god-automation-setup',
73
+ 'god-extension-scaffold',
74
+ 'god-extension-add',
75
+ 'god-extension-list',
76
+ 'god-extension-info',
77
+ 'god-extension-remove',
78
+ 'god-test-extension',
79
+ 'god-budget',
80
+ 'god-cost',
81
+ 'god-cache-clear',
82
+ 'god-logs',
83
+ 'god-metrics',
84
+ 'god-trace',
85
+ 'god-export-otel',
86
+ 'god-dogfood',
87
+ 'god-quick',
88
+ 'god-fast'
89
+ ],
90
+ suite: [
91
+ ...COMMON,
92
+ 'god-suite-init',
93
+ 'god-suite-status',
94
+ 'god-suite-sync',
95
+ 'god-suite-patch',
96
+ 'god-suite-release',
97
+ 'god-workstream',
98
+ 'god-pr-branch',
99
+ 'god-sync',
100
+ 'god-reconcile',
101
+ 'god-review',
102
+ 'god-quick',
103
+ 'god-fast'
104
+ ]
105
+ };
106
+
107
+ const PROFILE_DESCRIPTIONS = {
108
+ core: 'front door, status, init, build, review, sync, quick edits',
109
+ builder: 'core plus planning, design, stories, and runtime verification',
110
+ maintainer: 'core plus hygiene, deps, docs, repair, automation, and extensions',
111
+ suite: 'core plus multi-repo suite and workstream coordination',
112
+ full: 'all shipped slash commands'
113
+ };
114
+
115
+ function normalizeProfiles(value) {
116
+ if (!value) return ['full'];
117
+ const raw = String(value)
118
+ .split(',')
119
+ .map(part => part.trim().toLowerCase())
120
+ .filter(Boolean);
121
+ const profiles = raw.length > 0 ? raw : ['full'];
122
+ for (const profile of profiles) {
123
+ if (profile !== 'full' && !PROFILE_SKILLS[profile]) {
124
+ throw new Error(`Unknown install profile: ${profile}`);
125
+ }
126
+ }
127
+ if (profiles.includes('full')) return ['full'];
128
+ return [...new Set(profiles)];
129
+ }
130
+
131
+ function selectedSkillNames(profileValue, availableNames) {
132
+ const profiles = normalizeProfiles(profileValue);
133
+ if (profiles.includes('full')) return new Set(availableNames);
134
+ const selected = new Set();
135
+ for (const profile of profiles) {
136
+ for (const name of PROFILE_SKILLS[profile]) {
137
+ if (availableNames.includes(name)) selected.add(name);
138
+ }
139
+ }
140
+ return selected;
141
+ }
142
+
143
+ function describeProfiles(profileValue) {
144
+ return normalizeProfiles(profileValue)
145
+ .map(profile => `${profile}: ${PROFILE_DESCRIPTIONS[profile]}`)
146
+ .join('; ');
147
+ }
148
+
149
+ module.exports = {
150
+ PROFILE_SKILLS,
151
+ PROFILE_DESCRIPTIONS,
152
+ normalizeProfiles,
153
+ selectedSkillNames,
154
+ describeProfiles
155
+ };
@@ -29,6 +29,7 @@ function parseArgs(argv, cwd = process.cwd()) {
29
29
  all: false,
30
30
  help: false,
31
31
  uninstall: false,
32
+ profile: 'full',
32
33
  };
33
34
 
34
35
  for (let i = 0; i < args.length; i++) {
@@ -62,6 +63,15 @@ function parseArgs(argv, cwd = process.cwd()) {
62
63
  case '--all':
63
64
  opts.all = true;
64
65
  break;
66
+ case '--minimal':
67
+ opts.profile = 'core';
68
+ break;
69
+ case '--profile':
70
+ if (args[i + 1]) {
71
+ opts.profile = args[i + 1];
72
+ i++;
73
+ }
74
+ break;
65
75
  case '-h':
66
76
  case '--help':
67
77
  opts.help = true;
@@ -83,6 +93,8 @@ function parseArgs(argv, cwd = process.cwd()) {
83
93
  opts.extensionAgent = arg.slice('--agent='.length);
84
94
  } else if (arg.startsWith('--workflow=')) {
85
95
  opts.extensionWorkflow = arg.slice('--workflow='.length);
96
+ } else if (arg.startsWith('--profile=')) {
97
+ opts.profile = arg.slice('--profile='.length);
86
98
  } else if (arg.startsWith('--') && RUNTIMES[arg.slice(2)]) {
87
99
  opts.runtimes.push(arg.slice(2));
88
100
  }
@@ -3,8 +3,10 @@ const path = require('path');
3
3
 
4
4
  const { ensureDir, copyRecursive, copyRuntimeBundle } = require('./installer-files');
5
5
  const { resolveRuntime } = require('./installer-runtimes');
6
+ const { selectedSkillNames, normalizeProfiles } = require('./install-profiles');
7
+ const identity = require('./package-identity');
6
8
 
7
- const VERSION = require('../package.json').version;
9
+ const VERSION = identity.PACKAGE_VERSION;
8
10
 
9
11
  /**
10
12
  * @typedef {Object} InstallOptions
@@ -141,6 +143,15 @@ function removeSkillEntry(skillsDir, entry) {
141
143
  return false;
142
144
  }
143
145
 
146
+ function pruneGodpowersSkills(skillsDir) {
147
+ let removed = 0;
148
+ if (!fs.existsSync(skillsDir)) return removed;
149
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
150
+ if (removeSkillEntry(skillsDir, entry)) removed++;
151
+ }
152
+ return removed;
153
+ }
154
+
144
155
  function installForRuntime(runtimeKey, srcDir, opts = {}) {
145
156
  const runtime = resolveRuntime(runtimeKey, opts);
146
157
  if (!runtime) {
@@ -151,7 +162,7 @@ function installForRuntime(runtimeKey, srcDir, opts = {}) {
151
162
  log(`\n Installing for \x1b[36m${runtime.name}\x1b[0m to \x1b[36m${runtime.configDir}\x1b[0m\n`);
152
163
  ensureDir(runtime.configDir);
153
164
 
154
- installSkills(srcDir, runtimeKey, runtime);
165
+ installSkills(srcDir, runtimeKey, runtime, opts);
155
166
  installAgents(srcDir, runtime);
156
167
  installMasterSkill(srcDir, runtimeKey, runtime);
157
168
  installDataDirs(srcDir, runtime);
@@ -159,18 +170,33 @@ function installForRuntime(runtimeKey, srcDir, opts = {}) {
159
170
 
160
171
  fs.writeFileSync(path.join(runtime.configDir, 'GODPOWERS_VERSION'), VERSION);
161
172
  success(`Wrote GODPOWERS_VERSION (${VERSION})`);
173
+ fs.writeFileSync(path.join(runtime.configDir, 'GODPOWERS_PROFILE'), normalizeProfiles(opts.profile).join(','));
174
+ success(`Wrote GODPOWERS_PROFILE (${normalizeProfiles(opts.profile).join(',')})`);
162
175
  return true;
163
176
  }
164
177
 
165
- function installSkills(srcDir, runtimeKey, runtime) {
178
+ function availableSkillFiles(srcDir) {
179
+ const skillsSrc = path.join(srcDir, 'skills');
180
+ if (!fs.existsSync(skillsSrc)) return [];
181
+ return fs.readdirSync(skillsSrc)
182
+ .filter(file => file.endsWith('.md'))
183
+ .sort();
184
+ }
185
+
186
+ function installSkills(srcDir, runtimeKey, runtime, opts = {}) {
166
187
  const skillsSrc = path.join(srcDir, 'skills');
167
188
  const skillsDest = path.join(runtime.configDir, runtime.skillsDir);
168
189
  if (!fs.existsSync(skillsSrc)) return;
169
190
 
170
191
  ensureDir(skillsDest);
192
+ pruneGodpowersSkills(skillsDest);
193
+ const files = availableSkillFiles(srcDir);
194
+ const names = files.map(file => path.basename(file, '.md'));
195
+ const selected = selectedSkillNames(opts.profile, names);
171
196
  let count = 0;
172
- for (const file of fs.readdirSync(skillsSrc)) {
173
- if (file.endsWith('.md')) {
197
+ for (const file of files) {
198
+ const name = path.basename(file, '.md');
199
+ if (selected.has(name)) {
174
200
  installSkillFile(path.join(skillsSrc, file), skillsDest, runtimeKey);
175
201
  count++;
176
202
  }
@@ -327,8 +353,15 @@ function uninstallHooks(runtimeKey, runtime) {
327
353
  }
328
354
 
329
355
  function countInstalledSurface(srcDir) {
356
+ return countProfileSurface(srcDir, { profile: 'full' });
357
+ }
358
+
359
+ function countProfileSurface(srcDir, opts = {}) {
360
+ const files = availableSkillFiles(srcDir);
361
+ const names = files.map(file => path.basename(file, '.md'));
362
+ const selected = selectedSkillNames(opts.profile, names);
330
363
  return {
331
- skills: fs.readdirSync(path.join(srcDir, 'skills')).filter(f => f.endsWith('.md')).length,
364
+ skills: selected.size,
332
365
  agents: fs.readdirSync(path.join(srcDir, 'agents')).filter(f => /^god-.*\.md$/.test(f)).length
333
366
  };
334
367
  }
@@ -336,10 +369,12 @@ function countInstalledSurface(srcDir) {
336
369
  module.exports = {
337
370
  installForRuntime,
338
371
  uninstallForRuntime,
339
- countInstalledSurface,
372
+ countInstalledSurface: countProfileSurface,
340
373
  installSkillFile,
341
374
  parseAgentFrontmatter,
342
375
  stripFrontmatter,
343
376
  writeCodexAgentToml,
344
- removeSkillEntry
377
+ removeSkillEntry,
378
+ pruneGodpowersSkills,
379
+ installSkills
345
380
  };
package/lib/linkage.js CHANGED
@@ -23,6 +23,7 @@
23
23
 
24
24
  const fs = require('fs');
25
25
  const path = require('path');
26
+ const atomic = require('./atomic-write');
26
27
 
27
28
  const ID_PATTERNS = {
28
29
  prd: /^P-(MUST|SHOULD|COULD)-\d+$/,
@@ -77,7 +78,7 @@ function readMap(filePath) {
77
78
  }
78
79
 
79
80
  function writeMap(filePath, data) {
80
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n');
81
+ atomic.writeJsonAtomic(filePath, data);
81
82
  }
82
83
 
83
84
  function linkKey(artifactId, filePath) {
@@ -0,0 +1,38 @@
1
+ const pkg = require('../package.json');
2
+
3
+ const PACKAGE_NAME = pkg.name;
4
+ const PACKAGE_VERSION = pkg.version;
5
+ const REPOSITORY_URL = pkg.repository && pkg.repository.url
6
+ ? pkg.repository.url
7
+ : 'git+https://github.com/aihxp/godpowers.git';
8
+ const HOMEPAGE_URL = pkg.homepage || 'https://github.com/aihxp/godpowers#readme';
9
+ const BUGS_URL = pkg.bugs && pkg.bugs.url
10
+ ? pkg.bugs.url
11
+ : 'https://github.com/aihxp/godpowers/issues';
12
+ const BIN_NAME = pkg.bin && Object.keys(pkg.bin)[0]
13
+ ? Object.keys(pkg.bin)[0]
14
+ : 'godpowers';
15
+
16
+ function repoSlug() {
17
+ const url = REPOSITORY_URL
18
+ .replace(/^git\+/, '')
19
+ .replace(/^https:\/\/github\.com\//, '')
20
+ .replace(/^git@github\.com:/, '')
21
+ .replace(/\.git$/, '');
22
+ return url || 'aihxp/godpowers';
23
+ }
24
+
25
+ function npxCommand(version = 'latest') {
26
+ return `npx ${PACKAGE_NAME}@${version}`;
27
+ }
28
+
29
+ module.exports = {
30
+ PACKAGE_NAME,
31
+ PACKAGE_VERSION,
32
+ REPOSITORY_URL,
33
+ HOMEPAGE_URL,
34
+ BUGS_URL,
35
+ BIN_NAME,
36
+ repoSlug,
37
+ npxCommand
38
+ };