forge-workflow 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/.claude/commands/dev.md +345 -340
  2. package/.claude/commands/plan.md +566 -521
  3. package/.claude/commands/premerge.md +186 -176
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +448 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +212 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +90 -48
  10. package/.claude/commands/validate.md +288 -282
  11. package/.claude/commands/verify.md +269 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +121 -105
  14. package/.claude/scripts/greptile-resolve.sh +558 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +342 -337
  17. package/.cline/workflows/plan.md +563 -518
  18. package/.cline/workflows/premerge.md +183 -173
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +445 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +209 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +87 -45
  25. package/.cline/workflows/validate.md +285 -279
  26. package/.cline/workflows/verify.md +266 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +345 -340
  29. package/.codex/skills/plan/SKILL.md +566 -521
  30. package/.codex/skills/premerge/SKILL.md +186 -176
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +448 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +212 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +90 -48
  37. package/.codex/skills/validate/SKILL.md +288 -282
  38. package/.codex/skills/verify/SKILL.md +269 -221
  39. package/.cursor/commands/dev.md +342 -337
  40. package/.cursor/commands/plan.md +563 -518
  41. package/.cursor/commands/premerge.md +183 -173
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +445 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +209 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +87 -45
  48. package/.cursor/commands/validate.md +285 -279
  49. package/.cursor/commands/verify.md +266 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +347 -342
  54. package/.github/prompts/plan.prompt.md +568 -523
  55. package/.github/prompts/premerge.prompt.md +188 -178
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +450 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +214 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +92 -50
  62. package/.github/prompts/validate.prompt.md +290 -284
  63. package/.github/prompts/verify.prompt.md +271 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +346 -341
  67. package/.kilocode/workflows/plan.md +567 -522
  68. package/.kilocode/workflows/premerge.md +187 -177
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +449 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +213 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +91 -49
  75. package/.kilocode/workflows/validate.md +289 -283
  76. package/.kilocode/workflows/verify.md +270 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +345 -340
  79. package/.opencode/commands/plan.md +566 -521
  80. package/.opencode/commands/premerge.md +186 -176
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +448 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +212 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +90 -48
  87. package/.opencode/commands/validate.md +288 -282
  88. package/.opencode/commands/verify.md +269 -221
  89. package/.roo/commands/dev.md +346 -341
  90. package/.roo/commands/plan.md +567 -522
  91. package/.roo/commands/premerge.md +187 -177
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +449 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +213 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +91 -49
  98. package/.roo/commands/validate.md +289 -283
  99. package/.roo/commands/verify.md +270 -222
  100. package/AGENTS.md +272 -175
  101. package/CLAUDE.md +110 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +317 -313
  104. package/bin/forge-preflight.js +322 -309
  105. package/bin/forge.js +4765 -4303
  106. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  107. package/docs/BEADS_GITHUB_SYNC.md +251 -251
  108. package/docs/ENHANCED_ONBOARDING.md +612 -602
  109. package/docs/EXAMPLES.md +482 -482
  110. package/docs/GREPTILE_SETUP.md +400 -400
  111. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  112. package/docs/ROADMAP.md +359 -359
  113. package/docs/SETUP.md +663 -631
  114. package/docs/TOOLCHAIN.md +653 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +50 -39
  118. package/lib/agents/README.md +198 -198
  119. package/lib/agents/claude.plugin.json +28 -28
  120. package/lib/agents/cline.plugin.json +22 -22
  121. package/lib/agents/codex.plugin.json +19 -19
  122. package/lib/agents/copilot.plugin.json +24 -24
  123. package/lib/agents/cursor.plugin.json +25 -25
  124. package/lib/agents/kilocode.plugin.json +22 -22
  125. package/lib/agents/opencode.plugin.json +20 -20
  126. package/lib/agents/roo.plugin.json +23 -23
  127. package/lib/agents-config.js +2112 -2112
  128. package/lib/beads-health-check.js +143 -0
  129. package/lib/beads-setup.js +341 -0
  130. package/lib/beads-sync-scaffold.js +260 -0
  131. package/lib/commands/_registry.js +134 -0
  132. package/lib/commands/clean.js +181 -0
  133. package/lib/commands/dev.js +571 -513
  134. package/lib/commands/plan.js +692 -692
  135. package/lib/commands/push.js +196 -0
  136. package/lib/commands/recommend.js +119 -119
  137. package/lib/commands/ship.js +377 -377
  138. package/lib/commands/status.js +378 -378
  139. package/lib/commands/sync.js +55 -0
  140. package/lib/commands/team.js +37 -0
  141. package/lib/commands/test.js +207 -0
  142. package/lib/commands/validate.js +602 -602
  143. package/lib/commands/worktree.js +310 -0
  144. package/lib/context-merge.js +359 -359
  145. package/lib/dep-guard/analyzer.js +294 -294
  146. package/lib/dep-guard/behavior-detector.js +98 -98
  147. package/lib/dep-guard/contract-detector.js +162 -162
  148. package/lib/dep-guard/import-detector.js +498 -498
  149. package/lib/dep-guard/path-utils.js +13 -13
  150. package/lib/dep-guard/rubric.js +120 -120
  151. package/lib/dep-guard/task-parser.js +318 -318
  152. package/lib/detect-agent.js +191 -191
  153. package/lib/detect-worktree.js +47 -47
  154. package/lib/docs-command.js +51 -0
  155. package/lib/docs-copy.js +50 -0
  156. package/lib/file-hash.js +26 -26
  157. package/lib/freshness-token.js +148 -0
  158. package/lib/greptile-match.js +80 -0
  159. package/lib/husky-migration.js +450 -0
  160. package/lib/lefthook-check.js +65 -0
  161. package/lib/pat-setup.js +207 -0
  162. package/lib/plugin-catalog.js +350 -350
  163. package/lib/plugin-manager.js +166 -166
  164. package/lib/plugin-recommender.js +141 -141
  165. package/lib/project-discovery.js +491 -491
  166. package/lib/reset.js +309 -0
  167. package/lib/setup-action-log.js +139 -139
  168. package/lib/setup-summary-renderer.js +106 -106
  169. package/lib/setup-utils.js +96 -0
  170. package/lib/setup.js +192 -192
  171. package/lib/smart-merge.js +64 -0
  172. package/lib/symlink-utils.js +81 -0
  173. package/lib/task-ownership.js +117 -0
  174. package/lib/workflow-profiles.js +197 -197
  175. package/package.json +131 -128
  176. package/scripts/beads-context.sh +426 -0
  177. package/scripts/beads-context.test.js +567 -0
  178. package/scripts/behavioral-judge.sh +378 -0
  179. package/scripts/benchmark.js +85 -0
  180. package/scripts/branch-protection.js +183 -0
  181. package/scripts/check-agents.js +172 -0
  182. package/scripts/check-forge-token.js +98 -0
  183. package/scripts/commitlint.js +42 -0
  184. package/scripts/conflict-detect.sh +323 -0
  185. package/scripts/dep-guard-analyze.js +71 -0
  186. package/scripts/dep-guard.sh +789 -0
  187. package/scripts/eval_win.py +249 -0
  188. package/scripts/file-index.sh +493 -0
  189. package/scripts/forge-team/index.sh +86 -0
  190. package/scripts/forge-team/lib/agent-prompt.sh +52 -0
  191. package/scripts/forge-team/lib/claim.sh +256 -0
  192. package/scripts/forge-team/lib/dashboard.sh +341 -0
  193. package/scripts/forge-team/lib/epic.sh +332 -0
  194. package/scripts/forge-team/lib/hooks.sh +253 -0
  195. package/scripts/forge-team/lib/identity.sh +235 -0
  196. package/scripts/forge-team/lib/sync-github.sh +317 -0
  197. package/scripts/forge-team/lib/verify.sh +284 -0
  198. package/scripts/forge-team/lib/workload.sh +296 -0
  199. package/scripts/forge-team/tests/agent-prompt.test.sh +72 -0
  200. package/scripts/forge-team/tests/claim.test.sh +179 -0
  201. package/scripts/forge-team/tests/dashboard.test.sh +170 -0
  202. package/scripts/forge-team/tests/dispatcher.test.sh +79 -0
  203. package/scripts/forge-team/tests/epic.test.sh +176 -0
  204. package/scripts/forge-team/tests/hooks.test.sh +239 -0
  205. package/scripts/forge-team/tests/identity.test.sh +176 -0
  206. package/scripts/forge-team/tests/integration.test.sh +371 -0
  207. package/scripts/forge-team/tests/sync-github.test.sh +209 -0
  208. package/scripts/forge-team/tests/verify.test.sh +314 -0
  209. package/scripts/forge-team/tests/workflow-integration.test.sh +43 -0
  210. package/scripts/forge-team/tests/workload.test.sh +209 -0
  211. package/scripts/github-beads-sync/comment.mjs +64 -0
  212. package/scripts/github-beads-sync/config.mjs +148 -0
  213. package/scripts/github-beads-sync/github-api.mjs +131 -0
  214. package/scripts/github-beads-sync/index.mjs +332 -0
  215. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  216. package/scripts/github-beads-sync/mapping.mjs +78 -0
  217. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  218. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  219. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  220. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  221. package/scripts/github-beads-sync.config.json +26 -0
  222. package/scripts/improve-command.js +375 -0
  223. package/scripts/lib/eval-runner.js +268 -0
  224. package/scripts/lib/eval-schema.js +135 -0
  225. package/scripts/lib/eval-storage.js +78 -0
  226. package/scripts/lib/grading.js +203 -0
  227. package/scripts/lib/jsonl-lock.sh +48 -0
  228. package/scripts/lib/sanitize.sh +116 -0
  229. package/scripts/lib/transcript-parser.js +63 -0
  230. package/scripts/lint.js +47 -0
  231. package/scripts/migrate-to-bun-test.js +412 -0
  232. package/scripts/pr-coordinator.sh +706 -0
  233. package/scripts/run-command-eval.js +236 -0
  234. package/scripts/smart-status.sh +809 -0
  235. package/scripts/sync-commands.js +571 -0
  236. package/scripts/sync-utils.sh +455 -0
  237. package/scripts/test-dashboard.js +123 -0
  238. package/scripts/test.js +46 -0
  239. package/scripts/validate.sh +94 -0
  240. package/skills/parallel-deep-research/SKILL.md +108 -108
  241. package/skills/parallel-deep-research/evals/README.md +27 -27
  242. package/skills/parallel-deep-research/evals/evals.json +62 -62
  243. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  244. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  245. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  246. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  247. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  248. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -1,513 +1,571 @@
1
- /**
2
- * Dev Command - TDD Cycle Management
3
- * Guides developers through RED-GREEN-REFACTOR cycles
4
- *
5
- * Security: Uses execFileSync for test execution to prevent command injection
6
- * TDD Discipline: Enforces test-first development and validates phase transitions
7
- *
8
- * @module commands/dev
9
- */
10
-
11
- const { execFileSync } = require('node:child_process');
12
- const fs = require('node:fs');
13
- const path = require('node:path');
14
-
15
- function getExecOptions() {
16
- return { encoding: 'utf8', cwd: process.cwd(), timeout: 120000 };
17
- }
18
-
19
- /**
20
- * Detect current TDD phase based on project context
21
- *
22
- * Detection logic:
23
- * - RED: Tests exist, no implementation OR implementation exists but tests failing
24
- * - GREEN: Implementation exists, tests failing
25
- * - REFACTOR: Implementation exists, tests passing
26
- *
27
- * @param {{sourceFiles: string[], testFiles: string[], testsPassing?: boolean}} context - Project context
28
- * @returns {'RED'|'GREEN'|'REFACTOR'} Current TDD phase
29
- * @example
30
- * const phase = detectTDDPhase({ sourceFiles: ['lib/feature.js'], testFiles: ['test/feature.test.js'], testsPassing: true });
31
- * console.log(phase); // 'REFACTOR'
32
- */
33
- function detectTDDPhase(context) {
34
- const { sourceFiles = [], testFiles = [], testsPassing } = context;
35
-
36
- const hasTests = testFiles.length > 0;
37
- const hasImplementation = sourceFiles.length > 0;
38
-
39
- // RED: Tests exist but no implementation, or tests are failing
40
- if (hasTests && !hasImplementation) {
41
- return 'RED';
42
- }
43
-
44
- // GREEN: Tests failing with implementation
45
- if (hasTests && hasImplementation && testsPassing === false) {
46
- return 'GREEN';
47
- }
48
-
49
- // REFACTOR: Tests passing
50
- if (hasTests && hasImplementation && testsPassing === true) {
51
- return 'REFACTOR';
52
- }
53
-
54
- // Default to RED (write tests first)
55
- return 'RED';
56
- }
57
-
58
- /**
59
- * Identify source and test file pairs
60
- * Maps source files to their corresponding test files
61
- *
62
- * Conventions:
63
- * - lib/commands/feature.js → test/commands/feature.test.js
64
- * - src/utils/helper.js → test/utils/helper.test.js
65
- *
66
- * @param {string[]} files - List of file paths
67
- * @returns {{length: number, pairs?: Array<{source: string, test: string}>, orphanedTests?: string[], orphanedSources?: string[]}} File pair analysis
68
- * @example
69
- * const result = identifyFilePairs(['lib/feature.js', 'test/feature.test.js']);
70
- * console.log(result.pairs); // [{ source: 'lib/feature.js', test: 'test/feature.test.js' }]
71
- */
72
- function identifyFilePairs(files) {
73
- const testFiles = files.filter(f => f.includes('test') && f.endsWith('.test.js'));
74
- const sourceFiles = files.filter(f => !f.includes('test') && f.endsWith('.js'));
75
-
76
- const pairs = [];
77
- const orphanedTests = [];
78
- const orphanedSources = [];
79
-
80
- // Match test files to source files
81
- testFiles.forEach(testFile => {
82
- // Convert test/commands/feature.test.js → lib/commands/feature.js
83
- const sourceFile = testFile
84
- .replace(/^test\//, 'lib/')
85
- .replace(/\.test\.js$/, '.js');
86
-
87
- if (sourceFiles.includes(sourceFile)) {
88
- pairs.push({ source: sourceFile, test: testFile });
89
- } else {
90
- orphanedTests.push(testFile);
91
- }
92
- });
93
-
94
- // Find source files without tests
95
- sourceFiles.forEach(sourceFile => {
96
- const testFile = sourceFile
97
- .replace(/^(lib|src)\//, 'test/')
98
- .replace(/\.js$/, '.test.js');
99
-
100
- if (!testFiles.includes(testFile)) {
101
- orphanedSources.push(sourceFile);
102
- }
103
- });
104
-
105
- return {
106
- length: pairs.length,
107
- pairs: pairs.length > 0 ? pairs : undefined,
108
- orphanedTests: orphanedTests.length > 0 ? orphanedTests : undefined,
109
- orphanedSources: orphanedSources.length > 0 ? orphanedSources : undefined,
110
- };
111
- }
112
-
113
- /**
114
- * Run tests using bun test
115
- * Executes specified test file or all tests
116
- *
117
- * @param {string} [testFile] - Optional specific test file to run
118
- * @returns {Promise<{success: boolean, passed?: number, failed?: number, duration?: number, totalTests?: number, error?: string}>} Test execution result
119
- * @example
120
- * const result = await runTests('test/commands/feature.test.js');
121
- * console.log(`${result.passed}/${result.passed + result.failed} tests passed`);
122
- */
123
- async function runTests(testFile) {
124
- try {
125
- const args = ['test'];
126
- if (testFile) {
127
- // Check if test file exists
128
- if (!fs.existsSync(testFile)) {
129
- return {
130
- success: false,
131
- error: `Test file not found: ${testFile}\n\nEnsure the file exists and path is correct.`,
132
- };
133
- }
134
- args.push(testFile);
135
- }
136
-
137
- const startTime = Date.now();
138
- const result = execFileSync('bun', args, getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
139
- const duration = Date.now() - startTime;
140
-
141
- // Parse bun test output
142
- // Format: "X pass\nY fail\nRan Z tests"
143
- const passMatch = /(\d+) pass/.exec(result); // NOSONAR S5852 - bounded \d+ pattern, no backtracking
144
- const failMatch = /(\d+) fail/.exec(result); // NOSONAR S5852 - bounded \d+ pattern, no backtracking
145
- const totalMatch = /Ran (\d+) tests/.exec(result);
146
-
147
- const passed = passMatch ? parseInt(passMatch[1], 10) : 0;
148
- const failed = failMatch ? parseInt(failMatch[1], 10) : 0;
149
- const totalTests = totalMatch ? parseInt(totalMatch[1], 10) : passed + failed;
150
-
151
- return {
152
- success: failed === 0,
153
- passed,
154
- failed,
155
- totalTests,
156
- duration,
157
- };
158
- } catch (error) {
159
- // Check for timeout
160
- if (error.killed && error.signal === 'SIGTERM') {
161
- return {
162
- success: false,
163
- error: 'Test execution timed out after 2 minutes',
164
- };
165
- }
166
-
167
- // bun test exits non-zero when tests fail but stdout still contains results
168
- // Parse stdout to extract pass/fail counts before falling back to generic error
169
- if (error.stdout) {
170
- const output = error.stdout;
171
- const passMatch = /(\d+) pass/.exec(output); // NOSONAR S5852 - bounded \d+ pattern, no backtracking
172
- const failMatch = /(\d+) fail/.exec(output); // NOSONAR S5852 - bounded \d+ pattern, no backtracking
173
- const totalMatch = /Ran (\d+) tests/.exec(output);
174
-
175
- if (passMatch || failMatch) {
176
- const passed = passMatch ? Number.parseInt(passMatch[1], 10) : 0;
177
- const failed = failMatch ? Number.parseInt(failMatch[1], 10) : 0;
178
- const totalTests = totalMatch ? Number.parseInt(totalMatch[1], 10) : passed + failed;
179
- return {
180
- success: false,
181
- passed,
182
- failed,
183
- totalTests,
184
- error: `${failed} test${failed === 1 ? '' : 's'} failed`,
185
- };
186
- }
187
- }
188
-
189
- // Test execution failed (command not found or other error)
190
- const bunNotFound = error.message.includes('ENOENT') || error.message.includes('not found');
191
- const errorMsg = bunNotFound
192
- ? 'bun command not found. Ensure bun is installed and in PATH'
193
- : `Test execution failed: ${error.message}`;
194
-
195
- return {
196
- success: false,
197
- error: errorMsg,
198
- };
199
- }
200
- }
201
-
202
- /**
203
- * Get phase-specific TDD guidance
204
- * Provides actionable guidance for each TDD phase
205
- *
206
- * @param {'RED'|'GREEN'|'REFACTOR'} phase - Current TDD phase
207
- * @returns {string} Phase-specific guidance
208
- * @example
209
- * const guidance = getTDDGuidance('RED');
210
- * console.log(guidance); // "RED Phase: Write a failing test..."
211
- */
212
- function getTDDGuidance(phase) {
213
- const guidance = {
214
- RED: `RED Phase: Write a failing test
215
-
216
- 1. Write test BEFORE implementation
217
- 2. Test should fail (red)
218
- 3. Verify test fails for the right reason
219
- 4. Commit with "test: ..." message
220
-
221
- Next: GREEN phase (implement to make test pass)`,
222
-
223
- GREEN: `GREEN Phase: Make the test pass
224
-
225
- 1. Write MINIMAL code to pass the test
226
- 2. Don't worry about perfection
227
- 3. Focus on making tests green
228
- 4. Commit with "feat: ..." or "implement: ..." message
229
-
230
- Next: REFACTOR phase (improve code quality)`,
231
-
232
- REFACTOR: `REFACTOR Phase: Improve code quality
233
-
234
- 1. Maintain passing tests throughout refactoring
235
- 2. Extract duplicates, improve names, add docs
236
- 3. Run tests frequently while refactoring
237
- 4. Commit with "refactor: ..." message
238
-
239
- Next: RED phase (next feature) or done`,
240
- };
241
-
242
- return guidance[phase] || 'Unknown phase';
243
- }
244
-
245
- /**
246
- * Generate commit message for TDD phase
247
- * Creates standardized commit messages for each phase
248
- *
249
- * @param {{phase: 'RED'|'GREEN'|'REFACTOR', files: string[], testCount?: number, feature?: string, improvements?: string[]}} context - Commit context
250
- * @returns {string} Generated commit message
251
- * @example
252
- * const message = generateCommitMessage({ phase: 'RED', files: ['test/feature.test.js'], testCount: 15 });
253
- * console.log(message); // "test: add feature tests (RED)\n\n15 tests written"
254
- */
255
- function generateCommitMessage(context) {
256
- const { phase, files, testCount, feature, improvements } = context;
257
-
258
- if (phase === 'RED') {
259
- const fileNames = files.map(f => path.basename(f)).join(', ');
260
- return `test: add ${feature || 'feature'} tests (RED)
261
-
262
- ${testCount || files.length} tests written
263
- Files: ${fileNames}
264
-
265
- Tests are failing as expected (RED phase)`;
266
- }
267
-
268
- if (phase === 'GREEN') {
269
- const fileNames = files.map(f => path.basename(f)).join(', ');
270
- return `feat: implement ${feature || 'feature'} (GREEN)
271
-
272
- Implementation complete, tests passing
273
- Files: ${fileNames}
274
-
275
- GREEN phase complete`;
276
- }
277
-
278
- if (phase === 'REFACTOR') {
279
- const improvementList = improvements && improvements.length > 0
280
- ? '\n\n' + improvements.map(i => `- ${i}`).join('\n')
281
- : '';
282
- return `refactor: improve ${feature || 'code'} (REFACTOR)${improvementList}
283
-
284
- Code quality improvements while maintaining test coverage
285
- Tests remain green throughout refactoring`;
286
- }
287
-
288
- return `${phase}: ${feature || 'changes'}`;
289
- }
290
-
291
- /**
292
- * Identify independent features that can be worked on in parallel
293
- * Analyzes dependencies to find parallelizable work
294
- *
295
- * @param {Array<{name: string, files: string[], dependencies: string[]}>} features - Feature list with dependencies
296
- * @returns {{length: number, includes?: (name: string) => boolean, error?: string} | string[]} Parallel-safe features or error
297
- * @example
298
- * const parallel = identifyParallelWork([
299
- * { name: 'feature-a', files: ['lib/a.js'], dependencies: [] },
300
- * { name: 'feature-b', files: ['lib/b.js'], dependencies: [] }
301
- * ]);
302
- * console.log(parallel); // ['feature-a', 'feature-b']
303
- */
304
- function identifyParallelWork(features) {
305
- // Check for circular dependencies
306
- const visited = new Set();
307
- const recStack = new Set();
308
-
309
- function hasCycle(feature) {
310
- if (!visited.has(feature.name)) {
311
- visited.add(feature.name);
312
- recStack.add(feature.name);
313
-
314
- for (const dep of feature.dependencies) {
315
- const depFeature = features.find(f => f.name === dep);
316
- if (depFeature) {
317
- if (!visited.has(dep) && hasCycle(depFeature)) {
318
- return true;
319
- } else if (recStack.has(dep)) {
320
- return true;
321
- }
322
- }
323
- }
324
- }
325
- recStack.delete(feature.name);
326
- return false;
327
- }
328
-
329
- // Check for circular dependencies
330
- for (const feature of features) {
331
- if (hasCycle(feature)) {
332
- return {
333
- error: 'Circular dependency detected in features',
334
- };
335
- }
336
- }
337
-
338
- // Find features with no dependencies
339
- const parallelFeatures = features
340
- .filter(f => f.dependencies.length === 0)
341
- .map(f => f.name);
342
-
343
- return parallelFeatures;
344
- }
345
-
346
- /**
347
- * Execute dev command workflow
348
- * Main orchestrator for TDD development
349
- *
350
- * @param {string} featureName - Feature name
351
- * @param {{phase?: 'RED'|'GREEN'|'REFACTOR', testsPassing?: boolean}} [options] - Execution options
352
- * @returns {Promise<{success: boolean, phase?: string, detectedPhase?: string, guidance?: string, testResults?: object, summary?: string, nextPhase?: string, error?: string}>} Execution result
353
- * @example
354
- * const result = await executeDev('payment-integration', { phase: 'RED' });
355
- * console.log(result.guidance);
356
- */
357
- async function executeDev(featureName, options = {}) {
358
- if (!featureName || typeof featureName !== 'string') {
359
- return {
360
- success: false,
361
- error: 'Feature name is required and must be a string',
362
- };
363
- }
364
-
365
- const { phase, testsPassing } = options;
366
-
367
- try {
368
- // If no phase specified, auto-detect
369
- let currentPhase = phase;
370
- if (!currentPhase) {
371
- // Scan actual project directories to detect phase
372
- const readDirSafe = (dir) => {
373
- try { return fs.readdirSync(dir).filter(f => f.endsWith('.js')); }
374
- catch (_e) { return []; }
375
- };
376
- const sourceFiles = readDirSafe('lib/commands');
377
- const testFiles = readDirSafe('test/commands');
378
- // Note: testsPassing is undefined here, so detectTDDPhase defaults to RED.
379
- // RED is the safe default when pass/fail state is unknown (write tests first).
380
- // Use --phase flag to explicitly specify GREEN or REFACTOR.
381
- currentPhase = detectTDDPhase({ sourceFiles, testFiles, testsPassing });
382
-
383
- return {
384
- success: true,
385
- detectedPhase: currentPhase,
386
- autoDetected: true,
387
- guidance: getTDDGuidance(currentPhase),
388
- summary: `Default ${currentPhase} phase (use --phase flag for explicit phase control)`,
389
- };
390
- }
391
-
392
- // Validate REFACTOR requires passing tests
393
- if (phase === 'REFACTOR' && testsPassing === false) {
394
- return {
395
- success: false,
396
- error: 'Cannot proceed to REFACTOR phase: tests are failing. Complete GREEN phase first.',
397
- };
398
- }
399
-
400
- // Execute based on phase
401
- const result = {
402
- success: true,
403
- phase: currentPhase,
404
- guidance: getTDDGuidance(currentPhase),
405
- };
406
-
407
- if (currentPhase === 'RED') {
408
- result.nextPhase = 'GREEN';
409
- result.summary = 'Write failing tests for the feature';
410
- } else if (currentPhase === 'GREEN') {
411
- // Run tests to check status
412
- const testResults = await runTests();
413
- result.testResults = testResults;
414
- result.success = testResults.success; // Reflect actual test outcome
415
- if (!testResults.success) {
416
- result.error = testResults.error || 'Tests failed';
417
- }
418
- result.nextPhase = 'REFACTOR';
419
- result.summary = 'Implement feature to make tests pass';
420
- } else if (currentPhase === 'REFACTOR') {
421
- result.nextPhase = null; // Cycle complete
422
- result.summary = 'Improve code quality while keeping tests green';
423
- }
424
-
425
- return result;
426
- } catch (error) {
427
- return {
428
- success: false,
429
- error: `Failed to execute dev command: ${error.message}`,
430
- };
431
- }
432
- }
433
-
434
- /**
435
- * Decision gate route constants
436
- *
437
- * Routes used by the /dev workflow decision gate:
438
- * - PROCEED: Score 0-3, implementer makes the decision and documents it
439
- * - SPEC-REVIEWER: Score 4-7, route to spec reviewer subagent
440
- * - BLOCKED: Score 8+ or mandatory override, surface to developer
441
- */
442
- const DECISION_ROUTES = {
443
- PROCEED: 'PROCEED',
444
- SPEC_REVIEWER: 'SPEC-REVIEWER',
445
- BLOCKED: 'BLOCKED',
446
- };
447
-
448
- /**
449
- * Calculate the decision gate route based on a 7-dimension scoring rubric
450
- *
451
- * Each dimension is scored 0 (No), 1 (Possibly), or 2 (Yes):
452
- * 1. Files affected beyond the current task?
453
- * 2. Changes a function signature or public export?
454
- * 3. Changes a shared module used by other tasks?
455
- * 4. Changes or touches persistent data or schema?
456
- * 5. Changes user-visible behavior not discussed in design doc?
457
- * 6. Affects auth, permissions, or data exposure? (security — mandatory override if scored 2)
458
- * 7. Hard to reverse without cascading changes to other files?
459
- *
460
- * Score routing:
461
- * - 0-3: PROCEED
462
- * - 4-7: SPEC-REVIEWER
463
- * - 8+: BLOCKED
464
- *
465
- * Mandatory override: dimension 6 (index 5) scored 2 → always BLOCKED
466
- *
467
- * @param {number} score - Total score (0-14)
468
- * @param {number[]} dimensions - Array of 7 scores (each 0, 1, or 2)
469
- * @returns {{route: string, score: number, mandatoryOverride?: boolean}} Routing result
470
- * @example
471
- * const result = calculateDecisionRoute(2, [1, 1, 0, 0, 0, 0, 0]);
472
- * console.log(result.route); // 'PROCEED'
473
- *
474
- * const securityBlocked = calculateDecisionRoute(2, [0, 0, 0, 0, 0, 2, 0]);
475
- * console.log(securityBlocked.route); // 'BLOCKED'
476
- * console.log(securityBlocked.mandatoryOverride); // true
477
- */
478
- function calculateDecisionRoute(score, dimensions) {
479
- // Mandatory override: security dimension (dimension 6, zero-indexed as index 5) scored 2
480
- const securityScore = Array.isArray(dimensions) ? dimensions[5] : undefined;
481
- if (securityScore === 2) {
482
- return { route: DECISION_ROUTES.BLOCKED, score, mandatoryOverride: true };
483
- }
484
-
485
- // Derive authoritative total from dimensions when available to prevent score/dimensions drift
486
- const effectiveScore =
487
- Array.isArray(dimensions) && dimensions.length === 7
488
- ? dimensions.reduce((a, b) => a + b, 0)
489
- : score;
490
-
491
- // Score-based routing
492
- if (effectiveScore <= 3) {
493
- return { route: DECISION_ROUTES.PROCEED, score: effectiveScore };
494
- }
495
-
496
- if (effectiveScore <= 7) {
497
- return { route: DECISION_ROUTES.SPEC_REVIEWER, score: effectiveScore };
498
- }
499
-
500
- return { route: DECISION_ROUTES.BLOCKED, score: effectiveScore };
501
- }
502
-
503
- module.exports = {
504
- detectTDDPhase,
505
- identifyFilePairs,
506
- runTests,
507
- getTDDGuidance,
508
- generateCommitMessage,
509
- identifyParallelWork,
510
- executeDev,
511
- calculateDecisionRoute,
512
- DECISION_ROUTES,
513
- };
1
+ /**
2
+ * Dev Command - TDD Cycle Management
3
+ * Guides developers through RED-GREEN-REFACTOR cycles
4
+ *
5
+ * Security: Uses execFileSync for test execution to prevent command injection
6
+ * TDD Discipline: Enforces test-first development and validates phase transitions
7
+ *
8
+ * @module commands/dev
9
+ */
10
+
11
+ const { execFileSync } = require('node:child_process');
12
+ const fs = require('node:fs');
13
+ const path = require('node:path');
14
+
15
+ function getExecOptions() {
16
+ return { encoding: 'utf8', cwd: process.cwd(), timeout: 120000 };
17
+ }
18
+
19
+ /**
20
+ * Detect current TDD phase based on project context
21
+ *
22
+ * Detection logic:
23
+ * - RED: Tests exist, no implementation OR implementation exists but tests failing
24
+ * - GREEN: Implementation exists, tests failing
25
+ * - REFACTOR: Implementation exists, tests passing
26
+ *
27
+ * @param {{sourceFiles: string[], testFiles: string[], testsPassing?: boolean}} context - Project context
28
+ * @returns {'RED'|'GREEN'|'REFACTOR'} Current TDD phase
29
+ * @example
30
+ * const phase = detectTDDPhase({ sourceFiles: ['lib/feature.js'], testFiles: ['test/feature.test.js'], testsPassing: true });
31
+ * console.log(phase); // 'REFACTOR'
32
+ */
33
+ function detectTDDPhase(context) {
34
+ const { sourceFiles = [], testFiles = [], testsPassing } = context;
35
+
36
+ const hasTests = testFiles.length > 0;
37
+ const hasImplementation = sourceFiles.length > 0;
38
+
39
+ // RED: Tests exist but no implementation, or tests are failing
40
+ if (hasTests && !hasImplementation) {
41
+ return 'RED';
42
+ }
43
+
44
+ // GREEN: Tests failing with implementation
45
+ if (hasTests && hasImplementation && testsPassing === false) {
46
+ return 'GREEN';
47
+ }
48
+
49
+ // REFACTOR: Tests passing
50
+ if (hasTests && hasImplementation && testsPassing === true) {
51
+ return 'REFACTOR';
52
+ }
53
+
54
+ // Default to RED (write tests first)
55
+ return 'RED';
56
+ }
57
+
58
+ /**
59
+ * Identify source and test file pairs
60
+ * Maps source files to their corresponding test files
61
+ *
62
+ * Conventions:
63
+ * - lib/commands/feature.js → test/commands/feature.test.js
64
+ * - src/utils/helper.js → test/utils/helper.test.js
65
+ *
66
+ * @param {string[]} files - List of file paths
67
+ * @returns {{length: number, pairs?: Array<{source: string, test: string}>, orphanedTests?: string[], orphanedSources?: string[]}} File pair analysis
68
+ * @example
69
+ * const result = identifyFilePairs(['lib/feature.js', 'test/feature.test.js']);
70
+ * console.log(result.pairs); // [{ source: 'lib/feature.js', test: 'test/feature.test.js' }]
71
+ */
72
+ function identifyFilePairs(files) {
73
+ const testFiles = files.filter(f => f.includes('test') && f.endsWith('.test.js'));
74
+ const sourceFiles = files.filter(f => !f.includes('test') && f.endsWith('.js'));
75
+
76
+ const pairs = [];
77
+ const orphanedTests = [];
78
+ const orphanedSources = [];
79
+
80
+ // Match test files to source files
81
+ testFiles.forEach(testFile => {
82
+ // Convert test/commands/feature.test.js → lib/commands/feature.js
83
+ const sourceFile = testFile
84
+ .replace(/^test\//, 'lib/')
85
+ .replace(/\.test\.js$/, '.js');
86
+
87
+ if (sourceFiles.includes(sourceFile)) {
88
+ pairs.push({ source: sourceFile, test: testFile });
89
+ } else {
90
+ orphanedTests.push(testFile);
91
+ }
92
+ });
93
+
94
+ // Find source files without tests
95
+ sourceFiles.forEach(sourceFile => {
96
+ const testFile = sourceFile
97
+ .replace(/^(lib|src)\//, 'test/')
98
+ .replace(/\.js$/, '.test.js');
99
+
100
+ if (!testFiles.includes(testFile)) {
101
+ orphanedSources.push(sourceFile);
102
+ }
103
+ });
104
+
105
+ return {
106
+ length: pairs.length,
107
+ pairs: pairs.length > 0 ? pairs : undefined,
108
+ orphanedTests: orphanedTests.length > 0 ? orphanedTests : undefined,
109
+ orphanedSources: orphanedSources.length > 0 ? orphanedSources : undefined,
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Run tests using bun test
115
+ * Executes specified test file or all tests
116
+ *
117
+ * @param {string} [testFile] - Optional specific test file to run
118
+ * @returns {Promise<{success: boolean, passed?: number, failed?: number, duration?: number, totalTests?: number, error?: string}>} Test execution result
119
+ * @example
120
+ * const result = await runTests('test/commands/feature.test.js');
121
+ * console.log(`${result.passed}/${result.passed + result.failed} tests passed`);
122
+ */
123
+ async function runTests(testFile) {
124
+ try {
125
+ const args = ['test'];
126
+ if (testFile) {
127
+ // Check if test file exists
128
+ if (!fs.existsSync(testFile)) {
129
+ return {
130
+ success: false,
131
+ error: `Test file not found: ${testFile}\n\nEnsure the file exists and path is correct.`,
132
+ };
133
+ }
134
+ args.push(testFile);
135
+ }
136
+
137
+ const startTime = Date.now();
138
+ const result = execFileSync('bun', args, getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
139
+ const duration = Date.now() - startTime;
140
+
141
+ // Parse bun test output
142
+ // Format: "X pass\nY fail\nRan Z tests"
143
+ const passMatch = /(\d+) pass/.exec(result); // NOSONAR S5852 - bounded \d+ pattern, no backtracking
144
+ const failMatch = /(\d+) fail/.exec(result); // NOSONAR S5852 - bounded \d+ pattern, no backtracking
145
+ const totalMatch = /Ran (\d+) tests/.exec(result);
146
+
147
+ const passed = passMatch ? parseInt(passMatch[1], 10) : 0;
148
+ const failed = failMatch ? parseInt(failMatch[1], 10) : 0;
149
+ const totalTests = totalMatch ? parseInt(totalMatch[1], 10) : passed + failed;
150
+
151
+ return {
152
+ success: failed === 0,
153
+ passed,
154
+ failed,
155
+ totalTests,
156
+ duration,
157
+ };
158
+ } catch (error) {
159
+ // Check for timeout
160
+ if (error.killed && error.signal === 'SIGTERM') {
161
+ return {
162
+ success: false,
163
+ error: 'Test execution timed out after 2 minutes',
164
+ };
165
+ }
166
+
167
+ // bun test exits non-zero when tests fail but stdout still contains results
168
+ // Parse stdout to extract pass/fail counts before falling back to generic error
169
+ if (error.stdout) {
170
+ const output = error.stdout;
171
+ const passMatch = /(\d+) pass/.exec(output); // NOSONAR S5852 - bounded \d+ pattern, no backtracking
172
+ const failMatch = /(\d+) fail/.exec(output); // NOSONAR S5852 - bounded \d+ pattern, no backtracking
173
+ const totalMatch = /Ran (\d+) tests/.exec(output);
174
+
175
+ if (passMatch || failMatch) {
176
+ const passed = passMatch ? Number.parseInt(passMatch[1], 10) : 0;
177
+ const failed = failMatch ? Number.parseInt(failMatch[1], 10) : 0;
178
+ const totalTests = totalMatch ? Number.parseInt(totalMatch[1], 10) : passed + failed;
179
+ return {
180
+ success: false,
181
+ passed,
182
+ failed,
183
+ totalTests,
184
+ error: `${failed} test${failed === 1 ? '' : 's'} failed`,
185
+ };
186
+ }
187
+ }
188
+
189
+ // Test execution failed (command not found or other error)
190
+ const bunNotFound = error.message.includes('ENOENT') || error.message.includes('not found');
191
+ const errorMsg = bunNotFound
192
+ ? 'bun command not found. Ensure bun is installed and in PATH'
193
+ : `Test execution failed: ${error.message}`;
194
+
195
+ return {
196
+ success: false,
197
+ error: errorMsg,
198
+ };
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Get phase-specific TDD guidance
204
+ * Provides actionable guidance for each TDD phase
205
+ *
206
+ * @param {'RED'|'GREEN'|'REFACTOR'} phase - Current TDD phase
207
+ * @returns {string} Phase-specific guidance
208
+ * @example
209
+ * const guidance = getTDDGuidance('RED');
210
+ * console.log(guidance); // "RED Phase: Write a failing test..."
211
+ */
212
+ function getTDDGuidance(phase) {
213
+ const guidance = {
214
+ RED: `RED Phase: Write a failing test
215
+
216
+ 1. Write test BEFORE implementation
217
+ 2. Test should fail (red)
218
+ 3. Verify test fails for the right reason
219
+ 4. Commit with "test: ..." message
220
+
221
+ Next: GREEN phase (implement to make test pass)`,
222
+
223
+ GREEN: `GREEN Phase: Make the test pass
224
+
225
+ 1. Write MINIMAL code to pass the test
226
+ 2. Don't worry about perfection
227
+ 3. Focus on making tests green
228
+ 4. Commit with "feat: ..." or "implement: ..." message
229
+
230
+ Next: REFACTOR phase (improve code quality)`,
231
+
232
+ REFACTOR: `REFACTOR Phase: Improve code quality
233
+
234
+ 1. Maintain passing tests throughout refactoring
235
+ 2. Extract duplicates, improve names, add docs
236
+ 3. Run tests frequently while refactoring
237
+ 4. Commit with "refactor: ..." message
238
+
239
+ Next: RED phase (next feature) or done`,
240
+ };
241
+
242
+ return guidance[phase] || 'Unknown phase';
243
+ }
244
+
245
+ /**
246
+ * Generate commit message for TDD phase
247
+ * Creates standardized commit messages for each phase
248
+ *
249
+ * @param {{phase: 'RED'|'GREEN'|'REFACTOR', files: string[], testCount?: number, feature?: string, improvements?: string[]}} context - Commit context
250
+ * @returns {string} Generated commit message
251
+ * @example
252
+ * const message = generateCommitMessage({ phase: 'RED', files: ['test/feature.test.js'], testCount: 15 });
253
+ * console.log(message); // "test: add feature tests (RED)\n\n15 tests written"
254
+ */
255
+ function generateCommitMessage(context) {
256
+ const { phase, files, testCount, feature, improvements } = context;
257
+
258
+ if (phase === 'RED') {
259
+ const fileNames = files.map(f => path.basename(f)).join(', ');
260
+ return `test: add ${feature || 'feature'} tests (RED)
261
+
262
+ ${testCount || files.length} tests written
263
+ Files: ${fileNames}
264
+
265
+ Tests are failing as expected (RED phase)`;
266
+ }
267
+
268
+ if (phase === 'GREEN') {
269
+ const fileNames = files.map(f => path.basename(f)).join(', ');
270
+ return `feat: implement ${feature || 'feature'} (GREEN)
271
+
272
+ Implementation complete, tests passing
273
+ Files: ${fileNames}
274
+
275
+ GREEN phase complete`;
276
+ }
277
+
278
+ if (phase === 'REFACTOR') {
279
+ const improvementList = improvements && improvements.length > 0
280
+ ? '\n\n' + improvements.map(i => `- ${i}`).join('\n')
281
+ : '';
282
+ return `refactor: improve ${feature || 'code'} (REFACTOR)${improvementList}
283
+
284
+ Code quality improvements while maintaining test coverage
285
+ Tests remain green throughout refactoring`;
286
+ }
287
+
288
+ return `${phase}: ${feature || 'changes'}`;
289
+ }
290
+
291
+ /**
292
+ * Identify independent features that can be worked on in parallel
293
+ * Analyzes dependencies to find parallelizable work
294
+ *
295
+ * @param {Array<{name: string, files: string[], dependencies: string[]}>} features - Feature list with dependencies
296
+ * @returns {{length: number, includes?: (name: string) => boolean, error?: string} | string[]} Parallel-safe features or error
297
+ * @example
298
+ * const parallel = identifyParallelWork([
299
+ * { name: 'feature-a', files: ['lib/a.js'], dependencies: [] },
300
+ * { name: 'feature-b', files: ['lib/b.js'], dependencies: [] }
301
+ * ]);
302
+ * console.log(parallel); // ['feature-a', 'feature-b']
303
+ */
304
+ function identifyParallelWork(features) {
305
+ // Check for circular dependencies
306
+ const visited = new Set();
307
+ const recStack = new Set();
308
+
309
+ function hasCycle(feature) {
310
+ if (!visited.has(feature.name)) {
311
+ visited.add(feature.name);
312
+ recStack.add(feature.name);
313
+
314
+ for (const dep of feature.dependencies) {
315
+ const depFeature = features.find(f => f.name === dep);
316
+ if (depFeature) {
317
+ if (!visited.has(dep) && hasCycle(depFeature)) {
318
+ return true;
319
+ } else if (recStack.has(dep)) {
320
+ return true;
321
+ }
322
+ }
323
+ }
324
+ }
325
+ recStack.delete(feature.name);
326
+ return false;
327
+ }
328
+
329
+ // Check for circular dependencies
330
+ for (const feature of features) {
331
+ if (hasCycle(feature)) {
332
+ return {
333
+ error: 'Circular dependency detected in features',
334
+ };
335
+ }
336
+ }
337
+
338
+ // Find features with no dependencies
339
+ const parallelFeatures = features
340
+ .filter(f => f.dependencies.length === 0)
341
+ .map(f => f.name);
342
+
343
+ return parallelFeatures;
344
+ }
345
+
346
+ /**
347
+ * Execute dev command workflow
348
+ * Main orchestrator for TDD development
349
+ *
350
+ * @param {string} featureName - Feature name
351
+ * @param {{phase?: 'RED'|'GREEN'|'REFACTOR', testsPassing?: boolean}} [options] - Execution options
352
+ * @returns {Promise<{success: boolean, phase?: string, detectedPhase?: string, guidance?: string, testResults?: object, summary?: string, nextPhase?: string, error?: string}>} Execution result
353
+ * @example
354
+ * const result = await executeDev('payment-integration', { phase: 'RED' });
355
+ * console.log(result.guidance);
356
+ */
357
+ async function executeDev(featureName, options = {}) {
358
+ if (!featureName || typeof featureName !== 'string') {
359
+ return {
360
+ success: false,
361
+ error: 'Feature name is required and must be a string',
362
+ };
363
+ }
364
+
365
+ const { phase, testsPassing } = options;
366
+
367
+ try {
368
+ // If no phase specified, auto-detect
369
+ let currentPhase = phase;
370
+ if (!currentPhase) {
371
+ // Scan actual project directories to detect phase
372
+ const readDirSafe = (dir) => {
373
+ try { return fs.readdirSync(dir).filter(f => f.endsWith('.js')); }
374
+ catch (_e) { return []; }
375
+ };
376
+ const sourceFiles = readDirSafe('lib/commands');
377
+ const testFiles = readDirSafe('test/commands');
378
+ // Note: testsPassing is undefined here, so detectTDDPhase defaults to RED.
379
+ // RED is the safe default when pass/fail state is unknown (write tests first).
380
+ // Use --phase flag to explicitly specify GREEN or REFACTOR.
381
+ currentPhase = detectTDDPhase({ sourceFiles, testFiles, testsPassing });
382
+
383
+ return {
384
+ success: true,
385
+ detectedPhase: currentPhase,
386
+ autoDetected: true,
387
+ guidance: getTDDGuidance(currentPhase),
388
+ summary: `Default ${currentPhase} phase (use --phase flag for explicit phase control)`,
389
+ };
390
+ }
391
+
392
+ // Validate REFACTOR requires passing tests
393
+ if (phase === 'REFACTOR' && testsPassing === false) {
394
+ return {
395
+ success: false,
396
+ error: 'Cannot proceed to REFACTOR phase: tests are failing. Complete GREEN phase first.',
397
+ };
398
+ }
399
+
400
+ // Execute based on phase
401
+ const result = {
402
+ success: true,
403
+ phase: currentPhase,
404
+ guidance: getTDDGuidance(currentPhase),
405
+ };
406
+
407
+ if (currentPhase === 'RED') {
408
+ result.nextPhase = 'GREEN';
409
+ result.summary = 'Write failing tests for the feature';
410
+ } else if (currentPhase === 'GREEN') {
411
+ // Run tests to check status
412
+ const testResults = await runTests();
413
+ result.testResults = testResults;
414
+ result.success = testResults.success; // Reflect actual test outcome
415
+ if (!testResults.success) {
416
+ result.error = testResults.error || 'Tests failed';
417
+ }
418
+ result.nextPhase = 'REFACTOR';
419
+ result.summary = 'Implement feature to make tests pass';
420
+ } else if (currentPhase === 'REFACTOR') {
421
+ result.nextPhase = null; // Cycle complete
422
+ result.summary = 'Improve code quality while keeping tests green';
423
+ }
424
+
425
+ return result;
426
+ } catch (error) {
427
+ return {
428
+ success: false,
429
+ error: `Failed to execute dev command: ${error.message}`,
430
+ };
431
+ }
432
+ }
433
+
434
+ /**
435
+ * Decision gate route constants
436
+ *
437
+ * Routes used by the /dev workflow decision gate:
438
+ * - PROCEED: Score 0-3, implementer makes the decision and documents it
439
+ * - SPEC-REVIEWER: Score 4-7, route to spec reviewer subagent
440
+ * - BLOCKED: Score 8+ or mandatory override, surface to developer
441
+ */
442
+ const DECISION_ROUTES = {
443
+ PROCEED: 'PROCEED',
444
+ SPEC_REVIEWER: 'SPEC-REVIEWER',
445
+ BLOCKED: 'BLOCKED',
446
+ };
447
+
448
+ /**
449
+ * Calculate the decision gate route based on a 7-dimension scoring rubric
450
+ *
451
+ * Each dimension is scored 0 (No), 1 (Possibly), or 2 (Yes):
452
+ * 1. Files affected beyond the current task?
453
+ * 2. Changes a function signature or public export?
454
+ * 3. Changes a shared module used by other tasks?
455
+ * 4. Changes or touches persistent data or schema?
456
+ * 5. Changes user-visible behavior not discussed in design doc?
457
+ * 6. Affects auth, permissions, or data exposure? (security — mandatory override if scored 2)
458
+ * 7. Hard to reverse without cascading changes to other files?
459
+ *
460
+ * Score routing:
461
+ * - 0-3: PROCEED
462
+ * - 4-7: SPEC-REVIEWER
463
+ * - 8+: BLOCKED
464
+ *
465
+ * Mandatory override: dimension 6 (index 5) scored 2 → always BLOCKED
466
+ *
467
+ * @param {number} score - Total score (0-14)
468
+ * @param {number[]} dimensions - Array of 7 scores (each 0, 1, or 2)
469
+ * @returns {{route: string, score: number, mandatoryOverride?: boolean}} Routing result
470
+ * @example
471
+ * const result = calculateDecisionRoute(2, [1, 1, 0, 0, 0, 0, 0]);
472
+ * console.log(result.route); // 'PROCEED'
473
+ *
474
+ * const securityBlocked = calculateDecisionRoute(2, [0, 0, 0, 0, 0, 2, 0]);
475
+ * console.log(securityBlocked.route); // 'BLOCKED'
476
+ * console.log(securityBlocked.mandatoryOverride); // true
477
+ */
478
+ function calculateDecisionRoute(score, dimensions) {
479
+ // Mandatory override: security dimension (dimension 6, zero-indexed as index 5) scored 2
480
+ const securityScore = Array.isArray(dimensions) ? dimensions[5] : undefined;
481
+ if (securityScore === 2) {
482
+ return { route: DECISION_ROUTES.BLOCKED, score, mandatoryOverride: true };
483
+ }
484
+
485
+ // Derive authoritative total from dimensions when available to prevent score/dimensions drift
486
+ const effectiveScore =
487
+ Array.isArray(dimensions) && dimensions.length === 7
488
+ ? dimensions.reduce((a, b) => a + b, 0)
489
+ : score;
490
+
491
+ // Score-based routing
492
+ if (effectiveScore <= 3) {
493
+ return { route: DECISION_ROUTES.PROCEED, score: effectiveScore };
494
+ }
495
+
496
+ if (effectiveScore <= 7) {
497
+ return { route: DECISION_ROUTES.SPEC_REVIEWER, score: effectiveScore };
498
+ }
499
+
500
+ return { route: DECISION_ROUTES.BLOCKED, score: effectiveScore };
501
+ }
502
+
503
+ /**
504
+ * Verify task completion and auto-commit changes if needed.
505
+ *
506
+ * Checks for uncommitted changes, stages only scoped files, commits,
507
+ * and returns the result. NEVER uses `git add -A` or `git add .`.
508
+ *
509
+ * Security: Uses execFileSync only (OWASP A03 — no shell injection).
510
+ *
511
+ * @param {string} taskTitle - The task title for the commit message
512
+ * @param {string[]|null|undefined} ownedFiles - Files this task owns (scoped staging). If falsy, stages tracked modified files only.
513
+ * @param {object} [opts] - Options
514
+ * @param {Function} [opts._exec] - Injected execFileSync for testing
515
+ * @returns {{committed: boolean, autoCommitted: boolean, commitSha: string|null, hasChanges: boolean}}
516
+ * @example
517
+ * const result = verifyTaskCompletion('add login form', ['lib/auth.js'], { _exec: mockExec });
518
+ */
519
+ function verifyTaskCompletion(taskTitle, ownedFiles, opts = {}) {
520
+ const run = opts._exec || execFileSync;
521
+
522
+ // 1. Check for uncommitted changes
523
+ const status = run('git', ['status', '--porcelain']).toString().trim();
524
+
525
+ if (!status) {
526
+ return { committed: false, autoCommitted: false, commitSha: null, hasChanges: false };
527
+ }
528
+
529
+ // There are changes
530
+ let filesToStage = [];
531
+
532
+ if (ownedFiles && ownedFiles.length > 0) {
533
+ // 2. Stage only owned files — NEVER git add -A
534
+ filesToStage = ownedFiles;
535
+ } else {
536
+ // 3. Stage only tracked modified files from git diff --name-only
537
+ const diffOutput = run('git', ['diff', '--name-only']).toString().trim();
538
+ if (diffOutput) {
539
+ filesToStage = diffOutput.split('\n').filter(Boolean);
540
+ }
541
+ }
542
+
543
+ // If no files to stage, skip commit
544
+ if (filesToStage.length === 0) {
545
+ return { committed: false, autoCommitted: false, commitSha: null, hasChanges: true };
546
+ }
547
+
548
+ // Stage scoped files
549
+ run('git', ['add', ...filesToStage]);
550
+
551
+ // 4. Commit with standardized message
552
+ run('git', ['commit', '-m', `feat(task): ${taskTitle}`]);
553
+
554
+ // 5. Get commit sha
555
+ const commitSha = run('git', ['log', '-1', '--format=%H']).toString().trim();
556
+
557
+ return { committed: true, autoCommitted: true, commitSha, hasChanges: true };
558
+ }
559
+
560
+ module.exports = {
561
+ detectTDDPhase,
562
+ identifyFilePairs,
563
+ runTests,
564
+ getTDDGuidance,
565
+ generateCommitMessage,
566
+ identifyParallelWork,
567
+ executeDev,
568
+ calculateDecisionRoute,
569
+ DECISION_ROUTES,
570
+ verifyTaskCompletion,
571
+ };