forge-workflow 0.0.1
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/.claude/commands/dev.md +314 -0
- package/.claude/commands/plan.md +389 -0
- package/.claude/commands/premerge.md +179 -0
- package/.claude/commands/research.md +42 -0
- package/.claude/commands/review.md +442 -0
- package/.claude/commands/rollback.md +721 -0
- package/.claude/commands/ship.md +134 -0
- package/.claude/commands/sonarcloud.md +152 -0
- package/.claude/commands/status.md +77 -0
- package/.claude/commands/validate.md +237 -0
- package/.claude/commands/verify.md +221 -0
- package/.claude/rules/greptile-review-process.md +285 -0
- package/.claude/rules/workflow.md +105 -0
- package/.claude/scripts/greptile-resolve.sh +526 -0
- package/.claude/scripts/load-env.sh +32 -0
- package/.forge/hooks/check-tdd.js +240 -0
- package/.github/PLUGIN_TEMPLATE.json +32 -0
- package/.mcp.json.example +12 -0
- package/AGENTS.md +169 -0
- package/CLAUDE.md +99 -0
- package/LICENSE +21 -0
- package/README.md +414 -0
- package/bin/forge-cmd.js +313 -0
- package/bin/forge-validate.js +303 -0
- package/bin/forge.js +4228 -0
- package/docs/AGENT_INSTALL_PROMPT.md +342 -0
- package/docs/ENHANCED_ONBOARDING.md +602 -0
- package/docs/EXAMPLES.md +482 -0
- package/docs/GREPTILE_SETUP.md +400 -0
- package/docs/MANUAL_REVIEW_GUIDE.md +106 -0
- package/docs/ROADMAP.md +359 -0
- package/docs/SETUP.md +632 -0
- package/docs/TOOLCHAIN.md +849 -0
- package/docs/VALIDATION.md +363 -0
- package/docs/WORKFLOW.md +400 -0
- package/docs/planning/PROGRESS.md +396 -0
- package/docs/plans/.gitkeep +0 -0
- package/docs/plans/2026-02-27-forge-test-suite-v2-decisions.md +21 -0
- package/docs/plans/2026-02-27-forge-test-suite-v2-design.md +362 -0
- package/docs/plans/2026-02-27-forge-test-suite-v2-tasks.md +343 -0
- package/docs/plans/2026-03-02-superpowers-gaps-decisions.md +26 -0
- package/docs/plans/2026-03-02-superpowers-gaps-design.md +239 -0
- package/docs/plans/2026-03-02-superpowers-gaps-tasks.md +260 -0
- package/docs/plans/2026-03-04-agent-command-parity-design.md +163 -0
- package/docs/plans/2026-03-04-verify-worktree-cleanup-decisions.md +7 -0
- package/docs/plans/2026-03-04-verify-worktree-cleanup-design.md +165 -0
- package/docs/plans/2026-03-05-forge-uto-decisions.md +6 -0
- package/docs/plans/2026-03-05-forge-uto-design.md +116 -0
- package/docs/plans/2026-03-05-forge-uto-tasks.md +244 -0
- package/docs/plans/2026-03-10-command-creator-and-eval-decisions.md +52 -0
- package/docs/plans/2026-03-10-command-creator-and-eval-design.md +350 -0
- package/docs/plans/2026-03-10-command-creator-and-eval-tasks.md +426 -0
- package/docs/plans/2026-03-10-stale-workflow-refs-decisions.md +8 -0
- package/docs/plans/2026-03-10-stale-workflow-refs-design.md +80 -0
- package/docs/plans/2026-03-10-stale-workflow-refs-tasks.md +90 -0
- package/docs/plans/2026-03-14-beads-plan-context-decisions.md +9 -0
- package/docs/plans/2026-03-14-beads-plan-context-design.md +171 -0
- package/docs/plans/2026-03-14-beads-plan-context-tasks.md +160 -0
- package/docs/plans/2026-03-14-skill-eval-loop-decisions.md +33 -0
- package/docs/plans/2026-03-14-skill-eval-loop-design.md +118 -0
- package/docs/plans/2026-03-14-skill-eval-loop-results.md +78 -0
- package/docs/plans/2026-03-14-skill-eval-loop-tasks.md +160 -0
- package/docs/plans/2026-03-15-agent-command-parity-v2-decisions.md +11 -0
- package/docs/plans/2026-03-15-agent-command-parity-v2-design.md +145 -0
- package/docs/plans/2026-03-15-agent-command-parity-v2-tasks.md +211 -0
- package/docs/research/TEMPLATE.md +292 -0
- package/docs/research/advanced-testing.md +297 -0
- package/docs/research/agent-permissions.md +167 -0
- package/docs/research/dependency-chain.md +328 -0
- package/docs/research/forge-workflow-v2.md +550 -0
- package/docs/research/plugin-architecture.md +772 -0
- package/docs/research/pr4-cli-automation.md +326 -0
- package/docs/research/premerge-verify-restructure.md +205 -0
- package/docs/research/skills-restructure.md +508 -0
- package/docs/research/sonarcloud-perfection-plan.md +166 -0
- package/docs/research/sonarcloud-quality-gate.md +184 -0
- package/docs/research/superpowers-integration.md +403 -0
- package/docs/research/superpowers.md +319 -0
- package/docs/research/test-environment.md +519 -0
- package/install.sh +1062 -0
- package/lefthook.yml +39 -0
- package/lib/agents/README.md +198 -0
- package/lib/agents/claude.plugin.json +28 -0
- package/lib/agents/cline.plugin.json +22 -0
- package/lib/agents/codex.plugin.json +19 -0
- package/lib/agents/copilot.plugin.json +24 -0
- package/lib/agents/cursor.plugin.json +25 -0
- package/lib/agents/kilocode.plugin.json +22 -0
- package/lib/agents/opencode.plugin.json +20 -0
- package/lib/agents/roo.plugin.json +23 -0
- package/lib/agents-config.js +2112 -0
- package/lib/commands/dev.js +513 -0
- package/lib/commands/plan.js +696 -0
- package/lib/commands/recommend.js +119 -0
- package/lib/commands/ship.js +377 -0
- package/lib/commands/status.js +378 -0
- package/lib/commands/validate.js +602 -0
- package/lib/context-merge.js +359 -0
- package/lib/plugin-catalog.js +360 -0
- package/lib/plugin-manager.js +166 -0
- package/lib/plugin-recommender.js +141 -0
- package/lib/project-discovery.js +491 -0
- package/lib/setup.js +118 -0
- package/lib/workflow-profiles.js +203 -0
- package/package.json +115 -0
|
@@ -0,0 +1,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
|
+
module.exports = {
|
|
504
|
+
detectTDDPhase,
|
|
505
|
+
identifyFilePairs,
|
|
506
|
+
runTests,
|
|
507
|
+
getTDDGuidance,
|
|
508
|
+
generateCommitMessage,
|
|
509
|
+
identifyParallelWork,
|
|
510
|
+
executeDev,
|
|
511
|
+
calculateDecisionRoute,
|
|
512
|
+
DECISION_ROUTES,
|
|
513
|
+
};
|