sequant 1.11.0 → 1.13.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 (66) hide show
  1. package/README.md +93 -7
  2. package/dist/bin/cli.js +12 -9
  3. package/dist/src/commands/doctor.js +25 -20
  4. package/dist/src/commands/init.js +152 -65
  5. package/dist/src/commands/logs.js +7 -6
  6. package/dist/src/commands/run.d.ts +13 -1
  7. package/dist/src/commands/run.js +75 -12
  8. package/dist/src/commands/stats.js +67 -48
  9. package/dist/src/commands/status.js +30 -12
  10. package/dist/src/index.d.ts +6 -0
  11. package/dist/src/index.js +4 -0
  12. package/dist/src/lib/ac-linter.d.ts +116 -0
  13. package/dist/src/lib/ac-linter.js +304 -0
  14. package/dist/src/lib/cli-ui.d.ts +196 -0
  15. package/dist/src/lib/cli-ui.js +544 -0
  16. package/dist/src/lib/content-analyzer.d.ts +89 -0
  17. package/dist/src/lib/content-analyzer.js +437 -0
  18. package/dist/src/lib/phase-signal.d.ts +94 -0
  19. package/dist/src/lib/phase-signal.js +171 -0
  20. package/dist/src/lib/plugin-version-sync.d.ts +26 -0
  21. package/dist/src/lib/plugin-version-sync.js +91 -0
  22. package/dist/src/lib/project-name.d.ts +40 -0
  23. package/dist/src/lib/project-name.js +191 -0
  24. package/dist/src/lib/semgrep.d.ts +136 -0
  25. package/dist/src/lib/semgrep.js +406 -0
  26. package/dist/src/lib/solve-comment-parser.d.ts +84 -0
  27. package/dist/src/lib/solve-comment-parser.js +200 -0
  28. package/dist/src/lib/stack-config.d.ts +51 -0
  29. package/dist/src/lib/stack-config.js +77 -0
  30. package/dist/src/lib/stacks.d.ts +66 -0
  31. package/dist/src/lib/stacks.js +332 -0
  32. package/dist/src/lib/templates.d.ts +2 -0
  33. package/dist/src/lib/templates.js +12 -3
  34. package/dist/src/lib/upstream/assessment.d.ts +70 -0
  35. package/dist/src/lib/upstream/assessment.js +385 -0
  36. package/dist/src/lib/upstream/index.d.ts +11 -0
  37. package/dist/src/lib/upstream/index.js +14 -0
  38. package/dist/src/lib/upstream/issues.d.ts +38 -0
  39. package/dist/src/lib/upstream/issues.js +267 -0
  40. package/dist/src/lib/upstream/relevance.d.ts +50 -0
  41. package/dist/src/lib/upstream/relevance.js +209 -0
  42. package/dist/src/lib/upstream/report.d.ts +29 -0
  43. package/dist/src/lib/upstream/report.js +391 -0
  44. package/dist/src/lib/upstream/types.d.ts +207 -0
  45. package/dist/src/lib/upstream/types.js +5 -0
  46. package/dist/src/lib/workflow/log-writer.d.ts +1 -1
  47. package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
  48. package/dist/src/lib/workflow/qa-cache.d.ts +199 -0
  49. package/dist/src/lib/workflow/qa-cache.js +440 -0
  50. package/dist/src/lib/workflow/run-log-schema.d.ts +34 -6
  51. package/dist/src/lib/workflow/run-log-schema.js +12 -1
  52. package/dist/src/lib/workflow/state-schema.d.ts +4 -4
  53. package/dist/src/lib/workflow/types.d.ts +4 -0
  54. package/package.json +6 -1
  55. package/templates/hooks/pre-tool.sh +6 -0
  56. package/templates/memory/constitution.md +1 -5
  57. package/templates/skills/_shared/references/prompt-templates.md +350 -0
  58. package/templates/skills/_shared/references/subagent-types.md +131 -0
  59. package/templates/skills/exec/SKILL.md +82 -0
  60. package/templates/skills/fullsolve/SKILL.md +19 -2
  61. package/templates/skills/loop/SKILL.md +3 -1
  62. package/templates/skills/qa/SKILL.md +79 -9
  63. package/templates/skills/qa/references/quality-gates.md +85 -1
  64. package/templates/skills/qa/references/semgrep-rules.md +207 -0
  65. package/templates/skills/qa/scripts/quality-checks.sh +525 -15
  66. package/templates/skills/spec/SKILL.md +322 -9
@@ -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
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Solve Comment Parser
3
+ *
4
+ * Detects and parses /solve command output from GitHub issue comments.
5
+ * When a solve comment exists, its phase recommendations take precedence
6
+ * over content analysis (but not over labels).
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { findSolveComment, parseSolveWorkflow } from './solve-comment-parser';
11
+ *
12
+ * const comments = [
13
+ * { body: "Some regular comment" },
14
+ * { body: "## Solve Workflow for Issues: 123\n..." },
15
+ * ];
16
+ *
17
+ * const solveComment = findSolveComment(comments);
18
+ * if (solveComment) {
19
+ * const phases = parseSolveWorkflow(solveComment.body);
20
+ * // phases: { phases: ['spec', 'exec', 'test', 'qa'], qualityLoop: false }
21
+ * }
22
+ * ```
23
+ */
24
+ import type { Phase } from "./workflow/types.js";
25
+ import type { PhaseSignal } from "./phase-signal.js";
26
+ /**
27
+ * Result of parsing a solve comment
28
+ */
29
+ export interface SolveWorkflowResult {
30
+ /** Phases recommended by solve */
31
+ phases: Phase[];
32
+ /** Whether quality loop is recommended */
33
+ qualityLoop: boolean;
34
+ /** The issue numbers mentioned in the solve comment */
35
+ issueNumbers: number[];
36
+ /** Raw workflow string (e.g., "spec → exec → test → qa") */
37
+ workflowString?: string;
38
+ }
39
+ /**
40
+ * Comment structure (simplified from GitHub API)
41
+ */
42
+ export interface IssueComment {
43
+ body: string;
44
+ author?: {
45
+ login: string;
46
+ };
47
+ createdAt?: string;
48
+ }
49
+ /**
50
+ * Check if a comment is a solve command output
51
+ *
52
+ * @param body - The comment body
53
+ * @returns True if this appears to be a solve comment
54
+ */
55
+ export declare function isSolveComment(body: string): boolean;
56
+ /**
57
+ * Find the most recent solve comment from a list of comments
58
+ *
59
+ * @param comments - Array of issue comments
60
+ * @returns The solve comment if found, null otherwise
61
+ */
62
+ export declare function findSolveComment(comments: IssueComment[]): IssueComment | null;
63
+ /**
64
+ * Parse a solve comment to extract workflow information
65
+ *
66
+ * @param body - The solve comment body
67
+ * @returns Parsed workflow result
68
+ */
69
+ export declare function parseSolveWorkflow(body: string): SolveWorkflowResult;
70
+ /**
71
+ * Convert solve workflow result to phase signals
72
+ *
73
+ * @param workflow - The parsed solve workflow
74
+ * @returns Array of phase signals with 'solve' source
75
+ */
76
+ export declare function solveWorkflowToSignals(workflow: SolveWorkflowResult): PhaseSignal[];
77
+ /**
78
+ * Check if solve comment covers the current issue
79
+ *
80
+ * @param workflow - The parsed solve workflow
81
+ * @param issueNumber - The current issue number
82
+ * @returns True if the solve comment includes this issue
83
+ */
84
+ export declare function solveCoversIssue(workflow: SolveWorkflowResult, issueNumber: number): boolean;
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Solve Comment Parser
3
+ *
4
+ * Detects and parses /solve command output from GitHub issue comments.
5
+ * When a solve comment exists, its phase recommendations take precedence
6
+ * over content analysis (but not over labels).
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * import { findSolveComment, parseSolveWorkflow } from './solve-comment-parser';
11
+ *
12
+ * const comments = [
13
+ * { body: "Some regular comment" },
14
+ * { body: "## Solve Workflow for Issues: 123\n..." },
15
+ * ];
16
+ *
17
+ * const solveComment = findSolveComment(comments);
18
+ * if (solveComment) {
19
+ * const phases = parseSolveWorkflow(solveComment.body);
20
+ * // phases: { phases: ['spec', 'exec', 'test', 'qa'], qualityLoop: false }
21
+ * }
22
+ * ```
23
+ */
24
+ /**
25
+ * Markers that indicate a solve comment
26
+ */
27
+ const SOLVE_MARKERS = [
28
+ "## Solve Workflow for Issues:",
29
+ "## Solve Workflow for Issue:",
30
+ "### Recommended Workflow",
31
+ "*📝 Generated by `/solve",
32
+ ];
33
+ /**
34
+ * Pattern to extract phases from solve workflow
35
+ * Matches: `/spec`, `/exec`, `/test`, `/qa`, etc.
36
+ * Also matches without slash: `spec`, `exec`, `test`, `qa` (for arrow notation)
37
+ */
38
+ const PHASE_PATTERN = /\/?(?<!\w)(spec|exec|test|qa|security-review|testgen|loop)(?!\w)/g;
39
+ /**
40
+ * Pattern to detect quality loop recommendation
41
+ */
42
+ const QUALITY_LOOP_PATTERNS = [
43
+ /quality\s*loop.*auto-enable/i,
44
+ /--quality-loop/i,
45
+ /quality\s*loop.*recommended/i,
46
+ /enable.*quality\s*loop/i,
47
+ ];
48
+ /**
49
+ * Pattern to extract issue numbers from solve header
50
+ * Matches: "## Solve Workflow for Issues: 123, 456"
51
+ */
52
+ const ISSUE_NUMBER_PATTERN = /#?(\d+)/g;
53
+ /**
54
+ * Check if a comment is a solve command output
55
+ *
56
+ * @param body - The comment body
57
+ * @returns True if this appears to be a solve comment
58
+ */
59
+ export function isSolveComment(body) {
60
+ return SOLVE_MARKERS.some((marker) => body.includes(marker));
61
+ }
62
+ /**
63
+ * Find the most recent solve comment from a list of comments
64
+ *
65
+ * @param comments - Array of issue comments
66
+ * @returns The solve comment if found, null otherwise
67
+ */
68
+ export function findSolveComment(comments) {
69
+ // Search from most recent to oldest (assuming comments are in chronological order)
70
+ for (let i = comments.length - 1; i >= 0; i--) {
71
+ if (isSolveComment(comments[i].body)) {
72
+ return comments[i];
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ /**
78
+ * Parse phases from a solve workflow string
79
+ *
80
+ * @param workflowString - String like "spec → exec → test → qa"
81
+ * @returns Array of phases
82
+ */
83
+ function parseWorkflowString(workflowString) {
84
+ const phases = [];
85
+ const matches = workflowString.matchAll(PHASE_PATTERN);
86
+ for (const match of matches) {
87
+ const phase = match[1];
88
+ if (!phases.includes(phase)) {
89
+ phases.push(phase);
90
+ }
91
+ }
92
+ return phases;
93
+ }
94
+ /**
95
+ * Parse a solve comment to extract workflow information
96
+ *
97
+ * @param body - The solve comment body
98
+ * @returns Parsed workflow result
99
+ */
100
+ export function parseSolveWorkflow(body) {
101
+ const result = {
102
+ phases: [],
103
+ qualityLoop: false,
104
+ issueNumbers: [],
105
+ };
106
+ // Extract issue numbers from header
107
+ const headerMatch = body.match(/## Solve Workflow for Issues?:\s*([^\n]+)/i);
108
+ if (headerMatch) {
109
+ const numberMatches = headerMatch[1].matchAll(ISSUE_NUMBER_PATTERN);
110
+ for (const match of numberMatches) {
111
+ const num = parseInt(match[1], 10);
112
+ if (!isNaN(num) && !result.issueNumbers.includes(num)) {
113
+ result.issueNumbers.push(num);
114
+ }
115
+ }
116
+ }
117
+ // Find workflow lines (e.g., "/spec 152" or "spec → exec → qa")
118
+ const lines = body.split("\n");
119
+ // First pass: look for arrow notation (most reliable)
120
+ for (const line of lines) {
121
+ if (line.includes("→") || line.includes("->")) {
122
+ const phases = parseWorkflowString(line);
123
+ if (phases.length > 0) {
124
+ result.phases = phases;
125
+ result.workflowString = line.trim();
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ // Second pass: if no arrow notation found, collect phases from multiple lines
131
+ if (result.phases.length === 0) {
132
+ const collectedPhases = [];
133
+ for (const line of lines) {
134
+ // Look for slash command patterns on individual lines
135
+ if (line.includes("/spec") ||
136
+ line.includes("/exec") ||
137
+ line.includes("/test") ||
138
+ line.includes("/qa")) {
139
+ const linePhases = parseWorkflowString(line);
140
+ for (const phase of linePhases) {
141
+ if (!collectedPhases.includes(phase)) {
142
+ collectedPhases.push(phase);
143
+ }
144
+ }
145
+ }
146
+ }
147
+ if (collectedPhases.length > 0) {
148
+ result.phases = collectedPhases;
149
+ result.workflowString = collectedPhases.map((p) => `/${p}`).join(" → ");
150
+ }
151
+ }
152
+ // If no phases found from lines, try full body
153
+ if (result.phases.length === 0) {
154
+ result.phases = parseWorkflowString(body);
155
+ }
156
+ // Check for quality loop recommendation
157
+ for (const pattern of QUALITY_LOOP_PATTERNS) {
158
+ if (pattern.test(body)) {
159
+ result.qualityLoop = true;
160
+ break;
161
+ }
162
+ }
163
+ return result;
164
+ }
165
+ /**
166
+ * Convert solve workflow result to phase signals
167
+ *
168
+ * @param workflow - The parsed solve workflow
169
+ * @returns Array of phase signals with 'solve' source
170
+ */
171
+ export function solveWorkflowToSignals(workflow) {
172
+ const signals = [];
173
+ for (const phase of workflow.phases) {
174
+ signals.push({
175
+ phase,
176
+ source: "solve",
177
+ confidence: "high",
178
+ reason: `Recommended by /solve command: ${workflow.workflowString || phase}`,
179
+ });
180
+ }
181
+ if (workflow.qualityLoop) {
182
+ signals.push({
183
+ phase: "quality-loop",
184
+ source: "solve",
185
+ confidence: "high",
186
+ reason: "Quality loop recommended by /solve command",
187
+ });
188
+ }
189
+ return signals;
190
+ }
191
+ /**
192
+ * Check if solve comment covers the current issue
193
+ *
194
+ * @param workflow - The parsed solve workflow
195
+ * @param issueNumber - The current issue number
196
+ * @returns True if the solve comment includes this issue
197
+ */
198
+ export function solveCoversIssue(workflow, issueNumber) {
199
+ return workflow.issueNumbers.includes(issueNumber);
200
+ }