sequant 1.1.1 → 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 CHANGED
@@ -45,15 +45,21 @@ program
45
45
  .action(statusCommand);
46
46
  program
47
47
  .command("run")
48
- .description("Execute workflow for GitHub issues")
49
- .argument("<issues...>", "Issue numbers to process")
48
+ .description("Execute workflow for GitHub issues using Claude Agent SDK")
49
+ .argument("[issues...]", "Issue numbers to process")
50
50
  .option("--phases <list>", "Phases to run (default: spec,exec,qa)")
51
51
  .option("--sequential", "Run issues sequentially")
52
52
  .option("-d, --dry-run", "Preview without execution")
53
- .option("-v, --verbose", "Verbose output")
53
+ .option("-v, --verbose", "Verbose output with streaming")
54
54
  .option("--timeout <seconds>", "Timeout per phase in seconds", parseInt)
55
55
  .option("--log-json", "Enable structured JSON logging")
56
56
  .option("--log-path <path>", "Custom log directory path")
57
+ .option("-q, --quality-loop", "Enable quality loop with auto-retry")
58
+ .option("--max-iterations <n>", "Max iterations for quality loop (default: 3)", parseInt)
59
+ .option("--batch <issues>", 'Group of issues to run together (e.g., --batch "1 2" --batch "3")', (value, prev) => prev.concat([value]), [])
60
+ .option("--smart-tests", "Enable smart test detection (default)")
61
+ .option("--no-smart-tests", "Disable smart test detection")
62
+ .option("--testgen", "Run testgen phase after spec")
57
63
  .action(runCommand);
58
64
  program
59
65
  .command("logs")
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../bin/cli.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../bin/cli.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,8 @@
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
  interface RunOptions {
7
8
  phases?: string;
@@ -11,6 +12,12 @@ interface RunOptions {
11
12
  timeout?: number;
12
13
  logJson?: boolean;
13
14
  logPath?: string;
15
+ qualityLoop?: boolean;
16
+ maxIterations?: number;
17
+ batch?: string[];
18
+ smartTests?: boolean;
19
+ noSmartTests?: boolean;
20
+ testgen?: boolean;
14
21
  }
15
22
  /**
16
23
  * Main run command
@@ -1 +1 @@
1
- {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA8BH,UAAU,UAAU;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAyID;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CAwLf"}
1
+ {"version":3,"file":"run.d.ts","sourceRoot":"","sources":["../../../src/commands/run.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAwCH,UAAU,UAAU;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA2SD;;GAEG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,UAAU,GAClB,OAAO,CAAC,IAAI,CAAC,CA6Pf"}
@@ -1,23 +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 { spawn, spawnSync } from "child_process";
8
+ import { spawnSync } from "child_process";
9
+ import { query } from "@anthropic-ai/claude-agent-sdk";
8
10
  import { getManifest } from "../lib/manifest.js";
9
11
  import { LogWriter, createPhaseLogFromTiming, } from "../lib/workflow/log-writer.js";
12
+ import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
10
13
  /**
11
- * Check if claude CLI is available
14
+ * Natural language prompts for each phase
15
+ * These prompts will invoke the corresponding skills via natural language
12
16
  */
13
- function checkClaudeCli() {
14
- const result = spawnSync("claude", ["--version"], {
15
- stdio: "pipe",
16
- shell: true,
17
- });
18
- return result.status === 0;
19
- }
20
- import { DEFAULT_PHASES, DEFAULT_CONFIG, } from "../lib/workflow/types.js";
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"];
21
29
  /**
22
30
  * Format duration in human-readable format
23
31
  */
@@ -30,9 +38,15 @@ function formatDuration(seconds) {
30
38
  return `${mins}m ${secs.toFixed(0)}s`;
31
39
  }
32
40
  /**
33
- * Execute a single phase for an issue using claude CLI
41
+ * Get the prompt for a phase with the issue number substituted
34
42
  */
35
- async function executePhase(issueNumber, phase, config) {
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
48
+ */
49
+ async function executePhase(issueNumber, phase, config, sessionId) {
36
50
  const startTime = Date.now();
37
51
  if (config.dryRun) {
38
52
  // Dry run - just simulate
@@ -45,61 +59,130 @@ async function executePhase(issueNumber, phase, config) {
45
59
  durationSeconds: 0,
46
60
  };
47
61
  }
48
- // Execute claude CLI with the skill
49
- return new Promise((resolve) => {
50
- const command = `/${phase} ${issueNumber}`;
51
- const timeout = config.phaseTimeout * 1000;
52
- if (config.verbose) {
53
- console.log(chalk.gray(` Executing: ${command}`));
54
- }
55
- const proc = spawn("claude", ["--print", "--dangerously-skip-permissions", "-p", command], {
56
- stdio: config.verbose ? "inherit" : "pipe",
57
- shell: true,
58
- timeout,
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
+ },
59
97
  });
60
- let killed = false;
61
- const timer = setTimeout(() => {
62
- killed = true;
63
- proc.kill("SIGTERM");
64
- }, timeout);
65
- proc.on("close", (code) => {
66
- clearTimeout(timer);
67
- const durationSeconds = (Date.now() - startTime) / 1000;
68
- if (killed) {
69
- resolve({
70
- phase,
71
- success: false,
72
- durationSeconds,
73
- error: `Timeout after ${config.phaseTimeout}s`,
74
- });
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
+ }
115
+ }
116
+ // Capture the final result
117
+ if (message.type === "result") {
118
+ resultMessage = message;
75
119
  }
76
- else if (code === 0) {
77
- resolve({
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 {
78
127
  phase,
79
128
  success: true,
80
129
  durationSeconds,
81
- });
130
+ sessionId: resultSessionId,
131
+ };
82
132
  }
83
133
  else {
84
- resolve({
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 {
85
150
  phase,
86
151
  success: false,
87
152
  durationSeconds,
88
- error: `Exit code ${code}`,
89
- });
153
+ error: lastError,
154
+ sessionId: resultSessionId,
155
+ };
90
156
  }
91
- });
92
- proc.on("error", (err) => {
93
- clearTimeout(timer);
94
- const durationSeconds = (Date.now() - startTime) / 1000;
95
- resolve({
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 {
96
173
  phase,
97
174
  success: false,
98
175
  durationSeconds,
99
- error: err.message,
100
- });
101
- });
102
- });
176
+ error: `Timeout after ${config.phaseTimeout}s`,
177
+ };
178
+ }
179
+ return {
180
+ phase,
181
+ success: false,
182
+ durationSeconds,
183
+ error,
184
+ };
185
+ }
103
186
  }
104
187
  /**
105
188
  * Fetch issue info from GitHub
@@ -129,6 +212,68 @@ async function getIssueInfo(issueNumber) {
129
212
  }
130
213
  return { title: `Issue #${issueNumber}`, labels: [] };
131
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");
239
+ }
240
+ else {
241
+ phases.push("test");
242
+ }
243
+ }
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)));
276
+ }
132
277
  /**
133
278
  * Main run command
134
279
  */
@@ -140,36 +285,44 @@ export async function runCommand(issues, options) {
140
285
  console.log(chalk.red("❌ Sequant is not initialized. Run `sequant init` first."));
141
286
  return;
142
287
  }
143
- // Check if claude CLI is available (skip for dry-run)
144
- if (!options.dryRun && !checkClaudeCli()) {
145
- console.log(chalk.red("❌ Claude CLI not found. Install it from https://claude.ai/code"));
146
- console.log(chalk.gray(" Or use --dry-run to preview without execution."));
147
- return;
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));
148
301
  }
149
- // Parse issue numbers
150
- const issueNumbers = issues
151
- .map((i) => parseInt(i, 10))
152
- .filter((n) => !isNaN(n));
153
302
  if (issueNumbers.length === 0) {
154
303
  console.log(chalk.red("❌ No valid issue numbers provided."));
155
304
  console.log(chalk.gray("\nUsage: sequant run <issues...> [options]"));
156
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"'));
157
307
  return;
158
308
  }
159
309
  // Build config
160
310
  const config = {
161
311
  ...DEFAULT_CONFIG,
162
- phases: options.phases
163
- ? options.phases.split(",").map((p) => p.trim())
312
+ phases: mergedOptions.phases
313
+ ? mergedOptions.phases.split(",").map((p) => p.trim())
164
314
  : DEFAULT_PHASES,
165
- sequential: options.sequential ?? false,
166
- dryRun: options.dryRun ?? false,
167
- verbose: options.verbose ?? false,
168
- phaseTimeout: options.timeout ?? DEFAULT_CONFIG.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,
169
322
  };
170
323
  // Initialize log writer if JSON logging enabled
171
324
  let logWriter = null;
172
- if (options.logJson && !config.dryRun) {
325
+ if (mergedOptions.logJson && !config.dryRun) {
173
326
  const runConfig = {
174
327
  phases: config.phases,
175
328
  sequential: config.sequential,
@@ -177,7 +330,7 @@ export async function runCommand(issues, options) {
177
330
  maxIterations: config.maxIterations,
178
331
  };
179
332
  logWriter = new LogWriter({
180
- logPath: options.logPath,
333
+ logPath: mergedOptions.logPath,
181
334
  verbose: config.verbose,
182
335
  });
183
336
  await logWriter.initialize(runConfig);
@@ -186,6 +339,15 @@ export async function runCommand(issues, options) {
186
339
  console.log(chalk.gray(` Stack: ${manifest.stack}`));
187
340
  console.log(chalk.gray(` Phases: ${config.phases.join(" → ")}`));
188
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
+ }
189
351
  if (config.dryRun) {
190
352
  console.log(chalk.yellow(` ⚠️ DRY RUN - no actual execution`));
191
353
  }
@@ -195,15 +357,30 @@ export async function runCommand(issues, options) {
195
357
  console.log(chalk.gray(` Issues: ${issueNumbers.map((n) => `#${n}`).join(", ")}`));
196
358
  // Execute
197
359
  const results = [];
198
- if (config.sequential) {
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) {
199
376
  // Sequential execution
200
377
  for (const issueNumber of issueNumbers) {
378
+ const issueInfo = await getIssueInfo(issueNumber);
201
379
  // Start issue logging
202
380
  if (logWriter) {
203
- const issueInfo = await getIssueInfo(issueNumber);
204
381
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
205
382
  }
206
- const result = await runIssueWithLogging(issueNumber, config, logWriter);
383
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions);
207
384
  results.push(result);
208
385
  // Complete issue logging
209
386
  if (logWriter) {
@@ -219,12 +396,12 @@ export async function runCommand(issues, options) {
219
396
  // Parallel execution (for now, just run sequentially but don't stop on failure)
220
397
  // TODO: Add proper parallel execution with listr2
221
398
  for (const issueNumber of issueNumbers) {
399
+ const issueInfo = await getIssueInfo(issueNumber);
222
400
  // Start issue logging
223
401
  if (logWriter) {
224
- const issueInfo = await getIssueInfo(issueNumber);
225
402
  logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
226
403
  }
227
- const result = await runIssueWithLogging(issueNumber, config, logWriter);
404
+ const result = await runIssueWithLogging(issueNumber, config, logWriter, issueInfo.labels, mergedOptions);
228
405
  results.push(result);
229
406
  // Complete issue logging
230
407
  if (logWriter) {
@@ -252,7 +429,8 @@ export async function runCommand(issues, options) {
252
429
  const phases = result.phaseResults
253
430
  .map((p) => (p.success ? chalk.green(p.phase) : chalk.red(p.phase)))
254
431
  .join(" → ");
255
- console.log(` ${status} #${result.issueNumber}: ${phases}${duration}`);
432
+ const loopInfo = result.loopTriggered ? chalk.yellow(" [loop]") : "";
433
+ console.log(` ${status} #${result.issueNumber}: ${phases}${loopInfo}${duration}`);
256
434
  }
257
435
  console.log("");
258
436
  if (logPath) {
@@ -269,46 +447,114 @@ export async function runCommand(issues, options) {
269
447
  }
270
448
  }
271
449
  /**
272
- * Execute all phases for a single issue with logging
450
+ * Execute a batch of issues
273
451
  */
274
- async function runIssueWithLogging(issueNumber, config, logWriter) {
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) {
275
473
  const startTime = Date.now();
276
474
  const phaseResults = [];
475
+ let loopTriggered = false;
476
+ let sessionId;
277
477
  console.log(chalk.blue(`\n Issue #${issueNumber}`));
278
- for (const phase of config.phases) {
279
- console.log(chalk.gray(` ⏳ ${phase}...`));
280
- const phaseStartTime = new Date();
281
- const result = await executePhase(issueNumber, phase, config);
282
- const phaseEndTime = new Date();
283
- phaseResults.push(result);
284
- // Log phase result
285
- if (logWriter) {
286
- const phaseLog = createPhaseLogFromTiming(phase, issueNumber, phaseStartTime, phaseEndTime, result.success
287
- ? "success"
288
- : result.error?.includes("Timeout")
289
- ? "timeout"
290
- : "failure", { error: result.error });
291
- logWriter.logPhase(phaseLog);
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;
292
490
  }
293
- if (result.success) {
294
- const duration = result.durationSeconds
295
- ? ` (${formatDuration(result.durationSeconds)})`
296
- : "";
297
- console.log(chalk.green(` ✓ ${phase}${duration}`));
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
+ }
298
540
  }
299
- else {
300
- console.log(chalk.red(` ✗ ${phase}: ${result.error}`));
301
- // Stop on first failure
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) {
302
547
  break;
303
548
  }
304
549
  }
305
550
  const durationSeconds = (Date.now() - startTime) / 1000;
306
- const success = phaseResults.every((r) => r.success);
551
+ const success = phaseResults.length > 0 && phaseResults.every((r) => r.success);
307
552
  return {
308
553
  issueNumber,
309
554
  success,
310
555
  phaseResults,
311
556
  durationSeconds,
557
+ loopTriggered,
312
558
  };
313
559
  }
314
560
  //# sourceMappingURL=run.js.map