@brunosps00/dev-workflow 1.0.4 → 1.0.5

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 (66) hide show
  1. package/README.md +58 -14
  2. package/bin/dev-workflow.js +29 -3
  3. package/lib/agents.js +410 -0
  4. package/lib/constants.js +12 -0
  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 +44 -1
  35. package/scaffold/en/commands/dw-bugfix.md +9 -0
  36. package/scaffold/en/commands/dw-context-budget.md +28 -0
  37. package/scaffold/en/commands/dw-harness-audit.md +31 -0
  38. package/scaffold/en/commands/dw-help.md +15 -5
  39. package/scaffold/en/commands/dw-intel.md +6 -0
  40. package/scaffold/en/commands/dw-plan.md +11 -0
  41. package/scaffold/en/commands/dw-review.md +12 -0
  42. package/scaffold/en/commands/dw-skill-health.md +29 -0
  43. package/scaffold/en/commands/dw-subtask-complete.md +40 -0
  44. package/scaffold/en/commands/dw-subtask-resume.md +21 -0
  45. package/scaffold/en/commands/dw-subtask-start.md +22 -0
  46. package/scaffold/pt-br/agent-instructions.md +44 -1
  47. package/scaffold/pt-br/commands/dw-bugfix.md +9 -0
  48. package/scaffold/pt-br/commands/dw-context-budget.md +28 -0
  49. package/scaffold/pt-br/commands/dw-harness-audit.md +31 -0
  50. package/scaffold/pt-br/commands/dw-help.md +15 -5
  51. package/scaffold/pt-br/commands/dw-intel.md +6 -0
  52. package/scaffold/pt-br/commands/dw-plan.md +11 -0
  53. package/scaffold/pt-br/commands/dw-review.md +12 -0
  54. package/scaffold/pt-br/commands/dw-skill-health.md +29 -0
  55. package/scaffold/pt-br/commands/dw-subtask-complete.md +40 -0
  56. package/scaffold/pt-br/commands/dw-subtask-resume.md +21 -0
  57. package/scaffold/pt-br/commands/dw-subtask-start.md +22 -0
  58. package/scaffold/skill-registry.json +258 -0
  59. package/scaffold/skills/dw-codebase-intel/SKILL.md +12 -5
  60. package/scaffold/skills/dw-context-budget/SKILL.md +45 -0
  61. package/scaffold/skills/dw-search-first/SKILL.md +31 -0
  62. package/scaffold/skills/dw-silent-failure/SKILL.md +33 -0
  63. package/scaffold/skills/dw-verify/SKILL.md +2 -29
  64. package/scaffold/skills/dw-verify/references/verification-discovery-and-attribution.md +32 -0
  65. package/scaffold/skills/humanizer/SKILL.md +7 -412
  66. package/scaffold/skills/humanizer/references/ai-writing-patterns.md +418 -0
package/lib/constants.js CHANGED
@@ -11,6 +11,8 @@ const COMMANDS = {
11
11
  { name: 'dw-functional-doc', description: 'Maps screens and user flows into a functional doc, validated end-to-end with Playwright.' },
12
12
  { name: 'dw-generate-pr', description: 'Trigger when commits are ready and the branch needs a PR. Pushes branch and opens PR with summary + test plan + hard verify gate.' },
13
13
  { name: 'dw-help', description: 'Lists primary commands and the flows that connect them. Pass --advanced to see internal/hidden commands.' },
14
+ { name: 'dw-context-budget', description: 'Audits context overhead from commands, skills, agents, instructions, and MCPs; recommends token savings.' },
15
+ { name: 'dw-harness-audit', description: 'Runs a deterministic health scorecard for dev-workflow installation, agents, MCPs, gates, and context discipline.' },
14
16
  { name: 'dw-install-aws-skills', description: 'Trigger when user asks to install AWS expertise, setup AWS MCP server, or add AWS agent skills. Opt-in: clones curated skills from aws/agent-toolkit-for-aws into .agents/skills/aws/ and registers the unified AWS MCP Server (stdio via mcp-proxy-for-aws). Requires uv, aws cli, and AWS credentials.' },
15
17
  { name: 'dw-install-azure-skills', description: 'Trigger when user asks to install Azure expertise, setup Microsoft docs MCP, or add Azure agent skills. Opt-in: clones curated skills from MicrosoftDocs/Agent-Skills into .agents/skills/azure/ and registers the Microsoft Learn MCP server.' },
16
18
  { name: 'dw-intel', description: 'Codebase intelligence: query mode (default) answers questions citing .dw/intel/ + .dw/rules/; --build mode (re)builds the index.' },
@@ -23,6 +25,10 @@ const COMMANDS = {
23
25
  { name: 'dw-review', description: 'Trigger when user asks to review code, check quality, or verify PR readiness. Default runs L2 (PRD coverage) + L3 (code quality). Flags --coverage-only / --code-only / --bugfix <slug>.' },
24
26
  { name: 'dw-run', description: 'Trigger when user wants to execute tasks. Default runs all pending in dependency order; \'run <task-id>\' runs one; \'run --resume\' continues an interrupted plan.' },
25
27
  { name: 'dw-secure-audit', description: 'Unified security audit: OWASP + Trivy SCA/secret/IaC + lockfile + supply-chain compromise check. Hidden; auto-invoked by /dw-review and /dw-generate-pr.' },
28
+ { name: 'dw-skill-health', description: 'Audits installed skills and agents for bloat, duplication, missing references, and stale or unused modules.' },
29
+ { name: 'dw-subtask-start', description: 'Creates a minimal input packet for a subagent without injecting the parent transcript.' },
30
+ { name: 'dw-subtask-complete', description: 'Records a structured subagent handoff with result, files, decisions, risks, verification, and blockers.' },
31
+ { name: 'dw-subtask-resume', description: 'Consumes pending subagent handoffs, summarizes them for the parent, and archives the local packets.' },
26
32
  { name: 'dw-update', description: 'Updates dev-workflow to the latest npm release in-place, with a snapshot you can roll back to.' },
27
33
  ],
28
34
  'pt-br': [
@@ -37,6 +43,8 @@ const COMMANDS = {
37
43
  { name: 'dw-functional-doc', description: 'Mapeia telas e fluxos em um dossie funcional, validado E2E com Playwright.' },
38
44
  { name: 'dw-generate-pr', description: 'Trigger quando commits estao prontos e branch precisa de PR. Push da branch e abre PR com summary + test plan + hard verify gate.' },
39
45
  { name: 'dw-help', description: 'Lista comandos primarios e fluxos que os conectam. Passe --advanced para ver comandos internos/escondidos.' },
46
+ { name: 'dw-context-budget', description: 'Audita overhead de contexto em comandos, skills, agentes, instrucoes e MCPs; recomenda economia de tokens.' },
47
+ { name: 'dw-harness-audit', description: 'Roda um scorecard deterministico da instalacao dev-workflow, agentes, MCPs, gates e disciplina de contexto.' },
40
48
  { name: 'dw-install-aws-skills', description: 'Trigger quando usuario pede pra instalar expertise AWS, configurar MCP da AWS, ou adicionar agent skills AWS. Opt-in: clona skills curadas de aws/agent-toolkit-for-aws para .agents/skills/aws/ e registra o AWS MCP Server unificado (stdio via mcp-proxy-for-aws). Requer uv, aws cli e credenciais AWS.' },
41
49
  { name: 'dw-install-azure-skills', description: 'Trigger quando usuario pede pra instalar expertise Azure, configurar MCP do Microsoft docs, ou adicionar agent skills Azure. Opt-in: clona skills curadas de MicrosoftDocs/Agent-Skills para .agents/skills/azure/ e registra o Microsoft Learn MCP server.' },
42
50
  { name: 'dw-intel', description: 'Inteligencia do codebase: modo query (default) responde citando .dw/intel/ + .dw/rules/; modo --build (re)constroi indice.' },
@@ -49,6 +57,10 @@ const COMMANDS = {
49
57
  { name: 'dw-review', description: 'Trigger quando usuario pede pra revisar codigo, checar qualidade ou validar prontidao pra PR. Default roda L2 (cobertura PRD) + L3 (qualidade). Flags --coverage-only / --code-only / --bugfix <slug>.' },
50
58
  { name: 'dw-run', description: 'Trigger quando usuario quer executar tasks. Default roda todas pendentes em ordem de dependencia; \'run <task-id>\' roda uma; \'run --resume\' continua plan interrompido.' },
51
59
  { name: 'dw-secure-audit', description: 'Audit unificado de seguranca: OWASP + Trivy SCA/secret/IaC + lockfile + supply-chain compromise. Hidden; auto-invocado por /dw-review e /dw-generate-pr.' },
60
+ { name: 'dw-skill-health', description: 'Audita skills e agentes instalados por bloat, duplicacao, referencias quebradas e modulos sem uso.' },
61
+ { name: 'dw-subtask-start', description: 'Cria um input packet minimo para subagente sem injetar transcript bruto do parent.' },
62
+ { name: 'dw-subtask-complete', description: 'Registra handoff estruturado do subagente com resultado, arquivos, decisoes, riscos, verificacao e bloqueios.' },
63
+ { name: 'dw-subtask-resume', description: 'Consome handoffs pendentes de subagentes, resume para o parent e arquiva os pacotes locais.' },
52
64
  { name: 'dw-update', description: 'Atualiza o dev-workflow para o release mais recente no npm, com snapshot para rollback.' },
53
65
  ],
54
66
  };
package/lib/doctor.js ADDED
@@ -0,0 +1,213 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { COMMANDS, PLATFORMS, MCP_SERVERS } = require('./constants');
4
+ const {
5
+ listExpectedAgentFiles,
6
+ listLegacyAgentFiles,
7
+ readAgentRegistry,
8
+ validateAgentRegistry,
9
+ AGENT_TARGETS,
10
+ } = require('./agents');
11
+ const { readInstallState, STATE_RELATIVE_PATH } = require('./install-state');
12
+ const { validateSkillRegistry, readSkillRegistry } = require('./skill-registry');
13
+ const removedCommands = require('./removed-commands');
14
+
15
+ function run() {
16
+ const projectRoot = process.cwd();
17
+ const state = readInstallState(projectRoot);
18
+ const issues = [];
19
+ const warnings = [];
20
+
21
+ if (!state) {
22
+ issues.push(`Missing ${STATE_RELATIVE_PATH}. Run \`dev-workflow repair\`.`);
23
+ }
24
+
25
+ const lang = (state && state.lang) || 'en';
26
+ const modules = (state && state.modules) || ['core', 'security', 'frontend'];
27
+ const commands = COMMANDS[lang] || COMMANDS.en;
28
+
29
+ checkCommands(projectRoot, commands, issues);
30
+ checkWrappers(projectRoot, commands, issues);
31
+ checkAgents(projectRoot, modules, issues);
32
+ checkAgentRegistry(issues, warnings);
33
+ checkSubtaskLayout(projectRoot, issues);
34
+ checkSkillRegistry(projectRoot, issues, warnings);
35
+ checkMcp(projectRoot, issues);
36
+ checkLegacyCommands(projectRoot, warnings);
37
+
38
+ console.log('dev-workflow doctor\n');
39
+ if (issues.length === 0 && warnings.length === 0) {
40
+ console.log('PASS: managed commands, wrappers, agents, skills, and MCP config look healthy.');
41
+ return;
42
+ }
43
+
44
+ if (issues.length) {
45
+ console.log('Issues:');
46
+ for (const issue of issues) console.log(`- ${issue}`);
47
+ console.log();
48
+ }
49
+
50
+ if (warnings.length) {
51
+ console.log('Warnings:');
52
+ for (const warning of warnings) console.log(`- ${warning}`);
53
+ console.log();
54
+ }
55
+
56
+ console.log('Run `npx @brunosps00/dev-workflow repair` to reconcile managed files.');
57
+ if (issues.length) process.exitCode = 1;
58
+ }
59
+
60
+ function checkCommands(projectRoot, commands, issues) {
61
+ for (const cmd of commands) {
62
+ const file = path.join(projectRoot, '.dw', 'commands', `${cmd.name}.md`);
63
+ if (!fs.existsSync(file)) issues.push(`Missing command source: ${path.relative(projectRoot, file)}`);
64
+ }
65
+ }
66
+
67
+ function checkWrappers(projectRoot, commands, issues) {
68
+ for (const [, platform] of Object.entries(PLATFORMS)) {
69
+ for (const cmd of commands) {
70
+ const file = platform.flat
71
+ ? path.join(projectRoot, platform.dir, `${cmd.name}.md`)
72
+ : path.join(projectRoot, platform.dir, cmd.name, 'SKILL.md');
73
+ if (!fs.existsSync(file)) issues.push(`Missing wrapper: ${path.relative(projectRoot, file)}`);
74
+ }
75
+ }
76
+ }
77
+
78
+ function checkAgents(projectRoot, modules, issues) {
79
+ for (const file of listExpectedAgentFiles(projectRoot, modules)) {
80
+ if (!fs.existsSync(file)) issues.push(`Missing agent: ${path.relative(projectRoot, file)}`);
81
+ }
82
+ for (const file of listLegacyAgentFiles(projectRoot, modules)) {
83
+ if (fs.existsSync(file)) issues.push(`Legacy Copilot agent path still exists: ${path.relative(projectRoot, file)}`);
84
+ }
85
+ for (const file of listExpectedAgentFiles(projectRoot, modules)) {
86
+ if (fs.existsSync(file) && file.endsWith('.md') && fs.statSync(file).size > 8 * 1024) {
87
+ issues.push(`Agent prompt exceeds 8KB: ${path.relative(projectRoot, file)}`);
88
+ }
89
+ }
90
+ checkProviderAgentCompatibility(projectRoot, modules, issues);
91
+ }
92
+
93
+ function checkAgentRegistry(issues, warnings) {
94
+ const registry = readAgentRegistry();
95
+ const validation = validateAgentRegistry(registry);
96
+ issues.push(...validation.issues);
97
+ warnings.push(...validation.warnings);
98
+ const installedRegistryPath = path.join(process.cwd(), '.dw', 'agent-registry.json');
99
+ if (!fs.existsSync(installedRegistryPath)) {
100
+ issues.push(`Missing agent registry: ${path.relative(process.cwd(), installedRegistryPath)}`);
101
+ } else {
102
+ try {
103
+ const installedRegistry = JSON.parse(fs.readFileSync(installedRegistryPath, 'utf-8'));
104
+ if (installedRegistry.schema_version !== '2.0') {
105
+ issues.push('Installed agent registry must use schema_version 2.0.');
106
+ }
107
+ } catch {
108
+ issues.push(`Malformed agent registry: ${path.relative(process.cwd(), installedRegistryPath)}`);
109
+ }
110
+ }
111
+ const agentNames = new Set(registry.agents.map((agent) => agent.name));
112
+ for (const [commandName, agents] of Object.entries(registry.commands || {})) {
113
+ if (!COMMANDS.en.some((cmd) => cmd.name === commandName)) {
114
+ issues.push(`Agent registry references unknown command: ${commandName}`);
115
+ }
116
+ for (const agentName of agents) {
117
+ if (!agentNames.has(agentName)) {
118
+ issues.push(`Agent registry references missing agent: ${agentName}`);
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ function checkProviderAgentCompatibility(projectRoot, modules, issues) {
125
+ const expected = listExpectedAgentFiles(projectRoot, modules);
126
+ for (const file of expected) {
127
+ if (!fs.existsSync(file)) continue;
128
+ const rel = path.relative(projectRoot, file);
129
+ const content = fs.readFileSync(file, 'utf-8');
130
+ if (rel.startsWith(`${AGENT_TARGETS.copilot.dir}/`)) {
131
+ for (const unsupported of ['permissionMode:', 'maxTurns:', 'model:', 'mode: subagent']) {
132
+ if (content.includes(unsupported)) issues.push(`Copilot agent contains unsupported field ${unsupported} ${rel}`);
133
+ }
134
+ }
135
+ if (rel.startsWith(`${AGENT_TARGETS.claude.dir}/`) && !content.includes('permissionMode:')) {
136
+ issues.push(`Claude agent missing permissionMode: ${rel}`);
137
+ }
138
+ if (rel.startsWith(`${AGENT_TARGETS.opencode.dir}/`) && content.includes('\ntools:')) {
139
+ issues.push(`OpenCode agent should use permission instead of tools: ${rel}`);
140
+ }
141
+ }
142
+ }
143
+
144
+ function checkSubtaskLayout(projectRoot, issues) {
145
+ const gitignore = path.join(projectRoot, '.dw', 'subtasks', '.gitignore');
146
+ if (!fs.existsSync(gitignore)) issues.push(`Missing subtask gitignore: ${path.relative(projectRoot, gitignore)}`);
147
+ }
148
+
149
+ function checkSkillRegistry(projectRoot, issues, warnings) {
150
+ const validation = validateSkillRegistry();
151
+ issues.push(...validation.issues);
152
+ warnings.push(...validation.warnings);
153
+
154
+ const installedRegistryPath = path.join(projectRoot, '.dw', 'skill-registry.json');
155
+ if (!fs.existsSync(installedRegistryPath)) {
156
+ issues.push(`Missing skill registry: ${path.relative(projectRoot, installedRegistryPath)}`);
157
+ } else {
158
+ try {
159
+ const installedRegistry = JSON.parse(fs.readFileSync(installedRegistryPath, 'utf-8'));
160
+ const currentRegistry = readSkillRegistry();
161
+ if (installedRegistry.schema_version !== currentRegistry.schema_version) {
162
+ warnings.push('Installed skill registry schema differs from package registry; run repair.');
163
+ }
164
+ const installedNames = new Set((installedRegistry.skills || []).map((entry) => entry.name));
165
+ for (const entry of currentRegistry.skills) {
166
+ if (entry.bundled && !installedNames.has(entry.name)) {
167
+ warnings.push(`Installed skill registry missing bundled entry: ${entry.name}`);
168
+ }
169
+ }
170
+ } catch {
171
+ issues.push(`Malformed skill registry: ${path.relative(projectRoot, installedRegistryPath)}`);
172
+ }
173
+ }
174
+
175
+ const registry = readSkillRegistry();
176
+ for (const entry of registry.skills) {
177
+ if (!entry.bundled) continue;
178
+ const skillPath = path.join(projectRoot, '.agents', 'skills', entry.name, 'SKILL.md');
179
+ if (!fs.existsSync(skillPath)) {
180
+ issues.push(`Missing bundled skill: ${path.relative(projectRoot, skillPath)}`);
181
+ }
182
+ }
183
+ }
184
+
185
+ function checkMcp(projectRoot, issues) {
186
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
187
+ if (!fs.existsSync(settingsPath)) {
188
+ issues.push('Missing .claude/settings.json with MCP configuration');
189
+ return;
190
+ }
191
+
192
+ try {
193
+ const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
194
+ for (const name of Object.keys(MCP_SERVERS)) {
195
+ if (!settings.mcpServers || !settings.mcpServers[name]) {
196
+ issues.push(`Missing MCP server: ${name}`);
197
+ }
198
+ }
199
+ } catch {
200
+ issues.push('Malformed .claude/settings.json');
201
+ }
202
+ }
203
+
204
+ function checkLegacyCommands(projectRoot, warnings) {
205
+ for (const entry of removedCommands) {
206
+ const file = path.join(projectRoot, '.dw', 'commands', `${entry.name}.md`);
207
+ if (fs.existsSync(file)) {
208
+ warnings.push(`Legacy command source still exists: ${path.relative(projectRoot, file)} -> ${entry.replacedBy}`);
209
+ }
210
+ }
211
+ }
212
+
213
+ module.exports = { run };
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 };