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.
- package/README.md +10 -8
- package/dist/bin/cli.js +19 -9
- package/dist/src/commands/doctor.js +42 -20
- package/dist/src/commands/init.js +152 -65
- package/dist/src/commands/logs.js +7 -6
- package/dist/src/commands/run.d.ts +13 -1
- package/dist/src/commands/run.js +122 -32
- package/dist/src/commands/stats.js +67 -48
- package/dist/src/commands/status.js +30 -12
- package/dist/src/commands/sync.d.ts +28 -0
- package/dist/src/commands/sync.js +102 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +4 -0
- package/dist/src/lib/cli-ui.d.ts +196 -0
- package/dist/src/lib/cli-ui.js +544 -0
- package/dist/src/lib/content-analyzer.d.ts +89 -0
- package/dist/src/lib/content-analyzer.js +437 -0
- package/dist/src/lib/phase-signal.d.ts +94 -0
- package/dist/src/lib/phase-signal.js +171 -0
- package/dist/src/lib/phase-spinner.d.ts +146 -0
- package/dist/src/lib/phase-spinner.js +255 -0
- package/dist/src/lib/solve-comment-parser.d.ts +84 -0
- package/dist/src/lib/solve-comment-parser.js +200 -0
- package/dist/src/lib/stack-config.d.ts +51 -0
- package/dist/src/lib/stack-config.js +77 -0
- package/dist/src/lib/stacks.d.ts +52 -0
- package/dist/src/lib/stacks.js +173 -0
- package/dist/src/lib/templates.d.ts +2 -0
- package/dist/src/lib/templates.js +9 -2
- package/dist/src/lib/upstream/assessment.d.ts +70 -0
- package/dist/src/lib/upstream/assessment.js +385 -0
- package/dist/src/lib/upstream/index.d.ts +11 -0
- package/dist/src/lib/upstream/index.js +14 -0
- package/dist/src/lib/upstream/issues.d.ts +38 -0
- package/dist/src/lib/upstream/issues.js +267 -0
- package/dist/src/lib/upstream/relevance.d.ts +50 -0
- package/dist/src/lib/upstream/relevance.js +209 -0
- package/dist/src/lib/upstream/report.d.ts +29 -0
- package/dist/src/lib/upstream/report.js +391 -0
- package/dist/src/lib/upstream/types.d.ts +207 -0
- package/dist/src/lib/upstream/types.js +5 -0
- package/dist/src/lib/workflow/log-writer.d.ts +1 -1
- package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
- package/dist/src/lib/workflow/qa-cache.d.ts +199 -0
- package/dist/src/lib/workflow/qa-cache.js +440 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts +34 -6
- package/dist/src/lib/workflow/run-log-schema.js +12 -1
- package/dist/src/lib/workflow/state-schema.d.ts +4 -4
- package/dist/src/lib/workflow/types.d.ts +4 -0
- package/package.json +6 -1
- package/templates/skills/qa/scripts/quality-checks.sh +509 -53
- package/templates/skills/solve/SKILL.md +375 -83
- 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
|
+
}
|