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.
Files changed (51) hide show
  1. package/dist/bin/cli.js +11 -4
  2. package/dist/bin/cli.js.map +1 -1
  3. package/dist/src/commands/init.d.ts.map +1 -1
  4. package/dist/src/commands/init.js +38 -2
  5. package/dist/src/commands/init.js.map +1 -1
  6. package/dist/src/commands/init.test.js +37 -1
  7. package/dist/src/commands/init.test.js.map +1 -1
  8. package/dist/src/commands/run.d.ts +10 -1
  9. package/dist/src/commands/run.d.ts.map +1 -1
  10. package/dist/src/commands/run.js +549 -102
  11. package/dist/src/commands/run.js.map +1 -1
  12. package/dist/src/commands/update.d.ts.map +1 -1
  13. package/dist/src/commands/update.js +66 -2
  14. package/dist/src/commands/update.js.map +1 -1
  15. package/dist/src/index.d.ts +3 -1
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +2 -1
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/lib/config.d.ts +19 -0
  20. package/dist/src/lib/config.d.ts.map +1 -0
  21. package/dist/src/lib/config.js +31 -0
  22. package/dist/src/lib/config.js.map +1 -0
  23. package/dist/src/lib/settings.d.ts +69 -0
  24. package/dist/src/lib/settings.d.ts.map +1 -0
  25. package/dist/src/lib/settings.js +79 -0
  26. package/dist/src/lib/settings.js.map +1 -0
  27. package/dist/src/lib/stacks.d.ts +1 -0
  28. package/dist/src/lib/stacks.d.ts.map +1 -1
  29. package/dist/src/lib/stacks.js +6 -0
  30. package/dist/src/lib/stacks.js.map +1 -1
  31. package/dist/src/lib/templates.d.ts +5 -1
  32. package/dist/src/lib/templates.d.ts.map +1 -1
  33. package/dist/src/lib/templates.js +3 -2
  34. package/dist/src/lib/templates.js.map +1 -1
  35. package/dist/src/lib/workflow/log-writer.test.d.ts +7 -0
  36. package/dist/src/lib/workflow/log-writer.test.d.ts.map +1 -0
  37. package/dist/src/lib/workflow/log-writer.test.js +451 -0
  38. package/dist/src/lib/workflow/log-writer.test.js.map +1 -0
  39. package/dist/src/lib/workflow/run-log-schema.test.d.ts +2 -0
  40. package/dist/src/lib/workflow/run-log-schema.test.d.ts.map +1 -0
  41. package/dist/src/lib/workflow/run-log-schema.test.js +455 -0
  42. package/dist/src/lib/workflow/run-log-schema.test.js.map +1 -0
  43. package/dist/src/lib/workflow/types.d.ts +2 -0
  44. package/dist/src/lib/workflow/types.d.ts.map +1 -1
  45. package/package.json +2 -1
  46. package/templates/hooks/pre-tool.sh +14 -2
  47. package/templates/scripts/cleanup-worktree.sh +23 -1
  48. package/templates/skills/exec/SKILL.md +18 -0
  49. package/templates/skills/fullsolve/SKILL.md +26 -0
  50. package/templates/skills/solve/SKILL.md +27 -11
  51. package/templates/skills/spec/SKILL.md +30 -2
@@ -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 { 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";
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
- * Check if claude CLI is available
43
+ * Detect phases based on issue labels (like /solve logic)
12
44
  */
13
- function checkClaudeCli() {
14
- const result = spawnSync("claude", ["--version"], {
15
- stdio: "pipe",
16
- shell: true,
17
- });
18
- return result.status === 0;
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
- * Execute a single phase for an issue using claude CLI
120
+ * Get the prompt for a phase with the issue number substituted
34
121
  */
35
- async function executePhase(issueNumber, phase, config) {
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
- // 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,
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
- 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
- });
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
- else if (code === 0) {
77
- resolve({
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
- resolve({
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: `Exit code ${code}`,
89
- });
238
+ error: lastError,
239
+ sessionId: resultSessionId,
240
+ };
90
241
  }
91
- });
92
- proc.on("error", (err) => {
93
- clearTimeout(timer);
94
- const durationSeconds = (Date.now() - startTime) / 1000;
95
- resolve({
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: err.message,
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🚀 Sequant Workflow Execution\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
- // 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;
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: options.phases
163
- ? options.phases.split(",").map((p) => p.trim())
164
- : DEFAULT_PHASES,
165
- sequential: options.sequential ?? false,
166
- dryRun: options.dryRun ?? false,
167
- verbose: options.verbose ?? false,
168
- phaseTimeout: options.timeout ?? DEFAULT_CONFIG.phaseTimeout,
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
- if (options.logJson && !config.dryRun) {
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: options.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
- console.log(chalk.gray(` Phases: ${config.phases.join(" → ")}`));
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 (config.sequential) {
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
- console.log(` ${status} #${result.issueNumber}: ${phases}${duration}`);
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 all phases for a single issue with logging
564
+ * Execute a batch of issues
273
565
  */
274
- async function runIssueWithLogging(issueNumber, config, logWriter) {
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
- 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);
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
- if (result.success) {
294
- const duration = result.durationSeconds
295
- ? ` (${formatDuration(result.durationSeconds)})`
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(` ✓ ${phase}${duration}`));
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
- console.log(chalk.red(` ✗ ${phase}: ${result.error}`));
301
- // Stop on first failure
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