wogiflow 1.0.0
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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Code Review Step
|
|
5
|
+
*
|
|
6
|
+
* Hybrid code review: multi-agent for big/high-risk tasks, simple for small/low-risk.
|
|
7
|
+
* Uses confidence scoring (0-100) and only reports issues with confidence >= threshold.
|
|
8
|
+
*
|
|
9
|
+
* Multi-agent review runs 3 parallel perspectives:
|
|
10
|
+
* 1. Architecture/Design review
|
|
11
|
+
* 2. Implementation/Logic review
|
|
12
|
+
* 3. Security/Edge cases review
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const { getProjectRoot, colors } = require('./flow-utils');
|
|
18
|
+
|
|
19
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
20
|
+
|
|
21
|
+
// High-risk patterns that trigger multi-agent review
|
|
22
|
+
const HIGH_RISK_PATTERNS = [
|
|
23
|
+
'auth', 'authentication', 'authorization',
|
|
24
|
+
'payment', 'billing', 'checkout',
|
|
25
|
+
'security', 'crypto', 'encrypt', 'decrypt',
|
|
26
|
+
'password', 'credential', 'secret', 'token',
|
|
27
|
+
'admin', 'permission', 'role',
|
|
28
|
+
'database', 'migration', 'schema',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Run code review as a workflow step
|
|
33
|
+
*
|
|
34
|
+
* @param {object} options
|
|
35
|
+
* @param {string[]} options.files - Files modified
|
|
36
|
+
* @param {object} options.stepConfig - Step configuration
|
|
37
|
+
* @param {string} options.mode - Step mode (block/warn/prompt/auto)
|
|
38
|
+
* @param {string} options.taskType - Type of task (feature/bugfix/refactor)
|
|
39
|
+
* @returns {object} - { passed: boolean, message: string, details?: object[] }
|
|
40
|
+
*/
|
|
41
|
+
async function run(options = {}) {
|
|
42
|
+
const { files = [], stepConfig = {}, taskType } = options;
|
|
43
|
+
const multiAgentThreshold = stepConfig.multiAgentThreshold || 5;
|
|
44
|
+
const confidenceThreshold = stepConfig.confidenceThreshold || 80;
|
|
45
|
+
const highRiskPatterns = stepConfig.highRiskPatterns || HIGH_RISK_PATTERNS;
|
|
46
|
+
|
|
47
|
+
// Filter to reviewable files
|
|
48
|
+
const reviewableExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.rs'];
|
|
49
|
+
const reviewableFiles = files.filter(f =>
|
|
50
|
+
reviewableExtensions.some(ext => f.endsWith(ext)) &&
|
|
51
|
+
!f.includes('.test.') &&
|
|
52
|
+
!f.includes('.spec.') &&
|
|
53
|
+
!f.includes('.d.ts')
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (reviewableFiles.length === 0) {
|
|
57
|
+
return { passed: true, message: 'No reviewable files modified' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Determine if high-risk
|
|
61
|
+
const isHighRisk = taskType === 'refactor' ||
|
|
62
|
+
reviewableFiles.some(f => highRiskPatterns.some(p => f.toLowerCase().includes(p)));
|
|
63
|
+
|
|
64
|
+
// Choose review mode
|
|
65
|
+
const useMultiAgent = reviewableFiles.length > multiAgentThreshold || isHighRisk;
|
|
66
|
+
|
|
67
|
+
let issues;
|
|
68
|
+
if (useMultiAgent) {
|
|
69
|
+
console.log(colors.cyan + ' Running multi-agent code review...' + colors.reset);
|
|
70
|
+
issues = await runMultiAgentReview(reviewableFiles, stepConfig);
|
|
71
|
+
} else {
|
|
72
|
+
console.log(colors.cyan + ' Running simple code review...' + colors.reset);
|
|
73
|
+
issues = await runSimpleReview(reviewableFiles, stepConfig);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Filter by confidence threshold
|
|
77
|
+
const reportableIssues = issues.filter(i => i.confidence >= confidenceThreshold);
|
|
78
|
+
|
|
79
|
+
if (reportableIssues.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
passed: true,
|
|
82
|
+
message: useMultiAgent
|
|
83
|
+
? `Multi-agent review passed (${reviewableFiles.length} files)`
|
|
84
|
+
: `Simple review passed (${reviewableFiles.length} files)`,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Report issues
|
|
89
|
+
console.log(colors.yellow + '\n Code Review Issues:' + colors.reset);
|
|
90
|
+
|
|
91
|
+
const criticalIssues = reportableIssues.filter(i => i.severity === 'critical');
|
|
92
|
+
const importantIssues = reportableIssues.filter(i => i.severity === 'important');
|
|
93
|
+
|
|
94
|
+
if (criticalIssues.length > 0) {
|
|
95
|
+
console.log(colors.red + ' Critical:' + colors.reset);
|
|
96
|
+
for (const issue of criticalIssues) {
|
|
97
|
+
printIssue(issue);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (importantIssues.length > 0) {
|
|
102
|
+
console.log(colors.yellow + ' Important:' + colors.reset);
|
|
103
|
+
for (const issue of importantIssues) {
|
|
104
|
+
printIssue(issue);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Critical issues block, important issues warn
|
|
109
|
+
const hasCritical = criticalIssues.length > 0;
|
|
110
|
+
|
|
111
|
+
// Auto-capture learnings from issues found
|
|
112
|
+
if (reportableIssues.length > 0) {
|
|
113
|
+
try {
|
|
114
|
+
const { captureFromSessionReview } = require('./flow-auto-learn');
|
|
115
|
+
captureFromSessionReview(reportableIssues);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
// Auto-learn not available or failed - continue silently
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
passed: !hasCritical,
|
|
123
|
+
message: `${reportableIssues.length} issue(s) found (${criticalIssues.length} critical, ${importantIssues.length} important)`,
|
|
124
|
+
details: reportableIssues,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Print a single issue
|
|
130
|
+
*/
|
|
131
|
+
function printIssue(issue) {
|
|
132
|
+
const confidenceColor = issue.confidence >= 90 ? colors.red : colors.yellow;
|
|
133
|
+
console.log(` ${issue.file}:${issue.line}`);
|
|
134
|
+
console.log(` ${issue.description}`);
|
|
135
|
+
console.log(` ${confidenceColor}Confidence: ${issue.confidence}%${colors.reset}`);
|
|
136
|
+
if (issue.fix) {
|
|
137
|
+
console.log(colors.dim + ` Fix: ${issue.fix}` + colors.reset);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run multi-agent review (3 perspectives)
|
|
143
|
+
*/
|
|
144
|
+
async function runMultiAgentReview(files, config) {
|
|
145
|
+
const allIssues = [];
|
|
146
|
+
|
|
147
|
+
for (const file of files) {
|
|
148
|
+
const filePath = path.join(PROJECT_ROOT, file);
|
|
149
|
+
if (!fs.existsSync(filePath)) continue;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
153
|
+
|
|
154
|
+
// Run all 3 perspectives
|
|
155
|
+
const architectureIssues = reviewArchitecture(content, file);
|
|
156
|
+
const implementationIssues = reviewImplementation(content, file);
|
|
157
|
+
const securityIssues = reviewSecurity(content, file);
|
|
158
|
+
|
|
159
|
+
// Merge and dedupe issues
|
|
160
|
+
const fileIssues = mergeIssues([
|
|
161
|
+
...architectureIssues,
|
|
162
|
+
...implementationIssues,
|
|
163
|
+
...securityIssues,
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
allIssues.push(...fileIssues);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
// Skip unreadable files
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return allIssues;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Run simple review (single pass)
|
|
177
|
+
*/
|
|
178
|
+
async function runSimpleReview(files, config) {
|
|
179
|
+
const allIssues = [];
|
|
180
|
+
|
|
181
|
+
for (const file of files) {
|
|
182
|
+
const filePath = path.join(PROJECT_ROOT, file);
|
|
183
|
+
if (!fs.existsSync(filePath)) continue;
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
187
|
+
const fileIssues = runBasicChecks(content, file);
|
|
188
|
+
allIssues.push(...fileIssues);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
// Skip unreadable files
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return allIssues;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Architecture/Design review perspective
|
|
199
|
+
*/
|
|
200
|
+
function reviewArchitecture(content, fileName) {
|
|
201
|
+
const issues = [];
|
|
202
|
+
const lines = content.split('\n');
|
|
203
|
+
|
|
204
|
+
// Check for god objects (too many methods/properties)
|
|
205
|
+
const methodCount = (content.match(/(?:function\s+\w+|(?:const|let|var)\s+\w+\s*=\s*(?:async\s*)?\()/g) || []).length;
|
|
206
|
+
if (methodCount > 15) {
|
|
207
|
+
issues.push({
|
|
208
|
+
file: fileName,
|
|
209
|
+
line: 1,
|
|
210
|
+
perspective: 'architecture',
|
|
211
|
+
severity: methodCount > 25 ? 'critical' : 'important',
|
|
212
|
+
confidence: Math.min(95, 70 + methodCount),
|
|
213
|
+
description: `File has ${methodCount} functions - consider splitting into modules`,
|
|
214
|
+
fix: 'Extract related functions into separate modules with clear responsibilities',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check for circular dependency hints
|
|
219
|
+
const imports = content.match(/(?:require|import).*['"](\.\.?\/[^'"]+)['"]/g) || [];
|
|
220
|
+
const uniqueImports = new Set(imports);
|
|
221
|
+
if (uniqueImports.size > 10) {
|
|
222
|
+
issues.push({
|
|
223
|
+
file: fileName,
|
|
224
|
+
line: 1,
|
|
225
|
+
perspective: 'architecture',
|
|
226
|
+
severity: 'important',
|
|
227
|
+
confidence: 75,
|
|
228
|
+
description: `High import count (${uniqueImports.size}) may indicate tight coupling`,
|
|
229
|
+
fix: 'Review dependencies and consider introducing facades or reorganizing modules',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check for mixed concerns
|
|
234
|
+
const hasDOM = /querySelector|getElementById|document\./i.test(content);
|
|
235
|
+
const hasAPI = /fetch|axios|http/i.test(content);
|
|
236
|
+
const hasDB = /query|findOne|insertOne|mongoose|prisma/i.test(content);
|
|
237
|
+
|
|
238
|
+
const concerns = [hasDOM, hasAPI, hasDB].filter(Boolean).length;
|
|
239
|
+
if (concerns >= 2) {
|
|
240
|
+
issues.push({
|
|
241
|
+
file: fileName,
|
|
242
|
+
line: 1,
|
|
243
|
+
perspective: 'architecture',
|
|
244
|
+
severity: 'important',
|
|
245
|
+
confidence: 80,
|
|
246
|
+
description: 'File mixes multiple concerns (UI/API/DB)',
|
|
247
|
+
fix: 'Separate into distinct layers: presentation, business logic, data access',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return issues;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Implementation/Logic review perspective
|
|
256
|
+
*/
|
|
257
|
+
function reviewImplementation(content, fileName) {
|
|
258
|
+
const issues = [];
|
|
259
|
+
const lines = content.split('\n');
|
|
260
|
+
|
|
261
|
+
// Check for magic numbers
|
|
262
|
+
for (let i = 0; i < lines.length; i++) {
|
|
263
|
+
const line = lines[i];
|
|
264
|
+
// Skip comments, imports, and obvious cases
|
|
265
|
+
if (line.trim().startsWith('//') || line.includes('import') || line.includes('require')) continue;
|
|
266
|
+
|
|
267
|
+
const magicMatch = line.match(/[^a-zA-Z0-9_](\d{2,})[^a-zA-Z0-9_]/);
|
|
268
|
+
if (magicMatch) {
|
|
269
|
+
const num = parseInt(magicMatch[1]);
|
|
270
|
+
// Skip common values like 100, 1000, ports, etc.
|
|
271
|
+
if (![100, 1000, 10000, 3000, 8080, 8000, 443, 80].includes(num)) {
|
|
272
|
+
issues.push({
|
|
273
|
+
file: fileName,
|
|
274
|
+
line: i + 1,
|
|
275
|
+
perspective: 'implementation',
|
|
276
|
+
severity: 'important',
|
|
277
|
+
confidence: 70,
|
|
278
|
+
description: `Magic number ${num} - consider using a named constant`,
|
|
279
|
+
fix: `Extract to a descriptively named constant: const MEANINGFUL_NAME = ${num}`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check for deeply nested logic
|
|
286
|
+
let maxIndent = 0;
|
|
287
|
+
for (let i = 0; i < lines.length; i++) {
|
|
288
|
+
const indent = (lines[i].match(/^(\s*)/)?.[1] || '').replace(/\t/g, ' ').length;
|
|
289
|
+
if (indent > maxIndent) maxIndent = indent;
|
|
290
|
+
if (indent >= 16) { // 8+ levels
|
|
291
|
+
issues.push({
|
|
292
|
+
file: fileName,
|
|
293
|
+
line: i + 1,
|
|
294
|
+
perspective: 'implementation',
|
|
295
|
+
severity: 'critical',
|
|
296
|
+
confidence: 95,
|
|
297
|
+
description: 'Deeply nested code (8+ levels)',
|
|
298
|
+
fix: 'Use early returns, extract functions, or restructure logic',
|
|
299
|
+
});
|
|
300
|
+
break; // Only report once per file
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Check for duplicate string literals
|
|
305
|
+
const stringLiterals = content.match(/['"][^'"]{10,}['"]/g) || [];
|
|
306
|
+
const literalCounts = {};
|
|
307
|
+
for (const lit of stringLiterals) {
|
|
308
|
+
literalCounts[lit] = (literalCounts[lit] || 0) + 1;
|
|
309
|
+
}
|
|
310
|
+
for (const [lit, count] of Object.entries(literalCounts)) {
|
|
311
|
+
if (count >= 3) {
|
|
312
|
+
issues.push({
|
|
313
|
+
file: fileName,
|
|
314
|
+
line: 1,
|
|
315
|
+
perspective: 'implementation',
|
|
316
|
+
severity: 'important',
|
|
317
|
+
confidence: 85,
|
|
318
|
+
description: `String literal appears ${count} times - extract to constant`,
|
|
319
|
+
fix: `Create a constant: const MESSAGE = ${lit}`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return issues;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Security/Edge cases review perspective
|
|
329
|
+
*/
|
|
330
|
+
function reviewSecurity(content, fileName) {
|
|
331
|
+
const issues = [];
|
|
332
|
+
const lines = content.split('\n');
|
|
333
|
+
|
|
334
|
+
// Check for unsafe operations
|
|
335
|
+
const unsafePatterns = [
|
|
336
|
+
{ pattern: /eval\s*\(/i, desc: 'eval() is dangerous - allows code injection', severity: 'critical', confidence: 100 },
|
|
337
|
+
{ pattern: /innerHTML\s*=/i, desc: 'innerHTML can cause XSS - use textContent or sanitize', severity: 'critical', confidence: 95 },
|
|
338
|
+
{ pattern: /dangerouslySetInnerHTML/i, desc: 'dangerouslySetInnerHTML requires careful sanitization', severity: 'important', confidence: 85 },
|
|
339
|
+
{ pattern: /new Function\s*\(/i, desc: 'new Function() is similar to eval - avoid if possible', severity: 'critical', confidence: 95 },
|
|
340
|
+
{ pattern: /document\.write/i, desc: 'document.write can be exploited - use DOM methods', severity: 'important', confidence: 90 },
|
|
341
|
+
{ pattern: /exec\s*\(\s*[^)]*\+/i, desc: 'String concatenation in exec() may allow command injection', severity: 'critical', confidence: 90 },
|
|
342
|
+
{ pattern: /execSync\s*\(\s*[^)]*\+/i, desc: 'String concatenation in execSync() may allow command injection', severity: 'critical', confidence: 90 },
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
for (let i = 0; i < lines.length; i++) {
|
|
346
|
+
const line = lines[i];
|
|
347
|
+
for (const { pattern, desc, severity, confidence } of unsafePatterns) {
|
|
348
|
+
if (pattern.test(line)) {
|
|
349
|
+
issues.push({
|
|
350
|
+
file: fileName,
|
|
351
|
+
line: i + 1,
|
|
352
|
+
perspective: 'security',
|
|
353
|
+
severity,
|
|
354
|
+
confidence,
|
|
355
|
+
description: desc,
|
|
356
|
+
fix: 'Review and use safer alternatives',
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Check for missing error handling in async
|
|
363
|
+
const asyncFunctions = content.match(/async\s+(?:function\s+)?(\w+)/g) || [];
|
|
364
|
+
for (const asyncFunc of asyncFunctions) {
|
|
365
|
+
// Simple heuristic: check if there's a try-catch nearby
|
|
366
|
+
const funcName = asyncFunc.replace(/async\s+(?:function\s+)?/, '');
|
|
367
|
+
const funcIndex = content.indexOf(asyncFunc);
|
|
368
|
+
const funcContext = content.substring(funcIndex, funcIndex + 500);
|
|
369
|
+
if (!funcContext.includes('try') && !funcContext.includes('catch')) {
|
|
370
|
+
issues.push({
|
|
371
|
+
file: fileName,
|
|
372
|
+
line: content.substring(0, funcIndex).split('\n').length,
|
|
373
|
+
perspective: 'security',
|
|
374
|
+
severity: 'important',
|
|
375
|
+
confidence: 70,
|
|
376
|
+
description: `Async function "${funcName}" may lack error handling`,
|
|
377
|
+
fix: 'Add try-catch or ensure errors are handled by the caller',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Check for hardcoded credentials
|
|
383
|
+
const credPatterns = [
|
|
384
|
+
/(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]+['"]/i,
|
|
385
|
+
/(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]+['"]/i,
|
|
386
|
+
/(?:secret|token)\s*[:=]\s*['"][A-Za-z0-9+/=]{20,}['"]/i,
|
|
387
|
+
];
|
|
388
|
+
|
|
389
|
+
for (let i = 0; i < lines.length; i++) {
|
|
390
|
+
const line = lines[i];
|
|
391
|
+
// Skip comments and env references
|
|
392
|
+
if (line.trim().startsWith('//') || line.includes('process.env') || line.includes('.env')) continue;
|
|
393
|
+
|
|
394
|
+
for (const pattern of credPatterns) {
|
|
395
|
+
if (pattern.test(line)) {
|
|
396
|
+
issues.push({
|
|
397
|
+
file: fileName,
|
|
398
|
+
line: i + 1,
|
|
399
|
+
perspective: 'security',
|
|
400
|
+
severity: 'critical',
|
|
401
|
+
confidence: 90,
|
|
402
|
+
description: 'Potential hardcoded credential detected',
|
|
403
|
+
fix: 'Use environment variables or a secrets manager',
|
|
404
|
+
});
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return issues;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Run basic checks (for simple review mode)
|
|
415
|
+
*/
|
|
416
|
+
function runBasicChecks(content, fileName) {
|
|
417
|
+
const issues = [];
|
|
418
|
+
const lines = content.split('\n');
|
|
419
|
+
|
|
420
|
+
// Check for console.log left in code
|
|
421
|
+
for (let i = 0; i < lines.length; i++) {
|
|
422
|
+
if (/console\.(log|debug|info)\s*\(/.test(lines[i]) && !lines[i].trim().startsWith('//')) {
|
|
423
|
+
issues.push({
|
|
424
|
+
file: fileName,
|
|
425
|
+
line: i + 1,
|
|
426
|
+
perspective: 'basic',
|
|
427
|
+
severity: 'important',
|
|
428
|
+
confidence: 80,
|
|
429
|
+
description: 'console.log left in code',
|
|
430
|
+
fix: 'Remove or replace with proper logging',
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check for TODO/FIXME comments
|
|
436
|
+
for (let i = 0; i < lines.length; i++) {
|
|
437
|
+
if (/\b(TODO|FIXME|XXX|HACK)\b/i.test(lines[i])) {
|
|
438
|
+
issues.push({
|
|
439
|
+
file: fileName,
|
|
440
|
+
line: i + 1,
|
|
441
|
+
perspective: 'basic',
|
|
442
|
+
severity: 'important',
|
|
443
|
+
confidence: 75,
|
|
444
|
+
description: 'Unresolved TODO/FIXME comment',
|
|
445
|
+
fix: 'Address the TODO or create a task to track it',
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Check for empty catch blocks
|
|
451
|
+
const emptyCatch = content.match(/catch\s*\([^)]*\)\s*\{\s*\}/g);
|
|
452
|
+
if (emptyCatch) {
|
|
453
|
+
issues.push({
|
|
454
|
+
file: fileName,
|
|
455
|
+
line: 1,
|
|
456
|
+
perspective: 'basic',
|
|
457
|
+
severity: 'critical',
|
|
458
|
+
confidence: 95,
|
|
459
|
+
description: 'Empty catch block swallows errors silently',
|
|
460
|
+
fix: 'Log the error or rethrow it',
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Check for debugger statements
|
|
465
|
+
for (let i = 0; i < lines.length; i++) {
|
|
466
|
+
if (/\bdebugger\b/.test(lines[i])) {
|
|
467
|
+
issues.push({
|
|
468
|
+
file: fileName,
|
|
469
|
+
line: i + 1,
|
|
470
|
+
perspective: 'basic',
|
|
471
|
+
severity: 'critical',
|
|
472
|
+
confidence: 100,
|
|
473
|
+
description: 'debugger statement left in code',
|
|
474
|
+
fix: 'Remove the debugger statement',
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
return issues;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Merge and dedupe issues from multiple perspectives
|
|
484
|
+
*/
|
|
485
|
+
function mergeIssues(issues) {
|
|
486
|
+
// Group by file:line
|
|
487
|
+
const grouped = {};
|
|
488
|
+
for (const issue of issues) {
|
|
489
|
+
const key = `${issue.file}:${issue.line}`;
|
|
490
|
+
if (!grouped[key]) {
|
|
491
|
+
grouped[key] = [];
|
|
492
|
+
}
|
|
493
|
+
grouped[key].push(issue);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// For each group, keep highest confidence version
|
|
497
|
+
const merged = [];
|
|
498
|
+
for (const group of Object.values(grouped)) {
|
|
499
|
+
// Sort by confidence descending
|
|
500
|
+
group.sort((a, b) => b.confidence - a.confidence);
|
|
501
|
+
|
|
502
|
+
// If multiple perspectives found same issue, boost confidence
|
|
503
|
+
if (group.length > 1) {
|
|
504
|
+
const best = group[0];
|
|
505
|
+
best.confidence = Math.min(100, best.confidence + (group.length - 1) * 5);
|
|
506
|
+
best.perspectives = group.map(i => i.perspective);
|
|
507
|
+
merged.push(best);
|
|
508
|
+
} else {
|
|
509
|
+
merged.push(group[0]);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return merged;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
module.exports = { run, runMultiAgentReview, runSimpleReview };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Security Scan Step
|
|
5
|
+
*
|
|
6
|
+
* Workflow step for security scanning.
|
|
7
|
+
* Runs npm audit and checks for common vulnerabilities.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const { getProjectRoot } = require('./flow-utils');
|
|
14
|
+
|
|
15
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run security scan as a workflow step
|
|
19
|
+
*
|
|
20
|
+
* @param {object} options
|
|
21
|
+
* @param {string[]} options.files - Files modified
|
|
22
|
+
* @param {object} options.stepConfig - Step configuration
|
|
23
|
+
* @param {string} options.mode - Step mode (block/warn/prompt/auto)
|
|
24
|
+
* @returns {object} - { passed: boolean, message: string, details?: object }
|
|
25
|
+
*/
|
|
26
|
+
async function run(options = {}) {
|
|
27
|
+
const { files = [], stepConfig = {}, mode } = options;
|
|
28
|
+
const severity = stepConfig.severity || 'high';
|
|
29
|
+
const issues = [];
|
|
30
|
+
|
|
31
|
+
// 1. Check for secrets in modified files
|
|
32
|
+
const secretPatterns = [
|
|
33
|
+
/(?:api[_-]?key|apikey)\s*[:=]\s*['"][^'"]+['"]/gi,
|
|
34
|
+
/(?:secret|password|passwd|pwd)\s*[:=]\s*['"][^'"]+['"]/gi,
|
|
35
|
+
/(?:access[_-]?token|auth[_-]?token)\s*[:=]\s*['"][^'"]+['"]/gi,
|
|
36
|
+
/-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----/i,
|
|
37
|
+
/(?:aws[_-]?access[_-]?key[_-]?id)\s*[:=]\s*['"][A-Z0-9]+['"]/gi,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const file of files) {
|
|
41
|
+
const filePath = path.join(PROJECT_ROOT, file);
|
|
42
|
+
if (!fs.existsSync(filePath)) continue;
|
|
43
|
+
|
|
44
|
+
// Skip test files and config examples
|
|
45
|
+
if (file.includes('.test.') || file.includes('.spec.') || file.includes('.example')) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
51
|
+
for (const pattern of secretPatterns) {
|
|
52
|
+
if (pattern.test(content)) {
|
|
53
|
+
issues.push({
|
|
54
|
+
type: 'secret',
|
|
55
|
+
severity: 'high',
|
|
56
|
+
file,
|
|
57
|
+
message: 'Potential secret or credential detected',
|
|
58
|
+
});
|
|
59
|
+
break; // One issue per file is enough
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Skip unreadable files
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Run npm audit if package.json was modified
|
|
68
|
+
const packageModified = files.some(f => f.endsWith('package.json') || f.endsWith('package-lock.json'));
|
|
69
|
+
|
|
70
|
+
if (packageModified || stepConfig.alwaysAudit) {
|
|
71
|
+
try {
|
|
72
|
+
const auditResult = execSync('npm audit --json 2>/dev/null', {
|
|
73
|
+
cwd: PROJECT_ROOT,
|
|
74
|
+
encoding: 'utf8',
|
|
75
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const audit = JSON.parse(auditResult);
|
|
79
|
+
|
|
80
|
+
if (audit.metadata && audit.metadata.vulnerabilities) {
|
|
81
|
+
const vulns = audit.metadata.vulnerabilities;
|
|
82
|
+
|
|
83
|
+
if (severity === 'critical' && vulns.critical > 0) {
|
|
84
|
+
issues.push({
|
|
85
|
+
type: 'npm_audit',
|
|
86
|
+
severity: 'critical',
|
|
87
|
+
message: `${vulns.critical} critical vulnerabilities found`,
|
|
88
|
+
count: vulns.critical,
|
|
89
|
+
});
|
|
90
|
+
} else if (severity === 'high' && (vulns.critical > 0 || vulns.high > 0)) {
|
|
91
|
+
const count = vulns.critical + vulns.high;
|
|
92
|
+
issues.push({
|
|
93
|
+
type: 'npm_audit',
|
|
94
|
+
severity: 'high',
|
|
95
|
+
message: `${count} high/critical vulnerabilities found`,
|
|
96
|
+
count,
|
|
97
|
+
});
|
|
98
|
+
} else if (severity === 'moderate') {
|
|
99
|
+
const count = vulns.critical + vulns.high + vulns.moderate;
|
|
100
|
+
if (count > 0) {
|
|
101
|
+
issues.push({
|
|
102
|
+
type: 'npm_audit',
|
|
103
|
+
severity: 'moderate',
|
|
104
|
+
message: `${count} moderate+ vulnerabilities found`,
|
|
105
|
+
count,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
// npm audit failed or returned non-zero
|
|
112
|
+
if (e.stdout) {
|
|
113
|
+
try {
|
|
114
|
+
const audit = JSON.parse(e.stdout);
|
|
115
|
+
if (audit.metadata && audit.metadata.vulnerabilities) {
|
|
116
|
+
const vulns = audit.metadata.vulnerabilities;
|
|
117
|
+
const count = vulns.critical + vulns.high;
|
|
118
|
+
if (count > 0 && (severity === 'high' || severity === 'critical')) {
|
|
119
|
+
issues.push({
|
|
120
|
+
type: 'npm_audit',
|
|
121
|
+
severity: 'high',
|
|
122
|
+
message: `${count} high/critical vulnerabilities`,
|
|
123
|
+
count,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch (parseError) {
|
|
128
|
+
// Ignore parse errors
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 3. Evaluate results
|
|
135
|
+
if (issues.length === 0) {
|
|
136
|
+
return { passed: true, message: 'Security scan passed' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Filter by severity for blocking
|
|
140
|
+
const blockingIssues = issues.filter(i => {
|
|
141
|
+
if (severity === 'critical') return i.severity === 'critical';
|
|
142
|
+
if (severity === 'high') return i.severity === 'high' || i.severity === 'critical';
|
|
143
|
+
return true;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (blockingIssues.length > 0) {
|
|
147
|
+
return {
|
|
148
|
+
passed: false,
|
|
149
|
+
message: `${blockingIssues.length} security issue(s) found`,
|
|
150
|
+
details: blockingIssues,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Non-blocking issues
|
|
155
|
+
return {
|
|
156
|
+
passed: true,
|
|
157
|
+
message: `${issues.length} low-severity issue(s) found`,
|
|
158
|
+
details: issues,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { run };
|