sequant 1.1.0 → 1.1.2
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 +21 -3
- package/dist/bin/cli.js.map +1 -1
- package/dist/src/commands/doctor.d.ts.map +1 -1
- package/dist/src/commands/doctor.js +1 -25
- package/dist/src/commands/doctor.js.map +1 -1
- package/dist/src/commands/doctor.test.js +28 -97
- package/dist/src/commands/doctor.test.js.map +1 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +3 -27
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/init.test.js +32 -75
- package/dist/src/commands/init.test.js.map +1 -1
- package/dist/src/commands/logs.d.ts +18 -0
- package/dist/src/commands/logs.d.ts.map +1 -0
- package/dist/src/commands/logs.js +188 -0
- package/dist/src/commands/logs.js.map +1 -0
- 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 +429 -98
- 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 +16 -0
- package/dist/src/commands/update.js.map +1 -1
- package/dist/src/lib/system.d.ts +16 -0
- package/dist/src/lib/system.d.ts.map +1 -0
- package/dist/src/lib/system.js +52 -0
- package/dist/src/lib/system.js.map +1 -0
- package/dist/src/lib/system.test.d.ts +2 -0
- package/dist/src/lib/system.test.d.ts.map +1 -0
- package/dist/src/lib/system.test.js +80 -0
- package/dist/src/lib/system.test.js.map +1 -0
- package/dist/src/lib/workflow/log-writer.d.ts +83 -0
- package/dist/src/lib/workflow/log-writer.d.ts.map +1 -0
- package/dist/src/lib/workflow/log-writer.js +193 -0
- package/dist/src/lib/workflow/log-writer.js.map +1 -0
- 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.d.ts +261 -0
- package/dist/src/lib/workflow/run-log-schema.d.ts.map +1 -0
- package/dist/src/lib/workflow/run-log-schema.js +234 -0
- package/dist/src/lib/workflow/run-log-schema.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/package.json +4 -2
- package/templates/hooks/pre-tool.sh +14 -2
- package/templates/scripts/cleanup-worktree.sh +23 -1
- package/templates/skills/assess/SKILL.md +15 -0
- package/templates/skills/clean/SKILL.md +15 -0
- package/templates/skills/docs/SKILL.md +16 -0
- package/templates/skills/exec/SKILL.md +32 -0
- package/templates/skills/fullsolve/SKILL.md +42 -0
- package/templates/skills/loop/SKILL.md +14 -0
- package/templates/skills/qa/SKILL.md +67 -0
- package/templates/skills/reflect/SKILL.md +14 -0
- package/templates/skills/security-review/SKILL.md +15 -0
- package/templates/skills/solve/SKILL.md +44 -0
- package/templates/skills/spec/SKILL.md +59 -0
- package/templates/skills/test/SKILL.md +14 -0
- package/templates/skills/testgen/SKILL.md +15 -0
- package/templates/skills/verify/SKILL.md +15 -0
package/dist/src/commands/run.js
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
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 { LogWriter, createPhaseLogFromTiming, } from "../lib/workflow/log-writer.js";
|
|
12
|
+
import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
|
|
9
13
|
/**
|
|
10
|
-
*
|
|
14
|
+
* Natural language prompts for each phase
|
|
15
|
+
* These prompts will invoke the corresponding skills via natural language
|
|
11
16
|
*/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
17
|
+
const PHASE_PROMPTS = {
|
|
18
|
+
spec: "Review GitHub issue #{issue} and create an implementation plan with verification criteria. Run the /spec {issue} workflow.",
|
|
19
|
+
testgen: "Generate test stubs for GitHub issue #{issue} based on the specification. Run the /testgen {issue} workflow.",
|
|
20
|
+
exec: "Implement the feature for GitHub issue #{issue} following the spec. Run the /exec {issue} workflow.",
|
|
21
|
+
test: "Execute structured browser-based testing for GitHub issue #{issue}. Run the /test {issue} workflow.",
|
|
22
|
+
qa: "Review the implementation for GitHub issue #{issue} against acceptance criteria. Run the /qa {issue} workflow.",
|
|
23
|
+
loop: "Parse test/QA findings for GitHub issue #{issue} and iterate until quality gates pass. Run the /loop {issue} workflow.",
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* UI-related labels that trigger automatic test phase
|
|
27
|
+
*/
|
|
28
|
+
const UI_LABELS = ["ui", "frontend", "admin", "web", "browser"];
|
|
20
29
|
/**
|
|
21
30
|
* Format duration in human-readable format
|
|
22
31
|
*/
|
|
@@ -29,9 +38,15 @@ function formatDuration(seconds) {
|
|
|
29
38
|
return `${mins}m ${secs.toFixed(0)}s`;
|
|
30
39
|
}
|
|
31
40
|
/**
|
|
32
|
-
*
|
|
41
|
+
* Get the prompt for a phase with the issue number substituted
|
|
42
|
+
*/
|
|
43
|
+
function getPhasePrompt(phase, issueNumber) {
|
|
44
|
+
return PHASE_PROMPTS[phase].replace(/\{issue\}/g, String(issueNumber));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Execute a single phase for an issue using Claude Agent SDK
|
|
33
48
|
*/
|
|
34
|
-
async function executePhase(issueNumber, phase, config) {
|
|
49
|
+
async function executePhase(issueNumber, phase, config, sessionId) {
|
|
35
50
|
const startTime = Date.now();
|
|
36
51
|
if (config.dryRun) {
|
|
37
52
|
// Dry run - just simulate
|
|
@@ -44,93 +59,220 @@ async function executePhase(issueNumber, phase, config) {
|
|
|
44
59
|
durationSeconds: 0,
|
|
45
60
|
};
|
|
46
61
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
const prompt = getPhasePrompt(phase, issueNumber);
|
|
63
|
+
if (config.verbose) {
|
|
64
|
+
console.log(chalk.gray(` Prompt: ${prompt}`));
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
// Create abort controller for timeout
|
|
68
|
+
const abortController = new AbortController();
|
|
69
|
+
const timeoutId = setTimeout(() => {
|
|
70
|
+
abortController.abort();
|
|
71
|
+
}, config.phaseTimeout * 1000);
|
|
72
|
+
let resultSessionId;
|
|
73
|
+
let resultMessage;
|
|
74
|
+
let lastError;
|
|
75
|
+
// Execute using Claude Agent SDK
|
|
76
|
+
const queryInstance = query({
|
|
77
|
+
prompt,
|
|
78
|
+
options: {
|
|
79
|
+
abortController,
|
|
80
|
+
cwd: process.cwd(),
|
|
81
|
+
// Load project settings including skills
|
|
82
|
+
settingSources: ["project"],
|
|
83
|
+
// Use Claude Code's system prompt and tools
|
|
84
|
+
systemPrompt: { type: "preset", preset: "claude_code" },
|
|
85
|
+
tools: { type: "preset", preset: "claude_code" },
|
|
86
|
+
// Bypass permissions for headless execution
|
|
87
|
+
permissionMode: "bypassPermissions",
|
|
88
|
+
allowDangerouslySkipPermissions: true,
|
|
89
|
+
// Resume from previous session if provided
|
|
90
|
+
...(sessionId ? { resume: sessionId } : {}),
|
|
91
|
+
// Configure smart tests via environment
|
|
92
|
+
env: {
|
|
93
|
+
...process.env,
|
|
94
|
+
CLAUDE_HOOKS_SMART_TESTS: config.noSmartTests ? "false" : "true",
|
|
95
|
+
},
|
|
96
|
+
},
|
|
58
97
|
});
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
98
|
+
// Stream and process messages
|
|
99
|
+
for await (const message of queryInstance) {
|
|
100
|
+
// Capture session ID from system init message
|
|
101
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
102
|
+
resultSessionId = message.session_id;
|
|
103
|
+
}
|
|
104
|
+
// Show streaming output in verbose mode
|
|
105
|
+
if (config.verbose && message.type === "assistant") {
|
|
106
|
+
// Extract text content from the message
|
|
107
|
+
const content = message.message.content;
|
|
108
|
+
const textContent = content
|
|
109
|
+
.filter((c) => c.type === "text" && c.text)
|
|
110
|
+
.map((c) => c.text)
|
|
111
|
+
.join("");
|
|
112
|
+
if (textContent) {
|
|
113
|
+
process.stdout.write(chalk.gray(textContent));
|
|
114
|
+
}
|
|
74
115
|
}
|
|
75
|
-
|
|
76
|
-
|
|
116
|
+
// Capture the final result
|
|
117
|
+
if (message.type === "result") {
|
|
118
|
+
resultMessage = message;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
clearTimeout(timeoutId);
|
|
122
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
123
|
+
// Check result status
|
|
124
|
+
if (resultMessage) {
|
|
125
|
+
if (resultMessage.subtype === "success") {
|
|
126
|
+
return {
|
|
77
127
|
phase,
|
|
78
128
|
success: true,
|
|
79
129
|
durationSeconds,
|
|
80
|
-
|
|
130
|
+
sessionId: resultSessionId,
|
|
131
|
+
};
|
|
81
132
|
}
|
|
82
133
|
else {
|
|
83
|
-
|
|
134
|
+
// Handle error subtypes
|
|
135
|
+
const errorSubtype = resultMessage.subtype;
|
|
136
|
+
if (errorSubtype === "error_max_turns") {
|
|
137
|
+
lastError = "Max turns reached";
|
|
138
|
+
}
|
|
139
|
+
else if (errorSubtype === "error_during_execution") {
|
|
140
|
+
lastError =
|
|
141
|
+
resultMessage.errors?.join(", ") || "Error during execution";
|
|
142
|
+
}
|
|
143
|
+
else if (errorSubtype === "error_max_budget_usd") {
|
|
144
|
+
lastError = "Budget limit exceeded";
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
lastError = `Error: ${errorSubtype}`;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
84
150
|
phase,
|
|
85
151
|
success: false,
|
|
86
152
|
durationSeconds,
|
|
87
|
-
error:
|
|
88
|
-
|
|
153
|
+
error: lastError,
|
|
154
|
+
sessionId: resultSessionId,
|
|
155
|
+
};
|
|
89
156
|
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
157
|
+
}
|
|
158
|
+
// No result message received
|
|
159
|
+
return {
|
|
160
|
+
phase,
|
|
161
|
+
success: false,
|
|
162
|
+
durationSeconds: (Date.now() - startTime) / 1000,
|
|
163
|
+
error: "No result received from Claude",
|
|
164
|
+
sessionId: resultSessionId,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
169
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
170
|
+
// Check if it was an abort (timeout)
|
|
171
|
+
if (error.includes("abort") || error.includes("AbortError")) {
|
|
172
|
+
return {
|
|
95
173
|
phase,
|
|
96
174
|
success: false,
|
|
97
175
|
durationSeconds,
|
|
98
|
-
error:
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
176
|
+
error: `Timeout after ${config.phaseTimeout}s`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
phase,
|
|
181
|
+
success: false,
|
|
182
|
+
durationSeconds,
|
|
183
|
+
error,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
102
186
|
}
|
|
103
187
|
/**
|
|
104
|
-
*
|
|
188
|
+
* Fetch issue info from GitHub
|
|
105
189
|
*/
|
|
106
|
-
async function
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
190
|
+
async function getIssueInfo(issueNumber) {
|
|
191
|
+
try {
|
|
192
|
+
const result = spawnSync("gh", [
|
|
193
|
+
"issue",
|
|
194
|
+
"view",
|
|
195
|
+
String(issueNumber),
|
|
196
|
+
"--json",
|
|
197
|
+
"title,labels",
|
|
198
|
+
"--jq",
|
|
199
|
+
'"\(.title)|\(.labels | map(.name) | join(","))"',
|
|
200
|
+
], { stdio: "pipe", shell: true });
|
|
201
|
+
if (result.status === 0) {
|
|
202
|
+
const output = result.stdout.toString().trim().replace(/^"|"$/g, "");
|
|
203
|
+
const [title, labelsStr] = output.split("|");
|
|
204
|
+
return {
|
|
205
|
+
title: title || `Issue #${issueNumber}`,
|
|
206
|
+
labels: labelsStr ? labelsStr.split(",").filter(Boolean) : [],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Ignore errors, use defaults
|
|
212
|
+
}
|
|
213
|
+
return { title: `Issue #${issueNumber}`, labels: [] };
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Check if an issue has UI-related labels
|
|
217
|
+
*/
|
|
218
|
+
function hasUILabels(labels) {
|
|
219
|
+
return labels.some((label) => UI_LABELS.some((uiLabel) => label.toLowerCase().includes(uiLabel)));
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Determine phases to run based on options and issue labels
|
|
223
|
+
*/
|
|
224
|
+
function determinePhasesForIssue(basePhases, labels, options) {
|
|
225
|
+
let phases = [...basePhases];
|
|
226
|
+
// Add testgen phase after spec if requested
|
|
227
|
+
if (options.testgen && phases.includes("spec")) {
|
|
228
|
+
const specIndex = phases.indexOf("spec");
|
|
229
|
+
if (!phases.includes("testgen")) {
|
|
230
|
+
phases.splice(specIndex + 1, 0, "testgen");
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Auto-detect UI issues and add test phase
|
|
234
|
+
if (hasUILabels(labels) && !phases.includes("test")) {
|
|
235
|
+
// Add test phase before qa if present, otherwise at the end
|
|
236
|
+
const qaIndex = phases.indexOf("qa");
|
|
237
|
+
if (qaIndex !== -1) {
|
|
238
|
+
phases.splice(qaIndex, 0, "test");
|
|
119
239
|
}
|
|
120
240
|
else {
|
|
121
|
-
|
|
122
|
-
// Stop on first failure
|
|
123
|
-
break;
|
|
241
|
+
phases.push("test");
|
|
124
242
|
}
|
|
125
243
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
244
|
+
return phases;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Parse environment variables for CI configuration
|
|
248
|
+
*/
|
|
249
|
+
function getEnvConfig() {
|
|
250
|
+
const config = {};
|
|
251
|
+
if (process.env.SEQUANT_QUALITY_LOOP === "true") {
|
|
252
|
+
config.qualityLoop = true;
|
|
253
|
+
}
|
|
254
|
+
if (process.env.SEQUANT_MAX_ITERATIONS) {
|
|
255
|
+
const maxIter = parseInt(process.env.SEQUANT_MAX_ITERATIONS, 10);
|
|
256
|
+
if (!isNaN(maxIter)) {
|
|
257
|
+
config.maxIterations = maxIter;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (process.env.SEQUANT_SMART_TESTS === "false") {
|
|
261
|
+
config.noSmartTests = true;
|
|
262
|
+
}
|
|
263
|
+
if (process.env.SEQUANT_TESTGEN === "true") {
|
|
264
|
+
config.testgen = true;
|
|
265
|
+
}
|
|
266
|
+
return config;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Parse batch arguments into groups of issues
|
|
270
|
+
*/
|
|
271
|
+
function parseBatches(batchArgs) {
|
|
272
|
+
return batchArgs.map((batch) => batch
|
|
273
|
+
.split(/\s+/)
|
|
274
|
+
.map((n) => parseInt(n, 10))
|
|
275
|
+
.filter((n) => !isNaN(n)));
|
|
134
276
|
}
|
|
135
277
|
/**
|
|
136
278
|
* Main run command
|
|
@@ -143,48 +285,107 @@ export async function runCommand(issues, options) {
|
|
|
143
285
|
console.log(chalk.red("❌ Sequant is not initialized. Run `sequant init` first."));
|
|
144
286
|
return;
|
|
145
287
|
}
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
288
|
+
// Merge environment config with CLI options
|
|
289
|
+
const envConfig = getEnvConfig();
|
|
290
|
+
const mergedOptions = { ...envConfig, ...options };
|
|
291
|
+
// Parse issue numbers (or use batch mode)
|
|
292
|
+
let issueNumbers;
|
|
293
|
+
let batches = null;
|
|
294
|
+
if (mergedOptions.batch && mergedOptions.batch.length > 0) {
|
|
295
|
+
batches = parseBatches(mergedOptions.batch);
|
|
296
|
+
issueNumbers = batches.flat();
|
|
297
|
+
console.log(chalk.gray(` Batch mode: ${batches.map((b) => `[${b.join(", ")}]`).join(" → ")}`));
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
issueNumbers = issues.map((i) => parseInt(i, 10)).filter((n) => !isNaN(n));
|
|
151
301
|
}
|
|
152
|
-
// Parse issue numbers
|
|
153
|
-
const issueNumbers = issues
|
|
154
|
-
.map((i) => parseInt(i, 10))
|
|
155
|
-
.filter((n) => !isNaN(n));
|
|
156
302
|
if (issueNumbers.length === 0) {
|
|
157
303
|
console.log(chalk.red("❌ No valid issue numbers provided."));
|
|
158
304
|
console.log(chalk.gray("\nUsage: sequant run <issues...> [options]"));
|
|
159
305
|
console.log(chalk.gray("Example: sequant run 1 2 3 --sequential"));
|
|
306
|
+
console.log(chalk.gray('Batch example: sequant run --batch "1 2" --batch "3"'));
|
|
160
307
|
return;
|
|
161
308
|
}
|
|
162
309
|
// Build config
|
|
163
310
|
const config = {
|
|
164
311
|
...DEFAULT_CONFIG,
|
|
165
|
-
phases:
|
|
166
|
-
?
|
|
312
|
+
phases: mergedOptions.phases
|
|
313
|
+
? mergedOptions.phases.split(",").map((p) => p.trim())
|
|
167
314
|
: DEFAULT_PHASES,
|
|
168
|
-
sequential:
|
|
169
|
-
dryRun:
|
|
170
|
-
verbose:
|
|
171
|
-
phaseTimeout:
|
|
315
|
+
sequential: mergedOptions.sequential ?? false,
|
|
316
|
+
dryRun: mergedOptions.dryRun ?? false,
|
|
317
|
+
verbose: mergedOptions.verbose ?? false,
|
|
318
|
+
phaseTimeout: mergedOptions.timeout ?? DEFAULT_CONFIG.phaseTimeout,
|
|
319
|
+
qualityLoop: mergedOptions.qualityLoop ?? false,
|
|
320
|
+
maxIterations: mergedOptions.maxIterations ?? DEFAULT_CONFIG.maxIterations,
|
|
321
|
+
noSmartTests: mergedOptions.noSmartTests ?? false,
|
|
172
322
|
};
|
|
323
|
+
// Initialize log writer if JSON logging enabled
|
|
324
|
+
let logWriter = null;
|
|
325
|
+
if (mergedOptions.logJson && !config.dryRun) {
|
|
326
|
+
const runConfig = {
|
|
327
|
+
phases: config.phases,
|
|
328
|
+
sequential: config.sequential,
|
|
329
|
+
qualityLoop: config.qualityLoop,
|
|
330
|
+
maxIterations: config.maxIterations,
|
|
331
|
+
};
|
|
332
|
+
logWriter = new LogWriter({
|
|
333
|
+
logPath: mergedOptions.logPath,
|
|
334
|
+
verbose: config.verbose,
|
|
335
|
+
});
|
|
336
|
+
await logWriter.initialize(runConfig);
|
|
337
|
+
}
|
|
173
338
|
// Display configuration
|
|
174
339
|
console.log(chalk.gray(` Stack: ${manifest.stack}`));
|
|
175
340
|
console.log(chalk.gray(` Phases: ${config.phases.join(" → ")}`));
|
|
176
341
|
console.log(chalk.gray(` Mode: ${config.sequential ? "sequential" : "parallel"}`));
|
|
342
|
+
if (config.qualityLoop) {
|
|
343
|
+
console.log(chalk.gray(` Quality loop: enabled (max ${config.maxIterations} iterations)`));
|
|
344
|
+
}
|
|
345
|
+
if (mergedOptions.testgen) {
|
|
346
|
+
console.log(chalk.gray(` Testgen: enabled`));
|
|
347
|
+
}
|
|
348
|
+
if (config.noSmartTests) {
|
|
349
|
+
console.log(chalk.gray(` Smart tests: disabled`));
|
|
350
|
+
}
|
|
177
351
|
if (config.dryRun) {
|
|
178
352
|
console.log(chalk.yellow(` ⚠️ DRY RUN - no actual execution`));
|
|
179
353
|
}
|
|
354
|
+
if (logWriter) {
|
|
355
|
+
console.log(chalk.gray(` Logging: JSON (run ${logWriter.getRunId()?.slice(0, 8)}...)`));
|
|
356
|
+
}
|
|
180
357
|
console.log(chalk.gray(` Issues: ${issueNumbers.map((n) => `#${n}`).join(", ")}`));
|
|
181
358
|
// Execute
|
|
182
359
|
const results = [];
|
|
183
|
-
if (
|
|
360
|
+
if (batches) {
|
|
361
|
+
// Batch execution: run batches sequentially, issues within batch based on mode
|
|
362
|
+
for (let batchIdx = 0; batchIdx < batches.length; batchIdx++) {
|
|
363
|
+
const batch = batches[batchIdx];
|
|
364
|
+
console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${batches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
|
|
365
|
+
const batchResults = await executeBatch(batch, config, logWriter, mergedOptions);
|
|
366
|
+
results.push(...batchResults);
|
|
367
|
+
// Check if batch failed and we should stop
|
|
368
|
+
const batchFailed = batchResults.some((r) => !r.success);
|
|
369
|
+
if (batchFailed && config.sequential) {
|
|
370
|
+
console.log(chalk.yellow(`\n ⚠️ Batch ${batchIdx + 1} failed, stopping batch execution`));
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
else if (config.sequential) {
|
|
184
376
|
// Sequential execution
|
|
185
377
|
for (const issueNumber of issueNumbers) {
|
|
186
|
-
const
|
|
378
|
+
const issueInfo = await getIssueInfo(issueNumber);
|
|
379
|
+
// Start issue logging
|
|
380
|
+
if (logWriter) {
|
|
381
|
+
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
382
|
+
}
|
|
383
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions);
|
|
187
384
|
results.push(result);
|
|
385
|
+
// Complete issue logging
|
|
386
|
+
if (logWriter) {
|
|
387
|
+
logWriter.completeIssue();
|
|
388
|
+
}
|
|
188
389
|
if (!result.success) {
|
|
189
390
|
console.log(chalk.yellow(`\n ⚠️ Issue #${issueNumber} failed, stopping sequential execution`));
|
|
190
391
|
break;
|
|
@@ -195,10 +396,24 @@ export async function runCommand(issues, options) {
|
|
|
195
396
|
// Parallel execution (for now, just run sequentially but don't stop on failure)
|
|
196
397
|
// TODO: Add proper parallel execution with listr2
|
|
197
398
|
for (const issueNumber of issueNumbers) {
|
|
198
|
-
const
|
|
399
|
+
const issueInfo = await getIssueInfo(issueNumber);
|
|
400
|
+
// Start issue logging
|
|
401
|
+
if (logWriter) {
|
|
402
|
+
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
403
|
+
}
|
|
404
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions);
|
|
199
405
|
results.push(result);
|
|
406
|
+
// Complete issue logging
|
|
407
|
+
if (logWriter) {
|
|
408
|
+
logWriter.completeIssue();
|
|
409
|
+
}
|
|
200
410
|
}
|
|
201
411
|
}
|
|
412
|
+
// Finalize log
|
|
413
|
+
let logPath = null;
|
|
414
|
+
if (logWriter) {
|
|
415
|
+
logPath = await logWriter.finalize();
|
|
416
|
+
}
|
|
202
417
|
// Summary
|
|
203
418
|
console.log(chalk.blue("\n" + "━".repeat(50)));
|
|
204
419
|
console.log(chalk.blue(" Summary"));
|
|
@@ -214,9 +429,14 @@ export async function runCommand(issues, options) {
|
|
|
214
429
|
const phases = result.phaseResults
|
|
215
430
|
.map((p) => (p.success ? chalk.green(p.phase) : chalk.red(p.phase)))
|
|
216
431
|
.join(" → ");
|
|
217
|
-
|
|
432
|
+
const loopInfo = result.loopTriggered ? chalk.yellow(" [loop]") : "";
|
|
433
|
+
console.log(` ${status} #${result.issueNumber}: ${phases}${loopInfo}${duration}`);
|
|
218
434
|
}
|
|
219
435
|
console.log("");
|
|
436
|
+
if (logPath) {
|
|
437
|
+
console.log(chalk.gray(` 📝 Log: ${logPath}`));
|
|
438
|
+
console.log("");
|
|
439
|
+
}
|
|
220
440
|
if (config.dryRun) {
|
|
221
441
|
console.log(chalk.yellow(" ℹ️ This was a dry run. Use without --dry-run to execute."));
|
|
222
442
|
console.log("");
|
|
@@ -226,4 +446,115 @@ export async function runCommand(issues, options) {
|
|
|
226
446
|
process.exit(1);
|
|
227
447
|
}
|
|
228
448
|
}
|
|
449
|
+
/**
|
|
450
|
+
* Execute a batch of issues
|
|
451
|
+
*/
|
|
452
|
+
async function executeBatch(issueNumbers, config, logWriter, options) {
|
|
453
|
+
const results = [];
|
|
454
|
+
for (const issueNumber of issueNumbers) {
|
|
455
|
+
const issueInfo = await getIssueInfo(issueNumber);
|
|
456
|
+
// Start issue logging
|
|
457
|
+
if (logWriter) {
|
|
458
|
+
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
459
|
+
}
|
|
460
|
+
const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, options);
|
|
461
|
+
results.push(result);
|
|
462
|
+
// Complete issue logging
|
|
463
|
+
if (logWriter) {
|
|
464
|
+
logWriter.completeIssue();
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return results;
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Execute all phases for a single issue with logging and quality loop
|
|
471
|
+
*/
|
|
472
|
+
async function runIssueWithLogging(issueNumber, config, logWriter, labels, options) {
|
|
473
|
+
const startTime = Date.now();
|
|
474
|
+
const phaseResults = [];
|
|
475
|
+
let loopTriggered = false;
|
|
476
|
+
let sessionId;
|
|
477
|
+
console.log(chalk.blue(`\n Issue #${issueNumber}`));
|
|
478
|
+
// Determine phases for this specific issue
|
|
479
|
+
const phases = determinePhasesForIssue(config.phases, labels, options);
|
|
480
|
+
if (phases.length !== config.phases.length) {
|
|
481
|
+
console.log(chalk.gray(` Phases adjusted: ${phases.join(" → ")}`));
|
|
482
|
+
}
|
|
483
|
+
let iteration = 0;
|
|
484
|
+
const maxIterations = config.qualityLoop ? config.maxIterations : 1;
|
|
485
|
+
while (iteration < maxIterations) {
|
|
486
|
+
iteration++;
|
|
487
|
+
if (config.qualityLoop && iteration > 1) {
|
|
488
|
+
console.log(chalk.yellow(` Quality loop iteration ${iteration}/${maxIterations}`));
|
|
489
|
+
loopTriggered = true;
|
|
490
|
+
}
|
|
491
|
+
let phasesFailed = false;
|
|
492
|
+
for (const phase of phases) {
|
|
493
|
+
console.log(chalk.gray(` ⏳ ${phase}...`));
|
|
494
|
+
const phaseStartTime = new Date();
|
|
495
|
+
const result = await executePhase(issueNumber, phase, config, sessionId);
|
|
496
|
+
const phaseEndTime = new Date();
|
|
497
|
+
// Capture session ID for subsequent phases
|
|
498
|
+
if (result.sessionId) {
|
|
499
|
+
sessionId = result.sessionId;
|
|
500
|
+
}
|
|
501
|
+
phaseResults.push(result);
|
|
502
|
+
// Log phase result
|
|
503
|
+
if (logWriter) {
|
|
504
|
+
const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
|
|
505
|
+
? "success"
|
|
506
|
+
: result.error?.includes("Timeout")
|
|
507
|
+
? "timeout"
|
|
508
|
+
: "failure", { error: result.error });
|
|
509
|
+
logWriter.logPhase(phaseLog);
|
|
510
|
+
}
|
|
511
|
+
if (result.success) {
|
|
512
|
+
const duration = result.durationSeconds
|
|
513
|
+
? ` (${formatDuration(result.durationSeconds)})`
|
|
514
|
+
: "";
|
|
515
|
+
console.log(chalk.green(` ✓ ${phase}${duration}`));
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
console.log(chalk.red(` ✗ ${phase}: ${result.error}`));
|
|
519
|
+
phasesFailed = true;
|
|
520
|
+
// If quality loop enabled, run loop phase to fix issues
|
|
521
|
+
if (config.qualityLoop && iteration < maxIterations) {
|
|
522
|
+
console.log(chalk.yellow(` Running /loop to fix issues...`));
|
|
523
|
+
const loopResult = await executePhase(issueNumber, "loop", config, sessionId);
|
|
524
|
+
phaseResults.push(loopResult);
|
|
525
|
+
if (loopResult.sessionId) {
|
|
526
|
+
sessionId = loopResult.sessionId;
|
|
527
|
+
}
|
|
528
|
+
if (loopResult.success) {
|
|
529
|
+
console.log(chalk.green(` ✓ loop - retrying phases`));
|
|
530
|
+
// Continue to next iteration
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
console.log(chalk.red(` ✗ loop: ${loopResult.error}`));
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// Stop on first failure (if not in quality loop or loop failed)
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// If all phases passed, exit the loop
|
|
542
|
+
if (!phasesFailed) {
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
// If we're not in quality loop mode, don't retry
|
|
546
|
+
if (!config.qualityLoop) {
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
551
|
+
const success = phaseResults.length > 0 && phaseResults.every((r) => r.success);
|
|
552
|
+
return {
|
|
553
|
+
issueNumber,
|
|
554
|
+
success,
|
|
555
|
+
phaseResults,
|
|
556
|
+
durationSeconds,
|
|
557
|
+
loopTriggered,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
229
560
|
//# sourceMappingURL=run.js.map
|