sequant 1.12.0 → 1.13.1

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 (53) hide show
  1. package/README.md +10 -8
  2. package/dist/bin/cli.js +19 -9
  3. package/dist/src/commands/doctor.js +42 -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 +122 -32
  8. package/dist/src/commands/stats.js +67 -48
  9. package/dist/src/commands/status.js +30 -12
  10. package/dist/src/commands/sync.d.ts +28 -0
  11. package/dist/src/commands/sync.js +102 -0
  12. package/dist/src/index.d.ts +6 -0
  13. package/dist/src/index.js +4 -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/phase-spinner.d.ts +146 -0
  21. package/dist/src/lib/phase-spinner.js +255 -0
  22. package/dist/src/lib/solve-comment-parser.d.ts +84 -0
  23. package/dist/src/lib/solve-comment-parser.js +200 -0
  24. package/dist/src/lib/stack-config.d.ts +51 -0
  25. package/dist/src/lib/stack-config.js +77 -0
  26. package/dist/src/lib/stacks.d.ts +52 -0
  27. package/dist/src/lib/stacks.js +173 -0
  28. package/dist/src/lib/templates.d.ts +2 -0
  29. package/dist/src/lib/templates.js +9 -2
  30. package/dist/src/lib/upstream/assessment.d.ts +70 -0
  31. package/dist/src/lib/upstream/assessment.js +385 -0
  32. package/dist/src/lib/upstream/index.d.ts +11 -0
  33. package/dist/src/lib/upstream/index.js +14 -0
  34. package/dist/src/lib/upstream/issues.d.ts +38 -0
  35. package/dist/src/lib/upstream/issues.js +267 -0
  36. package/dist/src/lib/upstream/relevance.d.ts +50 -0
  37. package/dist/src/lib/upstream/relevance.js +209 -0
  38. package/dist/src/lib/upstream/report.d.ts +29 -0
  39. package/dist/src/lib/upstream/report.js +391 -0
  40. package/dist/src/lib/upstream/types.d.ts +207 -0
  41. package/dist/src/lib/upstream/types.js +5 -0
  42. package/dist/src/lib/workflow/log-writer.d.ts +1 -1
  43. package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
  44. package/dist/src/lib/workflow/qa-cache.d.ts +199 -0
  45. package/dist/src/lib/workflow/qa-cache.js +440 -0
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +34 -6
  47. package/dist/src/lib/workflow/run-log-schema.js +12 -1
  48. package/dist/src/lib/workflow/state-schema.d.ts +4 -4
  49. package/dist/src/lib/workflow/types.d.ts +4 -0
  50. package/package.json +6 -1
  51. package/templates/skills/qa/scripts/quality-checks.sh +509 -53
  52. package/templates/skills/solve/SKILL.md +375 -83
  53. package/templates/skills/spec/SKILL.md +107 -5
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Phase Spinner - Animated spinner with elapsed time for phase execution
3
+ *
4
+ * Wraps the cli-ui spinner with:
5
+ * - Elapsed time tracking (updates every 5 seconds)
6
+ * - Phase progress indicator (e.g., "spec (1/3)")
7
+ * - Integration with ShutdownManager for graceful cleanup
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const spinner = new PhaseSpinner({
12
+ * phase: 'exec',
13
+ * phaseIndex: 2,
14
+ * totalPhases: 3,
15
+ * shutdownManager,
16
+ * });
17
+ *
18
+ * spinner.start();
19
+ * // ... phase execution
20
+ * spinner.succeed(); // Shows "✓ exec (2/3) (45s)"
21
+ * ```
22
+ */
23
+ import type { ShutdownManager } from "./shutdown.js";
24
+ /**
25
+ * Options for creating a PhaseSpinner
26
+ */
27
+ export interface PhaseSpinnerOptions {
28
+ /** Phase name (e.g., "spec", "exec", "qa") */
29
+ phase: string;
30
+ /** Current phase index (1-based) */
31
+ phaseIndex: number;
32
+ /** Total number of phases */
33
+ totalPhases: number;
34
+ /** Optional ShutdownManager for graceful cleanup */
35
+ shutdownManager?: ShutdownManager;
36
+ /** Optional prefix for indentation (default: " ") */
37
+ prefix?: string;
38
+ /** Optional quality loop iteration (shows "iteration N" if > 1) */
39
+ iteration?: number;
40
+ }
41
+ /**
42
+ * Format elapsed time in human-readable format
43
+ *
44
+ * @param seconds - Elapsed time in seconds
45
+ * @returns Formatted string (e.g., "45s", "2m 15s", "1h 5m")
46
+ */
47
+ export declare function formatElapsedTime(seconds: number): string;
48
+ /**
49
+ * PhaseSpinner - Animated spinner with elapsed time and phase progress
50
+ *
51
+ * Features:
52
+ * - Animated spinner (or text fallback) via cli-ui
53
+ * - Elapsed time tracking with automatic updates
54
+ * - Phase progress indicator (e.g., "exec (2/3)")
55
+ * - ShutdownManager integration for graceful Ctrl+C cleanup
56
+ * - Pause/resume for verbose streaming output
57
+ */
58
+ export declare class PhaseSpinner {
59
+ private spinner;
60
+ private startTime;
61
+ private intervalId;
62
+ private disposed;
63
+ private paused;
64
+ private readonly phase;
65
+ private readonly phaseIndex;
66
+ private readonly totalPhases;
67
+ private readonly shutdownManager?;
68
+ private readonly prefix;
69
+ private readonly iteration?;
70
+ private readonly cleanupName;
71
+ constructor(options: PhaseSpinnerOptions);
72
+ /**
73
+ * Format the spinner text with phase, progress, and elapsed time
74
+ */
75
+ private formatText;
76
+ /**
77
+ * Update the spinner text with current elapsed time
78
+ */
79
+ private updateElapsedTime;
80
+ /**
81
+ * Start the spinner
82
+ *
83
+ * Begins the animation and elapsed time tracking.
84
+ */
85
+ start(): PhaseSpinner;
86
+ /**
87
+ * Mark the phase as succeeded
88
+ *
89
+ * Stops the spinner and shows a checkmark with total duration.
90
+ */
91
+ succeed(customText?: string): PhaseSpinner;
92
+ /**
93
+ * Mark the phase as failed
94
+ *
95
+ * Stops the spinner and shows an error indicator with message.
96
+ */
97
+ fail(error?: string): PhaseSpinner;
98
+ /**
99
+ * Stop the spinner without success/fail indication
100
+ *
101
+ * Use this for cleanup or when the phase is interrupted.
102
+ */
103
+ stop(): PhaseSpinner;
104
+ /**
105
+ * Pause the spinner (for verbose streaming output)
106
+ *
107
+ * Temporarily stops the animation without disposing.
108
+ * Call resume() to continue.
109
+ */
110
+ pause(): PhaseSpinner;
111
+ /**
112
+ * Resume the spinner after pause
113
+ *
114
+ * Restarts the animation with updated elapsed time.
115
+ */
116
+ resume(): PhaseSpinner;
117
+ /**
118
+ * Check if the spinner is currently active
119
+ */
120
+ get isSpinning(): boolean;
121
+ /**
122
+ * Get the total elapsed time in seconds
123
+ */
124
+ get elapsedSeconds(): number;
125
+ /**
126
+ * Dispose of the spinner and clean up resources
127
+ *
128
+ * Called automatically by succeed(), fail(), or stop().
129
+ * Safe to call multiple times.
130
+ */
131
+ dispose(): void;
132
+ /**
133
+ * Clear the elapsed time update interval
134
+ */
135
+ private clearInterval;
136
+ /**
137
+ * Unregister from ShutdownManager
138
+ */
139
+ private unregisterCleanup;
140
+ }
141
+ /**
142
+ * Create a PhaseSpinner with the given options
143
+ *
144
+ * Convenience function matching the pattern of cli-ui.spinner().
145
+ */
146
+ export declare function phaseSpinner(options: PhaseSpinnerOptions): PhaseSpinner;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Phase Spinner - Animated spinner with elapsed time for phase execution
3
+ *
4
+ * Wraps the cli-ui spinner with:
5
+ * - Elapsed time tracking (updates every 5 seconds)
6
+ * - Phase progress indicator (e.g., "spec (1/3)")
7
+ * - Integration with ShutdownManager for graceful cleanup
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * const spinner = new PhaseSpinner({
12
+ * phase: 'exec',
13
+ * phaseIndex: 2,
14
+ * totalPhases: 3,
15
+ * shutdownManager,
16
+ * });
17
+ *
18
+ * spinner.start();
19
+ * // ... phase execution
20
+ * spinner.succeed(); // Shows "✓ exec (2/3) (45s)"
21
+ * ```
22
+ */
23
+ import { spinner as createSpinner } from "./cli-ui.js";
24
+ /**
25
+ * Elapsed time update interval in milliseconds
26
+ */
27
+ const ELAPSED_UPDATE_INTERVAL_MS = 5000;
28
+ /**
29
+ * Format elapsed time in human-readable format
30
+ *
31
+ * @param seconds - Elapsed time in seconds
32
+ * @returns Formatted string (e.g., "45s", "2m 15s", "1h 5m")
33
+ */
34
+ export function formatElapsedTime(seconds) {
35
+ if (seconds < 60) {
36
+ return `${Math.floor(seconds)}s`;
37
+ }
38
+ if (seconds < 3600) {
39
+ const minutes = Math.floor(seconds / 60);
40
+ const remainingSeconds = Math.floor(seconds % 60);
41
+ return remainingSeconds > 0
42
+ ? `${minutes}m ${remainingSeconds}s`
43
+ : `${minutes}m`;
44
+ }
45
+ const hours = Math.floor(seconds / 3600);
46
+ const remainingMinutes = Math.floor((seconds % 3600) / 60);
47
+ return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
48
+ }
49
+ /**
50
+ * PhaseSpinner - Animated spinner with elapsed time and phase progress
51
+ *
52
+ * Features:
53
+ * - Animated spinner (or text fallback) via cli-ui
54
+ * - Elapsed time tracking with automatic updates
55
+ * - Phase progress indicator (e.g., "exec (2/3)")
56
+ * - ShutdownManager integration for graceful Ctrl+C cleanup
57
+ * - Pause/resume for verbose streaming output
58
+ */
59
+ export class PhaseSpinner {
60
+ spinner;
61
+ startTime = 0;
62
+ intervalId = null;
63
+ disposed = false;
64
+ paused = false;
65
+ phase;
66
+ phaseIndex;
67
+ totalPhases;
68
+ shutdownManager;
69
+ prefix;
70
+ iteration;
71
+ cleanupName;
72
+ constructor(options) {
73
+ this.phase = options.phase;
74
+ this.phaseIndex = options.phaseIndex;
75
+ this.totalPhases = options.totalPhases;
76
+ this.shutdownManager = options.shutdownManager;
77
+ this.prefix = options.prefix ?? " ";
78
+ this.iteration = options.iteration;
79
+ this.cleanupName = `phase-spinner-${options.phase}`;
80
+ // Create the underlying spinner with initial text
81
+ this.spinner = createSpinner(this.formatText(0));
82
+ }
83
+ /**
84
+ * Format the spinner text with phase, progress, and elapsed time
85
+ */
86
+ formatText(elapsedSeconds) {
87
+ const progress = `(${this.phaseIndex}/${this.totalPhases})`;
88
+ const elapsed = elapsedSeconds > 0 ? ` ${formatElapsedTime(elapsedSeconds)}` : "";
89
+ const iterationSuffix = this.iteration && this.iteration > 1
90
+ ? ` [iteration ${this.iteration}]`
91
+ : "";
92
+ return `${this.prefix}${this.phase} ${progress}...${elapsed}${iterationSuffix}`;
93
+ }
94
+ /**
95
+ * Update the spinner text with current elapsed time
96
+ */
97
+ updateElapsedTime() {
98
+ if (this.disposed || this.paused)
99
+ return;
100
+ const elapsedSeconds = (Date.now() - this.startTime) / 1000;
101
+ this.spinner.text = this.formatText(elapsedSeconds);
102
+ }
103
+ /**
104
+ * Start the spinner
105
+ *
106
+ * Begins the animation and elapsed time tracking.
107
+ */
108
+ start() {
109
+ if (this.disposed)
110
+ return this;
111
+ this.startTime = Date.now();
112
+ this.spinner.start(this.formatText(0));
113
+ // Start elapsed time update interval
114
+ this.intervalId = setInterval(() => {
115
+ this.updateElapsedTime();
116
+ }, ELAPSED_UPDATE_INTERVAL_MS);
117
+ // Register with ShutdownManager for graceful cleanup
118
+ if (this.shutdownManager) {
119
+ this.shutdownManager.registerCleanup(this.cleanupName, async () => {
120
+ this.stop();
121
+ });
122
+ }
123
+ return this;
124
+ }
125
+ /**
126
+ * Mark the phase as succeeded
127
+ *
128
+ * Stops the spinner and shows a checkmark with total duration.
129
+ */
130
+ succeed(customText) {
131
+ if (this.disposed)
132
+ return this;
133
+ this.clearInterval();
134
+ this.unregisterCleanup();
135
+ const elapsedSeconds = (Date.now() - this.startTime) / 1000;
136
+ const duration = formatElapsedTime(elapsedSeconds);
137
+ const progress = `(${this.phaseIndex}/${this.totalPhases})`;
138
+ const text = customText ?? `${this.prefix}${this.phase} ${progress} (${duration})`;
139
+ this.spinner.succeed(text);
140
+ this.disposed = true;
141
+ return this;
142
+ }
143
+ /**
144
+ * Mark the phase as failed
145
+ *
146
+ * Stops the spinner and shows an error indicator with message.
147
+ */
148
+ fail(error) {
149
+ if (this.disposed)
150
+ return this;
151
+ this.clearInterval();
152
+ this.unregisterCleanup();
153
+ const elapsedSeconds = (Date.now() - this.startTime) / 1000;
154
+ const duration = formatElapsedTime(elapsedSeconds);
155
+ const progress = `(${this.phaseIndex}/${this.totalPhases})`;
156
+ const errorSuffix = error ? `: ${error}` : "";
157
+ const text = `${this.prefix}${this.phase} ${progress} (${duration})${errorSuffix}`;
158
+ this.spinner.fail(text);
159
+ this.disposed = true;
160
+ return this;
161
+ }
162
+ /**
163
+ * Stop the spinner without success/fail indication
164
+ *
165
+ * Use this for cleanup or when the phase is interrupted.
166
+ */
167
+ stop() {
168
+ if (this.disposed)
169
+ return this;
170
+ this.clearInterval();
171
+ this.unregisterCleanup();
172
+ this.spinner.stop();
173
+ this.disposed = true;
174
+ return this;
175
+ }
176
+ /**
177
+ * Pause the spinner (for verbose streaming output)
178
+ *
179
+ * Temporarily stops the animation without disposing.
180
+ * Call resume() to continue.
181
+ */
182
+ pause() {
183
+ if (this.disposed || this.paused)
184
+ return this;
185
+ this.paused = true;
186
+ this.spinner.stop();
187
+ return this;
188
+ }
189
+ /**
190
+ * Resume the spinner after pause
191
+ *
192
+ * Restarts the animation with updated elapsed time.
193
+ */
194
+ resume() {
195
+ if (this.disposed || !this.paused)
196
+ return this;
197
+ this.paused = false;
198
+ const elapsedSeconds = (Date.now() - this.startTime) / 1000;
199
+ this.spinner.start(this.formatText(elapsedSeconds));
200
+ return this;
201
+ }
202
+ /**
203
+ * Check if the spinner is currently active
204
+ */
205
+ get isSpinning() {
206
+ return this.spinner.isSpinning && !this.disposed && !this.paused;
207
+ }
208
+ /**
209
+ * Get the total elapsed time in seconds
210
+ */
211
+ get elapsedSeconds() {
212
+ if (this.startTime === 0)
213
+ return 0;
214
+ return (Date.now() - this.startTime) / 1000;
215
+ }
216
+ /**
217
+ * Dispose of the spinner and clean up resources
218
+ *
219
+ * Called automatically by succeed(), fail(), or stop().
220
+ * Safe to call multiple times.
221
+ */
222
+ dispose() {
223
+ if (this.disposed)
224
+ return;
225
+ this.clearInterval();
226
+ this.unregisterCleanup();
227
+ this.spinner.stop();
228
+ this.disposed = true;
229
+ }
230
+ /**
231
+ * Clear the elapsed time update interval
232
+ */
233
+ clearInterval() {
234
+ if (this.intervalId !== null) {
235
+ clearInterval(this.intervalId);
236
+ this.intervalId = null;
237
+ }
238
+ }
239
+ /**
240
+ * Unregister from ShutdownManager
241
+ */
242
+ unregisterCleanup() {
243
+ if (this.shutdownManager) {
244
+ this.shutdownManager.unregisterCleanup(this.cleanupName);
245
+ }
246
+ }
247
+ }
248
+ /**
249
+ * Create a PhaseSpinner with the given options
250
+ *
251
+ * Convenience function matching the pattern of cli-ui.spinner().
252
+ */
253
+ export function phaseSpinner(options) {
254
+ return new PhaseSpinner(options);
255
+ }
@@ -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
+ }