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.
Files changed (63) hide show
  1. package/README.md +92 -3
  2. package/dist/bin/cli.js +55 -2
  3. package/dist/dashboard/server.d.ts +37 -0
  4. package/dist/dashboard/server.js +968 -0
  5. package/dist/src/commands/dashboard.d.ts +25 -0
  6. package/dist/src/commands/dashboard.js +44 -0
  7. package/dist/src/commands/doctor.d.ts +18 -1
  8. package/dist/src/commands/doctor.js +105 -2
  9. package/dist/src/commands/init.d.ts +1 -0
  10. package/dist/src/commands/init.js +26 -2
  11. package/dist/src/commands/run.d.ts +20 -0
  12. package/dist/src/commands/run.js +151 -3
  13. package/dist/src/commands/state.d.ts +60 -0
  14. package/dist/src/commands/state.js +267 -0
  15. package/dist/src/commands/stats.d.ts +3 -2
  16. package/dist/src/commands/stats.js +246 -38
  17. package/dist/src/commands/status.d.ts +2 -0
  18. package/dist/src/commands/status.js +28 -3
  19. package/dist/src/lib/ac-linter.d.ts +116 -0
  20. package/dist/src/lib/ac-linter.js +304 -0
  21. package/dist/src/lib/ac-parser.d.ts +61 -0
  22. package/dist/src/lib/ac-parser.js +156 -0
  23. package/dist/src/lib/fs.d.ts +19 -0
  24. package/dist/src/lib/fs.js +58 -1
  25. package/dist/src/lib/plugin-version-sync.d.ts +26 -0
  26. package/dist/src/lib/plugin-version-sync.js +91 -0
  27. package/dist/src/lib/project-name.d.ts +40 -0
  28. package/dist/src/lib/project-name.js +191 -0
  29. package/dist/src/lib/semgrep.d.ts +136 -0
  30. package/dist/src/lib/semgrep.js +406 -0
  31. package/dist/src/lib/settings.d.ts +7 -0
  32. package/dist/src/lib/settings.js +1 -0
  33. package/dist/src/lib/stacks.d.ts +14 -0
  34. package/dist/src/lib/stacks.js +159 -0
  35. package/dist/src/lib/system.d.ts +19 -0
  36. package/dist/src/lib/system.js +26 -0
  37. package/dist/src/lib/templates.d.ts +34 -1
  38. package/dist/src/lib/templates.js +117 -7
  39. package/dist/src/lib/workflow/metrics-schema.d.ts +153 -0
  40. package/dist/src/lib/workflow/metrics-schema.js +138 -0
  41. package/dist/src/lib/workflow/metrics-writer.d.ts +102 -0
  42. package/dist/src/lib/workflow/metrics-writer.js +189 -0
  43. package/dist/src/lib/workflow/state-manager.d.ts +18 -1
  44. package/dist/src/lib/workflow/state-manager.js +61 -1
  45. package/dist/src/lib/workflow/state-schema.d.ts +152 -1
  46. package/dist/src/lib/workflow/state-schema.js +99 -0
  47. package/dist/src/lib/workflow/state-utils.d.ts +67 -3
  48. package/dist/src/lib/workflow/state-utils.js +289 -8
  49. package/dist/src/lib/workflow/types.d.ts +2 -0
  50. package/dist/src/lib/workflow/types.js +1 -0
  51. package/package.json +5 -1
  52. package/templates/hooks/pre-tool.sh +6 -0
  53. package/templates/memory/constitution.md +1 -5
  54. package/templates/skills/_shared/references/prompt-templates.md +350 -0
  55. package/templates/skills/_shared/references/subagent-types.md +131 -0
  56. package/templates/skills/exec/SKILL.md +82 -0
  57. package/templates/skills/fullsolve/SKILL.md +19 -2
  58. package/templates/skills/loop/SKILL.md +3 -1
  59. package/templates/skills/qa/SKILL.md +79 -9
  60. package/templates/skills/qa/references/quality-gates.md +85 -1
  61. package/templates/skills/qa/references/semgrep-rules.md +207 -0
  62. package/templates/skills/qa/scripts/quality-checks.sh +54 -0
  63. 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
@@ -46,6 +46,7 @@ export const DEFAULT_SETTINGS = {
46
46
  maxIterations: 3,
47
47
  smartTests: true,
48
48
  rotation: DEFAULT_ROTATION_SETTINGS,
49
+ mcp: true, // Enable MCP servers by default in headless mode
49
50
  },
50
51
  agents: DEFAULT_AGENT_SETTINGS,
51
52
  };
@@ -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;