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,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validate Command - Validation Orchestration
|
|
3
|
+
* Runs all validation checks (type/lint/security/tests) in sequence
|
|
4
|
+
*
|
|
5
|
+
* Security: Uses execFileSync for command execution to prevent injection
|
|
6
|
+
* Validation: Orchestrates multiple check types with configurable options
|
|
7
|
+
*
|
|
8
|
+
* @module commands/validate
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { execFileSync } = require('node:child_process');
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
|
|
15
|
+
// Constants
|
|
16
|
+
const CHECK_TYPES = {
|
|
17
|
+
TYPE_CHECK: 'typeCheck',
|
|
18
|
+
LINT: 'lint',
|
|
19
|
+
SECURITY: 'security',
|
|
20
|
+
TESTS: 'tests',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function getExecOptions() {
|
|
24
|
+
return { encoding: 'utf8', cwd: process.cwd(), timeout: 120000 };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ERROR_PATTERNS = {
|
|
28
|
+
COMMAND_NOT_FOUND: ['ENOENT', 'not found'],
|
|
29
|
+
NO_LOCK_FILE: ['requires an existing', 'package-lock'],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if error indicates command not found
|
|
34
|
+
* @private
|
|
35
|
+
*/
|
|
36
|
+
function isCommandNotFound(error) {
|
|
37
|
+
return ERROR_PATTERNS.COMMAND_NOT_FOUND.some(pattern =>
|
|
38
|
+
error.message.includes(pattern),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse number from regex match
|
|
44
|
+
* @private
|
|
45
|
+
*/
|
|
46
|
+
function parseNumber(match, index = 1, defaultValue = 0) {
|
|
47
|
+
return match ? Number.parseInt(match[index], 10) : defaultValue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get status label for check result
|
|
52
|
+
* @private
|
|
53
|
+
*/
|
|
54
|
+
function getCheckStatus(check) {
|
|
55
|
+
if (!check) return null;
|
|
56
|
+
if (check.skipped) return 'SKIPPED';
|
|
57
|
+
return check.success ? 'PASS' : 'FAIL';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse vulnerability counts from audit output
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
function parseVulnerabilities(output) {
|
|
65
|
+
return {
|
|
66
|
+
critical: parseNumber(/(\d+) critical/i.exec(output)), // NOSONAR S5852 - bounded \d+ pattern, no backtracking
|
|
67
|
+
high: parseNumber(/(\d+) high/i.exec(output)), // NOSONAR S5852 - bounded \d+ pattern, no backtracking
|
|
68
|
+
moderate: parseNumber(/(\d+) moderate/i.exec(output)), // NOSONAR S5852 - bounded \d+ pattern, no backtracking
|
|
69
|
+
low: parseNumber(/(\d+) low/i.exec(output)), // NOSONAR S5852 - bounded \d+ pattern, no backtracking
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Run TypeScript type checking
|
|
75
|
+
* Executes tsc --noEmit if TypeScript is configured
|
|
76
|
+
*
|
|
77
|
+
* @returns {Promise<{success: boolean, duration: number, errors?: number, skipped?: boolean, message?: string}>} Type check result
|
|
78
|
+
* @example
|
|
79
|
+
* const result = await runTypeCheck();
|
|
80
|
+
* if (!result.success) console.log(`Type errors: ${result.errors}`);
|
|
81
|
+
*/
|
|
82
|
+
async function runTypeCheck() {
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
|
|
85
|
+
// Check if TypeScript is configured
|
|
86
|
+
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json');
|
|
87
|
+
if (!fs.existsSync(tsconfigPath)) {
|
|
88
|
+
return {
|
|
89
|
+
success: true,
|
|
90
|
+
skipped: true,
|
|
91
|
+
duration: Date.now() - startTime,
|
|
92
|
+
message: 'TypeScript not configured (no tsconfig.json)',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
// Run tsc --noEmit for type checking only
|
|
98
|
+
execFileSync('tsc', ['--noEmit'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
duration: Date.now() - startTime,
|
|
103
|
+
errors: 0,
|
|
104
|
+
message: 'Type checking passed',
|
|
105
|
+
};
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// Check for timeout
|
|
108
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
duration: Date.now() - startTime,
|
|
112
|
+
message: 'Type check timed out after 2 minutes',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// tsc not found or type errors
|
|
117
|
+
if (isCommandNotFound(error)) {
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
skipped: true,
|
|
121
|
+
duration: Date.now() - startTime,
|
|
122
|
+
message: 'TypeScript compiler not found (skipping type check)',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Parse error output for error count
|
|
127
|
+
const errorMatch = /Found (\d+) error/.exec(error.stdout);
|
|
128
|
+
const errors = parseNumber(errorMatch, 1, 1);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
duration: Date.now() - startTime,
|
|
133
|
+
errors,
|
|
134
|
+
message: `Type checking failed: ${errors} error(s) found`,
|
|
135
|
+
output: error.stdout || error.message,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Run ESLint
|
|
142
|
+
* Executes eslint . to check code quality
|
|
143
|
+
*
|
|
144
|
+
* @returns {Promise<{success: boolean, duration: number, warnings?: number, errors?: number, message?: string}>} Lint result
|
|
145
|
+
* @example
|
|
146
|
+
* const result = await runLint();
|
|
147
|
+
* console.log(`Warnings: ${result.warnings}, Errors: ${result.errors}`);
|
|
148
|
+
*/
|
|
149
|
+
async function runLint() {
|
|
150
|
+
const startTime = Date.now();
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// Run eslint with no output (exit code determines success)
|
|
154
|
+
execFileSync('eslint', ['.'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
duration: Date.now() - startTime,
|
|
159
|
+
warnings: 0,
|
|
160
|
+
errors: 0,
|
|
161
|
+
message: 'Linting passed (no errors)',
|
|
162
|
+
};
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Check for timeout
|
|
165
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
166
|
+
return {
|
|
167
|
+
success: false,
|
|
168
|
+
duration: Date.now() - startTime,
|
|
169
|
+
message: 'Lint check timed out after 2 minutes',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// eslint not found - skip gracefully (consistent with tsc behavior)
|
|
174
|
+
if (isCommandNotFound(error)) {
|
|
175
|
+
return {
|
|
176
|
+
success: true,
|
|
177
|
+
skipped: true,
|
|
178
|
+
duration: Date.now() - startTime,
|
|
179
|
+
message: 'ESLint not found (skipping lint check). Install with: bun add -D eslint',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Parse eslint output for warnings/errors
|
|
184
|
+
const output = error.stdout || error.message;
|
|
185
|
+
const problemsMatch = /(\d+) problems? \((\d+) errors?, (\d+) warnings?\)/.exec(output); // NOSONAR S5852 - bounded quantifiers, no backtracking
|
|
186
|
+
|
|
187
|
+
const errors = parseNumber(problemsMatch, 2, 0);
|
|
188
|
+
const warnings = parseNumber(problemsMatch, 3, 0);
|
|
189
|
+
|
|
190
|
+
// Only fail if there are errors (warnings are acceptable)
|
|
191
|
+
const success = errors === 0;
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
success,
|
|
195
|
+
duration: Date.now() - startTime,
|
|
196
|
+
warnings,
|
|
197
|
+
errors,
|
|
198
|
+
message: success
|
|
199
|
+
? `Linting passed with ${warnings} warning(s)`
|
|
200
|
+
: `Linting failed: ${errors} error(s), ${warnings} warning(s)`,
|
|
201
|
+
output,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Run security audit
|
|
208
|
+
* Executes bun audit or npm audit to check for vulnerabilities
|
|
209
|
+
*
|
|
210
|
+
* @returns {Promise<{success: boolean, duration: number, vulnerabilities?: {critical: number, high: number, moderate: number, low: number}, message?: string}>} Security scan result
|
|
211
|
+
* @example
|
|
212
|
+
* const result = await runSecurityScan();
|
|
213
|
+
* if (result.vulnerabilities.critical > 0) console.log('Critical vulnerabilities found!');
|
|
214
|
+
*/
|
|
215
|
+
async function runSecurityScan() { // NOSONAR S3776
|
|
216
|
+
const startTime = Date.now();
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
// Try bun audit first (faster and works without package-lock.json)
|
|
220
|
+
const result = execFileSync('bun', ['audit'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
221
|
+
|
|
222
|
+
// Parse bun audit output
|
|
223
|
+
const vulnerabilities = parseVulnerabilities(result);
|
|
224
|
+
const totalVulns = Object.values(vulnerabilities).reduce((sum, count) => sum + count, 0);
|
|
225
|
+
const hasCritical = vulnerabilities.critical > 0 || vulnerabilities.high > 0;
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
success: !hasCritical, // Fail only on critical/high
|
|
229
|
+
duration: Date.now() - startTime,
|
|
230
|
+
vulnerabilities,
|
|
231
|
+
message: totalVulns === 0
|
|
232
|
+
? 'No vulnerabilities found'
|
|
233
|
+
: `Found ${totalVulns} ${totalVulns === 1 ? 'vulnerability' : 'vulnerabilities'} (${vulnerabilities.critical} critical, ${vulnerabilities.high} high)`, // NOSONAR S3358 - simple format string
|
|
234
|
+
};
|
|
235
|
+
} catch (error) {
|
|
236
|
+
// Check if bun audit timed out - don't retry with npm audit
|
|
237
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
238
|
+
return {
|
|
239
|
+
success: false,
|
|
240
|
+
duration: Date.now() - startTime,
|
|
241
|
+
message: 'Security audit timed out after 2 minutes',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// bun audit exits non-zero when vulnerabilities are found; stdout still contains the data
|
|
245
|
+
if (error.stdout) {
|
|
246
|
+
const vulnerabilities = parseVulnerabilities(error.stdout);
|
|
247
|
+
const totalVulns = Object.values(vulnerabilities).reduce((sum, count) => sum + count, 0);
|
|
248
|
+
const hasCritical = vulnerabilities.critical > 0 || vulnerabilities.high > 0;
|
|
249
|
+
return {
|
|
250
|
+
success: !hasCritical,
|
|
251
|
+
duration: Date.now() - startTime,
|
|
252
|
+
vulnerabilities,
|
|
253
|
+
message: totalVulns === 0
|
|
254
|
+
? 'No vulnerabilities found'
|
|
255
|
+
: `Found ${totalVulns} ${totalVulns === 1 ? 'vulnerability' : 'vulnerabilities'} (${vulnerabilities.critical} critical, ${vulnerabilities.high} high)`, // NOSONAR S3358 - simple format string
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// bun audit failed without output, try npm audit
|
|
259
|
+
// Note: npm audit exits non-zero for ANY vulnerability (including low/moderate).
|
|
260
|
+
// Capture stdout from the error object to parse JSON output even on non-zero exit.
|
|
261
|
+
try {
|
|
262
|
+
let npmRawOutput = null;
|
|
263
|
+
try {
|
|
264
|
+
npmRawOutput = execFileSync('npm', ['audit', '--json', '--production'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
265
|
+
} catch (npmExitError) {
|
|
266
|
+
if (npmExitError.killed && npmExitError.signal === 'SIGTERM') {
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
duration: Date.now() - startTime,
|
|
270
|
+
message: 'Security audit timed out after 2 minutes',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
// npm audit exits non-zero when it finds any vulnerability - stdout still has JSON
|
|
274
|
+
if (npmExitError.stdout) {
|
|
275
|
+
npmRawOutput = npmExitError.stdout;
|
|
276
|
+
} else {
|
|
277
|
+
throw npmExitError; // Genuine failure (not found, no lock file, etc.)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (npmRawOutput !== null) {
|
|
282
|
+
let vulnerabilities;
|
|
283
|
+
try {
|
|
284
|
+
const auditData = JSON.parse(npmRawOutput);
|
|
285
|
+
const meta = auditData.metadata?.vulnerabilities || auditData.vulnerabilities || {};
|
|
286
|
+
vulnerabilities = {
|
|
287
|
+
critical: meta.critical || 0,
|
|
288
|
+
high: meta.high || 0,
|
|
289
|
+
moderate: meta.moderate || 0,
|
|
290
|
+
low: meta.low || 0,
|
|
291
|
+
};
|
|
292
|
+
} catch { // JSON parse failed — npm audit returned plain text
|
|
293
|
+
return {
|
|
294
|
+
success: true,
|
|
295
|
+
skipped: true,
|
|
296
|
+
duration: Date.now() - startTime,
|
|
297
|
+
message: 'Security audit skipped (npm audit returned non-JSON output)',
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const totalVulns = Object.values(vulnerabilities).reduce((sum, n) => sum + n, 0);
|
|
301
|
+
const hasCritical = vulnerabilities.critical > 0 || vulnerabilities.high > 0;
|
|
302
|
+
const vulnSuffix = totalVulns === 1 ? 'y' : 'ies';
|
|
303
|
+
return {
|
|
304
|
+
success: !hasCritical,
|
|
305
|
+
duration: Date.now() - startTime,
|
|
306
|
+
vulnerabilities,
|
|
307
|
+
message: totalVulns === 0
|
|
308
|
+
? 'No vulnerabilities found (npm audit)'
|
|
309
|
+
: `Found ${totalVulns} vulnerabilit${vulnSuffix} (${vulnerabilities.critical} critical, ${vulnerabilities.high} high)`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
// Defensive fallback: npmRawOutput was null (unreachable in practice)
|
|
313
|
+
return {
|
|
314
|
+
success: true,
|
|
315
|
+
skipped: true,
|
|
316
|
+
duration: Date.now() - startTime,
|
|
317
|
+
message: 'Security audit skipped (no audit output available)',
|
|
318
|
+
};
|
|
319
|
+
} catch (npmError) {
|
|
320
|
+
// Check for timeout first
|
|
321
|
+
if (npmError.killed && npmError.signal === 'SIGTERM') {
|
|
322
|
+
return {
|
|
323
|
+
success: false,
|
|
324
|
+
duration: Date.now() - startTime,
|
|
325
|
+
message: 'Security audit timed out after 2 minutes',
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Both failed - check if it's because tools aren't available
|
|
330
|
+
const bunNotFound = isCommandNotFound(error);
|
|
331
|
+
const noLockFile = ERROR_PATTERNS.NO_LOCK_FILE.some(pattern =>
|
|
332
|
+
npmError.message?.includes(pattern),
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
if (bunNotFound || noLockFile) {
|
|
336
|
+
return {
|
|
337
|
+
success: true,
|
|
338
|
+
skipped: true,
|
|
339
|
+
duration: Date.now() - startTime,
|
|
340
|
+
message: 'Security audit skipped (no package manager audit available)',
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Audit found issues
|
|
345
|
+
return {
|
|
346
|
+
success: false,
|
|
347
|
+
duration: Date.now() - startTime,
|
|
348
|
+
message: 'Security audit failed. Run: npm audit or bun audit to see details',
|
|
349
|
+
output: npmError.message || error.message,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Run all tests
|
|
357
|
+
* Executes bun test to run the test suite
|
|
358
|
+
*
|
|
359
|
+
* @returns {Promise<{success: boolean, duration: number, passed: number, failed: number, total: number, message?: string}>} Test execution result
|
|
360
|
+
* @example
|
|
361
|
+
* const result = await runAllTests();
|
|
362
|
+
* console.log(`${result.passed}/${result.total} tests passed`);
|
|
363
|
+
*/
|
|
364
|
+
async function runAllTests() {
|
|
365
|
+
const startTime = Date.now();
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const result = execFileSync('bun', ['test'], getExecOptions()); // NOSONAR S4036 - hardcoded CLI command, no user input, developer tool context
|
|
369
|
+
|
|
370
|
+
// Parse bun test output
|
|
371
|
+
const passed = parseNumber(/(\d+) pass/.exec(result)); // NOSONAR S5852 - bounded \d+ pattern
|
|
372
|
+
const failed = parseNumber(/(\d+) fail/.exec(result)); // NOSONAR S5852 - bounded \d+ pattern
|
|
373
|
+
const skipped = parseNumber(/(\d+) skip/.exec(result)); // NOSONAR S5852 - bounded \d+ pattern
|
|
374
|
+
const total = parseNumber(/Ran (\d+) tests/.exec(result), 1, passed + failed + skipped);
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
success: failed === 0,
|
|
378
|
+
duration: Date.now() - startTime,
|
|
379
|
+
passed,
|
|
380
|
+
failed,
|
|
381
|
+
total,
|
|
382
|
+
message: failed === 0
|
|
383
|
+
? `All ${total} tests passed`
|
|
384
|
+
: `${failed}/${total} tests failed`,
|
|
385
|
+
};
|
|
386
|
+
} catch (error) {
|
|
387
|
+
// Check for timeout
|
|
388
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
389
|
+
return {
|
|
390
|
+
success: false,
|
|
391
|
+
duration: Date.now() - startTime,
|
|
392
|
+
passed: 0,
|
|
393
|
+
failed: 0,
|
|
394
|
+
total: 0,
|
|
395
|
+
message: 'Test execution timed out after 2 minutes',
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Test execution failed
|
|
400
|
+
if (isCommandNotFound(error)) {
|
|
401
|
+
return {
|
|
402
|
+
success: true,
|
|
403
|
+
skipped: true,
|
|
404
|
+
duration: Date.now() - startTime,
|
|
405
|
+
passed: 0,
|
|
406
|
+
failed: 0,
|
|
407
|
+
total: 0,
|
|
408
|
+
message: 'Tests skipped: bun not found.',
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Parse test failures from output
|
|
413
|
+
const output = error.stdout || error.message;
|
|
414
|
+
const passed = parseNumber(/(\d+) pass/.exec(output)); // NOSONAR S5852 - bounded \d+ pattern
|
|
415
|
+
const failed = parseNumber(/(\d+) fail/.exec(output), 1, 1); // NOSONAR S5852 - bounded \d+ pattern
|
|
416
|
+
const skipped = parseNumber(/(\d+) skip/.exec(output)); // NOSONAR S5852 - bounded \d+ pattern
|
|
417
|
+
const total = passed + failed + skipped;
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
success: false,
|
|
421
|
+
duration: Date.now() - startTime,
|
|
422
|
+
passed,
|
|
423
|
+
failed,
|
|
424
|
+
total,
|
|
425
|
+
message: `${failed}/${total} tests failed`,
|
|
426
|
+
output,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Execute all checks
|
|
433
|
+
* Orchestrates type checking, linting, security scanning, and tests
|
|
434
|
+
*
|
|
435
|
+
* @param {{skip?: string[], continueOnError?: boolean}} [options] - Execution options
|
|
436
|
+
* @returns {Promise<{
|
|
437
|
+
* success: boolean,
|
|
438
|
+
* checks: {
|
|
439
|
+
* typeCheck?: object,
|
|
440
|
+
* lint?: object,
|
|
441
|
+
* security?: object,
|
|
442
|
+
* tests?: object
|
|
443
|
+
* },
|
|
444
|
+
* summary: string,
|
|
445
|
+
* failedChecks?: string[],
|
|
446
|
+
* errors?: string[]
|
|
447
|
+
* }>} Execution result
|
|
448
|
+
* @example
|
|
449
|
+
* const result = await executeValidate({ skip: ['typeCheck'] });
|
|
450
|
+
* console.log(result.summary);
|
|
451
|
+
*/
|
|
452
|
+
async function executeValidate(options = {}) { // NOSONAR S3776
|
|
453
|
+
const { skip = [], continueOnError = true } = options || {};
|
|
454
|
+
|
|
455
|
+
const checks = {};
|
|
456
|
+
const failedChecks = [];
|
|
457
|
+
const errors = [];
|
|
458
|
+
const startTime = Date.now();
|
|
459
|
+
|
|
460
|
+
// 1. Type checking
|
|
461
|
+
if (!skip.includes(CHECK_TYPES.TYPE_CHECK)) {
|
|
462
|
+
try {
|
|
463
|
+
checks.typeCheck = await runTypeCheck();
|
|
464
|
+
if (!checks.typeCheck.success && !checks.typeCheck.skipped) {
|
|
465
|
+
failedChecks.push(CHECK_TYPES.TYPE_CHECK);
|
|
466
|
+
if (!continueOnError) {
|
|
467
|
+
return buildResult(checks, failedChecks, errors, startTime);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
} catch (error) {
|
|
471
|
+
errors.push(`Type check error: ${error.message}`);
|
|
472
|
+
checks.typeCheck = { success: false, message: error.message };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// 2. Linting
|
|
477
|
+
if (!skip.includes(CHECK_TYPES.LINT)) {
|
|
478
|
+
try {
|
|
479
|
+
checks.lint = await runLint();
|
|
480
|
+
if (!checks.lint.success && !checks.lint.skipped) {
|
|
481
|
+
failedChecks.push(CHECK_TYPES.LINT);
|
|
482
|
+
if (!continueOnError) {
|
|
483
|
+
return buildResult(checks, failedChecks, errors, startTime);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
} catch (error) {
|
|
487
|
+
errors.push(`Lint error: ${error.message}`);
|
|
488
|
+
checks.lint = { success: false, message: error.message };
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 3. Security scanning
|
|
493
|
+
if (!skip.includes(CHECK_TYPES.SECURITY)) {
|
|
494
|
+
try {
|
|
495
|
+
checks.security = await runSecurityScan();
|
|
496
|
+
if (!checks.security.success && !checks.security.skipped) {
|
|
497
|
+
failedChecks.push(CHECK_TYPES.SECURITY);
|
|
498
|
+
if (!continueOnError) {
|
|
499
|
+
return buildResult(checks, failedChecks, errors, startTime);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
errors.push(`Security scan error: ${error.message}`);
|
|
504
|
+
checks.security = { success: false, message: error.message };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// 4. Tests
|
|
509
|
+
if (!skip.includes(CHECK_TYPES.TESTS)) {
|
|
510
|
+
try {
|
|
511
|
+
checks.tests = await runAllTests();
|
|
512
|
+
if (!checks.tests.success && !checks.tests.skipped) {
|
|
513
|
+
failedChecks.push(CHECK_TYPES.TESTS);
|
|
514
|
+
if (!continueOnError) {
|
|
515
|
+
return buildResult(checks, failedChecks, errors, startTime);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
} catch (error) {
|
|
519
|
+
errors.push(`Test execution error: ${error.message}`);
|
|
520
|
+
checks.tests = { success: false, message: error.message };
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return buildResult(checks, failedChecks, errors, startTime);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Build final result object
|
|
529
|
+
* @private
|
|
530
|
+
*/
|
|
531
|
+
function buildResult(checks, failedChecks, errors, startTime) {
|
|
532
|
+
const success = failedChecks.length === 0 && errors.length === 0;
|
|
533
|
+
const duration = Date.now() - startTime;
|
|
534
|
+
|
|
535
|
+
// Build summary using getCheckStatus helper
|
|
536
|
+
const checkResults = [];
|
|
537
|
+
const checkLabels = {
|
|
538
|
+
typeCheck: 'Type',
|
|
539
|
+
lint: 'Lint',
|
|
540
|
+
security: 'Security',
|
|
541
|
+
tests: 'Tests',
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
for (const [key, label] of Object.entries(checkLabels)) {
|
|
545
|
+
const status = getCheckStatus(checks[key]);
|
|
546
|
+
if (status) {
|
|
547
|
+
checkResults.push(`${label}: ${status}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const summary = success
|
|
552
|
+
? `All checks passed (${checkResults.join(', ')})`
|
|
553
|
+
: `Checks failed: ${failedChecks.join(', ')}`;
|
|
554
|
+
|
|
555
|
+
const result = {
|
|
556
|
+
success,
|
|
557
|
+
checks,
|
|
558
|
+
summary,
|
|
559
|
+
duration,
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
if (failedChecks.length > 0) {
|
|
563
|
+
result.failedChecks = failedChecks;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (errors.length > 0) {
|
|
567
|
+
result.errors = errors;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Execute 4-phase debug mode gate when validation fails.
|
|
575
|
+
* Always starts at D1 (Reproduce); D2 (Root-cause) → D3 (Fix) → D4 (Verify)
|
|
576
|
+
* progression is driven by validate.md workflow instructions, not by this function.
|
|
577
|
+
* This function only gates on escalation (3+ attempts) and weak completion claims.
|
|
578
|
+
*
|
|
579
|
+
* @param {{fixAttempts?: number, claim?: string}} [options] - Debug options
|
|
580
|
+
* @returns {{escalate: boolean, phase?: string, message?: string, valid?: boolean, reason?: string}} Debug result
|
|
581
|
+
*/
|
|
582
|
+
function executeDebugMode({ fixAttempts = 0, claim } = {}) {
|
|
583
|
+
// Escalation takes priority — even if claim is also weak, escalation fires first.
|
|
584
|
+
if (fixAttempts >= 3) {
|
|
585
|
+
return { escalate: true, message: 'STOP: 3+ fixes. Question architecture before Fix #4.' };
|
|
586
|
+
}
|
|
587
|
+
// If claim uses forbidden phrases from dev.md HARD-GATE (exact phrases, not bare words)
|
|
588
|
+
if (claim && /should pass|looks good|seems to work/i.test(claim)) {
|
|
589
|
+
return { valid: false, reason: 'No fresh verification evidence — run validation fresh' };
|
|
590
|
+
}
|
|
591
|
+
// Otherwise start at Phase D1
|
|
592
|
+
return { escalate: false, phase: 'D1' };
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
module.exports = {
|
|
596
|
+
runTypeCheck,
|
|
597
|
+
runLint,
|
|
598
|
+
runSecurityScan,
|
|
599
|
+
runAllTests,
|
|
600
|
+
executeValidate,
|
|
601
|
+
executeDebugMode,
|
|
602
|
+
};
|