@brunosps00/dev-workflow 1.0.2 → 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.
- package/README.md +74 -20
- package/bin/dev-workflow.js +53 -10
- package/lib/agents.js +410 -0
- package/lib/aws-categories.js +80 -0
- package/lib/azure-categories.js +168 -0
- package/lib/constants.js +16 -0
- package/lib/doctor.js +213 -0
- package/lib/init.js +96 -8
- package/lib/install-aws-skills.js +345 -0
- package/lib/install-azure-skills.js +231 -0
- package/lib/install-state.js +57 -0
- package/lib/list-installed.js +27 -0
- package/lib/mcp.js +32 -21
- package/lib/migrate-skills.js +26 -2
- package/lib/profiles.js +44 -0
- package/lib/prompts.js +38 -1
- package/lib/skill-registry.js +109 -0
- package/lib/subtasks.js +264 -0
- package/lib/uninstall.js +21 -12
- package/lib/utils.js +42 -1
- package/lib/wrappers.js +2 -1
- package/package.json +1 -1
- package/scaffold/agent-registry.json +272 -0
- package/scaffold/agents/core/dw-build-fixer.md +21 -0
- package/scaffold/agents/core/dw-code-explorer.md +32 -0
- package/scaffold/agents/core/dw-code-reviewer.md +28 -0
- package/scaffold/agents/core/dw-planner.md +23 -0
- package/scaffold/agents/core/dw-test-author.md +21 -0
- package/scaffold/agents/csharp/dw-csharp-reviewer.md +15 -0
- package/scaffold/agents/csharp/dw-dotnet-build-fixer.md +15 -0
- package/scaffold/agents/frontend/dw-qa-runner.md +21 -0
- package/scaffold/agents/python/dw-python-build-fixer.md +15 -0
- package/scaffold/agents/python/dw-python-reviewer.md +15 -0
- package/scaffold/agents/rust/dw-rust-build-fixer.md +15 -0
- package/scaffold/agents/rust/dw-rust-reviewer.md +15 -0
- package/scaffold/agents/security/dw-security-reviewer.md +24 -0
- package/scaffold/agents/security/dw-silent-failure-hunter.md +22 -0
- package/scaffold/agents/typescript/dw-typescript-build-fixer.md +15 -0
- package/scaffold/agents/typescript/dw-typescript-reviewer.md +15 -0
- package/scaffold/en/agent-instructions.md +46 -1
- package/scaffold/en/commands/dw-bugfix.md +9 -0
- package/scaffold/en/commands/dw-context-budget.md +28 -0
- package/scaffold/en/commands/dw-harness-audit.md +31 -0
- package/scaffold/en/commands/dw-help.md +15 -5
- package/scaffold/en/commands/dw-install-aws-skills.md +166 -0
- package/scaffold/en/commands/dw-install-azure-skills.md +138 -0
- package/scaffold/en/commands/dw-intel.md +6 -0
- package/scaffold/en/commands/dw-plan.md +11 -0
- package/scaffold/en/commands/dw-review.md +12 -0
- package/scaffold/en/commands/dw-skill-health.md +29 -0
- package/scaffold/en/commands/dw-subtask-complete.md +40 -0
- package/scaffold/en/commands/dw-subtask-resume.md +21 -0
- package/scaffold/en/commands/dw-subtask-start.md +22 -0
- package/scaffold/pt-br/agent-instructions.md +46 -1
- package/scaffold/pt-br/commands/dw-bugfix.md +9 -0
- package/scaffold/pt-br/commands/dw-context-budget.md +28 -0
- package/scaffold/pt-br/commands/dw-harness-audit.md +31 -0
- package/scaffold/pt-br/commands/dw-help.md +15 -5
- package/scaffold/pt-br/commands/dw-install-aws-skills.md +166 -0
- package/scaffold/pt-br/commands/dw-install-azure-skills.md +138 -0
- package/scaffold/pt-br/commands/dw-intel.md +6 -0
- package/scaffold/pt-br/commands/dw-plan.md +11 -0
- package/scaffold/pt-br/commands/dw-review.md +12 -0
- package/scaffold/pt-br/commands/dw-skill-health.md +29 -0
- package/scaffold/pt-br/commands/dw-subtask-complete.md +40 -0
- package/scaffold/pt-br/commands/dw-subtask-resume.md +21 -0
- package/scaffold/pt-br/commands/dw-subtask-start.md +22 -0
- package/scaffold/skill-registry.json +258 -0
- package/scaffold/skills/dw-codebase-intel/SKILL.md +12 -5
- package/scaffold/skills/dw-context-budget/SKILL.md +45 -0
- package/scaffold/skills/dw-search-first/SKILL.md +31 -0
- package/scaffold/skills/dw-silent-failure/SKILL.md +33 -0
- package/scaffold/skills/dw-verify/SKILL.md +2 -29
- package/scaffold/skills/dw-verify/references/verification-discovery-and-attribution.md +32 -0
- package/scaffold/skills/humanizer/SKILL.md +7 -412
- package/scaffold/skills/humanizer/references/ai-writing-patterns.md +418 -0
package/lib/mcp.js
CHANGED
|
@@ -3,38 +3,49 @@ const path = require('path');
|
|
|
3
3
|
const { ensureDir } = require('./utils');
|
|
4
4
|
const { MCP_SERVERS } = require('./constants');
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// Read .claude/settings.json (or return empty object if missing/malformed).
|
|
7
|
+
function readSettings(projectRoot) {
|
|
7
8
|
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} catch {
|
|
14
|
-
settings = {};
|
|
15
|
-
}
|
|
9
|
+
if (!fs.existsSync(settingsPath)) return {};
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
12
|
+
} catch {
|
|
13
|
+
return {};
|
|
16
14
|
}
|
|
15
|
+
}
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
// Write .claude/settings.json, ensuring directory exists and JSON ends with newline.
|
|
18
|
+
function writeSettings(projectRoot, settings) {
|
|
19
|
+
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
20
|
+
ensureDir(path.dirname(settingsPath));
|
|
21
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Upsert a single MCP server entry into .claude/settings.json. Returns "added" or
|
|
25
|
+
// "skipped". Existing entries are NEVER overwritten — the user's config wins.
|
|
26
|
+
// Shared by installMCPs (default init/update) and install-azure-skills.
|
|
27
|
+
function addMcpServer(projectRoot, name, config) {
|
|
28
|
+
const settings = readSettings(projectRoot);
|
|
29
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
30
|
+
if (settings.mcpServers[name]) {
|
|
31
|
+
return 'skipped';
|
|
20
32
|
}
|
|
33
|
+
settings.mcpServers[name] = config;
|
|
34
|
+
writeSettings(projectRoot, settings);
|
|
35
|
+
return 'added';
|
|
36
|
+
}
|
|
21
37
|
|
|
38
|
+
function installMCPs(projectRoot) {
|
|
22
39
|
let added = 0;
|
|
23
40
|
let skipped = 0;
|
|
24
41
|
|
|
25
42
|
for (const [name, config] of Object.entries(MCP_SERVERS)) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
settings.mcpServers[name] = config;
|
|
30
|
-
added++;
|
|
31
|
-
}
|
|
43
|
+
const status = addMcpServer(projectRoot, name, config);
|
|
44
|
+
if (status === 'added') added++;
|
|
45
|
+
else skipped++;
|
|
32
46
|
}
|
|
33
47
|
|
|
34
|
-
ensureDir(path.dirname(settingsPath));
|
|
35
|
-
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
36
|
-
|
|
37
48
|
return { added, skipped };
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
module.exports = { installMCPs };
|
|
51
|
+
module.exports = { installMCPs, addMcpServer, readSettings, writeSettings };
|
package/lib/migrate-skills.js
CHANGED
|
@@ -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 });
|
package/lib/profiles.js
ADDED
|
@@ -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 };
|
package/lib/prompts.js
CHANGED
|
@@ -33,4 +33,41 @@ async function selectLanguage(flagLang) {
|
|
|
33
33
|
return lang;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// Multi-select prompt. Accepts comma-separated indexes ("1,3,5"), "all" for every
|
|
37
|
+
// option, or a single index. Returns an array of selected option strings.
|
|
38
|
+
// Invalid or empty input falls back to the first option.
|
|
39
|
+
function multiSelect(prompt, options) {
|
|
40
|
+
const rl = readline.createInterface({
|
|
41
|
+
input: process.stdin,
|
|
42
|
+
output: process.stdout,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return new Promise((resolve) => {
|
|
46
|
+
const optionsStr = options.map((o, i) => ` ${i + 1}) ${o}`).join('\n');
|
|
47
|
+
const help = `\n Enter numbers separated by commas (e.g. 1,3,5) or "all" for everything.`;
|
|
48
|
+
rl.question(`\n${prompt}\n${optionsStr}${help}\n\n Choose: `, (answer) => {
|
|
49
|
+
rl.close();
|
|
50
|
+
const trimmed = (answer || '').trim().toLowerCase();
|
|
51
|
+
if (!trimmed) {
|
|
52
|
+
resolve([options[0]]);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (trimmed === 'all') {
|
|
56
|
+
resolve(options.slice());
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
const selected = [];
|
|
61
|
+
for (const part of trimmed.split(',')) {
|
|
62
|
+
const idx = parseInt(part.trim(), 10) - 1;
|
|
63
|
+
if (idx >= 0 && idx < options.length && !seen.has(idx)) {
|
|
64
|
+
seen.add(idx);
|
|
65
|
+
selected.push(options[idx]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
resolve(selected.length > 0 ? selected : [options[0]]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { selectLanguage, question, multiSelect };
|
|
@@ -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
|
+
};
|
package/lib/subtasks.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { ensureDir, writeFile } = require('./utils');
|
|
4
|
+
const { readAgentRegistry } = require('./agents');
|
|
5
|
+
|
|
6
|
+
const SUBTASKS_RELATIVE_DIR = path.join('.dw', 'subtasks');
|
|
7
|
+
|
|
8
|
+
function run(argv = []) {
|
|
9
|
+
const action = argv[0];
|
|
10
|
+
const flags = parseFlags(argv.slice(1));
|
|
11
|
+
const projectRoot = process.cwd();
|
|
12
|
+
|
|
13
|
+
switch (action) {
|
|
14
|
+
case 'create':
|
|
15
|
+
createSubtask(projectRoot, flags);
|
|
16
|
+
break;
|
|
17
|
+
case 'complete':
|
|
18
|
+
completeSubtask(projectRoot, flags);
|
|
19
|
+
break;
|
|
20
|
+
case 'consume':
|
|
21
|
+
consumeSubtasks(projectRoot);
|
|
22
|
+
break;
|
|
23
|
+
case 'list':
|
|
24
|
+
listSubtasks(projectRoot);
|
|
25
|
+
break;
|
|
26
|
+
case undefined:
|
|
27
|
+
case 'help':
|
|
28
|
+
case '--help':
|
|
29
|
+
case '-h':
|
|
30
|
+
printHelp();
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
throw new Error(`Unknown subtask action: ${action}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createSubtask(projectRoot, flags) {
|
|
38
|
+
const agent = required(flags, 'agent');
|
|
39
|
+
const goal = required(flags, 'goal');
|
|
40
|
+
validateAgent(agent);
|
|
41
|
+
ensureSubtaskLayout(projectRoot);
|
|
42
|
+
|
|
43
|
+
const now = new Date();
|
|
44
|
+
const stamp = now.toISOString().replace(/[-:]/g, '').replace(/\..+$/, 'Z');
|
|
45
|
+
const slug = `${stamp}-${slugify(goal)}`;
|
|
46
|
+
const dir = path.join(projectRoot, SUBTASKS_RELATIVE_DIR, 'pending', slug);
|
|
47
|
+
ensureDir(dir);
|
|
48
|
+
|
|
49
|
+
const task = [
|
|
50
|
+
`# Subtask: ${goal}`,
|
|
51
|
+
'',
|
|
52
|
+
`- Slug: \`${slug}\``,
|
|
53
|
+
`- Agent: \`${agent}\``,
|
|
54
|
+
`- Created: ${now.toISOString()}`,
|
|
55
|
+
'',
|
|
56
|
+
'## Objective',
|
|
57
|
+
goal,
|
|
58
|
+
'',
|
|
59
|
+
'## Allowed Files And Sources',
|
|
60
|
+
flags.files || 'Parent must fill this before dispatch if the boundary is not obvious.',
|
|
61
|
+
'',
|
|
62
|
+
'## Constraints',
|
|
63
|
+
flags.constraints || 'Use project rules. Do not request or paste the full parent transcript.',
|
|
64
|
+
'',
|
|
65
|
+
'## Expected Output',
|
|
66
|
+
flags.output || 'Structured handoff with result, files read, files changed, decisions, risks, verification, next steps, and blockers.',
|
|
67
|
+
'',
|
|
68
|
+
'## Context Budget',
|
|
69
|
+
flags.budget || 'Use the agent registry default. Return only summarized logs and relevant paths.',
|
|
70
|
+
'',
|
|
71
|
+
'## Stop Condition',
|
|
72
|
+
flags.stop || 'Stop when the expected output packet is complete or the scope exceeds the packet.',
|
|
73
|
+
'',
|
|
74
|
+
'## Handoff Command',
|
|
75
|
+
'',
|
|
76
|
+
`When complete, write a handoff markdown file and run:`,
|
|
77
|
+
'',
|
|
78
|
+
'```bash',
|
|
79
|
+
`npx @brunosps00/dev-workflow subtask complete --slug=${slug} --file=<handoff.md>`,
|
|
80
|
+
'```',
|
|
81
|
+
'',
|
|
82
|
+
].join('\n');
|
|
83
|
+
|
|
84
|
+
writeFile(path.join(dir, 'TASK.md'), task, true);
|
|
85
|
+
console.log(path.relative(projectRoot, path.join(dir, 'TASK.md')));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function completeSubtask(projectRoot, flags) {
|
|
89
|
+
const slug = required(flags, 'slug');
|
|
90
|
+
const handoffFile = required(flags, 'file');
|
|
91
|
+
const taskDir = resolvePendingSubtask(projectRoot, slug);
|
|
92
|
+
const sourcePath = path.resolve(projectRoot, handoffFile);
|
|
93
|
+
if (!fs.existsSync(sourcePath)) throw new Error(`Handoff file not found: ${handoffFile}`);
|
|
94
|
+
|
|
95
|
+
const content = fs.readFileSync(sourcePath, 'utf-8');
|
|
96
|
+
validateHandoff(content);
|
|
97
|
+
fs.writeFileSync(path.join(taskDir, 'HANDOFF.md'), content.endsWith('\n') ? content : `${content}\n`, 'utf-8');
|
|
98
|
+
console.log(path.relative(projectRoot, path.join(taskDir, 'HANDOFF.md')));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function consumeSubtasks(projectRoot) {
|
|
102
|
+
ensureSubtaskLayout(projectRoot);
|
|
103
|
+
const pendingDir = path.join(projectRoot, SUBTASKS_RELATIVE_DIR, 'pending');
|
|
104
|
+
const entries = fs.readdirSync(pendingDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
105
|
+
const ready = entries
|
|
106
|
+
.map((entry) => path.join(pendingDir, entry.name))
|
|
107
|
+
.filter((dir) => fs.existsSync(path.join(dir, 'HANDOFF.md')));
|
|
108
|
+
|
|
109
|
+
if (!ready.length) {
|
|
110
|
+
console.log('No completed subtasks to consume.');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
115
|
+
const archiveDateDir = path.join(projectRoot, SUBTASKS_RELATIVE_DIR, 'archive', date);
|
|
116
|
+
ensureDir(archiveDateDir);
|
|
117
|
+
|
|
118
|
+
console.log('Subtask handoffs consumed:\n');
|
|
119
|
+
for (const dir of ready) {
|
|
120
|
+
const handoff = fs.readFileSync(path.join(dir, 'HANDOFF.md'), 'utf-8');
|
|
121
|
+
const summary = summarizeHandoff(handoff);
|
|
122
|
+
console.log(`- ${path.basename(dir)}: ${summary}`);
|
|
123
|
+
fs.renameSync(dir, path.join(archiveDateDir, path.basename(dir)));
|
|
124
|
+
}
|
|
125
|
+
console.log('\nArchive updated. Promote durable lessons manually into .dw/STATE.md, .dw/rules/, or .dw/intel/ when they should survive beyond this workstream.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function listSubtasks(projectRoot) {
|
|
129
|
+
ensureSubtaskLayout(projectRoot);
|
|
130
|
+
const pendingDir = path.join(projectRoot, SUBTASKS_RELATIVE_DIR, 'pending');
|
|
131
|
+
const entries = fs.readdirSync(pendingDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
|
|
132
|
+
if (!entries.length) {
|
|
133
|
+
console.log('No pending subtasks.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const dir = path.join(pendingDir, entry.name);
|
|
138
|
+
const state = fs.existsSync(path.join(dir, 'HANDOFF.md')) ? 'complete' : 'pending';
|
|
139
|
+
console.log(`${entry.name}\t${state}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function ensureSubtaskLayout(projectRoot) {
|
|
144
|
+
const root = path.join(projectRoot, SUBTASKS_RELATIVE_DIR);
|
|
145
|
+
ensureDir(path.join(root, 'pending'));
|
|
146
|
+
ensureDir(path.join(root, 'archive'));
|
|
147
|
+
const gitignore = ['pending/', 'archive/', '', '!/.gitignore', ''].join('\n');
|
|
148
|
+
writeFile(path.join(root, '.gitignore'), gitignore, false);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function validateAgent(agentName) {
|
|
152
|
+
const registry = readAgentRegistry();
|
|
153
|
+
if (!registry.agents.some((agent) => agent.name === agentName)) {
|
|
154
|
+
throw new Error(`Unknown agent: ${agentName}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function validateHandoff(content) {
|
|
159
|
+
const requiredHeadings = [
|
|
160
|
+
'Objective',
|
|
161
|
+
'Result',
|
|
162
|
+
'Files Read',
|
|
163
|
+
'Files Changed',
|
|
164
|
+
'Decisions',
|
|
165
|
+
'Risks',
|
|
166
|
+
'Verification',
|
|
167
|
+
'Next Steps',
|
|
168
|
+
'Blocked Or Not Done',
|
|
169
|
+
];
|
|
170
|
+
const missing = requiredHeadings.filter((heading) => !new RegExp(`^##\\s+${escapeRegExp(heading)}\\b`, 'mi').test(content));
|
|
171
|
+
if (missing.length) {
|
|
172
|
+
throw new Error(`Handoff is missing required headings: ${missing.join(', ')}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function summarizeHandoff(content) {
|
|
177
|
+
const result = extractSection(content, 'Result') || firstNonEmptyLine(content) || 'handoff recorded';
|
|
178
|
+
const verification = extractSection(content, 'Verification');
|
|
179
|
+
const next = extractSection(content, 'Next Steps');
|
|
180
|
+
const parts = [compact(result)];
|
|
181
|
+
if (verification) parts.push(`verification: ${compact(verification)}`);
|
|
182
|
+
if (next) parts.push(`next: ${compact(next)}`);
|
|
183
|
+
return parts.join(' | ');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function extractSection(content, heading) {
|
|
187
|
+
const match = content.match(new RegExp(`^##\\s+${escapeRegExp(heading)}\\b\\s*\\n([\\s\\S]*?)(?=^##\\s+|\\s*$)`, 'mi'));
|
|
188
|
+
return match ? match[1].trim() : '';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function firstNonEmptyLine(content) {
|
|
192
|
+
return content.split('\n').map((line) => line.trim()).find(Boolean) || '';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function compact(value) {
|
|
196
|
+
return value.replace(/\s+/g, ' ').slice(0, 220);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolvePendingSubtask(projectRoot, slug) {
|
|
200
|
+
const pendingDir = path.join(projectRoot, SUBTASKS_RELATIVE_DIR, 'pending');
|
|
201
|
+
if (!fs.existsSync(pendingDir)) throw new Error('No pending subtasks directory. Run subtask create first.');
|
|
202
|
+
const direct = path.join(pendingDir, slug);
|
|
203
|
+
if (fs.existsSync(direct)) return direct;
|
|
204
|
+
const matches = fs.readdirSync(pendingDir).filter((entry) => entry === slug || entry.endsWith(`-${slug}`));
|
|
205
|
+
if (matches.length === 1) return path.join(pendingDir, matches[0]);
|
|
206
|
+
if (matches.length > 1) throw new Error(`Ambiguous slug: ${slug}`);
|
|
207
|
+
throw new Error(`Pending subtask not found: ${slug}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseFlags(args) {
|
|
211
|
+
const flags = {};
|
|
212
|
+
for (let index = 0; index < args.length; index++) {
|
|
213
|
+
const arg = args[index];
|
|
214
|
+
if (!arg.startsWith('--')) continue;
|
|
215
|
+
const raw = arg.slice(2);
|
|
216
|
+
if (raw.includes('=')) {
|
|
217
|
+
const [key, ...rest] = raw.split('=');
|
|
218
|
+
flags[key] = rest.join('=');
|
|
219
|
+
} else {
|
|
220
|
+
const next = args[index + 1];
|
|
221
|
+
flags[raw] = next && !next.startsWith('--') ? next : true;
|
|
222
|
+
if (flags[raw] === next) index++;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return flags;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function required(flags, name) {
|
|
229
|
+
if (!flags[name] || flags[name] === true) throw new Error(`Missing required flag: --${name}`);
|
|
230
|
+
return String(flags[name]);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function slugify(value) {
|
|
234
|
+
return String(value)
|
|
235
|
+
.toLowerCase()
|
|
236
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
237
|
+
.replace(/^-+|-+$/g, '')
|
|
238
|
+
.slice(0, 60) || 'subtask';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function escapeRegExp(value) {
|
|
242
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function printHelp() {
|
|
246
|
+
console.log(`
|
|
247
|
+
dev-workflow subtask
|
|
248
|
+
|
|
249
|
+
Usage:
|
|
250
|
+
npx @brunosps00/dev-workflow subtask create --agent=<name> --goal="<goal>"
|
|
251
|
+
npx @brunosps00/dev-workflow subtask complete --slug=<slug> --file=<handoff.md>
|
|
252
|
+
npx @brunosps00/dev-workflow subtask consume
|
|
253
|
+
npx @brunosps00/dev-workflow subtask list
|
|
254
|
+
`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
module.exports = {
|
|
258
|
+
run,
|
|
259
|
+
ensureSubtaskLayout,
|
|
260
|
+
createSubtask,
|
|
261
|
+
completeSubtask,
|
|
262
|
+
consumeSubtasks,
|
|
263
|
+
listSubtasks,
|
|
264
|
+
};
|
package/lib/uninstall.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { COMMANDS, PLATFORMS, MCP_SERVERS } = require('./constants');
|
|
4
|
+
const { AGENT_TARGETS, LEGACY_AGENT_TARGETS, readAgentRegistry } = require('./agents');
|
|
4
5
|
|
|
5
6
|
function run() {
|
|
6
7
|
const projectRoot = process.cwd();
|
|
@@ -37,6 +38,7 @@ function run() {
|
|
|
37
38
|
removeDir(path.join(projectRoot, '.dw', 'templates'), '.dw/templates/');
|
|
38
39
|
removeDir(path.join(projectRoot, '.dw', 'references'), '.dw/references/');
|
|
39
40
|
removeDir(path.join(projectRoot, '.dw', 'scripts'), '.dw/scripts/');
|
|
41
|
+
removeFile(path.join(projectRoot, '.dw', 'agent-registry.json'));
|
|
40
42
|
console.log();
|
|
41
43
|
|
|
42
44
|
// 2. Remove platform wrappers (only dw-* skills, not user skills)
|
|
@@ -56,23 +58,29 @@ function run() {
|
|
|
56
58
|
|
|
57
59
|
// 3. Remove bundled skills from .agents/skills/
|
|
58
60
|
console.log(' Bundled skills:');
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const bundledSkills = [
|
|
64
|
-
'dw-ui-discipline',
|
|
65
|
-
'dw-testing-discipline',
|
|
66
|
-
'humanizer',
|
|
67
|
-
'remotion-best-practices',
|
|
68
|
-
'security-review',
|
|
69
|
-
'vercel-react-best-practices',
|
|
70
|
-
];
|
|
61
|
+
const skillsDir = path.join(__dirname, '..', 'scaffold', 'skills');
|
|
62
|
+
const bundledSkills = fs.existsSync(skillsDir)
|
|
63
|
+
? fs.readdirSync(skillsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name)
|
|
64
|
+
: [];
|
|
71
65
|
for (const skill of bundledSkills) {
|
|
72
66
|
removeDir(path.join(projectRoot, '.agents', 'skills', skill));
|
|
73
67
|
}
|
|
74
68
|
console.log();
|
|
75
69
|
|
|
70
|
+
// 3.5. Remove generated dev-workflow agents from platform folders.
|
|
71
|
+
console.log(' Agents:');
|
|
72
|
+
const registry = readAgentRegistry();
|
|
73
|
+
for (const agent of registry.agents) {
|
|
74
|
+
for (const target of Object.values(AGENT_TARGETS)) {
|
|
75
|
+
removeFile(path.join(projectRoot, target.dir, target.fileName(agent.name)));
|
|
76
|
+
}
|
|
77
|
+
for (const target of Object.values(LEGACY_AGENT_TARGETS)) {
|
|
78
|
+
removeFile(path.join(projectRoot, target.dir, target.fileName(agent.name)));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
removeFile(path.join(projectRoot, '.agents', 'agents', 'README.md'));
|
|
82
|
+
console.log();
|
|
83
|
+
|
|
76
84
|
// 4. Remove MCP servers from .claude/settings.json
|
|
77
85
|
console.log(' MCP Servers:');
|
|
78
86
|
const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
|
|
@@ -97,6 +105,7 @@ function run() {
|
|
|
97
105
|
|
|
98
106
|
// 5. Remove .opencode/package.json
|
|
99
107
|
removeFile(path.join(projectRoot, '.opencode', 'package.json'));
|
|
108
|
+
removeFile(path.join(projectRoot, '.dw', 'install-state.json'));
|
|
100
109
|
|
|
101
110
|
// 6. Remove legacy .codex/skills/
|
|
102
111
|
removeDir(path.join(projectRoot, '.codex', 'skills'));
|