sequant 1.10.1 → 1.12.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/README.md +92 -3
- package/dist/bin/cli.js +55 -2
- package/dist/dashboard/server.d.ts +37 -0
- package/dist/dashboard/server.js +968 -0
- package/dist/src/commands/dashboard.d.ts +25 -0
- package/dist/src/commands/dashboard.js +44 -0
- package/dist/src/commands/doctor.d.ts +18 -1
- package/dist/src/commands/doctor.js +105 -2
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +26 -2
- package/dist/src/commands/run.d.ts +20 -0
- package/dist/src/commands/run.js +151 -3
- package/dist/src/commands/state.d.ts +60 -0
- package/dist/src/commands/state.js +267 -0
- package/dist/src/commands/stats.d.ts +3 -2
- package/dist/src/commands/stats.js +246 -38
- package/dist/src/commands/status.d.ts +2 -0
- package/dist/src/commands/status.js +28 -3
- package/dist/src/lib/ac-linter.d.ts +116 -0
- package/dist/src/lib/ac-linter.js +304 -0
- package/dist/src/lib/ac-parser.d.ts +61 -0
- package/dist/src/lib/ac-parser.js +156 -0
- package/dist/src/lib/fs.d.ts +19 -0
- package/dist/src/lib/fs.js +58 -1
- package/dist/src/lib/plugin-version-sync.d.ts +26 -0
- package/dist/src/lib/plugin-version-sync.js +91 -0
- package/dist/src/lib/project-name.d.ts +40 -0
- package/dist/src/lib/project-name.js +191 -0
- package/dist/src/lib/semgrep.d.ts +136 -0
- package/dist/src/lib/semgrep.js +406 -0
- package/dist/src/lib/settings.d.ts +7 -0
- package/dist/src/lib/settings.js +1 -0
- package/dist/src/lib/stacks.d.ts +14 -0
- package/dist/src/lib/stacks.js +159 -0
- package/dist/src/lib/system.d.ts +19 -0
- package/dist/src/lib/system.js +26 -0
- package/dist/src/lib/templates.d.ts +34 -1
- package/dist/src/lib/templates.js +117 -7
- package/dist/src/lib/workflow/metrics-schema.d.ts +153 -0
- package/dist/src/lib/workflow/metrics-schema.js +138 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +102 -0
- package/dist/src/lib/workflow/metrics-writer.js +189 -0
- package/dist/src/lib/workflow/state-manager.d.ts +18 -1
- package/dist/src/lib/workflow/state-manager.js +61 -1
- package/dist/src/lib/workflow/state-schema.d.ts +152 -1
- package/dist/src/lib/workflow/state-schema.js +99 -0
- package/dist/src/lib/workflow/state-utils.d.ts +67 -3
- package/dist/src/lib/workflow/state-utils.js +289 -8
- package/dist/src/lib/workflow/types.d.ts +2 -0
- package/dist/src/lib/workflow/types.js +1 -0
- package/package.json +5 -1
- package/templates/hooks/pre-tool.sh +6 -0
- package/templates/memory/constitution.md +1 -5
- package/templates/skills/_shared/references/prompt-templates.md +350 -0
- package/templates/skills/_shared/references/subagent-types.md +131 -0
- package/templates/skills/exec/SKILL.md +82 -0
- package/templates/skills/fullsolve/SKILL.md +19 -2
- package/templates/skills/loop/SKILL.md +3 -1
- package/templates/skills/qa/SKILL.md +79 -9
- package/templates/skills/qa/references/quality-gates.md +85 -1
- package/templates/skills/qa/references/semgrep-rules.md +207 -0
- package/templates/skills/qa/scripts/quality-checks.sh +54 -0
- package/templates/skills/spec/SKILL.md +215 -4
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semgrep integration for static analysis
|
|
3
|
+
*
|
|
4
|
+
* Provides stack-aware ruleset mapping and execution utilities
|
|
5
|
+
* for integrating Semgrep into the /qa workflow.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Semgrep finding severity levels
|
|
9
|
+
*/
|
|
10
|
+
export type SemgrepSeverity = "error" | "warning" | "info";
|
|
11
|
+
/**
|
|
12
|
+
* A single Semgrep finding
|
|
13
|
+
*/
|
|
14
|
+
export interface SemgrepFinding {
|
|
15
|
+
path: string;
|
|
16
|
+
line: number;
|
|
17
|
+
column?: number;
|
|
18
|
+
endLine?: number;
|
|
19
|
+
endColumn?: number;
|
|
20
|
+
message: string;
|
|
21
|
+
ruleId: string;
|
|
22
|
+
severity: SemgrepSeverity;
|
|
23
|
+
category?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Result of a Semgrep scan
|
|
27
|
+
*/
|
|
28
|
+
export interface SemgrepResult {
|
|
29
|
+
success: boolean;
|
|
30
|
+
findings: SemgrepFinding[];
|
|
31
|
+
criticalCount: number;
|
|
32
|
+
warningCount: number;
|
|
33
|
+
infoCount: number;
|
|
34
|
+
error?: string;
|
|
35
|
+
skipped?: boolean;
|
|
36
|
+
skipReason?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Semgrep ruleset configuration
|
|
40
|
+
*/
|
|
41
|
+
export interface SemgrepRuleset {
|
|
42
|
+
name: string;
|
|
43
|
+
description: string;
|
|
44
|
+
rules: string[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Stack-to-ruleset mapping
|
|
48
|
+
*
|
|
49
|
+
* Maps detected stacks to appropriate Semgrep rulesets.
|
|
50
|
+
* Uses Semgrep's public rule registry identifiers.
|
|
51
|
+
*/
|
|
52
|
+
export declare const STACK_RULESETS: Record<string, SemgrepRuleset>;
|
|
53
|
+
/**
|
|
54
|
+
* Get the appropriate Semgrep ruleset for a detected stack
|
|
55
|
+
*
|
|
56
|
+
* @param stack - The detected stack name (e.g., "nextjs", "python")
|
|
57
|
+
* @returns The ruleset configuration for the stack
|
|
58
|
+
*/
|
|
59
|
+
export declare function getRulesForStack(stack: string | null): SemgrepRuleset;
|
|
60
|
+
/**
|
|
61
|
+
* Path to custom rules file
|
|
62
|
+
*/
|
|
63
|
+
export declare const CUSTOM_RULES_PATH = ".sequant/semgrep-rules.yaml";
|
|
64
|
+
/**
|
|
65
|
+
* Check if custom rules file exists
|
|
66
|
+
*
|
|
67
|
+
* @param projectRoot - Root directory of the project
|
|
68
|
+
* @returns true if custom rules file exists
|
|
69
|
+
*/
|
|
70
|
+
export declare function hasCustomRules(projectRoot?: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Get path to custom rules file if it exists
|
|
73
|
+
*
|
|
74
|
+
* @param projectRoot - Root directory of the project
|
|
75
|
+
* @returns Path to custom rules file, or null if it doesn't exist
|
|
76
|
+
*/
|
|
77
|
+
export declare function getCustomRulesPath(projectRoot?: string): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Check if Semgrep is available
|
|
80
|
+
*
|
|
81
|
+
* Checks for both 'semgrep' command and 'npx semgrep' fallback
|
|
82
|
+
*
|
|
83
|
+
* @returns Object with availability info and command to use
|
|
84
|
+
*/
|
|
85
|
+
export declare function checkSemgrepAvailability(): Promise<{
|
|
86
|
+
available: boolean;
|
|
87
|
+
command: string;
|
|
88
|
+
useNpx: boolean;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Parse Semgrep JSON output into findings
|
|
92
|
+
*
|
|
93
|
+
* @param output - Raw JSON output from Semgrep
|
|
94
|
+
* @returns Array of parsed findings
|
|
95
|
+
*/
|
|
96
|
+
export declare function parseSemgrepOutput(output: string): SemgrepFinding[];
|
|
97
|
+
/**
|
|
98
|
+
* Count findings by severity
|
|
99
|
+
*
|
|
100
|
+
* @param findings - Array of Semgrep findings
|
|
101
|
+
* @returns Object with counts by severity
|
|
102
|
+
*/
|
|
103
|
+
export declare function countFindingsBySeverity(findings: SemgrepFinding[]): {
|
|
104
|
+
critical: number;
|
|
105
|
+
warning: number;
|
|
106
|
+
info: number;
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* Run Semgrep scan on specified files or directories
|
|
110
|
+
*
|
|
111
|
+
* @param options - Scan options
|
|
112
|
+
* @returns Scan result with findings
|
|
113
|
+
*/
|
|
114
|
+
export declare function runSemgrepScan(options: {
|
|
115
|
+
targets: string[];
|
|
116
|
+
stack?: string | null;
|
|
117
|
+
projectRoot?: string;
|
|
118
|
+
useCustomRules?: boolean;
|
|
119
|
+
}): Promise<SemgrepResult>;
|
|
120
|
+
/**
|
|
121
|
+
* Format findings for display in QA output
|
|
122
|
+
*
|
|
123
|
+
* @param findings - Array of findings to format
|
|
124
|
+
* @returns Formatted markdown string
|
|
125
|
+
*/
|
|
126
|
+
export declare function formatFindingsForDisplay(findings: SemgrepFinding[]): string;
|
|
127
|
+
/**
|
|
128
|
+
* Generate QA verdict contribution from Semgrep results
|
|
129
|
+
*
|
|
130
|
+
* @param result - Semgrep scan result
|
|
131
|
+
* @returns Verdict contribution (blocking if critical findings)
|
|
132
|
+
*/
|
|
133
|
+
export declare function getSemgrepVerdictContribution(result: SemgrepResult): {
|
|
134
|
+
blocking: boolean;
|
|
135
|
+
reason: string;
|
|
136
|
+
};
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semgrep integration for static analysis
|
|
3
|
+
*
|
|
4
|
+
* Provides stack-aware ruleset mapping and execution utilities
|
|
5
|
+
* for integrating Semgrep into the /qa workflow.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "child_process";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
/**
|
|
11
|
+
* Stack-to-ruleset mapping
|
|
12
|
+
*
|
|
13
|
+
* Maps detected stacks to appropriate Semgrep rulesets.
|
|
14
|
+
* Uses Semgrep's public rule registry identifiers.
|
|
15
|
+
*/
|
|
16
|
+
export const STACK_RULESETS = {
|
|
17
|
+
nextjs: {
|
|
18
|
+
name: "Next.js",
|
|
19
|
+
description: "TypeScript/JavaScript security and best practices for Next.js",
|
|
20
|
+
rules: [
|
|
21
|
+
"p/typescript",
|
|
22
|
+
"p/javascript",
|
|
23
|
+
"p/react",
|
|
24
|
+
"p/security-audit",
|
|
25
|
+
"p/secrets",
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
astro: {
|
|
29
|
+
name: "Astro",
|
|
30
|
+
description: "TypeScript/JavaScript security for Astro projects",
|
|
31
|
+
rules: ["p/typescript", "p/javascript", "p/security-audit", "p/secrets"],
|
|
32
|
+
},
|
|
33
|
+
sveltekit: {
|
|
34
|
+
name: "SvelteKit",
|
|
35
|
+
description: "TypeScript/JavaScript security for SvelteKit",
|
|
36
|
+
rules: ["p/typescript", "p/javascript", "p/security-audit", "p/secrets"],
|
|
37
|
+
},
|
|
38
|
+
remix: {
|
|
39
|
+
name: "Remix",
|
|
40
|
+
description: "TypeScript/JavaScript security for Remix",
|
|
41
|
+
rules: [
|
|
42
|
+
"p/typescript",
|
|
43
|
+
"p/javascript",
|
|
44
|
+
"p/react",
|
|
45
|
+
"p/security-audit",
|
|
46
|
+
"p/secrets",
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
nuxt: {
|
|
50
|
+
name: "Nuxt",
|
|
51
|
+
description: "TypeScript/JavaScript security for Nuxt/Vue",
|
|
52
|
+
rules: ["p/typescript", "p/javascript", "p/security-audit", "p/secrets"],
|
|
53
|
+
},
|
|
54
|
+
rust: {
|
|
55
|
+
name: "Rust",
|
|
56
|
+
description: "Rust security and best practices",
|
|
57
|
+
rules: ["p/rust", "p/security-audit", "p/secrets"],
|
|
58
|
+
},
|
|
59
|
+
python: {
|
|
60
|
+
name: "Python",
|
|
61
|
+
description: "Python security and best practices",
|
|
62
|
+
rules: ["p/python", "p/django", "p/flask", "p/security-audit", "p/secrets"],
|
|
63
|
+
},
|
|
64
|
+
go: {
|
|
65
|
+
name: "Go",
|
|
66
|
+
description: "Go security and best practices",
|
|
67
|
+
rules: ["p/golang", "p/security-audit", "p/secrets"],
|
|
68
|
+
},
|
|
69
|
+
generic: {
|
|
70
|
+
name: "Generic",
|
|
71
|
+
description: "General security rules for any codebase",
|
|
72
|
+
rules: ["p/security-audit", "p/secrets"],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Get the appropriate Semgrep ruleset for a detected stack
|
|
77
|
+
*
|
|
78
|
+
* @param stack - The detected stack name (e.g., "nextjs", "python")
|
|
79
|
+
* @returns The ruleset configuration for the stack
|
|
80
|
+
*/
|
|
81
|
+
export function getRulesForStack(stack) {
|
|
82
|
+
if (!stack) {
|
|
83
|
+
return STACK_RULESETS.generic;
|
|
84
|
+
}
|
|
85
|
+
return STACK_RULESETS[stack] || STACK_RULESETS.generic;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Path to custom rules file
|
|
89
|
+
*/
|
|
90
|
+
export const CUSTOM_RULES_PATH = ".sequant/semgrep-rules.yaml";
|
|
91
|
+
/**
|
|
92
|
+
* Check if custom rules file exists
|
|
93
|
+
*
|
|
94
|
+
* @param projectRoot - Root directory of the project
|
|
95
|
+
* @returns true if custom rules file exists
|
|
96
|
+
*/
|
|
97
|
+
export function hasCustomRules(projectRoot = process.cwd()) {
|
|
98
|
+
return existsSync(join(projectRoot, CUSTOM_RULES_PATH));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get path to custom rules file if it exists
|
|
102
|
+
*
|
|
103
|
+
* @param projectRoot - Root directory of the project
|
|
104
|
+
* @returns Path to custom rules file, or null if it doesn't exist
|
|
105
|
+
*/
|
|
106
|
+
export function getCustomRulesPath(projectRoot = process.cwd()) {
|
|
107
|
+
const customPath = join(projectRoot, CUSTOM_RULES_PATH);
|
|
108
|
+
return existsSync(customPath) ? customPath : null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if Semgrep is available
|
|
112
|
+
*
|
|
113
|
+
* Checks for both 'semgrep' command and 'npx semgrep' fallback
|
|
114
|
+
*
|
|
115
|
+
* @returns Object with availability info and command to use
|
|
116
|
+
*/
|
|
117
|
+
export async function checkSemgrepAvailability() {
|
|
118
|
+
// First try native semgrep
|
|
119
|
+
try {
|
|
120
|
+
await executeCommand("semgrep", ["--version"]);
|
|
121
|
+
return { available: true, command: "semgrep", useNpx: false };
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Native semgrep not available, try npx
|
|
125
|
+
}
|
|
126
|
+
// Try npx semgrep
|
|
127
|
+
try {
|
|
128
|
+
await executeCommand("npx", ["semgrep", "--version"]);
|
|
129
|
+
return { available: true, command: "npx", useNpx: true };
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// npx semgrep also not available
|
|
133
|
+
}
|
|
134
|
+
return { available: false, command: "", useNpx: false };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Execute a command and return stdout
|
|
138
|
+
*
|
|
139
|
+
* @param command - Command to execute
|
|
140
|
+
* @param args - Command arguments
|
|
141
|
+
* @returns stdout from the command
|
|
142
|
+
*/
|
|
143
|
+
function executeCommand(command, args) {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const proc = spawn(command, args, {
|
|
146
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
147
|
+
shell: process.platform === "win32",
|
|
148
|
+
});
|
|
149
|
+
let stdout = "";
|
|
150
|
+
let stderr = "";
|
|
151
|
+
proc.stdout?.on("data", (data) => {
|
|
152
|
+
stdout += data.toString();
|
|
153
|
+
});
|
|
154
|
+
proc.stderr?.on("data", (data) => {
|
|
155
|
+
stderr += data.toString();
|
|
156
|
+
});
|
|
157
|
+
proc.on("close", (code) => {
|
|
158
|
+
if (code === 0) {
|
|
159
|
+
resolve(stdout);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
reject(new Error(stderr || `Command exited with code ${code}`));
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
proc.on("error", (err) => {
|
|
166
|
+
reject(err);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Parse Semgrep JSON output into findings
|
|
172
|
+
*
|
|
173
|
+
* @param output - Raw JSON output from Semgrep
|
|
174
|
+
* @returns Array of parsed findings
|
|
175
|
+
*/
|
|
176
|
+
export function parseSemgrepOutput(output) {
|
|
177
|
+
try {
|
|
178
|
+
const data = JSON.parse(output);
|
|
179
|
+
const results = data.results || [];
|
|
180
|
+
return results.map((result) => ({
|
|
181
|
+
path: result.path,
|
|
182
|
+
line: result.start.line,
|
|
183
|
+
column: result.start.col,
|
|
184
|
+
endLine: result.end?.line,
|
|
185
|
+
endColumn: result.end?.col,
|
|
186
|
+
message: result.extra.message,
|
|
187
|
+
ruleId: result.check_id,
|
|
188
|
+
severity: mapSeverity(result.extra.severity || "warning"),
|
|
189
|
+
category: result.extra.metadata?.category,
|
|
190
|
+
}));
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Map Semgrep severity to our internal severity type
|
|
198
|
+
*/
|
|
199
|
+
function mapSeverity(severity) {
|
|
200
|
+
const lower = severity.toLowerCase();
|
|
201
|
+
if (lower === "error" || lower === "critical" || lower === "high") {
|
|
202
|
+
return "error";
|
|
203
|
+
}
|
|
204
|
+
if (lower === "warning" || lower === "medium") {
|
|
205
|
+
return "warning";
|
|
206
|
+
}
|
|
207
|
+
return "info";
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Count findings by severity
|
|
211
|
+
*
|
|
212
|
+
* @param findings - Array of Semgrep findings
|
|
213
|
+
* @returns Object with counts by severity
|
|
214
|
+
*/
|
|
215
|
+
export function countFindingsBySeverity(findings) {
|
|
216
|
+
return findings.reduce((acc, finding) => {
|
|
217
|
+
if (finding.severity === "error") {
|
|
218
|
+
acc.critical++;
|
|
219
|
+
}
|
|
220
|
+
else if (finding.severity === "warning") {
|
|
221
|
+
acc.warning++;
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
acc.info++;
|
|
225
|
+
}
|
|
226
|
+
return acc;
|
|
227
|
+
}, { critical: 0, warning: 0, info: 0 });
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Run Semgrep scan on specified files or directories
|
|
231
|
+
*
|
|
232
|
+
* @param options - Scan options
|
|
233
|
+
* @returns Scan result with findings
|
|
234
|
+
*/
|
|
235
|
+
export async function runSemgrepScan(options) {
|
|
236
|
+
const { targets, stack = null, projectRoot = process.cwd() } = options;
|
|
237
|
+
const useCustomRules = options.useCustomRules ?? true;
|
|
238
|
+
// Check availability
|
|
239
|
+
const availability = await checkSemgrepAvailability();
|
|
240
|
+
if (!availability.available) {
|
|
241
|
+
return {
|
|
242
|
+
success: true,
|
|
243
|
+
findings: [],
|
|
244
|
+
criticalCount: 0,
|
|
245
|
+
warningCount: 0,
|
|
246
|
+
infoCount: 0,
|
|
247
|
+
skipped: true,
|
|
248
|
+
skipReason: "Semgrep not installed (install with: pip install semgrep)",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// Build command arguments
|
|
252
|
+
const args = [];
|
|
253
|
+
// If using npx, prepend 'semgrep' to args
|
|
254
|
+
if (availability.useNpx) {
|
|
255
|
+
args.push("semgrep");
|
|
256
|
+
}
|
|
257
|
+
// Add output format
|
|
258
|
+
args.push("--json");
|
|
259
|
+
// Add rules from stack
|
|
260
|
+
const ruleset = getRulesForStack(stack);
|
|
261
|
+
for (const rule of ruleset.rules) {
|
|
262
|
+
args.push("--config", rule);
|
|
263
|
+
}
|
|
264
|
+
// Add custom rules if available
|
|
265
|
+
if (useCustomRules) {
|
|
266
|
+
const customRulesPath = getCustomRulesPath(projectRoot);
|
|
267
|
+
if (customRulesPath) {
|
|
268
|
+
args.push("--config", customRulesPath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Add targets
|
|
272
|
+
args.push(...targets);
|
|
273
|
+
try {
|
|
274
|
+
const output = await executeCommand(availability.command, args);
|
|
275
|
+
const findings = parseSemgrepOutput(output);
|
|
276
|
+
const counts = countFindingsBySeverity(findings);
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
findings,
|
|
280
|
+
criticalCount: counts.critical,
|
|
281
|
+
warningCount: counts.warning,
|
|
282
|
+
infoCount: counts.info,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
// Semgrep returns non-zero exit code when findings are present
|
|
287
|
+
// Try to parse the output anyway
|
|
288
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
289
|
+
// Check if this is just findings being reported (exit code 1 with JSON output)
|
|
290
|
+
try {
|
|
291
|
+
const findings = parseSemgrepOutput(errorMessage);
|
|
292
|
+
if (findings.length > 0) {
|
|
293
|
+
const counts = countFindingsBySeverity(findings);
|
|
294
|
+
return {
|
|
295
|
+
success: true,
|
|
296
|
+
findings,
|
|
297
|
+
criticalCount: counts.critical,
|
|
298
|
+
warningCount: counts.warning,
|
|
299
|
+
infoCount: counts.info,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// Not valid JSON output, real error
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
success: false,
|
|
308
|
+
findings: [],
|
|
309
|
+
criticalCount: 0,
|
|
310
|
+
warningCount: 0,
|
|
311
|
+
infoCount: 0,
|
|
312
|
+
error: errorMessage,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Format findings for display in QA output
|
|
318
|
+
*
|
|
319
|
+
* @param findings - Array of findings to format
|
|
320
|
+
* @returns Formatted markdown string
|
|
321
|
+
*/
|
|
322
|
+
export function formatFindingsForDisplay(findings) {
|
|
323
|
+
if (findings.length === 0) {
|
|
324
|
+
return "✅ No findings";
|
|
325
|
+
}
|
|
326
|
+
const lines = [];
|
|
327
|
+
const grouped = groupFindingsBySeverity(findings);
|
|
328
|
+
if (grouped.critical.length > 0) {
|
|
329
|
+
lines.push("### ❌ Critical Issues");
|
|
330
|
+
lines.push("");
|
|
331
|
+
for (const finding of grouped.critical) {
|
|
332
|
+
lines.push(formatSingleFinding(finding));
|
|
333
|
+
}
|
|
334
|
+
lines.push("");
|
|
335
|
+
}
|
|
336
|
+
if (grouped.warning.length > 0) {
|
|
337
|
+
lines.push("### ⚠️ Warnings");
|
|
338
|
+
lines.push("");
|
|
339
|
+
for (const finding of grouped.warning) {
|
|
340
|
+
lines.push(formatSingleFinding(finding));
|
|
341
|
+
}
|
|
342
|
+
lines.push("");
|
|
343
|
+
}
|
|
344
|
+
if (grouped.info.length > 0) {
|
|
345
|
+
lines.push("### ℹ️ Info");
|
|
346
|
+
lines.push("");
|
|
347
|
+
for (const finding of grouped.info) {
|
|
348
|
+
lines.push(formatSingleFinding(finding));
|
|
349
|
+
}
|
|
350
|
+
lines.push("");
|
|
351
|
+
}
|
|
352
|
+
return lines.join("\n");
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Group findings by severity
|
|
356
|
+
*/
|
|
357
|
+
function groupFindingsBySeverity(findings) {
|
|
358
|
+
return {
|
|
359
|
+
critical: findings.filter((f) => f.severity === "error"),
|
|
360
|
+
warning: findings.filter((f) => f.severity === "warning"),
|
|
361
|
+
info: findings.filter((f) => f.severity === "info"),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Format a single finding for display
|
|
366
|
+
*/
|
|
367
|
+
function formatSingleFinding(finding) {
|
|
368
|
+
const location = `${finding.path}:${finding.line}`;
|
|
369
|
+
return `- \`${location}\` - ${finding.message} (${finding.ruleId})`;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Generate QA verdict contribution from Semgrep results
|
|
373
|
+
*
|
|
374
|
+
* @param result - Semgrep scan result
|
|
375
|
+
* @returns Verdict contribution (blocking if critical findings)
|
|
376
|
+
*/
|
|
377
|
+
export function getSemgrepVerdictContribution(result) {
|
|
378
|
+
if (result.skipped) {
|
|
379
|
+
return {
|
|
380
|
+
blocking: false,
|
|
381
|
+
reason: `Semgrep skipped: ${result.skipReason}`,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (!result.success) {
|
|
385
|
+
return {
|
|
386
|
+
blocking: false,
|
|
387
|
+
reason: `Semgrep error: ${result.error}`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
if (result.criticalCount > 0) {
|
|
391
|
+
return {
|
|
392
|
+
blocking: true,
|
|
393
|
+
reason: `${result.criticalCount} critical security finding(s) detected`,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (result.warningCount > 0) {
|
|
397
|
+
return {
|
|
398
|
+
blocking: false,
|
|
399
|
+
reason: `${result.warningCount} warning(s) detected (review recommended)`,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
blocking: false,
|
|
404
|
+
reason: "No security issues detected",
|
|
405
|
+
};
|
|
406
|
+
}
|
|
@@ -73,6 +73,13 @@ export interface RunSettings {
|
|
|
73
73
|
* Example: "feature/dashboard" for feature integration branches
|
|
74
74
|
*/
|
|
75
75
|
defaultBase?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Enable MCP servers in headless mode.
|
|
78
|
+
* When true, reads MCP config from Claude Desktop and passes to SDK.
|
|
79
|
+
* When false or --no-mcp flag is used, MCPs are disabled.
|
|
80
|
+
* Default: true
|
|
81
|
+
*/
|
|
82
|
+
mcp: boolean;
|
|
76
83
|
}
|
|
77
84
|
/**
|
|
78
85
|
* Full settings schema
|
package/dist/src/lib/settings.js
CHANGED
package/dist/src/lib/stacks.d.ts
CHANGED
|
@@ -47,3 +47,17 @@ export interface StackConfig {
|
|
|
47
47
|
export declare const STACKS: Record<string, StackConfig>;
|
|
48
48
|
export declare function detectStack(): Promise<string | null>;
|
|
49
49
|
export declare function getStackConfig(stack: string): StackConfig;
|
|
50
|
+
/**
|
|
51
|
+
* Stack-specific notes for constitution templates
|
|
52
|
+
*
|
|
53
|
+
* These notes are injected into the constitution during setup to provide
|
|
54
|
+
* context-aware guidance for AI-assisted development.
|
|
55
|
+
*/
|
|
56
|
+
export declare const STACK_NOTES: Record<string, string>;
|
|
57
|
+
/**
|
|
58
|
+
* Get stack-specific notes for constitution template
|
|
59
|
+
*
|
|
60
|
+
* @param stack - The stack name (e.g., "nextjs", "python", "rust")
|
|
61
|
+
* @returns The stack-specific notes markdown content
|
|
62
|
+
*/
|
|
63
|
+
export declare function getStackNotes(stack: string): string;
|