forge-workflow 0.0.6 → 0.0.7
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/.cursorrules +149 -0
- package/bin/forge.js +36 -3
- package/lib/agents/README.md +46 -1
- package/lib/agents/cline.plugin.json +11 -4
- package/lib/agents/codex.plugin.json +2 -2
- package/lib/agents/copilot.plugin.json +5 -5
- package/lib/agents/cursor.plugin.json +1 -1
- package/lib/agents/kilocode.plugin.json +1 -1
- package/lib/agents/opencode.plugin.json +7 -4
- package/lib/agents/roo.plugin.json +10 -3
- package/lib/agents-config.js +127 -79
- package/lib/codex-skills.js +50 -0
- package/lib/commands/_registry.js +40 -1
- package/lib/commands/commands-reset.js +147 -0
- package/lib/commands/dev.js +26 -0
- package/lib/commands/plan.js +18 -0
- package/lib/commands/setup.js +4295 -0
- package/lib/commands/ship.js +20 -0
- package/lib/commands/status.js +210 -44
- package/lib/commands/sync.js +17 -1
- package/lib/commands/validate.js +13 -0
- package/lib/detect-agent.js +38 -8
- package/lib/detection-utils.js +405 -0
- package/lib/file-utils.js +260 -0
- package/lib/forge-context.js +42 -0
- package/lib/frontmatter.js +79 -0
- package/lib/husky-migration.js +113 -12
- package/lib/lefthook-check.js +27 -6
- package/lib/plugin-manager.js +225 -72
- package/lib/project-discovery.js +39 -5
- package/lib/runtime-health.js +305 -0
- package/lib/shell-utils.js +50 -0
- package/lib/ui-utils.js +43 -0
- package/lib/validation-utils.js +163 -0
- package/lib/workflow/enforce-stage.js +179 -0
- package/lib/workflow/stages.js +201 -0
- package/lib/workflow/state.js +332 -0
- package/opencode.json +67 -0
- package/package.json +15 -5
- package/scripts/beads-context.sh +12 -4
- package/scripts/check-agents.js +103 -0
- package/scripts/pr-coordinator.sh +71 -21
- package/scripts/smart-status.sh +21 -11
- package/scripts/sync-commands.js +49 -20
- package/scripts/test.js +16 -1
package/lib/agents-config.js
CHANGED
|
@@ -68,6 +68,25 @@ async function detectProjectMetadata(projectPath) {
|
|
|
68
68
|
return meta;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Write a file unless it already exists, unless overwrite is enabled.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} filePath
|
|
75
|
+
* @param {string} content
|
|
76
|
+
* @param {boolean} overwrite
|
|
77
|
+
* @returns {Promise<void>}
|
|
78
|
+
*/
|
|
79
|
+
async function writeIfNeeded(filePath, content, overwrite = false) {
|
|
80
|
+
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
81
|
+
if (!exists || overwrite) {
|
|
82
|
+
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createConditionalWriter(overwrite) {
|
|
87
|
+
return (filePath, content) => writeIfNeeded(filePath, content, overwrite);
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
/**
|
|
72
91
|
* Generate AGENTS.md content
|
|
73
92
|
* @param {Object} meta - Project metadata
|
|
@@ -241,7 +260,7 @@ While this universal AGENTS.md works with all agents, you can optionally enable
|
|
|
241
260
|
- **GitHub Copilot**: \`.github/copilot-instructions.md\` + path-specific instructions
|
|
242
261
|
- **Cursor**: \`.cursor/rules/*.mdc\` + native modes
|
|
243
262
|
- **OpenCode**: \`opencode.json\` + custom agents
|
|
244
|
-
- **Kilo**: \`.
|
|
263
|
+
- **Kilo**: \`.kilocode/workflows\`, \`.kilocode/rules\`, \`.kilocode/skills\`
|
|
245
264
|
|
|
246
265
|
Generate with: \`bunx forge setup --agent=<name>\`
|
|
247
266
|
|
|
@@ -277,38 +296,32 @@ async function generateCopilotConfig(projectPath, options = {}) {
|
|
|
277
296
|
await fs.promises.mkdir(instructionsDir, { recursive: true });
|
|
278
297
|
await fs.promises.mkdir(promptsDir, { recursive: true });
|
|
279
298
|
|
|
280
|
-
|
|
281
|
-
const writeIfNeeded = async (filePath, content) => {
|
|
282
|
-
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
283
|
-
if (!exists || overwrite) {
|
|
284
|
-
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
285
|
-
}
|
|
286
|
-
};
|
|
299
|
+
const writeConfigFile = createConditionalWriter(overwrite);
|
|
287
300
|
|
|
288
301
|
// 1. Create .github/copilot-instructions.md
|
|
289
302
|
const copilotInstructionsPath = path.join(githubDir, 'copilot-instructions.md');
|
|
290
303
|
const copilotInstructionsContent = generateCopilotInstructionsContent(projectMeta);
|
|
291
|
-
await
|
|
304
|
+
await writeConfigFile(copilotInstructionsPath, copilotInstructionsContent);
|
|
292
305
|
|
|
293
306
|
// 2. Create .github/instructions/typescript.instructions.md
|
|
294
307
|
const tsInstructionsPath = path.join(instructionsDir, 'typescript.instructions.md');
|
|
295
308
|
const tsInstructionsContent = generateTypeScriptInstructionsContent();
|
|
296
|
-
await
|
|
309
|
+
await writeConfigFile(tsInstructionsPath, tsInstructionsContent);
|
|
297
310
|
|
|
298
311
|
// 3. Create .github/instructions/testing.instructions.md
|
|
299
312
|
const testInstructionsPath = path.join(instructionsDir, 'testing.instructions.md');
|
|
300
313
|
const testInstructionsContent = generateTestingInstructionsContent();
|
|
301
|
-
await
|
|
314
|
+
await writeConfigFile(testInstructionsPath, testInstructionsContent);
|
|
302
315
|
|
|
303
316
|
// 4. Create .github/prompts/red.prompt.md
|
|
304
317
|
const redPromptPath = path.join(promptsDir, 'red.prompt.md');
|
|
305
318
|
const redPromptContent = generateRedPromptContent();
|
|
306
|
-
await
|
|
319
|
+
await writeConfigFile(redPromptPath, redPromptContent);
|
|
307
320
|
|
|
308
321
|
// 5. Create .github/prompts/green.prompt.md
|
|
309
322
|
const greenPromptPath = path.join(promptsDir, 'green.prompt.md');
|
|
310
323
|
const greenPromptContent = generateGreenPromptContent();
|
|
311
|
-
await
|
|
324
|
+
await writeConfigFile(greenPromptPath, greenPromptContent);
|
|
312
325
|
}
|
|
313
326
|
|
|
314
327
|
/**
|
|
@@ -643,33 +656,27 @@ async function generateCursorConfig(projectPath, options = {}) {
|
|
|
643
656
|
|
|
644
657
|
await fs.promises.mkdir(rulesDir, { recursive: true });
|
|
645
658
|
|
|
646
|
-
|
|
647
|
-
const writeIfNeeded = async (filePath, content) => {
|
|
648
|
-
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
649
|
-
if (!exists || overwrite) {
|
|
650
|
-
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
651
|
-
}
|
|
652
|
-
};
|
|
659
|
+
const writeConfigFile = createConditionalWriter(overwrite);
|
|
653
660
|
|
|
654
661
|
// 1. Create .cursor/rules/forge-workflow.mdc
|
|
655
662
|
const workflowPath = path.join(rulesDir, 'forge-workflow.mdc');
|
|
656
663
|
const workflowContent = generateCursorWorkflowContent(projectMeta);
|
|
657
|
-
await
|
|
664
|
+
await writeConfigFile(workflowPath, workflowContent);
|
|
658
665
|
|
|
659
666
|
// 2. Create .cursor/rules/tdd-enforcement.mdc
|
|
660
667
|
const tddPath = path.join(rulesDir, 'tdd-enforcement.mdc');
|
|
661
668
|
const tddContent = generateCursorTddContent();
|
|
662
|
-
await
|
|
669
|
+
await writeConfigFile(tddPath, tddContent);
|
|
663
670
|
|
|
664
671
|
// 3. Create .cursor/rules/security-scanning.mdc
|
|
665
672
|
const securityPath = path.join(rulesDir, 'security-scanning.mdc');
|
|
666
673
|
const securityContent = generateCursorSecurityContent();
|
|
667
|
-
await
|
|
674
|
+
await writeConfigFile(securityPath, securityContent);
|
|
668
675
|
|
|
669
676
|
// 4. Create .cursor/rules/documentation.mdc
|
|
670
677
|
const docsPath = path.join(rulesDir, 'documentation.mdc');
|
|
671
678
|
const docsContent = generateCursorDocumentationContent();
|
|
672
|
-
await
|
|
679
|
+
await writeConfigFile(docsPath, docsContent);
|
|
673
680
|
}
|
|
674
681
|
|
|
675
682
|
/**
|
|
@@ -1010,7 +1017,7 @@ Update documentation at relevant stages (not deferred to end):
|
|
|
1010
1017
|
}
|
|
1011
1018
|
|
|
1012
1019
|
/**
|
|
1013
|
-
* Generate Kilo Code configuration
|
|
1020
|
+
* Generate Kilo Code configuration files
|
|
1014
1021
|
* @param {string} projectPath - Path to the project root
|
|
1015
1022
|
* @param {Object} options - Generation options
|
|
1016
1023
|
* @param {boolean} options.overwrite - Whether to overwrite existing files (default: false)
|
|
@@ -1022,29 +1029,45 @@ async function generateKiloConfig(projectPath, options = {}) {
|
|
|
1022
1029
|
// Detect project metadata
|
|
1023
1030
|
const projectMeta = await detectProjectMetadata(projectPath);
|
|
1024
1031
|
|
|
1025
|
-
const
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
const
|
|
1029
|
-
if (exists && !overwrite) {
|
|
1030
|
-
return; // Don't overwrite
|
|
1031
|
-
}
|
|
1032
|
+
const kilocodeDir = path.join(projectPath, '.kilocode');
|
|
1033
|
+
const workflowsDir = path.join(kilocodeDir, 'workflows');
|
|
1034
|
+
const rulesDir = path.join(kilocodeDir, 'rules');
|
|
1035
|
+
const skillsDir = path.join(kilocodeDir, 'skills', 'forge-workflow');
|
|
1032
1036
|
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
+
await fs.promises.mkdir(workflowsDir, { recursive: true });
|
|
1038
|
+
await fs.promises.mkdir(rulesDir, { recursive: true });
|
|
1039
|
+
await fs.promises.mkdir(skillsDir, { recursive: true });
|
|
1040
|
+
|
|
1041
|
+
await writeIfNeeded(
|
|
1042
|
+
path.join(workflowsDir, 'forge-workflow.md'),
|
|
1043
|
+
generateKiloWorkflowContent(projectMeta),
|
|
1044
|
+
overwrite
|
|
1045
|
+
);
|
|
1046
|
+
await writeIfNeeded(
|
|
1047
|
+
path.join(rulesDir, 'workflow.md'),
|
|
1048
|
+
generateKiloRulesContent(projectMeta),
|
|
1049
|
+
overwrite
|
|
1050
|
+
);
|
|
1051
|
+
await writeIfNeeded(
|
|
1052
|
+
path.join(skillsDir, 'SKILL.md'),
|
|
1053
|
+
generateKiloSkillContent(projectMeta),
|
|
1054
|
+
overwrite
|
|
1055
|
+
);
|
|
1037
1056
|
}
|
|
1038
1057
|
|
|
1039
1058
|
/**
|
|
1040
|
-
* Generate .
|
|
1059
|
+
* Generate .kilocode/workflows/forge-workflow.md content
|
|
1041
1060
|
*/
|
|
1042
|
-
function
|
|
1061
|
+
function generateKiloWorkflowContent(meta) {
|
|
1043
1062
|
const packageManager = meta.testCommand?.includes('bun') ? 'bun' : 'npm';
|
|
1044
1063
|
|
|
1045
|
-
return
|
|
1064
|
+
return `---
|
|
1065
|
+
description: "Forge workflow for Kilo Code"
|
|
1066
|
+
---
|
|
1046
1067
|
|
|
1047
|
-
|
|
1068
|
+
# Forge Workflow Framework - Kilo Code
|
|
1069
|
+
|
|
1070
|
+
This project uses the **Forge 7-Stage TDD Workflow** with Kilo Code native workflows.
|
|
1048
1071
|
|
|
1049
1072
|
## Quick Start
|
|
1050
1073
|
|
|
@@ -1054,12 +1077,12 @@ ${meta.testCommand} # Run tests
|
|
|
1054
1077
|
${meta.buildCommand} # Build project
|
|
1055
1078
|
\`\`\`
|
|
1056
1079
|
|
|
1057
|
-
## Kilo
|
|
1080
|
+
## Native Kilo Surface
|
|
1058
1081
|
|
|
1059
|
-
Kilo Code
|
|
1060
|
-
-
|
|
1061
|
-
-
|
|
1062
|
-
-
|
|
1082
|
+
Kilo Code uses native workflows, rules, and skills. Forge generates:
|
|
1083
|
+
- \`.kilocode/workflows/forge-workflow.md\`
|
|
1084
|
+
- \`.kilocode/rules/workflow.md\`
|
|
1085
|
+
- \`.kilocode/skills/forge-workflow/SKILL.md\`
|
|
1063
1086
|
|
|
1064
1087
|
These complement the Forge workflow stages below.
|
|
1065
1088
|
|
|
@@ -1117,36 +1140,67 @@ Final documentation verification
|
|
|
1117
1140
|
- Verify all docs updated
|
|
1118
1141
|
- Check for broken links
|
|
1119
1142
|
- Validate code examples
|
|
1143
|
+
`;
|
|
1144
|
+
}
|
|
1120
1145
|
|
|
1121
|
-
|
|
1146
|
+
/**
|
|
1147
|
+
* Generate .kilocode/rules/workflow.md content
|
|
1148
|
+
*/
|
|
1149
|
+
function generateKiloRulesContent(meta) {
|
|
1150
|
+
const packageManager = meta.testCommand?.includes('bun') ? 'bun' : 'npm';
|
|
1122
1151
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
- Refactor and commit (REFACTOR phase)
|
|
1127
|
-
- No code changes without corresponding tests
|
|
1152
|
+
return `---
|
|
1153
|
+
description: "Forge workflow rules for Kilo Code"
|
|
1154
|
+
---
|
|
1128
1155
|
|
|
1129
|
-
|
|
1156
|
+
# Forge Workflow Rules
|
|
1130
1157
|
|
|
1131
|
-
|
|
1132
|
-
-
|
|
1133
|
-
-
|
|
1134
|
-
-
|
|
1135
|
-
-
|
|
1158
|
+
- Use Forge as the source of truth for stage order and hard stops.
|
|
1159
|
+
- Run \`forge <stage>\` before continuing any workflow stage.
|
|
1160
|
+
- Treat \`.kilocode/workflows\` as the canonical workflow surface.
|
|
1161
|
+
- Keep \`AGENTS.md\` as shared context, not the workflow authority.
|
|
1162
|
+
- If Forge blocks a stage, fix the prerequisite or provide an explicit override payload.
|
|
1136
1163
|
|
|
1137
|
-
##
|
|
1164
|
+
## Environment
|
|
1138
1165
|
|
|
1139
|
-
|
|
1140
|
-
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1166
|
+
- Package manager: ${packageManager}
|
|
1167
|
+
- Language: ${meta.language ? meta.language : 'JavaScript'}
|
|
1168
|
+
`;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Generate .kilocode/skills/forge-workflow/SKILL.md content
|
|
1173
|
+
*/
|
|
1174
|
+
function generateKiloSkillContent(meta) {
|
|
1175
|
+
const packageManager = meta.testCommand?.includes('bun') ? 'bun' : 'npm';
|
|
1176
|
+
|
|
1177
|
+
return `---
|
|
1178
|
+
name: forge-workflow
|
|
1179
|
+
description: Forge stage adapter for Kilo Code
|
|
1180
|
+
---
|
|
1181
|
+
|
|
1182
|
+
# Forge Workflow Skill
|
|
1183
|
+
|
|
1184
|
+
Before starting any stage, invoke \`forge <stage>\` so Forge can enforce stage order, prerequisites, and overrides.
|
|
1185
|
+
If Forge blocks the stage, stop and resolve it first.
|
|
1186
|
+
|
|
1187
|
+
## Workflow Context
|
|
1188
|
+
|
|
1189
|
+
- Package manager: ${packageManager}
|
|
1190
|
+
- Language: ${meta.language ? meta.language : 'JavaScript'}
|
|
1191
|
+
- Canonical workflow surface: \`.kilocode/workflows/forge-workflow.md\`
|
|
1192
|
+
- Shared context: \`AGENTS.md\`
|
|
1193
|
+
|
|
1194
|
+
## Supported Stages
|
|
1195
|
+
|
|
1196
|
+
- \`/status\`
|
|
1197
|
+
- \`/plan\`
|
|
1198
|
+
- \`/dev\`
|
|
1199
|
+
- \`/validate\`
|
|
1200
|
+
- \`/ship\`
|
|
1201
|
+
- \`/review\`
|
|
1202
|
+
- \`/premerge\`
|
|
1203
|
+
- \`/verify\`
|
|
1150
1204
|
`;
|
|
1151
1205
|
}
|
|
1152
1206
|
|
|
@@ -1168,28 +1222,22 @@ async function generateOpenCodeConfig(projectPath, options = {}) {
|
|
|
1168
1222
|
const agentsDir = path.join(opencodeDir, 'agents');
|
|
1169
1223
|
await fs.promises.mkdir(agentsDir, { recursive: true });
|
|
1170
1224
|
|
|
1171
|
-
|
|
1172
|
-
const writeIfNeeded = async (filePath, content) => {
|
|
1173
|
-
const exists = await fs.promises.access(filePath).then(() => true).catch(() => false);
|
|
1174
|
-
if (!exists || overwrite) {
|
|
1175
|
-
await fs.promises.writeFile(filePath, content, 'utf-8');
|
|
1176
|
-
}
|
|
1177
|
-
};
|
|
1225
|
+
const writeConfigFile = createConditionalWriter(overwrite);
|
|
1178
1226
|
|
|
1179
1227
|
// 1. Create opencode.json
|
|
1180
1228
|
const opencodeJsonPath = path.join(projectPath, 'opencode.json');
|
|
1181
1229
|
const opencodeJsonContent = generateOpenCodeJsonContent(projectMeta);
|
|
1182
|
-
await
|
|
1230
|
+
await writeConfigFile(opencodeJsonPath, opencodeJsonContent);
|
|
1183
1231
|
|
|
1184
1232
|
// 2. Create .opencode/agents/plan-review.md
|
|
1185
1233
|
const planAgentPath = path.join(agentsDir, 'plan-review.md');
|
|
1186
1234
|
const planAgentContent = generateOpenCodePlanAgentContent();
|
|
1187
|
-
await
|
|
1235
|
+
await writeConfigFile(planAgentPath, planAgentContent);
|
|
1188
1236
|
|
|
1189
1237
|
// 3. Create .opencode/agents/tdd-build.md
|
|
1190
1238
|
const buildAgentPath = path.join(agentsDir, 'tdd-build.md');
|
|
1191
1239
|
const buildAgentContent = generateOpenCodeBuildAgentContent();
|
|
1192
|
-
await
|
|
1240
|
+
await writeConfigFile(buildAgentPath, buildAgentContent);
|
|
1193
1241
|
}
|
|
1194
1242
|
|
|
1195
1243
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
function injectForgeAdapter(content, commandName) {
|
|
5
|
+
const adapter = [
|
|
6
|
+
'> Forge stage adapter',
|
|
7
|
+
'',
|
|
8
|
+
`Before executing this workflow, invoke \`forge ${commandName}\` so Forge can enforce stage order, runtime prerequisites, and override rules.`,
|
|
9
|
+
'If Forge blocks the stage or asks for an explicit override payload, stop and resolve that first.',
|
|
10
|
+
'',
|
|
11
|
+
].join('\n');
|
|
12
|
+
|
|
13
|
+
const match = content.match(/^(---\r?\n[\s\S]*?\r?\n---\r?\n?)([\s\S]*)$/);
|
|
14
|
+
if (!match) {
|
|
15
|
+
return `${adapter}\n${content}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return `${match[1]}\n${adapter}${match[2]}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function listCodexSkillEntries(sourceRoot) {
|
|
22
|
+
const skillsDir = path.join(sourceRoot, '.codex', 'skills');
|
|
23
|
+
|
|
24
|
+
if (!fs.existsSync(skillsDir)) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
29
|
+
.filter((entry) => entry.isDirectory())
|
|
30
|
+
.map((entry) => entry.name)
|
|
31
|
+
.sort()
|
|
32
|
+
.map((commandName) => {
|
|
33
|
+
const sourceFile = path.join(skillsDir, commandName, 'SKILL.md');
|
|
34
|
+
if (!fs.existsSync(sourceFile)) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
commandName,
|
|
40
|
+
dir: `.codex/skills/${commandName}/`,
|
|
41
|
+
filename: 'SKILL.md',
|
|
42
|
+
content: injectForgeAdapter(fs.readFileSync(sourceFile, 'utf8'), commandName),
|
|
43
|
+
};
|
|
44
|
+
})
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
listCodexSkillEntries,
|
|
50
|
+
};
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const { existsSync, readdirSync } = require('node:fs');
|
|
13
13
|
const path = require('node:path');
|
|
14
|
+
const { normalizeStageId } = require('../workflow/stages');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* @typedef {Object} CommandModule
|
|
@@ -131,4 +132,42 @@ function buildHelp(commands) {
|
|
|
131
132
|
return lines.join('\n');
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
function isStageCommand(commandName) {
|
|
136
|
+
return normalizeStageId(commandName) !== null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function executeCommand(commands, commandName, args, flags, projectRoot, options = {}) {
|
|
140
|
+
const command = commands.get(commandName);
|
|
141
|
+
if (!command) {
|
|
142
|
+
return { success: false, error: `Unknown command: ${commandName}` };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
if (typeof options.enforceStage === 'function' && isStageCommand(commandName)) {
|
|
147
|
+
const enforcement = await options.enforceStage({
|
|
148
|
+
commandName,
|
|
149
|
+
args,
|
|
150
|
+
flags,
|
|
151
|
+
projectRoot,
|
|
152
|
+
command,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (enforcement?.allowed === false) {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
error: enforcement.error ?? `Stage ${commandName} is blocked.`,
|
|
159
|
+
enforcement,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return await command.handler(args, flags, projectRoot);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
error: err?.message ?? `Failed to execute ${commandName}`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { loadCommands, validateCommand, executeCommand };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @module commands-reset
|
|
5
|
+
*
|
|
6
|
+
* Reset agent command files to match the canonical source (commands/*.md or .claude/commands/*.md).
|
|
7
|
+
* Rebuilds adapted files via sync-commands and optionally writes them.
|
|
8
|
+
*
|
|
9
|
+
* Used by `forge commands reset [--dry-run] [--all] [command-name]`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const {
|
|
15
|
+
syncCommands,
|
|
16
|
+
contentHash,
|
|
17
|
+
resolveCanonicalCommandsDir,
|
|
18
|
+
writeSyncManifest,
|
|
19
|
+
} = require('../../scripts/sync-commands.js');
|
|
20
|
+
|
|
21
|
+
function validateCommandName(commandName) {
|
|
22
|
+
if (commandName && !/^[a-z0-9-]+$/.test(commandName)) {
|
|
23
|
+
return [`Invalid command name: "${commandName}" - only lowercase letters, numbers, and hyphens allowed`];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ensureCanonicalCommandExists(repoRoot, canonicalDir, commandName) {
|
|
30
|
+
if (!commandName) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync(path.join(canonicalDir, `${commandName}.md`))) {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const canonicalLabel = path.relative(repoRoot, canonicalDir).replaceAll('\\', '/');
|
|
39
|
+
return [`Command not found: ${canonicalLabel}/${commandName}.md`];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function filterPlannedEntries(entries, commandName, all) {
|
|
43
|
+
if (commandName && all !== true) {
|
|
44
|
+
return entries.filter(entry =>
|
|
45
|
+
entry.filename.replaceAll('.prompt.md', '').replaceAll('.md', '') === commandName ||
|
|
46
|
+
entry.dir.includes(`/${commandName}/`)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return entries;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function recordEntryResult(entry, reset, skipped, dryRun) {
|
|
54
|
+
const relativeFile = path.join(entry.dir, entry.filename);
|
|
55
|
+
|
|
56
|
+
if (dryRun) {
|
|
57
|
+
if (fs.existsSync(entry.filePath)) {
|
|
58
|
+
const existing = fs.readFileSync(entry.filePath, 'utf8');
|
|
59
|
+
if (contentHash(existing) === contentHash(entry.content)) {
|
|
60
|
+
skipped.push({ agent: entry.agent, file: relativeFile });
|
|
61
|
+
} else {
|
|
62
|
+
reset.push({ agent: entry.agent, file: relativeFile });
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
reset.push({ agent: entry.agent, file: relativeFile });
|
|
66
|
+
}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const targetDir = path.dirname(entry.filePath);
|
|
71
|
+
if (!fs.existsSync(targetDir)) {
|
|
72
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (fs.existsSync(entry.filePath)) {
|
|
76
|
+
const existing = fs.readFileSync(entry.filePath, 'utf8');
|
|
77
|
+
if (contentHash(existing) === contentHash(entry.content)) {
|
|
78
|
+
skipped.push({ agent: entry.agent, file: relativeFile });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(entry.filePath, entry.content);
|
|
84
|
+
reset.push({ agent: entry.agent, file: relativeFile });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resetCommands({ repoRoot, commandName, all, dryRun }) {
|
|
88
|
+
const errors = [];
|
|
89
|
+
const reset = [];
|
|
90
|
+
const skipped = [];
|
|
91
|
+
|
|
92
|
+
errors.push(...validateCommandName(commandName));
|
|
93
|
+
if (errors.length > 0) {
|
|
94
|
+
return { reset, skipped, errors };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const canonicalDir = resolveCanonicalCommandsDir(repoRoot);
|
|
98
|
+
if (!canonicalDir) {
|
|
99
|
+
errors.push('Canonical source directory not found: commands/ or .claude/commands/');
|
|
100
|
+
return { reset, skipped, errors };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
errors.push(...ensureCanonicalCommandExists(repoRoot, canonicalDir, commandName));
|
|
104
|
+
if (errors.length > 0) {
|
|
105
|
+
return { reset, skipped, errors };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = syncCommands({ dryRun: true, check: false, repoRoot, canonicalDir });
|
|
109
|
+
if (!result.planned || result.planned.length === 0) {
|
|
110
|
+
return { reset, skipped, errors };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const entries = filterPlannedEntries(result.planned, commandName, all);
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
recordEntryResult(entry, reset, skipped, dryRun);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!dryRun) {
|
|
119
|
+
// Refresh the manifest using the full expected sync set without rewriting
|
|
120
|
+
// unrelated command files that were intentionally excluded from this reset.
|
|
121
|
+
writeSyncManifest(repoRoot, result.planned);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { reset, skipped, errors };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
module.exports = {
|
|
128
|
+
name: 'commands-reset',
|
|
129
|
+
description: 'Reset generated agent command files to canonical content',
|
|
130
|
+
handler: async (args, flags, repoRoot) => {
|
|
131
|
+
const commandName = args.find(arg => !arg.startsWith('-'));
|
|
132
|
+
const result = resetCommands({
|
|
133
|
+
repoRoot,
|
|
134
|
+
commandName,
|
|
135
|
+
all: Boolean(flags.all),
|
|
136
|
+
dryRun: Boolean(flags.dryRun),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
if (result.errors.length > 0) {
|
|
140
|
+
return { success: false, error: result.errors.join('; ') };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return { success: true, ...result };
|
|
144
|
+
},
|
|
145
|
+
resetCommands,
|
|
146
|
+
resolveCanonicalCommandsDir,
|
|
147
|
+
};
|
package/lib/commands/dev.js
CHANGED
|
@@ -558,6 +558,32 @@ function verifyTaskCompletion(taskTitle, ownedFiles, opts = {}) {
|
|
|
558
558
|
}
|
|
559
559
|
|
|
560
560
|
module.exports = {
|
|
561
|
+
name: 'dev',
|
|
562
|
+
description: 'Run the TDD development stage with phase guidance',
|
|
563
|
+
handler: async (args, flags = {}) => {
|
|
564
|
+
const featureName = args[0] || 'feature';
|
|
565
|
+
const phaseInput = flags.phase || args[1];
|
|
566
|
+
const phase = typeof phaseInput === 'string' ? phaseInput.toUpperCase() : undefined;
|
|
567
|
+
if (phase && !['RED', 'GREEN', 'REFACTOR'].includes(phase)) {
|
|
568
|
+
return {
|
|
569
|
+
success: false,
|
|
570
|
+
error: `Invalid phase '${phaseInput}'. Valid phases: red, green, refactor`,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const result = await executeDev(featureName, phase ? { phase } : {});
|
|
575
|
+
if (!result.success) {
|
|
576
|
+
return result;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const lines = [`TDD Phase: ${result.phase || result.detectedPhase}`];
|
|
580
|
+
if (result.guidance) lines.push('', result.guidance);
|
|
581
|
+
|
|
582
|
+
return {
|
|
583
|
+
...result,
|
|
584
|
+
output: lines.join('\n'),
|
|
585
|
+
};
|
|
586
|
+
},
|
|
561
587
|
detectTDDPhase,
|
|
562
588
|
identifyFilePairs,
|
|
563
589
|
runTests,
|
package/lib/commands/plan.js
CHANGED
|
@@ -680,6 +680,24 @@ async function executePlan(featureName) { // NOSONAR S3776
|
|
|
680
680
|
}
|
|
681
681
|
|
|
682
682
|
module.exports = {
|
|
683
|
+
name: 'plan',
|
|
684
|
+
description: 'Create implementation plan from researched feature context',
|
|
685
|
+
handler: async (args) => {
|
|
686
|
+
const result = await executePlan(args[0]);
|
|
687
|
+
if (!result.success) {
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const lines = [`Plan created: ${result.summary || result.branchName || args[0]}`];
|
|
692
|
+
if (result.beadsIssueId) lines.push(`Beads: ${result.beadsIssueId}`);
|
|
693
|
+
if (result.branchName) lines.push(`Branch: ${result.branchName}`);
|
|
694
|
+
if (result.nextCommand) lines.push(`Next: ${result.nextCommand}`);
|
|
695
|
+
|
|
696
|
+
return {
|
|
697
|
+
...result,
|
|
698
|
+
output: lines.join('\n'),
|
|
699
|
+
};
|
|
700
|
+
},
|
|
683
701
|
readResearchDoc,
|
|
684
702
|
detectScope,
|
|
685
703
|
createBeadsIssue,
|