@brunosps00/dev-workflow 1.0.4 → 1.0.6

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 (70) hide show
  1. package/README.md +71 -23
  2. package/bin/dev-workflow.js +29 -3
  3. package/lib/agents.js +410 -0
  4. package/lib/constants.js +16 -2
  5. package/lib/doctor.js +213 -0
  6. package/lib/init.js +96 -8
  7. package/lib/install-state.js +57 -0
  8. package/lib/list-installed.js +27 -0
  9. package/lib/migrate-skills.js +26 -2
  10. package/lib/profiles.js +44 -0
  11. package/lib/skill-registry.js +109 -0
  12. package/lib/subtasks.js +264 -0
  13. package/lib/uninstall.js +21 -12
  14. package/lib/utils.js +42 -1
  15. package/lib/wrappers.js +2 -1
  16. package/package.json +1 -1
  17. package/scaffold/agent-registry.json +272 -0
  18. package/scaffold/agents/core/dw-build-fixer.md +21 -0
  19. package/scaffold/agents/core/dw-code-explorer.md +32 -0
  20. package/scaffold/agents/core/dw-code-reviewer.md +28 -0
  21. package/scaffold/agents/core/dw-planner.md +23 -0
  22. package/scaffold/agents/core/dw-test-author.md +21 -0
  23. package/scaffold/agents/csharp/dw-csharp-reviewer.md +15 -0
  24. package/scaffold/agents/csharp/dw-dotnet-build-fixer.md +15 -0
  25. package/scaffold/agents/frontend/dw-qa-runner.md +21 -0
  26. package/scaffold/agents/python/dw-python-build-fixer.md +15 -0
  27. package/scaffold/agents/python/dw-python-reviewer.md +15 -0
  28. package/scaffold/agents/rust/dw-rust-build-fixer.md +15 -0
  29. package/scaffold/agents/rust/dw-rust-reviewer.md +15 -0
  30. package/scaffold/agents/security/dw-security-reviewer.md +24 -0
  31. package/scaffold/agents/security/dw-silent-failure-hunter.md +22 -0
  32. package/scaffold/agents/typescript/dw-typescript-build-fixer.md +15 -0
  33. package/scaffold/agents/typescript/dw-typescript-reviewer.md +15 -0
  34. package/scaffold/en/agent-instructions.md +51 -6
  35. package/scaffold/en/commands/dw-autopilot.md +156 -275
  36. package/scaffold/en/commands/dw-bugfix.md +12 -2
  37. package/scaffold/en/commands/dw-context-budget.md +28 -0
  38. package/scaffold/en/commands/dw-goal.md +136 -0
  39. package/scaffold/en/commands/dw-harness-audit.md +31 -0
  40. package/scaffold/en/commands/dw-help.md +22 -10
  41. package/scaffold/en/commands/dw-intel.md +6 -0
  42. package/scaffold/en/commands/dw-plan.md +20 -3
  43. package/scaffold/en/commands/dw-review.md +12 -0
  44. package/scaffold/en/commands/dw-skill-health.md +29 -0
  45. package/scaffold/en/commands/dw-subtask-complete.md +40 -0
  46. package/scaffold/en/commands/dw-subtask-resume.md +21 -0
  47. package/scaffold/en/commands/dw-subtask-start.md +22 -0
  48. package/scaffold/pt-br/agent-instructions.md +51 -6
  49. package/scaffold/pt-br/commands/dw-autopilot.md +159 -278
  50. package/scaffold/pt-br/commands/dw-bugfix.md +12 -2
  51. package/scaffold/pt-br/commands/dw-context-budget.md +28 -0
  52. package/scaffold/pt-br/commands/dw-goal.md +136 -0
  53. package/scaffold/pt-br/commands/dw-harness-audit.md +31 -0
  54. package/scaffold/pt-br/commands/dw-help.md +22 -10
  55. package/scaffold/pt-br/commands/dw-intel.md +6 -0
  56. package/scaffold/pt-br/commands/dw-plan.md +20 -3
  57. package/scaffold/pt-br/commands/dw-review.md +12 -0
  58. package/scaffold/pt-br/commands/dw-skill-health.md +29 -0
  59. package/scaffold/pt-br/commands/dw-subtask-complete.md +40 -0
  60. package/scaffold/pt-br/commands/dw-subtask-resume.md +21 -0
  61. package/scaffold/pt-br/commands/dw-subtask-start.md +22 -0
  62. package/scaffold/skill-registry.json +258 -0
  63. package/scaffold/skills/dw-codebase-intel/SKILL.md +12 -5
  64. package/scaffold/skills/dw-context-budget/SKILL.md +45 -0
  65. package/scaffold/skills/dw-search-first/SKILL.md +31 -0
  66. package/scaffold/skills/dw-silent-failure/SKILL.md +33 -0
  67. package/scaffold/skills/dw-verify/SKILL.md +2 -29
  68. package/scaffold/skills/dw-verify/references/verification-discovery-and-attribution.md +32 -0
  69. package/scaffold/skills/humanizer/SKILL.md +7 -412
  70. package/scaffold/skills/humanizer/references/ai-writing-patterns.md +418 -0
package/lib/init.js CHANGED
@@ -1,38 +1,55 @@
1
1
  const path = require('path');
2
2
  const { COMMANDS } = require('./constants');
3
- const { ensureDir, copyDir, writeFile, upsertDelimitedBlock, log } = require('./utils');
3
+ const { ensureDir, copyDir, writeFile, upsertDelimitedBlock, log, listFilesRecursive } = require('./utils');
4
4
  const { selectLanguage } = require('./prompts');
5
5
  const { generateWrappers } = require('./wrappers');
6
6
  const { installMCPs } = require('./mcp');
7
+ const { generateAgents } = require('./agents');
7
8
  const { migrate: migrateGsd } = require('./migrate-gsd');
8
9
  const { migrate: migrateSkills } = require('./migrate-skills');
10
+ const { hasExistingInstall, readInstallState, writeInstallState, listManagedFiles } = require('./install-state');
11
+ const { resolveInstallSelection } = require('./profiles');
12
+ const { ensureSubtaskLayout } = require('./subtasks');
9
13
 
10
14
  const SCAFFOLD_DIR = path.join(__dirname, '..', 'scaffold');
11
15
 
12
- async function run({ force = false, lang = null, mode = 'init' }) {
16
+ async function run({ force = false, lang = null, mode = 'init', profile = null, modules = null }) {
13
17
  const projectRoot = process.cwd();
14
18
  const isUpdate = mode === 'update';
19
+ const isRepair = mode === 'repair';
20
+ const existingInstall = hasExistingInstall(projectRoot);
21
+ const priorState = readInstallState(projectRoot);
15
22
 
16
- // In update mode, always overwrite managed files (commands, templates, references, scripts, skills)
17
- // but never touch user files (rules, spec, MCPs)
18
- const managedForce = isUpdate ? true : force;
23
+ // Update/repair and init over an existing dev-workflow install reconcile managed
24
+ // files to the current scaffold. Fresh init keeps normal non-destructive behavior.
25
+ const shouldReconcile = isUpdate || isRepair || (mode === 'init' && existingInstall);
26
+ const managedForce = shouldReconcile ? true : force;
19
27
 
20
- console.log(`\n dev-workflow ${isUpdate ? 'update' : 'init'}`);
28
+ console.log(`\n dev-workflow ${isRepair ? 'repair' : (isUpdate ? 'update' : 'init')}`);
21
29
  console.log(` ${'='.repeat(40)}\n`);
30
+ if (mode === 'init' && existingInstall) {
31
+ console.log(' Existing dev-workflow install detected; reconciling managed files.\n');
32
+ }
22
33
 
23
34
  // 0. GSD migration (idempotent; only runs if legacy GSD artifacts are detected)
24
35
  // 0.1. Skill migration: removed bundled skills + orphan dw-* wrappers (idempotent)
25
- if (isUpdate) {
36
+ if (shouldReconcile) {
26
37
  migrateGsd(projectRoot);
27
38
  migrateSkills(projectRoot);
28
39
  }
29
40
 
30
41
  // 1. Select language
31
- const selectedLang = await selectLanguage(lang);
42
+ const selectedLang = await selectLanguage(lang || (priorState && priorState.lang));
43
+ const selection = resolveInstallSelection({
44
+ profile: profile || (priorState && priorState.profile),
45
+ modules: modules || (priorState && Array.isArray(priorState.modules) ? priorState.modules.join(',') : null),
46
+ });
32
47
  console.log(`\n Language: ${selectedLang}\n`);
48
+ console.log(` Profile: ${selection.profile} (${selection.modules.join(', ')})\n`);
33
49
 
34
50
  const commands = COMMANDS[selectedLang];
35
51
  const langDir = path.join(SCAFFOLD_DIR, selectedLang);
52
+ const touchedFiles = [];
36
53
 
37
54
  // Track totals
38
55
  let totalCreated = 0;
@@ -49,6 +66,7 @@ async function run({ force = false, lang = null, mode = 'init' }) {
49
66
  totalCreated += cmdResults.created;
50
67
  totalSkipped += cmdResults.skipped;
51
68
  totalOverwritten += cmdResults.overwritten;
69
+ touchedFiles.push(...listFilesRecursive(path.join(projectRoot, '.dw', 'commands')));
52
70
  console.log(` ${cmdResults.created} created, ${cmdResults.skipped} skipped, ${cmdResults.overwritten} overwritten\n`);
53
71
 
54
72
  // 3. Copy templates (override-aware: .dw/templates/overrides/<name> wins over scaffold core)
@@ -64,6 +82,7 @@ async function run({ force = false, lang = null, mode = 'init' }) {
64
82
  totalCreated += tplResults.created;
65
83
  totalSkipped += tplResults.skipped;
66
84
  totalOverwritten += tplResults.overwritten;
85
+ touchedFiles.push(...listFilesRecursive(templatesDir));
67
86
  console.log(` ${tplResults.created} created, ${tplResults.skipped} skipped, ${tplResults.overwritten} overwritten\n`);
68
87
 
69
88
  // 3.1. Seed .dw/templates/overrides/ with README on first init (never overwritten)
@@ -78,6 +97,7 @@ async function run({ force = false, lang = null, mode = 'init' }) {
78
97
  );
79
98
  if (ovrStatus === 'created') totalCreated++;
80
99
  else totalSkipped++;
100
+ touchedFiles.push(overridesReadmeDest);
81
101
  }
82
102
 
83
103
  // 3.5. Copy references (language-specific)
@@ -92,6 +112,7 @@ async function run({ force = false, lang = null, mode = 'init' }) {
92
112
  totalCreated += refsResults.created;
93
113
  totalSkipped += refsResults.skipped;
94
114
  totalOverwritten += refsResults.overwritten;
115
+ touchedFiles.push(...listFilesRecursive(path.join(projectRoot, '.dw', 'references')));
95
116
  console.log(` ${refsResults.created} created, ${refsResults.skipped} skipped, ${refsResults.overwritten} overwritten\n`);
96
117
  }
97
118
 
@@ -107,9 +128,46 @@ async function run({ force = false, lang = null, mode = 'init' }) {
107
128
  totalCreated += scriptsResults.created;
108
129
  totalSkipped += scriptsResults.skipped;
109
130
  totalOverwritten += scriptsResults.overwritten;
131
+ touchedFiles.push(...listFilesRecursive(path.join(projectRoot, '.dw', 'scripts')));
110
132
  console.log(` ${scriptsResults.created} created, ${scriptsResults.skipped} skipped, ${scriptsResults.overwritten} overwritten\n`);
111
133
  }
112
134
 
135
+ // 3.7. Copy skill registry for agent-facing skill health audits.
136
+ const skillRegistrySrc = path.join(SCAFFOLD_DIR, 'skill-registry.json');
137
+ if (require('fs').existsSync(skillRegistrySrc)) {
138
+ console.log(' Skill registry:');
139
+ const skillRegistryDest = path.join(projectRoot, '.dw', 'skill-registry.json');
140
+ const registryStatus = writeFile(
141
+ skillRegistryDest,
142
+ require('fs').readFileSync(skillRegistrySrc, 'utf-8'),
143
+ managedForce
144
+ );
145
+ log(registryStatus, skillRegistryDest);
146
+ if (registryStatus === 'created') totalCreated++;
147
+ else if (registryStatus === 'overwritten') totalOverwritten++;
148
+ else totalSkipped++;
149
+ touchedFiles.push(skillRegistryDest);
150
+ console.log();
151
+ }
152
+
153
+ // 3.8. Copy agent registry for dispatch, budgets, and provider health audits.
154
+ const agentRegistrySrc = path.join(SCAFFOLD_DIR, 'agent-registry.json');
155
+ if (require('fs').existsSync(agentRegistrySrc)) {
156
+ console.log(' Agent registry:');
157
+ const agentRegistryDest = path.join(projectRoot, '.dw', 'agent-registry.json');
158
+ const registryStatus = writeFile(
159
+ agentRegistryDest,
160
+ require('fs').readFileSync(agentRegistrySrc, 'utf-8'),
161
+ managedForce
162
+ );
163
+ log(registryStatus, agentRegistryDest);
164
+ if (registryStatus === 'created') totalCreated++;
165
+ else if (registryStatus === 'overwritten') totalOverwritten++;
166
+ else totalSkipped++;
167
+ touchedFiles.push(agentRegistryDest);
168
+ console.log();
169
+ }
170
+
113
171
  // 4. Create .dw/rules/ with README
114
172
  if (!isUpdate) {
115
173
  console.log(' Rules:');
@@ -124,6 +182,7 @@ async function run({ force = false, lang = null, mode = 'init' }) {
124
182
  log(status, rulesReadmeDest);
125
183
  if (status === 'created') totalCreated++;
126
184
  else totalSkipped++;
185
+ touchedFiles.push(rulesReadmeDest);
127
186
  console.log();
128
187
  }
129
188
 
@@ -146,6 +205,7 @@ async function run({ force = false, lang = null, mode = 'init' }) {
146
205
  log(stateStatus, stateDestPath);
147
206
  if (stateStatus === 'created') totalCreated++;
148
207
  else totalSkipped++;
208
+ touchedFiles.push(stateDestPath);
149
209
  }
150
210
  console.log();
151
211
  }
@@ -158,8 +218,13 @@ async function run({ force = false, lang = null, mode = 'init' }) {
158
218
  const gitkeepStatus = writeFile(gitkeepPath, '', false);
159
219
  if (gitkeepStatus === 'created') totalCreated++;
160
220
  else totalSkipped++;
221
+ touchedFiles.push(gitkeepPath);
161
222
  }
162
223
 
224
+ // 5.3. Create local ephemeral subtask handoff directories.
225
+ ensureSubtaskLayout(projectRoot);
226
+ touchedFiles.push(path.join(projectRoot, '.dw', 'subtasks', '.gitignore'));
227
+
163
228
  // 5.5. Copy bundled skills to .agents/skills/
164
229
  const skillsSrcDir = path.join(SCAFFOLD_DIR, 'skills');
165
230
  if (require('fs').existsSync(skillsSrcDir)) {
@@ -172,9 +237,19 @@ async function run({ force = false, lang = null, mode = 'init' }) {
172
237
  totalCreated += skillsResults.created;
173
238
  totalSkipped += skillsResults.skipped;
174
239
  totalOverwritten += skillsResults.overwritten;
240
+ touchedFiles.push(...listFilesRecursive(path.join(projectRoot, '.agents', 'skills')));
175
241
  console.log(` ${skillsResults.created} created, ${skillsResults.skipped} skipped, ${skillsResults.overwritten} overwritten\n`);
176
242
  }
177
243
 
244
+ // 5.6. Generate platform-native/fallback agents from the selected modules.
245
+ console.log(' Agents:');
246
+ const agentResults = generateAgents(projectRoot, selection.modules, managedForce);
247
+ totalCreated += agentResults.created;
248
+ totalSkipped += agentResults.skipped;
249
+ totalOverwritten += agentResults.overwritten;
250
+ touchedFiles.push(...agentResults.files);
251
+ console.log(` ${agentResults.created} created, ${agentResults.skipped} skipped, ${agentResults.overwritten} overwritten\n`);
252
+
178
253
  // 5.7. Install CLAUDE.md + AGENTS.md with auto-trigger decision tree
179
254
  // Both files receive the same content (Claude Code reads CLAUDE.md; Codex/Copilot/OpenCode
180
255
  // converge on AGENTS.md). Merge-aware: user edits outside the dev-workflow markers are preserved.
@@ -191,6 +266,7 @@ async function run({ force = false, lang = null, mode = 'init' }) {
191
266
  if (status === 'created') totalCreated++;
192
267
  else if (status === 'updated') totalOverwritten++;
193
268
  else totalSkipped++;
269
+ touchedFiles.push(target);
194
270
  }
195
271
  console.log();
196
272
  }
@@ -201,6 +277,7 @@ async function run({ force = false, lang = null, mode = 'init' }) {
201
277
  const opcStatus = writeFile(opencodePackageJson, opencodeContent, false);
202
278
  if (opcStatus === 'created') totalCreated++;
203
279
  else totalSkipped++;
280
+ touchedFiles.push(opencodePackageJson);
204
281
 
205
282
  // 6.5. Clean up legacy .codex/skills/ (now served by .agents/skills/)
206
283
  const legacyCodexDir = path.join(projectRoot, '.codex', 'skills');
@@ -216,6 +293,7 @@ async function run({ force = false, lang = null, mode = 'init' }) {
216
293
  totalCreated += wrapperResults.created;
217
294
  totalSkipped += wrapperResults.skipped;
218
295
  totalOverwritten += wrapperResults.overwritten;
296
+ touchedFiles.push(...wrapperResults.files);
219
297
  console.log();
220
298
 
221
299
  // 9. Install MCPs (always, including updates)
@@ -223,6 +301,16 @@ async function run({ force = false, lang = null, mode = 'init' }) {
223
301
  const mcpResults = installMCPs(projectRoot);
224
302
  console.log(` ${mcpResults.added} configured, ${mcpResults.skipped} already present\n`);
225
303
 
304
+ // 9.5. Persist install state for diagnostics and repair.
305
+ const installState = writeInstallState(projectRoot, {
306
+ lang: selectedLang,
307
+ profile: selection.profile,
308
+ modules: selection.modules,
309
+ platforms: ['claude', 'agents', 'opencode', 'copilot'],
310
+ managedFiles: listManagedFiles(projectRoot, touchedFiles),
311
+ });
312
+ console.log(` Install state: ${path.relative(projectRoot, path.join(projectRoot, '.dw', 'install-state.json'))} (${installState.managed_files.length} files tracked)\n`);
313
+
226
314
  // 10. Summary
227
315
  console.log(` ${'='.repeat(40)}`);
228
316
  console.log(` Done! ${totalCreated} created, ${totalSkipped} skipped, ${totalOverwritten} overwritten`);
@@ -0,0 +1,57 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const pkg = require('../package.json');
4
+ const { readJson, writeJson } = require('./utils');
5
+
6
+ const STATE_RELATIVE_PATH = path.join('.dw', 'install-state.json');
7
+
8
+ function statePath(projectRoot) {
9
+ return path.join(projectRoot, STATE_RELATIVE_PATH);
10
+ }
11
+
12
+ function readInstallState(projectRoot) {
13
+ return readJson(statePath(projectRoot), null);
14
+ }
15
+
16
+ function hasExistingInstall(projectRoot) {
17
+ return Boolean(
18
+ fs.existsSync(path.join(projectRoot, '.dw', 'commands')) ||
19
+ fs.existsSync(path.join(projectRoot, 'CLAUDE.md')) ||
20
+ fs.existsSync(path.join(projectRoot, 'AGENTS.md')) ||
21
+ readInstallState(projectRoot)
22
+ );
23
+ }
24
+
25
+ function writeInstallState(projectRoot, state) {
26
+ const existing = readInstallState(projectRoot);
27
+ const now = new Date().toISOString();
28
+ const next = {
29
+ schema_version: '1.0',
30
+ package: pkg.name,
31
+ version: pkg.version,
32
+ lang: state.lang,
33
+ profile: state.profile,
34
+ modules: state.modules || [],
35
+ platforms: state.platforms || [],
36
+ managed_files: state.managedFiles || [],
37
+ installed_at: existing && existing.installed_at ? existing.installed_at : now,
38
+ updated_at: now,
39
+ };
40
+ writeJson(statePath(projectRoot), next);
41
+ return next;
42
+ }
43
+
44
+ function listManagedFiles(projectRoot, relativeFiles) {
45
+ return relativeFiles
46
+ .filter(Boolean)
47
+ .map((filePath) => path.relative(projectRoot, filePath))
48
+ .sort();
49
+ }
50
+
51
+ module.exports = {
52
+ STATE_RELATIVE_PATH,
53
+ hasExistingInstall,
54
+ readInstallState,
55
+ writeInstallState,
56
+ listManagedFiles,
57
+ };
@@ -0,0 +1,27 @@
1
+ const path = require('path');
2
+ const { readInstallState, STATE_RELATIVE_PATH } = require('./install-state');
3
+
4
+ function run() {
5
+ const projectRoot = process.cwd();
6
+ const state = readInstallState(projectRoot);
7
+
8
+ if (!state) {
9
+ console.log(`No dev-workflow install state found at ${STATE_RELATIVE_PATH}.`);
10
+ console.log('Run `npx @brunosps00/dev-workflow init` or `update` to create it.');
11
+ return;
12
+ }
13
+
14
+ console.log('dev-workflow install\n');
15
+ console.log(`Root: ${projectRoot}`);
16
+ console.log(`Package: ${state.package || '@brunosps00/dev-workflow'}@${state.version || '(unknown)'}`);
17
+ console.log(`Language: ${state.lang || '(unknown)'}`);
18
+ console.log(`Profile: ${state.profile || '(unknown)'}`);
19
+ console.log(`Modules: ${(state.modules || []).join(', ') || '(none)'}`);
20
+ console.log(`Platforms: ${(state.platforms || []).join(', ') || '(none)'}`);
21
+ console.log(`Managed files tracked: ${(state.managed_files || []).length}`);
22
+ console.log(`Installed at: ${state.installed_at || '(unknown)'}`);
23
+ console.log(`Updated at: ${state.updated_at || '(unknown)'}`);
24
+ console.log(`State: ${path.join(projectRoot, STATE_RELATIVE_PATH)}`);
25
+ }
26
+
27
+ module.exports = { run };
@@ -28,8 +28,9 @@ const removedCommands = require('./removed-commands');
28
28
  function migrate(projectRoot) {
29
29
  const removed = scanRemovedBundledSkills(projectRoot);
30
30
  const orphans = scanOrphanCommandWrappers(projectRoot);
31
+ const commandSources = scanRemovedCommandSources(projectRoot);
31
32
 
32
- if (removed.length === 0 && orphans.length === 0) {
33
+ if (removed.length === 0 && orphans.length === 0 && commandSources.length === 0) {
33
34
  return null; // silent — nothing to clean
34
35
  }
35
36
 
@@ -56,8 +57,16 @@ function migrate(projectRoot) {
56
57
  rmRecursive(entry.path);
57
58
  }
58
59
 
60
+ for (const entry of commandSources) {
61
+ const rel = path.relative(projectRoot, entry.path);
62
+ console.log(
63
+ ` \x1b[33m-\x1b[0m [legacy] Removing command source ${rel} (replaced in v${entry.removedIn} by \`${entry.replacedBy}\`)`
64
+ );
65
+ rmRecursive(entry.path);
66
+ }
67
+
59
68
  console.log();
60
- return { removed: removed.length, orphans: orphans.length };
69
+ return { removed: removed.length, orphans: orphans.length, commandSources: commandSources.length };
61
70
  }
62
71
 
63
72
  function scanRemovedBundledSkills(projectRoot) {
@@ -128,6 +137,21 @@ function scanOrphanCommandWrappers(projectRoot) {
128
137
  return orphans;
129
138
  }
130
139
 
140
+ function scanRemovedCommandSources(projectRoot) {
141
+ const matches = [];
142
+ const commandsDir = path.join(projectRoot, '.dw', 'commands');
143
+ if (!fs.existsSync(commandsDir)) return matches;
144
+
145
+ for (const entry of removedCommands) {
146
+ const target = path.join(commandsDir, `${entry.name}.md`);
147
+ if (fs.existsSync(target)) {
148
+ matches.push({ ...entry, path: target });
149
+ }
150
+ }
151
+
152
+ return matches;
153
+ }
154
+
131
155
  function rmRecursive(target) {
132
156
  if (!fs.existsSync(target)) return;
133
157
  fs.rmSync(target, { recursive: true, force: true });
@@ -0,0 +1,44 @@
1
+ const PROFILES = {
2
+ core: {
3
+ description: 'Core workflow agents only.',
4
+ modules: ['core'],
5
+ },
6
+ standard: {
7
+ description: 'Core workflow plus security and frontend QA agents.',
8
+ modules: ['core', 'security', 'frontend'],
9
+ },
10
+ full: {
11
+ description: 'All bundled dev-workflow agents.',
12
+ modules: ['core', 'security', 'frontend', 'typescript', 'python', 'csharp', 'rust'],
13
+ },
14
+ };
15
+
16
+ const DEFAULT_PROFILE = 'standard';
17
+
18
+ function parseCsv(value) {
19
+ if (!value || value === true) return [];
20
+ return String(value)
21
+ .split(',')
22
+ .map((item) => item.trim())
23
+ .filter(Boolean);
24
+ }
25
+
26
+ function resolveInstallSelection({ profile, modules }) {
27
+ const selectedProfile = profile || DEFAULT_PROFILE;
28
+ if (!PROFILES[selectedProfile]) {
29
+ throw new Error(`Unknown profile: ${selectedProfile}. Valid profiles: ${Object.keys(PROFILES).join(', ')}`);
30
+ }
31
+
32
+ const explicitModules = parseCsv(modules);
33
+ const selectedModules = new Set(PROFILES[selectedProfile].modules);
34
+ for (const moduleName of explicitModules) {
35
+ selectedModules.add(moduleName);
36
+ }
37
+
38
+ return {
39
+ profile: selectedProfile,
40
+ modules: Array.from(selectedModules).sort(),
41
+ };
42
+ }
43
+
44
+ module.exports = { DEFAULT_PROFILE, PROFILES, resolveInstallSelection };
@@ -0,0 +1,109 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { readJson } = require('./utils');
4
+
5
+ const SCAFFOLD_DIR = path.join(__dirname, '..', 'scaffold');
6
+ const REGISTRY_PATH = path.join(SCAFFOLD_DIR, 'skill-registry.json');
7
+ const SKILLS_DIR = path.join(SCAFFOLD_DIR, 'skills');
8
+
9
+ const VALID_KINDS = new Set(['protocol', 'domain-pack', 'recipe-pack', 'asset-pack']);
10
+ const VALID_LOAD_POLICIES = new Set(['always-small', 'lazy-references', 'opt-in-runtime']);
11
+
12
+ function readSkillRegistry() {
13
+ const registry = readJson(REGISTRY_PATH, null);
14
+ if (!registry || !Array.isArray(registry.skills)) {
15
+ throw new Error('Invalid scaffold/skill-registry.json');
16
+ }
17
+ return registry;
18
+ }
19
+
20
+ function listScaffoldSkillNames() {
21
+ if (!fs.existsSync(SKILLS_DIR)) return [];
22
+ return fs
23
+ .readdirSync(SKILLS_DIR, { withFileTypes: true })
24
+ .filter((entry) => entry.isDirectory())
25
+ .map((entry) => entry.name)
26
+ .sort();
27
+ }
28
+
29
+ function validateSkillRegistry() {
30
+ const registry = readSkillRegistry();
31
+ const issues = [];
32
+ const warnings = [];
33
+ const entriesByName = new Map();
34
+ const scaffoldNames = new Set(listScaffoldSkillNames());
35
+
36
+ for (const entry of registry.skills) {
37
+ validateEntry(entry, issues);
38
+ if (entriesByName.has(entry.name)) {
39
+ issues.push(`Skill registry duplicates entry: ${entry.name}`);
40
+ }
41
+ entriesByName.set(entry.name, entry);
42
+
43
+ if (!scaffoldNames.has(entry.name)) {
44
+ issues.push(`Skill registry references missing scaffold skill: ${entry.name}`);
45
+ continue;
46
+ }
47
+
48
+ const skillPath = path.join(SKILLS_DIR, entry.name, 'SKILL.md');
49
+ if (!fs.existsSync(skillPath)) {
50
+ issues.push(`Scaffold skill missing SKILL.md: ${entry.name}`);
51
+ continue;
52
+ }
53
+
54
+ const size = fs.statSync(skillPath).size;
55
+ if (entry.context_limit && size > entry.context_limit) {
56
+ warnings.push(`Skill ${entry.name} SKILL.md is ${size} bytes; registry limit is ${entry.context_limit}`);
57
+ }
58
+
59
+ if (entry.load_policy === 'always-small' && size > 8000) {
60
+ warnings.push(`Skill ${entry.name} is marked always-small but SKILL.md is ${size} bytes`);
61
+ }
62
+ }
63
+
64
+ for (const skillName of scaffoldNames) {
65
+ if (!entriesByName.has(skillName)) {
66
+ issues.push(`Scaffold skill missing from registry: ${skillName}`);
67
+ }
68
+ }
69
+
70
+ return { issues, warnings, registry };
71
+ }
72
+
73
+ function validateEntry(entry, issues) {
74
+ for (const field of ['name', 'kind', 'tier', 'owner', 'trigger', 'expected_output', 'load_policy', 'context_limit']) {
75
+ if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
76
+ issues.push(`Skill registry entry ${entry.name || '(unknown)'} missing field: ${field}`);
77
+ }
78
+ }
79
+
80
+ if (entry.kind && !VALID_KINDS.has(entry.kind)) {
81
+ issues.push(`Skill registry entry ${entry.name} has invalid kind: ${entry.kind}`);
82
+ }
83
+
84
+ if (entry.load_policy && !VALID_LOAD_POLICIES.has(entry.load_policy)) {
85
+ issues.push(`Skill registry entry ${entry.name} has invalid load_policy: ${entry.load_policy}`);
86
+ }
87
+
88
+ if (typeof entry.context_limit !== 'number' || entry.context_limit <= 0) {
89
+ issues.push(`Skill registry entry ${entry.name} must set a positive numeric context_limit`);
90
+ }
91
+ }
92
+
93
+ function listInstalledSkillNames(projectRoot) {
94
+ const installedDir = path.join(projectRoot, '.agents', 'skills');
95
+ if (!fs.existsSync(installedDir)) return [];
96
+ return fs
97
+ .readdirSync(installedDir, { withFileTypes: true })
98
+ .filter((entry) => entry.isDirectory() && fs.existsSync(path.join(installedDir, entry.name, 'SKILL.md')))
99
+ .map((entry) => entry.name)
100
+ .sort();
101
+ }
102
+
103
+ module.exports = {
104
+ REGISTRY_PATH,
105
+ readSkillRegistry,
106
+ validateSkillRegistry,
107
+ listScaffoldSkillNames,
108
+ listInstalledSkillNames,
109
+ };