sequant 1.1.1 → 1.1.3
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/dist/bin/cli.js +11 -4
- package/dist/bin/cli.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +38 -2
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/init.test.js +37 -1
- package/dist/src/commands/init.test.js.map +1 -1
- package/dist/src/commands/run.d.ts +10 -1
- package/dist/src/commands/run.d.ts.map +1 -1
- package/dist/src/commands/run.js +549 -102
- package/dist/src/commands/run.js.map +1 -1
- package/dist/src/commands/update.d.ts.map +1 -1
- package/dist/src/commands/update.js +66 -2
- package/dist/src/commands/update.js.map +1 -1
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/config.d.ts +19 -0
- package/dist/src/lib/config.d.ts.map +1 -0
- package/dist/src/lib/config.js +31 -0
- package/dist/src/lib/config.js.map +1 -0
- package/dist/src/lib/settings.d.ts +69 -0
- package/dist/src/lib/settings.d.ts.map +1 -0
- package/dist/src/lib/settings.js +79 -0
- package/dist/src/lib/settings.js.map +1 -0
- package/dist/src/lib/stacks.d.ts +1 -0
- package/dist/src/lib/stacks.d.ts.map +1 -1
- package/dist/src/lib/stacks.js +6 -0
- package/dist/src/lib/stacks.js.map +1 -1
- package/dist/src/lib/templates.d.ts +5 -1
- package/dist/src/lib/templates.d.ts.map +1 -1
- package/dist/src/lib/templates.js +3 -2
- package/dist/src/lib/templates.js.map +1 -1
- package/dist/src/lib/workflow/log-writer.test.d.ts +7 -0
- package/dist/src/lib/workflow/log-writer.test.d.ts.map +1 -0
- package/dist/src/lib/workflow/log-writer.test.js +451 -0
- package/dist/src/lib/workflow/log-writer.test.js.map +1 -0
- package/dist/src/lib/workflow/run-log-schema.test.d.ts +2 -0
- package/dist/src/lib/workflow/run-log-schema.test.d.ts.map +1 -0
- package/dist/src/lib/workflow/run-log-schema.test.js +455 -0
- package/dist/src/lib/workflow/run-log-schema.test.js.map +1 -0
- package/dist/src/lib/workflow/types.d.ts +2 -0
- package/dist/src/lib/workflow/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/templates/hooks/pre-tool.sh +14 -2
- package/templates/scripts/cleanup-worktree.sh +23 -1
- package/templates/skills/exec/SKILL.md +18 -0
- package/templates/skills/fullsolve/SKILL.md +26 -0
- package/templates/skills/solve/SKILL.md +27 -11
- package/templates/skills/spec/SKILL.md +30 -2
package/dist/src/commands/run.js
CHANGED
|
@@ -1,23 +1,110 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* sequant run - Execute workflow for GitHub issues
|
|
3
3
|
*
|
|
4
|
-
* Runs the Sequant workflow (/spec → /exec → /qa) for one or more issues
|
|
4
|
+
* Runs the Sequant workflow (/spec → /exec → /qa) for one or more issues
|
|
5
|
+
* using the Claude Agent SDK for proper skill invocation.
|
|
5
6
|
*/
|
|
6
7
|
import chalk from "chalk";
|
|
7
|
-
import {
|
|
8
|
+
import { spawnSync } from "child_process";
|
|
9
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
8
10
|
import { getManifest } from "../lib/manifest.js";
|
|
11
|
+
import { getSettings } from "../lib/settings.js";
|
|
9
12
|
import { LogWriter, createPhaseLogFromTiming, } from "../lib/workflow/log-writer.js";
|
|
13
|
+
import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
|
|
14
|
+
/**
|
|
15
|
+
* Natural language prompts for each phase
|
|
16
|
+
* These prompts will invoke the corresponding skills via natural language
|
|
17
|
+
*/
|
|
18
|
+
const PHASE_PROMPTS = {
|
|
19
|
+
spec: "Review GitHub issue #{issue} and create an implementation plan with verification criteria. Run the /spec {issue} workflow.",
|
|
20
|
+
testgen: "Generate test stubs for GitHub issue #{issue} based on the specification. Run the /testgen {issue} workflow.",
|
|
21
|
+
exec: "Implement the feature for GitHub issue #{issue} following the spec. Run the /exec {issue} workflow.",
|
|
22
|
+
test: "Execute structured browser-based testing for GitHub issue #{issue}. Run the /test {issue} workflow.",
|
|
23
|
+
qa: "Review the implementation for GitHub issue #{issue} against acceptance criteria. Run the /qa {issue} workflow.",
|
|
24
|
+
loop: "Parse test/QA findings for GitHub issue #{issue} and iterate until quality gates pass. Run the /loop {issue} workflow.",
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* UI-related labels that trigger automatic test phase
|
|
28
|
+
*/
|
|
29
|
+
const UI_LABELS = ["ui", "frontend", "admin", "web", "browser"];
|
|
30
|
+
/**
|
|
31
|
+
* Bug-related labels that skip spec phase
|
|
32
|
+
*/
|
|
33
|
+
const BUG_LABELS = ["bug", "fix", "hotfix", "patch"];
|
|
34
|
+
/**
|
|
35
|
+
* Documentation labels that skip spec phase
|
|
36
|
+
*/
|
|
37
|
+
const DOCS_LABELS = ["docs", "documentation", "readme"];
|
|
38
|
+
/**
|
|
39
|
+
* Complex labels that enable quality loop
|
|
40
|
+
*/
|
|
41
|
+
const COMPLEX_LABELS = ["complex", "refactor", "breaking", "major"];
|
|
10
42
|
/**
|
|
11
|
-
*
|
|
43
|
+
* Detect phases based on issue labels (like /solve logic)
|
|
12
44
|
*/
|
|
13
|
-
function
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
45
|
+
function detectPhasesFromLabels(labels) {
|
|
46
|
+
const lowerLabels = labels.map((l) => l.toLowerCase());
|
|
47
|
+
// Check for bug/fix labels → exec → qa (skip spec)
|
|
48
|
+
const isBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label.includes(bugLabel)));
|
|
49
|
+
// Check for docs labels → exec → qa (skip spec)
|
|
50
|
+
const isDocs = lowerLabels.some((label) => DOCS_LABELS.some((docsLabel) => label.includes(docsLabel)));
|
|
51
|
+
// Check for UI labels → add test phase
|
|
52
|
+
const isUI = lowerLabels.some((label) => UI_LABELS.some((uiLabel) => label.includes(uiLabel)));
|
|
53
|
+
// Check for complex labels → enable quality loop
|
|
54
|
+
const isComplex = lowerLabels.some((label) => COMPLEX_LABELS.some((complexLabel) => label.includes(complexLabel)));
|
|
55
|
+
// Build phase list
|
|
56
|
+
let phases;
|
|
57
|
+
if (isBugFix || isDocs) {
|
|
58
|
+
// Simple workflow: exec → qa
|
|
59
|
+
phases = ["exec", "qa"];
|
|
60
|
+
}
|
|
61
|
+
else if (isUI) {
|
|
62
|
+
// UI workflow: spec → exec → test → qa
|
|
63
|
+
phases = ["spec", "exec", "test", "qa"];
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Standard workflow: spec → exec → qa
|
|
67
|
+
phases = ["spec", "exec", "qa"];
|
|
68
|
+
}
|
|
69
|
+
return { phases, qualityLoop: isComplex };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse recommended workflow from /spec output
|
|
73
|
+
*
|
|
74
|
+
* Looks for:
|
|
75
|
+
* ## Recommended Workflow
|
|
76
|
+
* **Phases:** exec → qa
|
|
77
|
+
* **Quality Loop:** enabled|disabled
|
|
78
|
+
*/
|
|
79
|
+
function parseRecommendedWorkflow(output) {
|
|
80
|
+
// Find the Recommended Workflow section
|
|
81
|
+
const workflowMatch = output.match(/## Recommended Workflow[\s\S]*?\*\*Phases:\*\*\s*([^\n]+)/i);
|
|
82
|
+
if (!workflowMatch) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// Parse phases from "exec → qa" or "spec → exec → test → qa" format
|
|
86
|
+
const phasesStr = workflowMatch[1].trim();
|
|
87
|
+
const phaseNames = phasesStr
|
|
88
|
+
.split(/\s*→\s*|\s*->\s*|\s*,\s*/)
|
|
89
|
+
.map((p) => p.trim().toLowerCase())
|
|
90
|
+
.filter((p) => p.length > 0);
|
|
91
|
+
// Validate and convert to Phase type
|
|
92
|
+
const validPhases = [];
|
|
93
|
+
for (const name of phaseNames) {
|
|
94
|
+
if (["spec", "testgen", "exec", "test", "qa", "loop"].includes(name)) {
|
|
95
|
+
validPhases.push(name);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (validPhases.length === 0) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
// Parse quality loop setting
|
|
102
|
+
const qualityLoopMatch = output.match(/\*\*Quality Loop:\*\*\s*(enabled|disabled|true|false|yes|no)/i);
|
|
103
|
+
const qualityLoop = qualityLoopMatch
|
|
104
|
+
? ["enabled", "true", "yes"].includes(qualityLoopMatch[1].toLowerCase())
|
|
105
|
+
: false;
|
|
106
|
+
return { phases: validPhases, qualityLoop };
|
|
19
107
|
}
|
|
20
|
-
import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
|
|
21
108
|
/**
|
|
22
109
|
* Format duration in human-readable format
|
|
23
110
|
*/
|
|
@@ -30,9 +117,15 @@ function formatDuration(seconds) {
|
|
|
30
117
|
return `${mins}m ${secs.toFixed(0)}s`;
|
|
31
118
|
}
|
|
32
119
|
/**
|
|
33
|
-
*
|
|
120
|
+
* Get the prompt for a phase with the issue number substituted
|
|
34
121
|
*/
|
|
35
|
-
|
|
122
|
+
function getPhasePrompt(phase, issueNumber) {
|
|
123
|
+
return PHASE_PROMPTS[phase].replace(/\{issue\}/g, String(issueNumber));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Execute a single phase for an issue using Claude Agent SDK
|
|
127
|
+
*/
|
|
128
|
+
async function executePhase(issueNumber, phase, config, sessionId) {
|
|
36
129
|
const startTime = Date.now();
|
|
37
130
|
if (config.dryRun) {
|
|
38
131
|
// Dry run - just simulate
|
|
@@ -45,61 +138,136 @@ async function executePhase(issueNumber, phase, config) {
|
|
|
45
138
|
durationSeconds: 0,
|
|
46
139
|
};
|
|
47
140
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
141
|
+
const prompt = getPhasePrompt(phase, issueNumber);
|
|
142
|
+
if (config.verbose) {
|
|
143
|
+
console.log(chalk.gray(` Prompt: ${prompt}`));
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
// Create abort controller for timeout
|
|
147
|
+
const abortController = new AbortController();
|
|
148
|
+
const timeoutId = setTimeout(() => {
|
|
149
|
+
abortController.abort();
|
|
150
|
+
}, config.phaseTimeout * 1000);
|
|
151
|
+
let resultSessionId;
|
|
152
|
+
let resultMessage;
|
|
153
|
+
let lastError;
|
|
154
|
+
let capturedOutput = "";
|
|
155
|
+
// Execute using Claude Agent SDK
|
|
156
|
+
const queryInstance = query({
|
|
157
|
+
prompt,
|
|
158
|
+
options: {
|
|
159
|
+
abortController,
|
|
160
|
+
cwd: process.cwd(),
|
|
161
|
+
// Load project settings including skills
|
|
162
|
+
settingSources: ["project"],
|
|
163
|
+
// Use Claude Code's system prompt and tools
|
|
164
|
+
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
165
|
+
tools: { type: "preset", preset: "claude_code" },
|
|
166
|
+
// Bypass permissions for headless execution
|
|
167
|
+
permissionMode: "bypassPermissions",
|
|
168
|
+
allowDangerouslySkipPermissions: true,
|
|
169
|
+
// Resume from previous session if provided
|
|
170
|
+
...(sessionId ? { resume: sessionId } : {}),
|
|
171
|
+
// Configure smart tests via environment
|
|
172
|
+
env: {
|
|
173
|
+
...process.env,
|
|
174
|
+
CLAUDE_HOOKS_SMART_TESTS: config.noSmartTests ? "false" : "true",
|
|
175
|
+
},
|
|
176
|
+
},
|
|
59
177
|
});
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
178
|
+
// Stream and process messages
|
|
179
|
+
for await (const message of queryInstance) {
|
|
180
|
+
// Capture session ID from system init message
|
|
181
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
182
|
+
resultSessionId = message.session_id;
|
|
183
|
+
}
|
|
184
|
+
// Capture output from assistant messages
|
|
185
|
+
if (message.type === "assistant") {
|
|
186
|
+
// Extract text content from the message
|
|
187
|
+
const content = message.message.content;
|
|
188
|
+
const textContent = content
|
|
189
|
+
.filter((c) => c.type === "text" && c.text)
|
|
190
|
+
.map((c) => c.text)
|
|
191
|
+
.join("");
|
|
192
|
+
if (textContent) {
|
|
193
|
+
capturedOutput += textContent;
|
|
194
|
+
// Show streaming output in verbose mode
|
|
195
|
+
if (config.verbose) {
|
|
196
|
+
process.stdout.write(chalk.gray(textContent));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Capture the final result
|
|
201
|
+
if (message.type === "result") {
|
|
202
|
+
resultMessage = message;
|
|
75
203
|
}
|
|
76
|
-
|
|
77
|
-
|
|
204
|
+
}
|
|
205
|
+
clearTimeout(timeoutId);
|
|
206
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
207
|
+
// Check result status
|
|
208
|
+
if (resultMessage) {
|
|
209
|
+
if (resultMessage.subtype === "success") {
|
|
210
|
+
return {
|
|
78
211
|
phase,
|
|
79
212
|
success: true,
|
|
80
213
|
durationSeconds,
|
|
81
|
-
|
|
214
|
+
sessionId: resultSessionId,
|
|
215
|
+
output: capturedOutput,
|
|
216
|
+
};
|
|
82
217
|
}
|
|
83
218
|
else {
|
|
84
|
-
|
|
219
|
+
// Handle error subtypes
|
|
220
|
+
const errorSubtype = resultMessage.subtype;
|
|
221
|
+
if (errorSubtype === "error_max_turns") {
|
|
222
|
+
lastError = "Max turns reached";
|
|
223
|
+
}
|
|
224
|
+
else if (errorSubtype === "error_during_execution") {
|
|
225
|
+
lastError =
|
|
226
|
+
resultMessage.errors?.join(", ") || "Error during execution";
|
|
227
|
+
}
|
|
228
|
+
else if (errorSubtype === "error_max_budget_usd") {
|
|
229
|
+
lastError = "Budget limit exceeded";
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
lastError = `Error: ${errorSubtype}`;
|
|
233
|
+
}
|
|
234
|
+
return {
|
|
85
235
|
phase,
|
|
86
236
|
success: false,
|
|
87
237
|
durationSeconds,
|
|
88
|
-
error:
|
|
89
|
-
|
|
238
|
+
error: lastError,
|
|
239
|
+
sessionId: resultSessionId,
|
|
240
|
+
};
|
|
90
241
|
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
242
|
+
}
|
|
243
|
+
// No result message received
|
|
244
|
+
return {
|
|
245
|
+
phase,
|
|
246
|
+
success: false,
|
|
247
|
+
durationSeconds: (Date.now() - startTime) / 1000,
|
|
248
|
+
error: "No result received from Claude",
|
|
249
|
+
sessionId: resultSessionId,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
catch (err) {
|
|
253
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
254
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
255
|
+
// Check if it was an abort (timeout)
|
|
256
|
+
if (error.includes("abort") || error.includes("AbortError")) {
|
|
257
|
+
return {
|
|
96
258
|
phase,
|
|
97
259
|
success: false,
|
|
98
260
|
durationSeconds,
|
|
99
|
-
error:
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
261
|
+
error: `Timeout after ${config.phaseTimeout}s`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
phase,
|
|
266
|
+
success: false,
|
|
267
|
+
durationSeconds,
|
|
268
|
+
error,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
103
271
|
}
|
|
104
272
|
/**
|
|
105
273
|
* Fetch issue info from GitHub
|
|
@@ -129,47 +297,141 @@ async function getIssueInfo(issueNumber) {
|
|
|
129
297
|
}
|
|
130
298
|
return { title: `Issue #${issueNumber}`, labels: [] };
|
|
131
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* Check if an issue has UI-related labels
|
|
302
|
+
*/
|
|
303
|
+
function hasUILabels(labels) {
|
|
304
|
+
return labels.some((label) => UI_LABELS.some((uiLabel) => label.toLowerCase().includes(uiLabel)));
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Determine phases to run based on options and issue labels
|
|
308
|
+
*/
|
|
309
|
+
function determinePhasesForIssue(basePhases, labels, options) {
|
|
310
|
+
let phases = [...basePhases];
|
|
311
|
+
// Add testgen phase after spec if requested
|
|
312
|
+
if (options.testgen && phases.includes("spec")) {
|
|
313
|
+
const specIndex = phases.indexOf("spec");
|
|
314
|
+
if (!phases.includes("testgen")) {
|
|
315
|
+
phases.splice(specIndex + 1, 0, "testgen");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Auto-detect UI issues and add test phase
|
|
319
|
+
if (hasUILabels(labels) && !phases.includes("test")) {
|
|
320
|
+
// Add test phase before qa if present, otherwise at the end
|
|
321
|
+
const qaIndex = phases.indexOf("qa");
|
|
322
|
+
if (qaIndex !== -1) {
|
|
323
|
+
phases.splice(qaIndex, 0, "test");
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
phases.push("test");
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return phases;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Parse environment variables for CI configuration
|
|
333
|
+
*/
|
|
334
|
+
function getEnvConfig() {
|
|
335
|
+
const config = {};
|
|
336
|
+
if (process.env.SEQUANT_QUALITY_LOOP === "true") {
|
|
337
|
+
config.qualityLoop = true;
|
|
338
|
+
}
|
|
339
|
+
if (process.env.SEQUANT_MAX_ITERATIONS) {
|
|
340
|
+
const maxIter = parseInt(process.env.SEQUANT_MAX_ITERATIONS, 10);
|
|
341
|
+
if (!isNaN(maxIter)) {
|
|
342
|
+
config.maxIterations = maxIter;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (process.env.SEQUANT_SMART_TESTS === "false") {
|
|
346
|
+
config.noSmartTests = true;
|
|
347
|
+
}
|
|
348
|
+
if (process.env.SEQUANT_TESTGEN === "true") {
|
|
349
|
+
config.testgen = true;
|
|
350
|
+
}
|
|
351
|
+
return config;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Parse batch arguments into groups of issues
|
|
355
|
+
*/
|
|
356
|
+
function parseBatches(batchArgs) {
|
|
357
|
+
return batchArgs.map((batch) => batch
|
|
358
|
+
.split(/\s+/)
|
|
359
|
+
.map((n) => parseInt(n, 10))
|
|
360
|
+
.filter((n) => !isNaN(n)));
|
|
361
|
+
}
|
|
132
362
|
/**
|
|
133
363
|
* Main run command
|
|
134
364
|
*/
|
|
135
365
|
export async function runCommand(issues, options) {
|
|
136
|
-
console.log(chalk.blue("\n
|
|
366
|
+
console.log(chalk.blue("\n🌐 Sequant Workflow Execution\n"));
|
|
137
367
|
// Check if initialized
|
|
138
368
|
const manifest = await getManifest();
|
|
139
369
|
if (!manifest) {
|
|
140
370
|
console.log(chalk.red("❌ Sequant is not initialized. Run `sequant init` first."));
|
|
141
371
|
return;
|
|
142
372
|
}
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
373
|
+
// Load settings and merge with environment config and CLI options
|
|
374
|
+
const settings = await getSettings();
|
|
375
|
+
const envConfig = getEnvConfig();
|
|
376
|
+
// Settings provide defaults, env overrides settings, CLI overrides all
|
|
377
|
+
// Note: phases are auto-detected per-issue unless --phases is explicitly set
|
|
378
|
+
const mergedOptions = {
|
|
379
|
+
// Settings defaults (phases removed - now auto-detected)
|
|
380
|
+
sequential: options.sequential ?? settings.run.sequential,
|
|
381
|
+
timeout: options.timeout ?? settings.run.timeout,
|
|
382
|
+
logPath: options.logPath ?? settings.run.logPath,
|
|
383
|
+
qualityLoop: options.qualityLoop ?? settings.run.qualityLoop,
|
|
384
|
+
maxIterations: options.maxIterations ?? settings.run.maxIterations,
|
|
385
|
+
noSmartTests: options.noSmartTests ?? !settings.run.smartTests,
|
|
386
|
+
// Env overrides
|
|
387
|
+
...envConfig,
|
|
388
|
+
// CLI explicit options override all
|
|
389
|
+
...options,
|
|
390
|
+
};
|
|
391
|
+
// Determine if we should auto-detect phases from labels
|
|
392
|
+
const autoDetectPhases = !options.phases && settings.run.autoDetectPhases;
|
|
393
|
+
mergedOptions.autoDetectPhases = autoDetectPhases;
|
|
394
|
+
// Parse issue numbers (or use batch mode)
|
|
395
|
+
let issueNumbers;
|
|
396
|
+
let batches = null;
|
|
397
|
+
if (mergedOptions.batch && mergedOptions.batch.length > 0) {
|
|
398
|
+
batches = parseBatches(mergedOptions.batch);
|
|
399
|
+
issueNumbers = batches.flat();
|
|
400
|
+
console.log(chalk.gray(` Batch mode: ${batches.map((b) => `[${b.join(", ")}]`).join(" → ")}`));
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
issueNumbers = issues.map((i) => parseInt(i, 10)).filter((n) => !isNaN(n));
|
|
148
404
|
}
|
|
149
|
-
// Parse issue numbers
|
|
150
|
-
const issueNumbers = issues
|
|
151
|
-
.map((i) => parseInt(i, 10))
|
|
152
|
-
.filter((n) => !isNaN(n));
|
|
153
405
|
if (issueNumbers.length === 0) {
|
|
154
406
|
console.log(chalk.red("❌ No valid issue numbers provided."));
|
|
155
407
|
console.log(chalk.gray("\nUsage: sequant run <issues...> [options]"));
|
|
156
408
|
console.log(chalk.gray("Example: sequant run 1 2 3 --sequential"));
|
|
409
|
+
console.log(chalk.gray('Batch example: sequant run --batch "1 2" --batch "3"'));
|
|
157
410
|
return;
|
|
158
411
|
}
|
|
159
412
|
// Build config
|
|
413
|
+
// Note: config.phases is only used when --phases is explicitly set or autoDetect fails
|
|
414
|
+
const explicitPhases = mergedOptions.phases
|
|
415
|
+
? mergedOptions.phases.split(",").map((p) => p.trim())
|
|
416
|
+
: null;
|
|
160
417
|
const config = {
|
|
161
418
|
...DEFAULT_CONFIG,
|
|
162
|
-
phases:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
419
|
+
phases: explicitPhases ?? DEFAULT_PHASES,
|
|
420
|
+
sequential: mergedOptions.sequential ?? false,
|
|
421
|
+
dryRun: mergedOptions.dryRun ?? false,
|
|
422
|
+
verbose: mergedOptions.verbose ?? false,
|
|
423
|
+
phaseTimeout: mergedOptions.timeout ?? DEFAULT_CONFIG.phaseTimeout,
|
|
424
|
+
qualityLoop: mergedOptions.qualityLoop ?? false,
|
|
425
|
+
maxIterations: mergedOptions.maxIterations ?? DEFAULT_CONFIG.maxIterations,
|
|
426
|
+
noSmartTests: mergedOptions.noSmartTests ?? false,
|
|
169
427
|
};
|
|
170
428
|
// Initialize log writer if JSON logging enabled
|
|
429
|
+
// Default: enabled via settings (logJson: true), can be disabled with --no-log
|
|
171
430
|
let logWriter = null;
|
|
172
|
-
|
|
431
|
+
const shouldLog = !mergedOptions.noLog &&
|
|
432
|
+
!config.dryRun &&
|
|
433
|
+
(mergedOptions.logJson ?? settings.run.logJson);
|
|
434
|
+
if (shouldLog) {
|
|
173
435
|
const runConfig = {
|
|
174
436
|
phases: config.phases,
|
|
175
437
|
sequential: config.sequential,
|
|
@@ -177,15 +439,29 @@ export async function runCommand(issues, options) {
|
|
|
177
439
|
maxIterations: config.maxIterations,
|
|
178
440
|
};
|
|
179
441
|
logWriter = new LogWriter({
|
|
180
|
-
logPath:
|
|
442
|
+
logPath: mergedOptions.logPath ?? settings.run.logPath,
|
|
181
443
|
verbose: config.verbose,
|
|
182
444
|
});
|
|
183
445
|
await logWriter.initialize(runConfig);
|
|
184
446
|
}
|
|
185
447
|
// Display configuration
|
|
186
448
|
console.log(chalk.gray(` Stack: ${manifest.stack}`));
|
|
187
|
-
|
|
449
|
+
if (autoDetectPhases) {
|
|
450
|
+
console.log(chalk.gray(` Phases: auto-detect from labels`));
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
console.log(chalk.gray(` Phases: ${config.phases.join(" → ")}`));
|
|
454
|
+
}
|
|
188
455
|
console.log(chalk.gray(` Mode: ${config.sequential ? "sequential" : "parallel"}`));
|
|
456
|
+
if (config.qualityLoop) {
|
|
457
|
+
console.log(chalk.gray(` Quality loop: enabled (max ${config.maxIterations} iterations)`));
|
|
458
|
+
}
|
|
459
|
+
if (mergedOptions.testgen) {
|
|
460
|
+
console.log(chalk.gray(` Testgen: enabled`));
|
|
461
|
+
}
|
|
462
|
+
if (config.noSmartTests) {
|
|
463
|
+
console.log(chalk.gray(` Smart tests: disabled`));
|
|
464
|
+
}
|
|
189
465
|
if (config.dryRun) {
|
|
190
466
|
console.log(chalk.yellow(` ⚠️ DRY RUN - no actual execution`));
|
|
191
467
|
}
|
|
@@ -195,15 +471,30 @@ export async function runCommand(issues, options) {
|
|
|
195
471
|
console.log(chalk.gray(` Issues: ${issueNumbers.map((n) => `#${n}`).join(", ")}`));
|
|
196
472
|
// Execute
|
|
197
473
|
const results = [];
|
|
198
|
-
if (
|
|
474
|
+
if (batches) {
|
|
475
|
+
// Batch execution: run batches sequentially, issues within batch based on mode
|
|
476
|
+
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
|
|
477
|
+
const batch = batches[batchIdx];
|
|
478
|
+
console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${batches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
|
|
479
|
+
const batchResults = await executeBatch(batch, config, logWriter, mergedOptions);
|
|
480
|
+
results.push(...batchResults);
|
|
481
|
+
// Check if batch failed and we should stop
|
|
482
|
+
const batchFailed = batchResults.some((r) => !r.success);
|
|
483
|
+
if (batchFailed && config.sequential) {
|
|
484
|
+
console.log(chalk.yellow(`\n ⚠️ Batch ${batchIdx + 1} failed, stopping batch execution`));
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
else if (config.sequential) {
|
|
199
490
|
// Sequential execution
|
|
200
491
|
for (const issueNumber of issueNumbers) {
|
|
492
|
+
const issueInfo = await getIssueInfo(issueNumber);
|
|
201
493
|
// Start issue logging
|
|
202
494
|
if (logWriter) {
|
|
203
|
-
const issueInfo = await getIssueInfo(issueNumber);
|
|
204
495
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
205
496
|
}
|
|
206
|
-
const result = await runIssueWithLogging(issueNumber, config, logWriter);
|
|
497
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions);
|
|
207
498
|
results.push(result);
|
|
208
499
|
// Complete issue logging
|
|
209
500
|
if (logWriter) {
|
|
@@ -219,12 +510,12 @@ export async function runCommand(issues, options) {
|
|
|
219
510
|
// Parallel execution (for now, just run sequentially but don't stop on failure)
|
|
220
511
|
// TODO: Add proper parallel execution with listr2
|
|
221
512
|
for (const issueNumber of issueNumbers) {
|
|
513
|
+
const issueInfo = await getIssueInfo(issueNumber);
|
|
222
514
|
// Start issue logging
|
|
223
515
|
if (logWriter) {
|
|
224
|
-
const issueInfo = await getIssueInfo(issueNumber);
|
|
225
516
|
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
226
517
|
}
|
|
227
|
-
const result = await runIssueWithLogging(issueNumber, config, logWriter);
|
|
518
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions);
|
|
228
519
|
results.push(result);
|
|
229
520
|
// Complete issue logging
|
|
230
521
|
if (logWriter) {
|
|
@@ -252,7 +543,8 @@ export async function runCommand(issues, options) {
|
|
|
252
543
|
const phases = result.phaseResults
|
|
253
544
|
.map((p) => (p.success ? chalk.green(p.phase) : chalk.red(p.phase)))
|
|
254
545
|
.join(" → ");
|
|
255
|
-
|
|
546
|
+
const loopInfo = result.loopTriggered ? chalk.yellow(" [loop]") : "";
|
|
547
|
+
console.log(` ${status} #${result.issueNumber}: ${phases}${loopInfo}${duration}`);
|
|
256
548
|
}
|
|
257
549
|
console.log("");
|
|
258
550
|
if (logPath) {
|
|
@@ -269,46 +561,201 @@ export async function runCommand(issues, options) {
|
|
|
269
561
|
}
|
|
270
562
|
}
|
|
271
563
|
/**
|
|
272
|
-
* Execute
|
|
564
|
+
* Execute a batch of issues
|
|
273
565
|
*/
|
|
274
|
-
async function
|
|
566
|
+
async function executeBatch(issueNumbers, config, logWriter, options) {
|
|
567
|
+
const results = [];
|
|
568
|
+
for (const issueNumber of issueNumbers) {
|
|
569
|
+
const issueInfo = await getIssueInfo(issueNumber);
|
|
570
|
+
// Start issue logging
|
|
571
|
+
if (logWriter) {
|
|
572
|
+
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
573
|
+
}
|
|
574
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, options);
|
|
575
|
+
results.push(result);
|
|
576
|
+
// Complete issue logging
|
|
577
|
+
if (logWriter) {
|
|
578
|
+
logWriter.completeIssue();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return results;
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Execute all phases for a single issue with logging and quality loop
|
|
585
|
+
*/
|
|
586
|
+
async function runIssueWithLogging(issueNumber, config, logWriter, labels, options) {
|
|
275
587
|
const startTime = Date.now();
|
|
276
588
|
const phaseResults = [];
|
|
589
|
+
let loopTriggered = false;
|
|
590
|
+
let sessionId;
|
|
277
591
|
console.log(chalk.blue(`\n Issue #${issueNumber}`));
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
: "failure", { error: result.error });
|
|
291
|
-
logWriter.logPhase(phaseLog);
|
|
592
|
+
// Determine phases for this specific issue
|
|
593
|
+
let phases;
|
|
594
|
+
let detectedQualityLoop = false;
|
|
595
|
+
let specAlreadyRan = false;
|
|
596
|
+
if (options.autoDetectPhases) {
|
|
597
|
+
// Check if labels indicate a simple bug/fix (skip spec entirely)
|
|
598
|
+
const lowerLabels = labels.map((l) => l.toLowerCase());
|
|
599
|
+
const isSimpleBugFix = lowerLabels.some((label) => BUG_LABELS.some((bugLabel) => label.includes(bugLabel)));
|
|
600
|
+
if (isSimpleBugFix) {
|
|
601
|
+
// Simple bug fix: skip spec, go straight to exec → qa
|
|
602
|
+
phases = ["exec", "qa"];
|
|
603
|
+
console.log(chalk.gray(` Bug fix detected: ${phases.join(" → ")}`));
|
|
292
604
|
}
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
605
|
+
else {
|
|
606
|
+
// Run spec first to get recommended workflow
|
|
607
|
+
console.log(chalk.gray(` Running spec to determine workflow...`));
|
|
608
|
+
console.log(chalk.gray(` ⏳ spec...`));
|
|
609
|
+
const specStartTime = new Date();
|
|
610
|
+
const specResult = await executePhase(issueNumber, "spec", config, sessionId);
|
|
611
|
+
const specEndTime = new Date();
|
|
612
|
+
if (specResult.sessionId) {
|
|
613
|
+
sessionId = specResult.sessionId;
|
|
614
|
+
}
|
|
615
|
+
phaseResults.push(specResult);
|
|
616
|
+
specAlreadyRan = true;
|
|
617
|
+
// Log spec phase result
|
|
618
|
+
if (logWriter) {
|
|
619
|
+
const phaseLog = createPhaseLogFromTiming("spec", issueNumber, specStartTime, specEndTime, specResult.success
|
|
620
|
+
? "success"
|
|
621
|
+
: specResult.error?.includes("Timeout")
|
|
622
|
+
? "timeout"
|
|
623
|
+
: "failure", { error: specResult.error });
|
|
624
|
+
logWriter.logPhase(phaseLog);
|
|
625
|
+
}
|
|
626
|
+
if (!specResult.success) {
|
|
627
|
+
console.log(chalk.red(` ✗ spec: ${specResult.error}`));
|
|
628
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
629
|
+
return {
|
|
630
|
+
issueNumber,
|
|
631
|
+
success: false,
|
|
632
|
+
phaseResults,
|
|
633
|
+
durationSeconds,
|
|
634
|
+
loopTriggered: false,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
const duration = specResult.durationSeconds
|
|
638
|
+
? ` (${formatDuration(specResult.durationSeconds)})`
|
|
296
639
|
: "";
|
|
297
|
-
console.log(chalk.green(` ✓ ${
|
|
640
|
+
console.log(chalk.green(` ✓ spec${duration}`));
|
|
641
|
+
// Parse recommended workflow from spec output
|
|
642
|
+
let parsedWorkflow = specResult.output
|
|
643
|
+
? parseRecommendedWorkflow(specResult.output)
|
|
644
|
+
: null;
|
|
645
|
+
if (parsedWorkflow) {
|
|
646
|
+
// Remove spec from phases since we already ran it
|
|
647
|
+
phases = parsedWorkflow.phases.filter((p) => p !== "spec");
|
|
648
|
+
detectedQualityLoop = parsedWorkflow.qualityLoop;
|
|
649
|
+
console.log(chalk.gray(` Spec recommends: ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
// Fall back to label-based detection
|
|
653
|
+
console.log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
|
|
654
|
+
const detected = detectPhasesFromLabels(labels);
|
|
655
|
+
phases = detected.phases.filter((p) => p !== "spec");
|
|
656
|
+
detectedQualityLoop = detected.qualityLoop;
|
|
657
|
+
console.log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
// Use explicit phases with adjustments
|
|
663
|
+
phases = determinePhasesForIssue(config.phases, labels, options);
|
|
664
|
+
if (phases.length !== config.phases.length) {
|
|
665
|
+
console.log(chalk.gray(` Phases adjusted: ${phases.join(" → ")}`));
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
// Add testgen phase if requested (and spec was in the phases)
|
|
669
|
+
if (options.testgen &&
|
|
670
|
+
(phases.includes("spec") || specAlreadyRan) &&
|
|
671
|
+
!phases.includes("testgen")) {
|
|
672
|
+
// Insert testgen at the beginning if spec already ran, otherwise after spec
|
|
673
|
+
if (specAlreadyRan) {
|
|
674
|
+
phases.unshift("testgen");
|
|
298
675
|
}
|
|
299
676
|
else {
|
|
300
|
-
|
|
301
|
-
|
|
677
|
+
const specIndex = phases.indexOf("spec");
|
|
678
|
+
if (specIndex !== -1) {
|
|
679
|
+
phases.splice(specIndex + 1, 0, "testgen");
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
let iteration = 0;
|
|
684
|
+
const useQualityLoop = config.qualityLoop || detectedQualityLoop;
|
|
685
|
+
const maxIterations = useQualityLoop ? config.maxIterations : 1;
|
|
686
|
+
while (iteration < maxIterations) {
|
|
687
|
+
iteration++;
|
|
688
|
+
if (useQualityLoop && iteration > 1) {
|
|
689
|
+
console.log(chalk.yellow(` Quality loop iteration ${iteration}/${maxIterations}`));
|
|
690
|
+
loopTriggered = true;
|
|
691
|
+
}
|
|
692
|
+
let phasesFailed = false;
|
|
693
|
+
for (const phase of phases) {
|
|
694
|
+
console.log(chalk.gray(` ⏳ ${phase}...`));
|
|
695
|
+
const phaseStartTime = new Date();
|
|
696
|
+
const result = await executePhase(issueNumber, phase, config, sessionId);
|
|
697
|
+
const phaseEndTime = new Date();
|
|
698
|
+
// Capture session ID for subsequent phases
|
|
699
|
+
if (result.sessionId) {
|
|
700
|
+
sessionId = result.sessionId;
|
|
701
|
+
}
|
|
702
|
+
phaseResults.push(result);
|
|
703
|
+
// Log phase result
|
|
704
|
+
if (logWriter) {
|
|
705
|
+
const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
|
|
706
|
+
? "success"
|
|
707
|
+
: result.error?.includes("Timeout")
|
|
708
|
+
? "timeout"
|
|
709
|
+
: "failure", { error: result.error });
|
|
710
|
+
logWriter.logPhase(phaseLog);
|
|
711
|
+
}
|
|
712
|
+
if (result.success) {
|
|
713
|
+
const duration = result.durationSeconds
|
|
714
|
+
? ` (${formatDuration(result.durationSeconds)})`
|
|
715
|
+
: "";
|
|
716
|
+
console.log(chalk.green(` ✓ ${phase}${duration}`));
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
console.log(chalk.red(` ✗ ${phase}: ${result.error}`));
|
|
720
|
+
phasesFailed = true;
|
|
721
|
+
// If quality loop enabled, run loop phase to fix issues
|
|
722
|
+
if (useQualityLoop && iteration < maxIterations) {
|
|
723
|
+
console.log(chalk.yellow(` Running /loop to fix issues...`));
|
|
724
|
+
const loopResult = await executePhase(issueNumber, "loop", config, sessionId);
|
|
725
|
+
phaseResults.push(loopResult);
|
|
726
|
+
if (loopResult.sessionId) {
|
|
727
|
+
sessionId = loopResult.sessionId;
|
|
728
|
+
}
|
|
729
|
+
if (loopResult.success) {
|
|
730
|
+
console.log(chalk.green(` ✓ loop - retrying phases`));
|
|
731
|
+
// Continue to next iteration
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
console.log(chalk.red(` ✗ loop: ${loopResult.error}`));
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
// Stop on first failure (if not in quality loop or loop failed)
|
|
739
|
+
break;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// If all phases passed, exit the loop
|
|
743
|
+
if (!phasesFailed) {
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
// If we're not in quality loop mode, don't retry
|
|
747
|
+
if (!config.qualityLoop) {
|
|
302
748
|
break;
|
|
303
749
|
}
|
|
304
750
|
}
|
|
305
751
|
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
306
|
-
const success = phaseResults.every((r) => r.success);
|
|
752
|
+
const success = phaseResults.length > 0 && phaseResults.every((r) => r.success);
|
|
307
753
|
return {
|
|
308
754
|
issueNumber,
|
|
309
755
|
success,
|
|
310
756
|
phaseResults,
|
|
311
757
|
durationSeconds,
|
|
758
|
+
loopTriggered,
|
|
312
759
|
};
|
|
313
760
|
}
|
|
314
761
|
//# sourceMappingURL=run.js.map
|