forge-workflow 0.0.4 → 0.0.5

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 (209) hide show
  1. package/.claude/commands/dev.md +340 -340
  2. package/.claude/commands/plan.md +521 -521
  3. package/.claude/commands/premerge.md +176 -176
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -48
  10. package/.claude/commands/validate.md +282 -282
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -337
  17. package/.cline/workflows/plan.md +518 -518
  18. package/.cline/workflows/premerge.md +173 -173
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -45
  25. package/.cline/workflows/validate.md +279 -279
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -340
  29. package/.codex/skills/plan/SKILL.md +521 -521
  30. package/.codex/skills/premerge/SKILL.md +176 -176
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -48
  37. package/.codex/skills/validate/SKILL.md +282 -282
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -337
  40. package/.cursor/commands/plan.md +518 -518
  41. package/.cursor/commands/premerge.md +173 -173
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -45
  48. package/.cursor/commands/validate.md +279 -279
  49. package/.cursor/commands/verify.md +218 -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 +342 -342
  54. package/.github/prompts/plan.prompt.md +523 -523
  55. package/.github/prompts/premerge.prompt.md +178 -178
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -50
  62. package/.github/prompts/validate.prompt.md +284 -284
  63. package/.github/prompts/verify.prompt.md +223 -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 +341 -341
  67. package/.kilocode/workflows/plan.md +522 -522
  68. package/.kilocode/workflows/premerge.md +177 -177
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -49
  75. package/.kilocode/workflows/validate.md +283 -283
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -340
  79. package/.opencode/commands/plan.md +521 -521
  80. package/.opencode/commands/premerge.md +176 -176
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -48
  87. package/.opencode/commands/validate.md +282 -282
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -341
  90. package/.roo/commands/plan.md +522 -522
  91. package/.roo/commands/premerge.md +177 -177
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -49
  98. package/.roo/commands/validate.md +283 -283
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -175
  101. package/CLAUDE.md +100 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +313 -313
  104. package/bin/forge-preflight.js +309 -309
  105. package/bin/forge.js +4596 -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 +602 -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 +630 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +39 -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/dev.js +513 -513
  132. package/lib/commands/plan.js +692 -692
  133. package/lib/commands/recommend.js +119 -119
  134. package/lib/commands/ship.js +377 -377
  135. package/lib/commands/status.js +378 -378
  136. package/lib/commands/validate.js +602 -602
  137. package/lib/context-merge.js +359 -359
  138. package/lib/dep-guard/analyzer.js +294 -294
  139. package/lib/dep-guard/behavior-detector.js +98 -98
  140. package/lib/dep-guard/contract-detector.js +162 -162
  141. package/lib/dep-guard/import-detector.js +498 -498
  142. package/lib/dep-guard/path-utils.js +13 -13
  143. package/lib/dep-guard/rubric.js +120 -120
  144. package/lib/dep-guard/task-parser.js +318 -318
  145. package/lib/detect-agent.js +191 -191
  146. package/lib/detect-worktree.js +47 -47
  147. package/lib/file-hash.js +26 -26
  148. package/lib/husky-migration.js +450 -0
  149. package/lib/lefthook-check.js +65 -0
  150. package/lib/pat-setup.js +207 -0
  151. package/lib/plugin-catalog.js +350 -350
  152. package/lib/plugin-manager.js +166 -166
  153. package/lib/plugin-recommender.js +141 -141
  154. package/lib/project-discovery.js +491 -491
  155. package/lib/setup-action-log.js +139 -139
  156. package/lib/setup-summary-renderer.js +106 -106
  157. package/lib/setup-utils.js +96 -0
  158. package/lib/setup.js +192 -192
  159. package/lib/smart-merge.js +64 -0
  160. package/lib/symlink-utils.js +81 -0
  161. package/lib/workflow-profiles.js +197 -197
  162. package/package.json +131 -128
  163. package/scripts/beads-context.sh +291 -0
  164. package/scripts/beads-context.test.js +563 -0
  165. package/scripts/behavioral-judge.sh +378 -0
  166. package/scripts/benchmark.js +85 -0
  167. package/scripts/branch-protection.js +183 -0
  168. package/scripts/check-agents.js +172 -0
  169. package/scripts/commitlint.js +42 -0
  170. package/scripts/conflict-detect.sh +323 -0
  171. package/scripts/dep-guard-analyze.js +71 -0
  172. package/scripts/dep-guard.sh +811 -0
  173. package/scripts/eval_win.py +249 -0
  174. package/scripts/file-index.sh +399 -0
  175. package/scripts/github-beads-sync/comment.mjs +64 -0
  176. package/scripts/github-beads-sync/config.mjs +148 -0
  177. package/scripts/github-beads-sync/github-api.mjs +131 -0
  178. package/scripts/github-beads-sync/index.mjs +332 -0
  179. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  180. package/scripts/github-beads-sync/mapping.mjs +78 -0
  181. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  182. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  183. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  184. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  185. package/scripts/github-beads-sync.config.json +26 -0
  186. package/scripts/improve-command.js +375 -0
  187. package/scripts/lib/eval-runner.js +229 -0
  188. package/scripts/lib/eval-schema.js +135 -0
  189. package/scripts/lib/eval-storage.js +78 -0
  190. package/scripts/lib/grading.js +203 -0
  191. package/scripts/lib/transcript-parser.js +63 -0
  192. package/scripts/lint.js +47 -0
  193. package/scripts/migrate-to-bun-test.js +412 -0
  194. package/scripts/run-command-eval.js +236 -0
  195. package/scripts/smart-status.sh +782 -0
  196. package/scripts/sync-commands.js +571 -0
  197. package/scripts/sync-utils.sh +460 -0
  198. package/scripts/test-dashboard.js +123 -0
  199. package/scripts/test.js +44 -0
  200. package/scripts/validate.sh +94 -0
  201. package/skills/parallel-deep-research/SKILL.md +108 -108
  202. package/skills/parallel-deep-research/evals/README.md +27 -27
  203. package/skills/parallel-deep-research/evals/evals.json +62 -62
  204. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  205. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  206. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  207. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  208. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  209. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -1,602 +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
- };
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
+ };