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.
Files changed (45) hide show
  1. package/.cursorrules +149 -0
  2. package/bin/forge.js +36 -3
  3. package/lib/agents/README.md +46 -1
  4. package/lib/agents/cline.plugin.json +11 -4
  5. package/lib/agents/codex.plugin.json +2 -2
  6. package/lib/agents/copilot.plugin.json +5 -5
  7. package/lib/agents/cursor.plugin.json +1 -1
  8. package/lib/agents/kilocode.plugin.json +1 -1
  9. package/lib/agents/opencode.plugin.json +7 -4
  10. package/lib/agents/roo.plugin.json +10 -3
  11. package/lib/agents-config.js +127 -79
  12. package/lib/codex-skills.js +50 -0
  13. package/lib/commands/_registry.js +40 -1
  14. package/lib/commands/commands-reset.js +147 -0
  15. package/lib/commands/dev.js +26 -0
  16. package/lib/commands/plan.js +18 -0
  17. package/lib/commands/setup.js +4295 -0
  18. package/lib/commands/ship.js +20 -0
  19. package/lib/commands/status.js +210 -44
  20. package/lib/commands/sync.js +17 -1
  21. package/lib/commands/validate.js +13 -0
  22. package/lib/detect-agent.js +38 -8
  23. package/lib/detection-utils.js +405 -0
  24. package/lib/file-utils.js +260 -0
  25. package/lib/forge-context.js +42 -0
  26. package/lib/frontmatter.js +79 -0
  27. package/lib/husky-migration.js +113 -12
  28. package/lib/lefthook-check.js +27 -6
  29. package/lib/plugin-manager.js +225 -72
  30. package/lib/project-discovery.js +39 -5
  31. package/lib/runtime-health.js +305 -0
  32. package/lib/shell-utils.js +50 -0
  33. package/lib/ui-utils.js +43 -0
  34. package/lib/validation-utils.js +163 -0
  35. package/lib/workflow/enforce-stage.js +179 -0
  36. package/lib/workflow/stages.js +201 -0
  37. package/lib/workflow/state.js +332 -0
  38. package/opencode.json +67 -0
  39. package/package.json +15 -5
  40. package/scripts/beads-context.sh +12 -4
  41. package/scripts/check-agents.js +103 -0
  42. package/scripts/pr-coordinator.sh +71 -21
  43. package/scripts/smart-status.sh +21 -11
  44. package/scripts/sync-commands.js +49 -20
  45. package/scripts/test.js +16 -1
@@ -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**: \`.kilo.md\` + built-in commands
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
- // Helper to write file if it doesn't exist or overwrite is true
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 writeIfNeeded(copilotInstructionsPath, copilotInstructionsContent);
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 writeIfNeeded(tsInstructionsPath, tsInstructionsContent);
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 writeIfNeeded(testInstructionsPath, testInstructionsContent);
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 writeIfNeeded(redPromptPath, redPromptContent);
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 writeIfNeeded(greenPromptPath, greenPromptContent);
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
- // Helper to write file if it doesn't exist or overwrite is true
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 writeIfNeeded(workflowPath, workflowContent);
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 writeIfNeeded(tddPath, tddContent);
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 writeIfNeeded(securityPath, securityContent);
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 writeIfNeeded(docsPath, docsContent);
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 file
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 kiloMdPath = path.join(projectPath, '.kilo.md');
1026
-
1027
- // Check if file exists
1028
- const exists = await fs.promises.access(kiloMdPath).then(() => true).catch(() => false);
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
- // Generate .kilo.md content (plain markdown, no frontmatter)
1034
- const content = generateKiloMdContent(projectMeta);
1035
-
1036
- await fs.promises.writeFile(kiloMdPath, content, 'utf-8');
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 .kilo.md content
1059
+ * Generate .kilocode/workflows/forge-workflow.md content
1041
1060
  */
1042
- function generateKiloMdContent(meta) {
1061
+ function generateKiloWorkflowContent(meta) {
1043
1062
  const packageManager = meta.testCommand?.includes('bun') ? 'bun' : 'npm';
1044
1063
 
1045
- return `# Forge Workflow Framework - Kilo Code
1064
+ return `---
1065
+ description: "Forge workflow for Kilo Code"
1066
+ ---
1046
1067
 
1047
- This project uses the **Forge 7-Stage TDD Workflow** with Kilo Code.
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 Built-in Commands
1080
+ ## Native Kilo Surface
1058
1081
 
1059
- Kilo Code provides built-in commands:
1060
- - \`/plan\` - Create implementation plans (similar to Forge Stage 3)
1061
- - \`/deploy\` - Deploy to production
1062
- - \`/review\` - Code review assistance
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
- ## TDD Requirements
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
- **MANDATORY**:
1124
- - Write test FIRST (RED phase)
1125
- - Implement minimal code to pass (GREEN phase)
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
- ## Tech Stack
1156
+ # Forge Workflow Rules
1130
1157
 
1131
- ${meta.language ? `- **Language**: ${meta.language}` : '- **Language**: JavaScript'}
1132
- - **Package Manager**: ${packageManager}
1133
- - **Testing**: TDD-first approach
1134
- - **Security**: OWASP Top 10 compliance
1135
- - **Version Control**: Git with conventional commits
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
- ## Security
1164
+ ## Environment
1138
1165
 
1139
- For every new feature, analyze OWASP Top 10:
1140
- - A01: Broken Access Control
1141
- - A02: Cryptographic Failures
1142
- - A03: Injection
1143
- - A04: Insecure Design
1144
- - A05: Security Misconfiguration
1145
- - A06: Vulnerable and Outdated Components
1146
- - A07: Authentication Failures
1147
- - A08: Software and Data Integrity Failures
1148
- - A09: Security Logging and Monitoring Failures
1149
- - A10: Server-Side Request Forgery (SSRF)
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
- // Helper to write file if it doesn't exist or overwrite is true
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 writeIfNeeded(opencodeJsonPath, opencodeJsonContent);
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 writeIfNeeded(planAgentPath, planAgentContent);
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 writeIfNeeded(buildAgentPath, buildAgentContent);
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
- module.exports = { loadCommands, validateCommand };
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
+ };
@@ -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,
@@ -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,